feat(表单): 五个模块正文换成模块页同款富文本,公告/宿舍的接收对象换成自绘控件
build / build-push (push) Successful in 1m15s

彻底对齐各模块「新建 / 发布」页里的两类控件(此前只有通知公告搬到位):

正文 → wangEditor(widget=richtext)
  · 校园公告 content、宿舍通知 content(发布 + 编辑)、家校活动 content(发布 + 编辑)、
    作业 description
  · 教师专业发展 remark:只声明形态、**不声明** .richText(...) —— 该模块 file 域只有
    Upload/Preview/Down(没有 UploadImage),模块页 RecordEdit.vue 也没配 uploadImage,
    所以下发空地址,前端据此把工具栏的「图片」组整组摘掉(模块页就是这个形态)

接收对象 → 自绘控件(widget=scopepicker / roompicker)
  · 校园公告 pushScopes:换成 ClassScopeInput 同款(全部班级 / 指定班级 + 弹窗班级树);
    提交口径不变,仍是 [{scopeType,scopeId,scopeName}]
  · 宿舍通知 receiveList:换成模块页「选择接收宿舍」弹窗同款(左楼栋 / 右按楼层分组的房间);
    v-model 是字符串(房间 id 逗号串或 ALL 哨兵),与模块 receiveList 的真实契约一致

基建
  · ToolParam 新增 richText(upload,preview) / scopePicker(type) / roomPicker() /
    pickerSource(页签名, 模块内相对路径);对应 FormField 四个字段
  · AgentOrchestrator:新增 resolveModulePath 通用形态,richtext 与 pickerSources 的地址
    一律只写模块内相对路径、前缀在这里拼;richtext **没有兜底**(不声明就是空)
  · 带 optionCall 的字段会被改写成 select 的白名单改用 isSelfDrawnPicker()
    (原先是硬编码 userpicker),并放开为 public 供契约测试复用生产口径

测试(139 tests, 0 failures)
  · ModuleFormParityTest 新增两条通用不变量:正文图片地址与弹窗数据源都必须带模块前缀、
    不许自己带前缀、不许双斜杠;另加「专业发展不得拿到兜底上传地址」
  · FormFieldWidgetTest 新增 richtext(含「不声明 = 空」反向断言)与自绘控件透传两条
  · IntentDirectFormTest / DormActivityPublishIntentTest 同步改成新控件口径
This commit is contained in:
lijing
2026-10-14 16:56:07 +08:00
parent c084c978aa
commit 006003c842
11 changed files with 411 additions and 39 deletions
@@ -183,4 +183,82 @@ class FormFieldWidgetTest {
assertEquals(4, f.getRows().intValue());
assertEquals(1000, f.getMaxLength().intValue());
}
/**
* ⑪ 富文本正文:控件形态 + **插图地址必须原样下发**,且「不声明 = 空」。
*
* <p>两个反向断言都是踩过的坑:
* <ul>
* <li>地址**没有兜底** —— 像专业发展那种模块编辑器压根没配 uploadImage,
* 工具里不写 {@code .richText(...)} 就不能凭空给一个默认值,
* 否则前端会长出模块页没有的「图片」按钮;</li>
* <li>{@code .as("richtext")} 与 {@code .richText(...)} 是两件事:前者管形态,
* 后者管地址。只写前者(专业发展的写法)地址必须是 null。</li>
* </ul>
*/
@Test
void richTextWidgetCarriesUploadAndPreviewPathsOnlyWhenDeclared() {
FormField withImages = ToolParam.required("content", "string", "公告正文")
.as("richtext")
.richText("/api/file/UploadImage", "/api/file/Preview?id={id}")
.toFormField();
assertEquals("richtext", withImages.getWidget());
assertEquals("/api/file/UploadImage", withImages.getRichTextUploadPath());
assertEquals("/api/file/Preview?id={id}", withImages.getRichTextPreviewPath());
// 只声明形态、不声明地址 —— 地址必须为空(前缀由编排层拼,不在这里兜底)
FormField noImages = ToolParam.optional("remark", "string", "正文内容")
.as("richtext")
.toFormField();
assertEquals("richtext", noImages.getWidget());
assertNull(noImages.getRichTextUploadPath(), "不声明 .richText(...) 时上传地址必须是空,不能兜底");
assertNull(noImages.getRichTextPreviewPath());
}
/**
* ⑫ 自绘「接收对象」控件:三种形态各自透传,且 pickerSources 的页签名与地址一一对应。
*
* <p>⚠️ 这里只断言**工具层声明**({@code ToolParam.pickerSources} 的原始
* {@code {页签名, 模块内相对路径}}),不断言 {@code FormField.getPickerSources()} ——
* 后者的 {@code call} 是在**编排层**把相对路径拼上前缀后才建出来的,
* 纯单元测试里 {@code toFormField()} 根本不会填它(误断言会拿到 null)。
* 「拼好前缀之后」的那一层由 {@code ModuleFormParityTest} 的通用不变量兜住。
*/
@Test
void selfDrawnPickersPassThroughWidgetAndSources() {
ToolParam scope = ToolParam.optional("pushScopes", "array", "接收班级")
.label("接收班级").scopePicker(4)
.pickerSource("班级", "/api/base/getStuTree?toUser=false")
.allOption("ALL", "全部班级");
FormField scopeField = scope.toFormField();
assertEquals("scopepicker", scopeField.getWidget());
assertEquals(Integer.valueOf(4), scopeField.getScopeType());
assertEquals("ALL", scopeField.getAllToken());
assertEquals("全部班级", scopeField.getAllTokenLabel());
assertEquals(1, scope.getPickerSources().size(), "scopepicker 是单页签(班级)");
assertEquals("班级", scope.getPickerSources().get(0)[0]);
assertEquals("/api/base/getStuTree?toUser=false", scope.getPickerSources().get(0)[1],
"只写模块内相对路径,前缀留给编排层");
ToolParam room = ToolParam.optional("receiveList", "array", "接收宿舍")
.label("接收宿舍").roomPicker()
.pickerSource("楼栋", "/api/dorm/BuildingList")
.pickerSource("房间", "/api/dorm/RoomList")
.allOption("ALL", "全部宿舍");
FormField roomField = room.toFormField();
assertEquals("roompicker", roomField.getWidget());
assertEquals("ALL", roomField.getAllToken());
assertEquals(2, room.getPickerSources().size(), "房间弹窗是「楼栋 + 房间」两个数据源");
assertEquals("楼栋", room.getPickerSources().get(0)[0]);
assertEquals("/api/dorm/BuildingList", room.getPickerSources().get(0)[1]);
assertEquals("房间", room.getPickerSources().get(1)[0]);
assertEquals("/api/dorm/RoomList", room.getPickerSources().get(1)[1]);
// 没声明数据源的普通字段不能凭空长出 pickerSources
assertNull(ToolParam.required("title", "string", "标题").getPickerSources());
}
}