Files
yunhai-agent-service/src/main/java/com/yunhai/agent/tool/FormField.java
T
lijing c084c978aa
build / build-push (push) Successful in 2m1s
feat(通知公告): 发布表单支持模块页的富文本正文与选人控件
前端已把 NoticeFront 的 wangEditor 与自绘「通知对象」控件 1:1 搬进助手表单,
后端补上这两个控件所需的契约:

- ToolParam / FormField 新增:
  · pickerSources:选人弹窗的**数据源页签**(形如 {name, 模块内相对地址}),
    由 AgentOrchestrator 拼上模块前缀后下发(沿用「只写相对路径」的铁律);
  · richTextUploadPath / richTextPreviewPath:正文插图的上传与预览地址。
- AgentOrchestrator:把 resolveUploadPath 抽出通用形态 resolveModulePath(幂等、只接受
  模块内相对路径);buildForm 为 widget=userpicker 下发 pickerSources;
  optionCall 强制改写 widget 的白名单放行 userpicker(否则会被降级成 select)。
- NoticeTools:notify.publish 与 notify.editNotice 的 receiveList 改为 userpicker,
  两个数据源为「基础平台」/api/base/GetSchoolAll 与「自定义组」
  /api/contacts/GetGroupTree?type=1;content 走 richtext。

测试:NotifyIntentFormTest / IntentDirectFormTest 补 pickerSources 断言
(**只写相对路径**,不带前缀 —— 前缀拼错是本项目反复出过的坑),
ModuleFormParityTest 补 userpicker 取值口径断言(字符串原样透传,不得包成数组)。
mvn -o test:Tests run: 135, Failures: 0, Errors: 0,BUILD SUCCESS。
2026-10-14 15:19:24 +08:00

205 lines
8.8 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.agent.tool;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 表单里的一个字段:由 {@link ToolParam} 生成,前端据此选择控件(下拉 / 日期时间 / 多行文本…)。
*/
@Data
public class FormField {
/** 提交时的参数名(与业务接口一致) */
private String name;
/** 中文标签,如「开始时间」 */
private String label;
/** 控件类型:text / textarea / number / date / datetime / select / multiselect / file / scopes / flow / radio / switch / richtext / userpicker */
private String widget;
/** 数据类型:string / integer / number / boolean / array */
private String type;
/**
* 数组值的序列化分隔符(仅 POST_JSON 有意义)。
* <p>前端展示时不需要它(用户看到的是多选),但下发给前端便于「提交前预览」如实呈现最终值。
*/
private String joinSeparator;
/**
* {@code widget=file} 时的上传地址(相对站点根),如 {@code /dormnotice/api/base/FileUpload}。
* <p>模块的附件上传只认浏览器 CAS 会话,必须由前端带用户登录态 POST(multipart),
* 拿到 {@code data.fileid} 后作为 fileIds 提交。
*/
private String uploadPath;
/** {@code widget=file} 时是否允许多个附件 */
private boolean multiple;
/**
* {@code widget=richtext}(富文本正文)时,「插入图片」的上传地址(**已带模块前缀**的绝对相对路径),
* 如 {@code /notice/api/file/UploadImage}。
* <p>与附件同理:模块的上传接口只认浏览器 CAS 会话,必须由前端带用户登录态 POST(multipart)。
*/
private String richTextUploadPath;
/**
* {@code widget=richtext} 时,正文内图片的预览地址模板(**已带模块前缀**,含 {@code {id}} 占位),
* 如 {@code /notice/api/file/Preview?id={id}} —— 前端拿到上传返回的文件 id 后替换占位符,
* 得到可直接放进 {@code <img src>} 的地址。
*/
private String richTextPreviewPath;
/**
* {@code widget=userpicker}(通知对象)的**数据源页签**,形如
* {@code [{name:"基础平台", call:{...}}, {name:"自定义组", call:{...}}]}。
*
* <p>搬运自通知公告模块页 {@code components/UserCheckModal.vue} 的两个页签,
* 每个 {@code call} 都已经是「**带模块前缀**的浏览器代取调用」(同 {@link #optionCall} 的形态),
* 由前端用用户登录态去取原始**嵌套树**(不做拍平 —— 拍平会丢掉层级,树就画不出来)。
*/
private List<PickerSource> pickerSources;
/** {@code widget=userpicker} 的一个数据源页签:页签名 + 浏览器代取调用 */
@lombok.Data
public static class PickerSource {
private String name;
private com.yunhai.agent.model.BrowserToolRequest.BrowserCall call;
}
/**
* 「全部」哨兵值(如 {@code ALL})与其显示名(如「全部宿舍」)。
* <p>前端把它作为多选的第一个选项;用户只勾它 → 提交 {@code ["ALL"]};
* 同时勾了具体项 → 提交时折叠掉它,以具体项为准。
*/
private String allToken;
/** 「全部」选项的显示名 */
private String allTokenLabel;
/**
* {@code widget=scopes} 时的节点类型(校园公告 pushScopes):
* 1 全校 / 2 学部 / 3 年级 / 4 班级 / 5 训练队 / 6 指定学员。
* <p>前端把多选结果拼成 {@code [{scopeType,scopeId,scopeName}]}。
*/
private Integer scopeType;
/**
* 「镜像字段」:本字段不由用户填写({@code hidden}),提交时自动填入同一表单中
* {@code mirrorOf} 指定字段**选中项的显示名**。
*
* <p>★ 用途:公告/宿舍/活动三个模块页在提交时会用字典把部门 id 反查成名称一并发出
* ({@code deptId} + {@code deptName},后端字典缺失时回退该名称)。助手只有下拉选中的 id,
* 但前端手里就有选项的 label —— 由它顺手镜像过去,提交体才和模块页一模一样。
*/
private String mirrorOf;
/**
* 「连续请假」展开说明:{@code {count: 天数字段, start: 开始时间字段, end: 结束时间字段}}。
*
* <p>作用在请假工具上:当本字段({@code continuousLeave})取 1 时,前端按
* {@code continuousLeaveNum} 天把 {@code startDate}/{@code endDate} 复制成逐天时间段,
* 生成模块要的 {@code continuousLeaveTime}({@code [{startDate,endDate}]})——
* 与模块页 {@code LeaveTimeSection.regenerate()} 的口径一致。
*/
private Map<String, String> dailyExpand;
/**
* {@code widget=file} 且为 true 时,提交的不是 fileId 数组,而是模块要的**附件对象数组**。
*
* <p>作业模块(coursehomework V2)的 {@code attachments} 形如
* {@code [{fileId,fileUrl,fileType,fileName,sort}]},与其余模块的 {@code fileIds="1,2"} 不同,
* 故由前端按上传结果组装成对象。
*/
private boolean fileAsObject;
/**
* 单选控件({@code widget=radio})在选项就绪后自动选中第一项。
* <p>对齐模块页:{@code LeaveApply.vue} 的「请假类型」一进页面就已选中第一个类型。
*/
private boolean autoPickFirst;
/**
* 选项只有一个时自动填入并隐藏本字段。
* <p>对齐模块页:{@code LeaveApply.vue} 的「班级」用 {@code v-if="myClasses.length > 1"}`,
* 单班时不渲染、值直接带上。
*/
private boolean hideWhenSingleOption;
/**
* 快捷时段按钮组:{@code {start: 开始字段名, end: 结束字段名,
* items: [{label, start, end}]}};{@code start/end} 为时刻({@code HH:mm:ss})。
* <p>对齐模块页 {@code LeaveTimeSection.quickSet()}:沿用已选日期,只换时分秒。
*/
private Map<String, Object> quickPeriods;
/** {@code widget=switch} 勾选时提交的值 */
private String switchOn;
/** {@code widget=switch} 未勾选时提交的值 */
private String switchOff;
/** 多行文本框显示行数 */
private Integer rows;
/** 文本最大长度(前端显示字数统计) */
private Integer maxLength;
private boolean required;
/** 说明(来自工具定义,写清格式与含义) */
private String description;
/** 格式示例,前端用作 placeholder,如 2026-10-11 08:00:00 */
private String example;
/**
* 显式的占位提示(优先于 {@link #example})。
* <p>用于对齐业务模块页的原文案,如请假事由的「请输入请假原因.....」——
* 这里不该塞它的示例值(「发烧就医,需在家休息」),那是给模型看的参考,不是给用户看的提示。
*/
private String placeholder;
private String defaultValue;
/**
* 下拉选项的动态来源(字典工具名,如 leave.types)。
* <p>有值但 {@link #options} 为空 ⇒ 字典接口没取到(多半是下游 302/超时),
* 前端据此提示用户「可先手填编号」,而不是干瞪眼。
*/
private String optionsFrom;
/**
* 当前 {@link #options} 是否来自内置兜底(而非学校实时字典)。
* <p>为 true 时前端提示「用的是标准类型,自定义类型暂时没取到」,而不是静默当成权威值。
*/
private boolean optionsFallback;
/** 下拉选项 [{value,label}] */
private List<Map<String, String>> options = new ArrayList<>();
/* ==================== 动态选项(浏览器代取 + 联动) ==================== */
/**
* 「浏览器代取选项」的调用描述:业务模块的字典接口(班级、学生名单…)只认浏览器 CAS 会话,
* 服务端代取必然 302,所以把地址下发前端,由前端用**用户自己的登录态**取回原始字典再渲染下拉。
*
* <p>地址串里可能含 {@code {{字段名}}} 占位符(见 {@link #dependsOn}),
* 前端在依赖字段有值时做替换(值需 URL 编码)后才发起请求。
*/
private com.yunhai.agent.model.BrowserToolRequest.BrowserCall optionCall;
/** 本字段选项依赖的先序字段名(如「学生」依赖「班级」);依赖未填时前端应清空并提示先选 */
private List<String> dependsOn = new ArrayList<>();
/** 从字典返回对象里取哪个字段作为选项「值」(如 classId / stuId) */
private String valueKey;
/** 从字典返回对象里取哪个字段作为选项「显示名」(如 className / stuName) */
private String labelKey;
}