- ToolParam / FormField 新增控件能力: radio(单选按钮组)、switch(勾选框+开关值映射)、quickPeriods(快捷时段)、 dailyExpand(按天展开)、autoPickFirst(默认选中第一项)、 hideIfSingleOption(单选项隐藏)、placeholder(照搬模块页原文案) - LeaveTools: * leave.submit / leave.register 的 leaveType 改单选按钮组并默认选中第一个 * 请假时间补齐快捷时段与「连续请假」勾选框(勾上展开连续天数 + 逐天时段) * 事由 / 附件对齐模块页文案;学生端 classId 单班隐藏、教师端恒显示 - ToolExecutor.expandDailyArgs 认前端显式发来的 continuousLeaveTime (用户在逐天时段里做过微调就以他改的为准,不再按基准覆盖) - 只读查询因下游 302 失败时也回传 landingPath,让「去业务系统查看」按钮有着落
This commit is contained in:
@@ -252,8 +252,12 @@ public class AgentOrchestrator {
|
|||||||
ToolCallTrace trace = buildTrace(def, args, tr);
|
ToolCallTrace trace = buildTrace(def, args, tr);
|
||||||
resp.getToolCalls().add(trace);
|
resp.getToolCalls().add(trace);
|
||||||
|
|
||||||
if (!def.isReadOnly()) {
|
// 写操作 → 去系统里确认结果;只读**失败**(下游 302 / 会话没建起来)→
|
||||||
resp.setWroteSomething(true);
|
// 同样要给「去系统里自己查看」入口,否则模型嘴上说「点下方按钮」而界面没有按钮。
|
||||||
|
if (!def.isReadOnly() || !tr.isSuccess()) {
|
||||||
|
if (!def.isReadOnly()) {
|
||||||
|
resp.setWroteSomething(true);
|
||||||
|
}
|
||||||
if (StringUtils.isNotBlank(def.getLandingPath())) {
|
if (StringUtils.isNotBlank(def.getLandingPath())) {
|
||||||
resp.setLandingPath(def.getLandingPath());
|
resp.setLandingPath(def.getLandingPath());
|
||||||
}
|
}
|
||||||
@@ -361,8 +365,12 @@ public class AgentOrchestrator {
|
|||||||
}
|
}
|
||||||
auditService.record(session.getSessionId(), caller, def, args, tr);
|
auditService.record(session.getSessionId(), caller, def, args, tr);
|
||||||
resp.getToolCalls().add(buildTrace(def, args, tr));
|
resp.getToolCalls().add(buildTrace(def, args, tr));
|
||||||
if (!def.isReadOnly()) {
|
// 写操作 → 去系统里确认结果;只读**失败**(如下游 302 / 会话没建起来)→
|
||||||
resp.setWroteSomething(true);
|
// 同样要给「去系统里自己查看」入口,否则模型嘴上说「点下方按钮」而界面没有按钮。
|
||||||
|
if (!def.isReadOnly() || !tr.isSuccess()) {
|
||||||
|
if (!def.isReadOnly()) {
|
||||||
|
resp.setWroteSomething(true);
|
||||||
|
}
|
||||||
if (StringUtils.isNotBlank(def.getLandingPath())) {
|
if (StringUtils.isNotBlank(def.getLandingPath())) {
|
||||||
resp.setLandingPath(def.getLandingPath());
|
resp.setLandingPath(def.getLandingPath());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class FormField {
|
|||||||
/** 中文标签,如「开始时间」 */
|
/** 中文标签,如「开始时间」 */
|
||||||
private String label;
|
private String label;
|
||||||
|
|
||||||
/** 控件类型:text / textarea / number / date / datetime / select / multiselect / file */
|
/** 控件类型:text / textarea / number / date / datetime / select / multiselect / file / scopes / flow / radio / switch */
|
||||||
private String widget;
|
private String widget;
|
||||||
|
|
||||||
/** 数据类型:string / integer / number / boolean / array */
|
/** 数据类型:string / integer / number / boolean / array */
|
||||||
@@ -86,6 +86,38 @@ public class FormField {
|
|||||||
*/
|
*/
|
||||||
private boolean fileAsObject;
|
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 boolean required;
|
||||||
|
|
||||||
/** 说明(来自工具定义,写清格式与含义) */
|
/** 说明(来自工具定义,写清格式与含义) */
|
||||||
@@ -94,6 +126,13 @@ public class FormField {
|
|||||||
/** 格式示例,前端用作 placeholder,如 2026-10-11 08:00:00 */
|
/** 格式示例,前端用作 placeholder,如 2026-10-11 08:00:00 */
|
||||||
private String example;
|
private String example;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显式的占位提示(优先于 {@link #example})。
|
||||||
|
* <p>用于对齐业务模块页的原文案,如请假事由的「请输入请假原因.....」——
|
||||||
|
* 这里不该塞它的示例值(「发烧就医,需在家休息」),那是给模型看的参考,不是给用户看的提示。
|
||||||
|
*/
|
||||||
|
private String placeholder;
|
||||||
|
|
||||||
private String defaultValue;
|
private String defaultValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -619,9 +619,29 @@ public class ToolExecutor {
|
|||||||
*
|
*
|
||||||
* <p>与模块页 {@code LeaveTimeSection.regenerate()} 口径一致(每天一段,时分沿用基准)。
|
* <p>与模块页 {@code LeaveTimeSection.regenerate()} 口径一致(每天一段,时分沿用基准)。
|
||||||
* 放在执行器里做,浏览器代执行与服务端直调共用同一份逻辑。
|
* 放在执行器里做,浏览器代执行与服务端直调共用同一份逻辑。
|
||||||
|
*
|
||||||
|
* <p>★ 若入参里**已经有** {@code continuousLeaveTime}(助手表单让用户逐天微调过,
|
||||||
|
* 前端会显式发来),则以它为准、不再自行展开 —— 否则用户改好的个别天会被覆盖掉。
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> expandDailyArgs(ToolDef def, Map<String, Object> args) {
|
private Map<String, Object> expandDailyArgs(ToolDef def, Map<String, Object> args) {
|
||||||
Map<String, Object> extra = new LinkedHashMap<>();
|
Map<String, Object> extra = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
// ① 前端已经算好逐天时段(用户可单天微调)→ 原样采用,只把基准对齐到第一天
|
||||||
|
Object explicit = args.get("continuousLeaveTime");
|
||||||
|
if (explicit instanceof List && !((List<?>) explicit).isEmpty()) {
|
||||||
|
Object first = ((List<?>) explicit).get(0);
|
||||||
|
if (first instanceof Map) {
|
||||||
|
Object s0 = ((Map<?, ?>) first).get("startDate");
|
||||||
|
Object e0 = ((Map<?, ?>) first).get("endDate");
|
||||||
|
if (s0 != null && e0 != null) {
|
||||||
|
extra.put("startDate", String.valueOf(s0));
|
||||||
|
extra.put("endDate", String.valueOf(e0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ② 否则按「基准时间 + 天数」自行展开(模型直接调用 / 没走表单时的路径)
|
||||||
for (ToolParam p : def.getParams()) {
|
for (ToolParam p : def.getParams()) {
|
||||||
Map<String, String> spec = p.getDailyExpandSpec();
|
Map<String, String> spec = p.getDailyExpandSpec();
|
||||||
if (spec == null || spec.isEmpty()) continue;
|
if (spec == null || spec.isEmpty()) continue;
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ public class ToolParam {
|
|||||||
/** 示例值(表单 placeholder,如 2026-10-11 08:00:00),也会进入给模型的描述 */
|
/** 示例值(表单 placeholder,如 2026-10-11 08:00:00),也会进入给模型的描述 */
|
||||||
private String example;
|
private String example;
|
||||||
|
|
||||||
|
/** 显式占位提示(优先于 {@link #example}),用于照搬业务模块页的原文案 */
|
||||||
|
private String placeholderText;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控件类型:text / textarea / number / date / datetime / select / multiselect / file;
|
* 控件类型:text / textarea / number / date / datetime / select / multiselect / file;
|
||||||
* 缺省按参数名与类型自动推断。
|
* 缺省按参数名与类型自动推断。
|
||||||
@@ -148,8 +151,52 @@ public class ToolParam {
|
|||||||
* <p>作用有两个:① 生成表单时把依赖名带给前端,前端据此清空/重新拉取选项;
|
* <p>作用有两个:① 生成表单时把依赖名带给前端,前端据此清空/重新拉取选项;
|
||||||
* ② 生成「浏览器代取选项」的地址时,把依赖字段位置留成占位符({@code ?classId={{classId}}})。
|
* ② 生成「浏览器代取选项」的地址时,把依赖字段位置留成占位符({@code ?classId={{classId}}})。
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 依赖的先序字段名(用于联动下拉,如「学生」依赖「班级」)
|
||||||
|
*/
|
||||||
private List<String> dependsOn;
|
private List<String> dependsOn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单选控件({@code widget=radio})在选项就绪后**自动选中第一项**。
|
||||||
|
*
|
||||||
|
* <p>★ 对齐模块页行为:HomeLeave 的 {@code LeaveApply.vue} 里「请假类型」是单选按钮组,
|
||||||
|
* 且 {@code watch(leaveTypeOptions, opts => { if (formData.leaveType == null && opts.length)
|
||||||
|
* formData.leaveType = opts[0].id })} —— 一进页面就已经选中第一个类型。
|
||||||
|
* 助手的表单若不自动选中,用户就得比模块页多点一步,且容易漏填导致必填校验卡住。
|
||||||
|
*/
|
||||||
|
private boolean autoPickFirst;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选项只有一个时**自动填入并隐藏本字段**。
|
||||||
|
*
|
||||||
|
* <p>★ 对齐模块页行为:{@code LeaveApply.vue} 的「班级」是
|
||||||
|
* {@code v-if="myClasses.length > 1"} —— 教师只有 1 个班时页面上根本没有这个控件,
|
||||||
|
* 值在 {@code onMounted} 里直接带上。助手表单同理:单班自动带上、不显示,
|
||||||
|
* 多班(或选项没取到)才让用户选。
|
||||||
|
*/
|
||||||
|
private boolean hideWhenSingleOption;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快捷时段按钮组(对齐 {@code LeaveTimeSection.vue} 的全天/上午/下午)。
|
||||||
|
*
|
||||||
|
* <p>结构:{@code {start: 开始字段名, end: 结束字段名,
|
||||||
|
* items: [{label, start, end}, ...]}};{@code start/end} 是**时刻**({@code HH:mm:ss}),
|
||||||
|
* 点按钮时套用到当前已选日期上(未选日期则用今天)。
|
||||||
|
*/
|
||||||
|
private Map<String, Object> quickPeriods;
|
||||||
|
|
||||||
|
/** {@code widget=switch} 的「开」值(勾选时提交的值),如请假 continuousLeave 的 {@code 1} */
|
||||||
|
private String switchOn;
|
||||||
|
|
||||||
|
/** {@code widget=switch} 的「关」值(未勾选时提交的值),如 {@code 0} */
|
||||||
|
private String switchOff;
|
||||||
|
|
||||||
|
/** 多行文本框显示行数(缺省 3,对齐模块页「请假事由」的 4 行) */
|
||||||
|
private Integer rows;
|
||||||
|
|
||||||
|
/** 文本最大长度(对齐模块页 {@code maxlength} + 字数统计) */
|
||||||
|
private Integer maxLength;
|
||||||
|
|
||||||
public ToolParam() {
|
public ToolParam() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +254,15 @@ public class ToolParam {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显式占位提示,照搬业务模块页的原文案(如请假事由的「请输入请假原因.....」)。
|
||||||
|
* <p>与 {@link #eg} 的区别:{@code eg} 是给模型看的**示例值**,这里纯粹是给用户看的提示。
|
||||||
|
*/
|
||||||
|
public ToolParam placeholder(String value) {
|
||||||
|
this.placeholderText = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/** 指定控件类型 */
|
/** 指定控件类型 */
|
||||||
public ToolParam as(String value) {
|
public ToolParam as(String value) {
|
||||||
this.widget = value;
|
this.widget = value;
|
||||||
@@ -323,6 +379,88 @@ public class ToolParam {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单选按钮组(对齐模块页的 {@code RadioGroup})。
|
||||||
|
* <p>取值仍走 {@link #options}/{@link #optionsFrom},只是渲染成平铺的单选按钮而不是下拉。
|
||||||
|
*/
|
||||||
|
public ToolParam radio() {
|
||||||
|
this.widget = "radio";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 勾选框 / 开关控件。
|
||||||
|
*
|
||||||
|
* @param onValue 勾选时提交的值(如 {@code "1"})
|
||||||
|
* @param offValue 未勾选时提交的值(如 {@code "0"})
|
||||||
|
*/
|
||||||
|
public ToolParam switchOf(String onValue, String offValue) {
|
||||||
|
this.widget = "switch";
|
||||||
|
this.switchOn = onValue;
|
||||||
|
this.switchOff = offValue;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选项就绪后自动选中第一项(模块页「请假类型」的默认行为) */
|
||||||
|
public ToolParam autoPickFirst() {
|
||||||
|
this.autoPickFirst = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选项只有一个时自动填入并隐藏本字段(模块页「班级」:只教一个班时不显示) */
|
||||||
|
public ToolParam hideIfSingleOption() {
|
||||||
|
this.hideWhenSingleOption = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快捷时段按钮组(全天 / 上午 / 下午 …),点一下同时填好开始与结束**时刻**。
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* ToolParam.required("startDate", "string", "...")
|
||||||
|
* .as("datetime")
|
||||||
|
* .quickPeriods("startDate", "endDate",
|
||||||
|
* "全天", "08:00:00", "18:00:00",
|
||||||
|
* "上午", "08:00:00", "12:00:00",
|
||||||
|
* "下午", "12:00:00", "18:00:00")
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p>三元组按「标签、开始时刻、结束时刻」顺序依次传入(个数必须为 3 的倍数)。
|
||||||
|
* 与模块页 {@code LeaveTimeSection.quickSet()} 口径一致:**沿用已选的日期**,
|
||||||
|
* 只把时分秒换掉;还没选日期就按今天。
|
||||||
|
*/
|
||||||
|
public ToolParam quickPeriods(String startField, String endField, String... labelStartEnd) {
|
||||||
|
if (labelStartEnd == null || labelStartEnd.length < 3 || labelStartEnd.length % 3 != 0) {
|
||||||
|
throw new IllegalArgumentException("quickPeriods 需要「标签, 开始时刻, 结束时刻」三元组");
|
||||||
|
}
|
||||||
|
List<Map<String, String>> items = new ArrayList<>();
|
||||||
|
for (int i = 0; i + 2 < labelStartEnd.length; i += 3) {
|
||||||
|
Map<String, String> it = new LinkedHashMap<>();
|
||||||
|
it.put("label", labelStartEnd[i]);
|
||||||
|
it.put("start", labelStartEnd[i + 1]);
|
||||||
|
it.put("end", labelStartEnd[i + 2]);
|
||||||
|
items.add(it);
|
||||||
|
}
|
||||||
|
Map<String, Object> spec = new LinkedHashMap<>();
|
||||||
|
spec.put("start", startField);
|
||||||
|
spec.put("end", endField);
|
||||||
|
spec.put("items", items);
|
||||||
|
this.quickPeriods = spec;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 多行文本框行数 */
|
||||||
|
public ToolParam rows(int value) {
|
||||||
|
this.rows = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 文本最大长度(前端显示字数统计,超出时截断提示) */
|
||||||
|
public ToolParam maxLength(int value) {
|
||||||
|
this.maxLength = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/** 覆盖附件上传地址(缺省由编排层按 {@code /<模块>/api/base/FileUpload} 兜底) */
|
/** 覆盖附件上传地址(缺省由编排层按 {@code /<模块>/api/base/FileUpload} 兜底) */
|
||||||
public ToolParam upload(String path) {
|
public ToolParam upload(String path) {
|
||||||
this.uploadPathOverride = path;
|
this.uploadPathOverride = path;
|
||||||
@@ -407,6 +545,7 @@ public class ToolParam {
|
|||||||
f.setRequired(required);
|
f.setRequired(required);
|
||||||
f.setDescription(description);
|
f.setDescription(description);
|
||||||
f.setExample(example);
|
f.setExample(example);
|
||||||
|
f.setPlaceholder(placeholderText);
|
||||||
f.setDefaultValue(defaultValue);
|
f.setDefaultValue(defaultValue);
|
||||||
f.setOptions(optionList());
|
f.setOptions(optionList());
|
||||||
// 把「动态选项来源」一并下发:前端据此区分「本来就没有选项」和「字典没取到」
|
// 把「动态选项来源」一并下发:前端据此区分「本来就没有选项」和「字典没取到」
|
||||||
@@ -422,6 +561,14 @@ public class ToolParam {
|
|||||||
f.setMirrorOf(mirrorLabelOf);
|
f.setMirrorOf(mirrorLabelOf);
|
||||||
f.setDailyExpand(dailyExpandSpec);
|
f.setDailyExpand(dailyExpandSpec);
|
||||||
f.setFileAsObject(fileObject);
|
f.setFileAsObject(fileObject);
|
||||||
|
// ★ 表单呈现细节(对齐模块页的控件形态)
|
||||||
|
f.setAutoPickFirst(autoPickFirst);
|
||||||
|
f.setHideWhenSingleOption(hideWhenSingleOption);
|
||||||
|
f.setQuickPeriods(quickPeriods);
|
||||||
|
f.setSwitchOn(switchOn);
|
||||||
|
f.setSwitchOff(switchOff);
|
||||||
|
f.setRows(rows);
|
||||||
|
f.setMaxLength(maxLength);
|
||||||
// 选项是内置兜底(不是学校实时字典)时如实告知,前端可提示用户
|
// 选项是内置兜底(不是学校实时字典)时如实告知,前端可提示用户
|
||||||
f.setOptionsFallback(isUsingFallbackOptions());
|
f.setOptionsFallback(isUsingFallbackOptions());
|
||||||
return f;
|
return f;
|
||||||
|
|||||||
@@ -35,15 +35,34 @@ public final class LeaveTools {
|
|||||||
private static final String DT_HINT = "格式 yyyy-MM-dd HH:mm:ss(如 2026-10-12 08:00:00)";
|
private static final String DT_HINT = "格式 yyyy-MM-dd HH:mm:ss(如 2026-10-12 08:00:00)";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请假类型内置兜底(与 HomeLeave 前端 {@code LEAVE_TYPE_OPTIONS}、后端字典种子一致)。
|
* 请假类型内置兜底(与 HomeLeave 前端 {@code LEAVE_TYPE_OPTIONS} 的种子一致)。
|
||||||
* <p>权威来源是字典接口 {@code GET /api/dict/LeaveTypeOptions}(学校可在「应用设置」里自定义),
|
*
|
||||||
* 该接口需登录会话、服务端取不到 —— 表单会带上下拉并交给浏览器用用户登录态取实时字典,
|
* <p>★ 权威来源是字典接口 {@code GET /api/dict/LeaveTypeOptions}(学校可在「应用设置」里
|
||||||
* 这份兜底保证「取不到时下拉里也还有标准项」,不会退化成让用户填「类型编号」的数字框。
|
* 增删改,种子就是下面两项)。该接口需登录会话、服务端取不到 —— 表单会带上下拉并交给
|
||||||
|
* 浏览器用用户登录态取实时字典,**取到就以学校的为准**(`.optionsFrom` 全部覆盖)。
|
||||||
|
* 这份兜底只保证「实时字典没取到时下拉不是空的」,避免退化成让用户填「类型编号」的数字框。
|
||||||
*/
|
*/
|
||||||
private static final String[] LEAVE_TYPE_VALUES = {"1", "2"};
|
private static final String[] LEAVE_TYPE_VALUES = {"1", "2"};
|
||||||
|
|
||||||
private static final String[] LEAVE_TYPE_LABELS = {"病假", "事假及其他"};
|
private static final String[] LEAVE_TYPE_LABELS = {"病假", "事假及其他"};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给模型看的请假类型说明。
|
||||||
|
*
|
||||||
|
* <p>★ 这里**不能**写死「1 病假、2 事假及其他」的编号含义 —— 编号是学校自定义的,
|
||||||
|
* 换一所学校含义就变了。模型也不需要知道编号:表单的「请假类型」下拉会带出实时字典。
|
||||||
|
*/
|
||||||
|
private static final String LEAVE_TYPE_HINT =
|
||||||
|
"请假类型编号(来自学校「应用设置」里配置的字典,不同学校编号含义可能不同);"
|
||||||
|
+ "不确定就省略,表单会列出当前学校可选类型让你选";
|
||||||
|
|
||||||
|
/** 请假时间快捷时段(对齐模块页 LeaveTimeSection 的全天 / 上午 / 下午按钮) */
|
||||||
|
private static final String[] QUICK_PERIODS = {
|
||||||
|
"全天", "08:00:00", "18:00:00",
|
||||||
|
"上午", "08:00:00", "12:00:00",
|
||||||
|
"下午", "12:00:00", "18:00:00",
|
||||||
|
};
|
||||||
|
|
||||||
private LeaveTools() {
|
private LeaveTools() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +103,10 @@ public final class LeaveTools {
|
|||||||
.p(ToolParam.optional("pageIndex", "integer", "页码,从 1 开始").dft("1"),
|
.p(ToolParam.optional("pageIndex", "integer", "页码,从 1 开始").dft("1"),
|
||||||
ToolParam.optional("pageSize", "integer", "每页条数,默认 10").dft("10"),
|
ToolParam.optional("pageSize", "integer", "每页条数,默认 10").dft("10"),
|
||||||
ToolParam.optional("status", "integer", "按状态筛选:0待审核 1已通过 2待撤销 3已撤销 4撤销拒绝 5待销假 6已销假 7销假拒绝 8已拒绝"),
|
ToolParam.optional("status", "integer", "按状态筛选:0待审核 1已通过 2待撤销 3已撤销 4撤销拒绝 5待销假 6已销假 7销假拒绝 8已拒绝"),
|
||||||
ToolParam.optional("leaveType", "integer", "按请假类型筛选:1 病假、2 事假及其他")
|
ToolParam.optional("leaveType", "integer", "按请假类型筛选(编号含义由学校的「应用设置」定义,表单会列出可选项)")
|
||||||
.fallbackOptions(LEAVE_TYPE_VALUES, LEAVE_TYPE_LABELS),
|
.label("请假类型")
|
||||||
|
.fallbackOptions(LEAVE_TYPE_VALUES, LEAVE_TYPE_LABELS)
|
||||||
|
.optionsFrom("leave.types").optionsKeys("k", "v"),
|
||||||
ToolParam.optional("startDate", "string", "开始日期(含),yyyy-MM-dd"),
|
ToolParam.optional("startDate", "string", "开始日期(含),yyyy-MM-dd"),
|
||||||
ToolParam.optional("endDate", "string", "结束日期(含),yyyy-MM-dd"),
|
ToolParam.optional("endDate", "string", "结束日期(含),yyyy-MM-dd"),
|
||||||
ToolParam.optional("keyword", "string", "关键词(学生姓名/事由)"))
|
ToolParam.optional("keyword", "string", "关键词(学生姓名/事由)"))
|
||||||
@@ -103,8 +124,10 @@ public final class LeaveTools {
|
|||||||
.p(ToolParam.optional("startDate", "string", "开始日期 yyyy-MM-dd"),
|
.p(ToolParam.optional("startDate", "string", "开始日期 yyyy-MM-dd"),
|
||||||
ToolParam.optional("endDate", "string", "结束日期 yyyy-MM-dd"),
|
ToolParam.optional("endDate", "string", "结束日期 yyyy-MM-dd"),
|
||||||
ToolParam.optional("classId", "integer", "班级 id"),
|
ToolParam.optional("classId", "integer", "班级 id"),
|
||||||
ToolParam.optional("leaveType", "integer", "请假类型:1 病假、2 事假及其他")
|
ToolParam.optional("leaveType", "integer", "请假类型(编号含义由学校的「应用设置」定义,表单会列出可选项)")
|
||||||
.fallbackOptions(LEAVE_TYPE_VALUES, LEAVE_TYPE_LABELS))
|
.label("请假类型")
|
||||||
|
.fallbackOptions(LEAVE_TYPE_VALUES, LEAVE_TYPE_LABELS)
|
||||||
|
.optionsFrom("leave.types").optionsKeys("k", "v"))
|
||||||
.readAt("/stats")
|
.readAt("/stats")
|
||||||
.modId(6));
|
.modId(6));
|
||||||
|
|
||||||
@@ -115,30 +138,25 @@ public final class LeaveTools {
|
|||||||
+ "尽量确认:请假类型 leaveType、开始时间 startDate、结束时间 endDate、事由 remarks;"
|
+ "尽量确认:请假类型 leaveType、开始时间 startDate、结束时间 endDate、事由 remarks;"
|
||||||
+ "不确定的字段直接省略,系统会弹表单让用户补齐,不要用文字反复追问。",
|
+ "不确定的字段直接省略,系统会弹表单让用户补齐,不要用文字反复追问。",
|
||||||
"POST_JSON", "/api/leave/Save")
|
"POST_JSON", "/api/leave/Save")
|
||||||
.p(ToolParam.required("leaveType", "integer", "请假类型:1 病假、2 事假及其他")
|
// 字段顺序与模块页「请假申请」完全一致:班级 → 请假类型 → 请假时间 → 事由 → 佐证材料
|
||||||
|
.p(ToolParam.optional("classId", "integer", "班级 id;只有一个班级时可不传(后端会取默认班级)")
|
||||||
|
.label("班级")
|
||||||
|
.optionsFrom("leave.myClasses").optionsKeys("classId", "className")
|
||||||
|
// 模块页:只有多个班级时才显示「班级」,单班自动带上
|
||||||
|
.hideIfSingleOption(),
|
||||||
|
ToolParam.required("leaveType", "integer", LEAVE_TYPE_HINT)
|
||||||
.label("请假类型")
|
.label("请假类型")
|
||||||
.fallbackOptions(LEAVE_TYPE_VALUES, LEAVE_TYPE_LABELS)
|
.fallbackOptions(LEAVE_TYPE_VALUES, LEAVE_TYPE_LABELS)
|
||||||
.optionsFrom("leave.types").optionsKeys("k", "v"),
|
.optionsFrom("leave.types").optionsKeys("k", "v")
|
||||||
ToolParam.required("startDate", "string", "请假开始时间," + DT_HINT)
|
// 模块页是单选按钮组,且一进页面就已选中第一个类型
|
||||||
.label("开始时间").eg("2026-10-12 08:00:00"),
|
.radio().autoPickFirst())
|
||||||
ToolParam.required("endDate", "string", "请假结束时间," + DT_HINT)
|
.p(leaveTimeParams())
|
||||||
.label("结束时间").eg("2026-10-12 17:00:00"),
|
.p(ToolParam.required("remarks", "string", "请假事由")
|
||||||
ToolParam.required("remarks", "string", "请假事由")
|
.label("请假事由").as("textarea").rows(4).maxLength(1000)
|
||||||
.label("请假事由").as("textarea").eg("发烧就医,需在家休息"),
|
// placeholder 照搬模块页原文案(eg 只给模型当填写参考)
|
||||||
ToolParam.optional("classId", "integer", "班级 id;只有一个班级时可不传(后端会取默认班级)")
|
.placeholder("请输入请假原因.....")
|
||||||
.label("班级")
|
.eg("发烧就医,需在家休息"),
|
||||||
.optionsFrom("leave.myClasses").optionsKeys("classId", "className"),
|
evidenceParam())
|
||||||
ToolParam.optional("continuousLeave", "integer", "是否连续请假(同一时段连续多天)")
|
|
||||||
.label("连续请假").dft("0")
|
|
||||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"})
|
|
||||||
// 选「是」时按下面的连续天数把起止时间复制成逐天时间段,
|
|
||||||
// 生成模块要的 continuousLeaveTime(与模块页 LeaveTimeSection 一致)
|
|
||||||
.expandDaily("continuousLeaveNum", "startDate", "endDate"),
|
|
||||||
ToolParam.optional("continuousLeaveNum", "integer", "连续天数(continuousLeave=1 时有效,1~30)")
|
|
||||||
.label("连续天数").dft("1"),
|
|
||||||
// ★ 模块传的是 number[](JSON 数组),不是逗号串,故不声明 join
|
|
||||||
ToolParam.optional("fileIds", "array", "佐证材料附件 id 列表(由表单的附件上传得到,可多份)")
|
|
||||||
.label("佐证材料").as("file").multiple(true))
|
|
||||||
.fix("type", 2)
|
.fix("type", 2)
|
||||||
.fullForm()
|
.fullForm()
|
||||||
.write("/my")
|
.write("/my")
|
||||||
@@ -151,6 +169,7 @@ public final class LeaveTools {
|
|||||||
+ "学生自己请假请改用「提交请假申请」,不要用本工具。"
|
+ "学生自己请假请改用「提交请假申请」,不要用本工具。"
|
||||||
+ "不确定的字段直接省略,系统会弹表单让用户补齐,不要用文字反复追问。",
|
+ "不确定的字段直接省略,系统会弹表单让用户补齐,不要用文字反复追问。",
|
||||||
"POST_JSON", "/api/leave/Save")
|
"POST_JSON", "/api/leave/Save")
|
||||||
|
// 字段顺序与模块页「请假登记」一致:班级 → 学生 → 请假类型 → 请假时间 → 事由 → 佐证材料
|
||||||
.p(ToolParam.required("classId", "integer", "学生所在班级 id(决定下面「学生」下拉的可选项)")
|
.p(ToolParam.required("classId", "integer", "学生所在班级 id(决定下面「学生」下拉的可选项)")
|
||||||
.label("班级")
|
.label("班级")
|
||||||
.optionsFrom("leave.myClasses").optionsKeys("classId", "className"),
|
.optionsFrom("leave.myClasses").optionsKeys("classId", "className"),
|
||||||
@@ -158,25 +177,17 @@ public final class LeaveTools {
|
|||||||
.label("学生")
|
.label("学生")
|
||||||
.optionsFrom("leave.classStudents").optionsKeys("stuId", "stuName")
|
.optionsFrom("leave.classStudents").optionsKeys("stuId", "stuName")
|
||||||
.dependsOn("classId"),
|
.dependsOn("classId"),
|
||||||
ToolParam.required("leaveType", "integer", "请假类型:1 病假、2 事假及其他")
|
ToolParam.required("leaveType", "integer", LEAVE_TYPE_HINT)
|
||||||
.label("请假类型")
|
.label("请假类型")
|
||||||
.fallbackOptions(LEAVE_TYPE_VALUES, LEAVE_TYPE_LABELS)
|
.fallbackOptions(LEAVE_TYPE_VALUES, LEAVE_TYPE_LABELS)
|
||||||
.optionsFrom("leave.types").optionsKeys("k", "v"),
|
.optionsFrom("leave.types").optionsKeys("k", "v")
|
||||||
ToolParam.required("startDate", "string", "请假开始时间," + DT_HINT)
|
.radio().autoPickFirst())
|
||||||
.label("开始时间").eg("2026-10-12 08:00:00"),
|
.p(leaveTimeParams())
|
||||||
ToolParam.required("endDate", "string", "请假结束时间," + DT_HINT)
|
.p(ToolParam.required("remarks", "string", "请假事由")
|
||||||
.label("结束时间").eg("2026-10-12 17:00:00"),
|
.label("请假事由").as("textarea").rows(4).maxLength(1000)
|
||||||
ToolParam.required("remarks", "string", "请假事由")
|
.placeholder("请输入请假原因.....")
|
||||||
.label("请假事由").as("textarea").eg("家中有事,需请假一天"),
|
.eg("家中有事,需请假一天"),
|
||||||
ToolParam.optional("continuousLeave", "integer", "是否连续请假(同一时段连续多天)")
|
evidenceParam())
|
||||||
.label("连续请假").dft("0")
|
|
||||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"})
|
|
||||||
.expandDaily("continuousLeaveNum", "startDate", "endDate"),
|
|
||||||
ToolParam.optional("continuousLeaveNum", "integer", "连续天数(continuousLeave=1 时有效,1~30)")
|
|
||||||
.label("连续天数").dft("1"),
|
|
||||||
// ★ 模块传的是 number[](JSON 数组),不是逗号串,故不声明 join
|
|
||||||
ToolParam.optional("fileIds", "array", "佐证材料附件 id 列表(由表单的附件上传得到,可多份)")
|
|
||||||
.label("佐证材料").as("file").multiple(true))
|
|
||||||
.fix("type", 1)
|
.fix("type", 1)
|
||||||
.fullForm()
|
.fullForm()
|
||||||
.write("/register")
|
.write("/register")
|
||||||
@@ -229,6 +240,39 @@ public final class LeaveTools {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「请假时间」区(两个写工具共用,保证与模块页 {@code LeaveTimeSection} 口径一致):
|
||||||
|
* 起止时间 + <b>快捷时段按钮</b>(全天/上午/下午)+ <b>连续请假勾选框</b> + 连续天数。
|
||||||
|
*/
|
||||||
|
private static ToolParam[] leaveTimeParams() {
|
||||||
|
return new ToolParam[]{
|
||||||
|
ToolParam.required("startDate", "string", "请假开始时间," + DT_HINT)
|
||||||
|
.label("开始时间").eg("2026-10-12 08:00:00")
|
||||||
|
.as("datetime")
|
||||||
|
// 与模块页一致:点「全天 / 上午 / 下午」直接填好时段的时分秒(日期沿用已选的)
|
||||||
|
.quickPeriods("startDate", "endDate", QUICK_PERIODS),
|
||||||
|
ToolParam.required("endDate", "string", "请假结束时间," + DT_HINT)
|
||||||
|
.label("结束时间").eg("2026-10-12 17:00:00")
|
||||||
|
.as("datetime"),
|
||||||
|
ToolParam.optional("continuousLeave", "integer", "是否连续请假(同一时段连续多天)")
|
||||||
|
.label("连续请假").dft("0")
|
||||||
|
// 模块页是一个勾选框(勾上才展开天数与逐天时段),不是「是/否」下拉
|
||||||
|
.switchOf("1", "0")
|
||||||
|
// 勾选时按天数把起止时间复制成逐天时间段,生成 continuousLeaveTime
|
||||||
|
.expandDaily("continuousLeaveNum", "startDate", "endDate"),
|
||||||
|
ToolParam.optional("continuousLeaveNum", "integer", "连续天数(1~30,仅勾选「连续请假」时有效)")
|
||||||
|
.label("连续天数").dft("1"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 佐证材料(附件):模块传的是 {@code number[]}(JSON 数组),不是逗号串,故不声明 join */
|
||||||
|
private static ToolParam evidenceParam() {
|
||||||
|
return ToolParam.optional("fileIds", "array", "佐证材料附件 id 列表(由表单的附件上传得到,可多份)")
|
||||||
|
.label("佐证材料").as("file").multiple(true)
|
||||||
|
// 与模块页附件区下方的说明一致
|
||||||
|
.placeholder("可选,大小不超过 200MB");
|
||||||
|
}
|
||||||
|
|
||||||
/** 供文档/目录使用 */
|
/** 供文档/目录使用 */
|
||||||
public static List<String> moduleAlias() {
|
public static List<String> moduleAlias() {
|
||||||
return Arrays.asList("请假", "学生请假", "homeleave");
|
return Arrays.asList("请假", "学生请假", "homeleave");
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ class FormFieldWidgetTest {
|
|||||||
assertFalse(f.isOptionsFallback());
|
assertFalse(f.isOptionsFallback());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ⑥ 真实工具定义回归:请假类型必须是带中文选项的下拉框,不能退回数字框 */
|
/** ⑥ 真实工具定义回归:请假类型必须是带中文选项的**单选按钮组**(对齐模块页 RadioGroup) */
|
||||||
@Test
|
@Test
|
||||||
void leaveSubmitTypeIsASelectInRealToolDefinition() {
|
void leaveSubmitTypeIsARadioInRealToolDefinition() {
|
||||||
ToolDef def = LeaveTools.defs().stream()
|
ToolDef def = LeaveTools.defs().stream()
|
||||||
.filter(d -> "leave.submit".equals(d.getName()))
|
.filter(d -> "leave.submit".equals(d.getName()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
@@ -111,9 +111,76 @@ class FormFieldWidgetTest {
|
|||||||
|
|
||||||
FormField f = leaveType.toFormField();
|
FormField f = leaveType.toFormField();
|
||||||
|
|
||||||
assertEquals("select", f.getWidget());
|
// ★ 2026-10-13:从下拉改为单选按钮组 —— 模块页 LeaveApply.vue 用的就是 RadioGroup,
|
||||||
|
// 且进页面即选中第一项,助手表单要一致。
|
||||||
|
assertEquals("radio", f.getWidget());
|
||||||
|
assertTrue(f.isAutoPickFirst(), "选项就绪后应自动选中第一项(模块页的默认行为)");
|
||||||
|
assertEquals("leave.types", f.getOptionsFrom(), "选项仍走学校自定义的实时字典");
|
||||||
assertEquals(2, f.getOptions().size());
|
assertEquals(2, f.getOptions().size());
|
||||||
assertEquals("病假", f.getOptions().get(0).get("label"));
|
assertEquals("病假", f.getOptions().get(0).get("label"));
|
||||||
assertEquals("事假及其他", f.getOptions().get(1).get("label"));
|
assertEquals("事假及其他", f.getOptions().get(1).get("label"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ⑦ 单选控件的值与 label 必须同类型(前端 Radio 用严格比较判定选中态) */
|
||||||
|
@Test
|
||||||
|
void radioOptionValuesStayStringsForStrictComparison() {
|
||||||
|
ToolParam p = ToolParam.required("leaveType", "integer", "请假类型")
|
||||||
|
.radio().autoPickFirst()
|
||||||
|
.fallbackOptions(new String[]{"1", "2"}, new String[]{"病假", "事假及其他"});
|
||||||
|
|
||||||
|
FormField f = p.toFormField();
|
||||||
|
|
||||||
|
assertEquals("radio", f.getWidget());
|
||||||
|
assertEquals("1", f.getOptions().get(0).get("value"), "选项值必须是字符串,否则前端单选按钮选不上");
|
||||||
|
assertTrue(f.isAutoPickFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ⑧ 勾选框:连续请假用 switch(模块页是 Checkbox,不是「是/否」下拉) */
|
||||||
|
@Test
|
||||||
|
void switchWidgetCarriesOnOffValues() {
|
||||||
|
ToolParam p = ToolParam.optional("continuousLeave", "integer", "是否连续请假")
|
||||||
|
.switchOf("1", "0");
|
||||||
|
|
||||||
|
FormField f = p.toFormField();
|
||||||
|
|
||||||
|
assertEquals("switch", f.getWidget());
|
||||||
|
assertEquals("1", f.getSwitchOn());
|
||||||
|
assertEquals("0", f.getSwitchOff());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ⑨ 快捷时段:三元组按「标签, 开始, 结束」解析,个数不是 3 的倍数要当场报错 */
|
||||||
|
@Test
|
||||||
|
void quickPeriodsParsesTriplesAndRejectsBadArity() {
|
||||||
|
ToolParam p = ToolParam.required("startDate", "string", "开始时间")
|
||||||
|
.quickPeriods("startDate", "endDate",
|
||||||
|
"全天", "08:00:00", "18:00:00",
|
||||||
|
"上午", "08:00:00", "12:00:00",
|
||||||
|
"下午", "12:00:00", "18:00:00");
|
||||||
|
|
||||||
|
FormField f = p.toFormField();
|
||||||
|
|
||||||
|
assertEquals("startDate", f.getQuickPeriods().get("start"));
|
||||||
|
assertEquals("endDate", f.getQuickPeriods().get("end"));
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
java.util.List<java.util.Map<String, String>> items =
|
||||||
|
(java.util.List<java.util.Map<String, String>>) f.getQuickPeriods().get("items");
|
||||||
|
assertEquals(3, items.size());
|
||||||
|
assertEquals("全天", items.get(0).get("label"));
|
||||||
|
assertEquals("08:00:00", items.get(0).get("start"));
|
||||||
|
assertEquals("18:00:00", items.get(0).get("end"));
|
||||||
|
|
||||||
|
org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> ToolParam.required("s", "string", "").quickPeriods("s", "e", "仅一个标签"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ⑩ 多行文本框的行数与字数上限也要下发(对齐模块页「请假事由」的 4 行 / 1000 字) */
|
||||||
|
@Test
|
||||||
|
void textareaRowsAndMaxLengthArePassedThrough() {
|
||||||
|
FormField f = ToolParam.required("remarks", "string", "请假事由")
|
||||||
|
.as("textarea").rows(4).maxLength(1000).toFormField();
|
||||||
|
|
||||||
|
assertEquals("textarea", f.getWidget());
|
||||||
|
assertEquals(4, f.getRows().intValue());
|
||||||
|
assertEquals(1000, f.getMaxLength().intValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,112 @@ class LeaveIntentFormTest {
|
|||||||
"模块没配地址时不该下发一个必然打不通的代取地址");
|
"模块没配地址时不该下发一个必然打不通的代取地址");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== 表单呈现:与模块页 LeaveApply.vue 逐项对齐 ==================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段顺序与模块页「请假申请」一致:班级 → 请假类型 → 请假时间 → 事由 → 佐证材料。
|
||||||
|
* <p>顺序不是小事:用户是照着模块页的肌肉记忆填的,顺序错了他会找不到字段。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void studentFormFieldOrderMatchesModulePage() {
|
||||||
|
ToolDef submit = registry.get("leave.submit");
|
||||||
|
java.util.List<String> order = submit.getParams().stream()
|
||||||
|
.map(ToolParam::getName)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
|
||||||
|
assertEquals(Arrays.asList("classId", "leaveType", "startDate", "endDate",
|
||||||
|
"continuousLeave", "continuousLeaveNum", "remarks", "fileIds"), order,
|
||||||
|
"字段顺序必须与 HomeLeave 的 LeaveApply.vue 一致");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「班级」只在多个班时才显示 —— 模块页是 {@code v-if="myClasses.length > 1"}`,
|
||||||
|
* 单班时值自动带上、控件根本不渲染。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void studentClassPickerHidesItselfWhenOnlyOneClass() {
|
||||||
|
ToolParam classId = param(registry.get("leave.submit"), "classId");
|
||||||
|
assertTrue(classId.isHideWhenSingleOption(),
|
||||||
|
"只教一个班时不该再让用户选一次(与模块页一致)");
|
||||||
|
assertEquals("classId", classId.getOptionValueKey());
|
||||||
|
assertEquals("className", classId.getOptionLabelKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请假时间要带「全天 / 上午 / 下午」快捷时段 —— 模块页 {@code LeaveTimeSection} 的
|
||||||
|
* {@code quickSet()},点一下按已选日期补齐时分秒。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void leaveTimeHasQuickPeriodButtons() {
|
||||||
|
ToolParam startDate = param(registry.get("leave.submit"), "startDate");
|
||||||
|
assertNotNull(startDate.getQuickPeriods(), "开始时间应带快捷时段按钮组");
|
||||||
|
assertEquals("startDate", startDate.getQuickPeriods().get("start"));
|
||||||
|
assertEquals("endDate", startDate.getQuickPeriods().get("end"));
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
java.util.List<java.util.Map<String, String>> items =
|
||||||
|
(java.util.List<java.util.Map<String, String>>) startDate.getQuickPeriods().get("items");
|
||||||
|
assertEquals(Arrays.asList("全天", "上午", "下午"),
|
||||||
|
items.stream().map(i -> i.get("label")).collect(java.util.stream.Collectors.toList()),
|
||||||
|
"三个快捷时段要与模块页同名同序");
|
||||||
|
assertEquals("08:00:00", items.get(0).get("start"));
|
||||||
|
assertEquals("18:00:00", items.get(0).get("end"));
|
||||||
|
assertEquals("12:00:00", items.get(1).get("end"), "上午到 12:00 截止");
|
||||||
|
assertEquals("12:00:00", items.get(2).get("start"), "下午从 12:00 开始");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 「连续请假」是勾选框(勾上才展开连续天数与逐天时段),不是「是/否」下拉 */
|
||||||
|
@Test
|
||||||
|
void continuousLeaveIsACheckboxNotASelect() {
|
||||||
|
ToolParam cl = param(registry.get("leave.submit"), "continuousLeave");
|
||||||
|
assertEquals("switch", cl.resolveWidget());
|
||||||
|
assertEquals("1", cl.getSwitchOn());
|
||||||
|
assertEquals("0", cl.getSwitchOff());
|
||||||
|
assertNotNull(cl.getDailyExpandSpec(), "勾选后要能按天数展开逐天时段");
|
||||||
|
assertEquals("continuousLeaveNum", cl.getDailyExpandSpec().get("count"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 事由的呈现细节也要对齐:模块页是 4 行、最多 1000 字、带字数统计 */
|
||||||
|
@Test
|
||||||
|
void remarksMatchesModulePageTextarea() {
|
||||||
|
ToolParam remarks = param(registry.get("leave.submit"), "remarks");
|
||||||
|
assertEquals("textarea", remarks.resolveWidget());
|
||||||
|
assertEquals(Integer.valueOf(4), remarks.getRows());
|
||||||
|
assertEquals(Integer.valueOf(1000), remarks.getMaxLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ★ 请假类型的编号含义<b>不能写死</b>:字典是学校在「应用设置」里自定义的,
|
||||||
|
* 换一所学校编号含义就变了。给模型的说明里只讲「来源」,不讲具体编号。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void leaveTypeDescriptionDoesNotHardcodeSchoolSpecificNumbers() {
|
||||||
|
for (String tool : Arrays.asList("leave.submit", "leave.register")) {
|
||||||
|
ToolParam leaveType = param(registry.get(tool), "leaveType");
|
||||||
|
String desc = leaveType.getDescription();
|
||||||
|
assertFalse(desc.contains("1 病假"),
|
||||||
|
tool + " 的请假类型说明不该写死「1 病假」——编号是学校自定义的");
|
||||||
|
assertFalse(desc.contains("2 事假"),
|
||||||
|
tool + " 的请假类型说明不该写死「2 事假」——编号是学校自定义的");
|
||||||
|
assertEquals("leave.types", leaveType.getOptionsFrom(),
|
||||||
|
"选项必须走学校自定义的实时字典接口");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 两个写工具的「请假时间 + 事由 + 附件」区完全一致(同一份 helper 生成) */
|
||||||
|
@Test
|
||||||
|
void bothWriteFlowsShareTheSameTimeAndEvidenceParams() {
|
||||||
|
for (String tool : Arrays.asList("leave.submit", "leave.register")) {
|
||||||
|
ToolDef def = registry.get(tool);
|
||||||
|
assertEquals("datetime", param(def, "startDate").resolveWidget());
|
||||||
|
assertEquals("datetime", param(def, "endDate").resolveWidget());
|
||||||
|
assertNotNull(param(def, "startDate").getQuickPeriods(), tool + " 缺快捷时段");
|
||||||
|
assertEquals("switch", param(def, "continuousLeave").resolveWidget());
|
||||||
|
assertEquals("radio", param(def, "leaveType").resolveWidget());
|
||||||
|
assertTrue(param(def, "leaveType").isAutoPickFirst(), tool + " 的请假类型应默认选中第一项");
|
||||||
|
assertEquals("file", param(def, "fileIds").resolveWidget());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== 辅助 ==================== */
|
/* ==================== 辅助 ==================== */
|
||||||
|
|
||||||
private java.util.List<String> requiredNames(ToolDef def) {
|
private java.util.List<String> requiredNames(ToolDef def) {
|
||||||
|
|||||||
Reference in New Issue
Block a user