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
@@ -190,9 +190,14 @@ class IntentDirectFormTest {
assertEquals("/list/new", resp.getForm().getLandingPath());
assertTrue(resp.getReply().contains("发布"), "引导语应说明这是发布,实际:" + resp.getReply());
// 公告表单:标题(单行)+ 正文(多行)+ 发文部门(下拉)
// 公告表单:标题(单行)+ 正文(富文本,与模块页 NewNotice.vue 的 wangEditor 一致)+ 发文部门(下拉)
assertEquals("text", field(resp, "title").getWidget());
assertEquals("textarea", field(resp, "content").getWidget());
FormField content = field(resp, "content");
assertEquals("richtext", content.getWidget(), "正文用富文本编辑器(模块页 wangEditor)");
assertEquals("/campusnotice/api/file/UploadImage", content.getRichTextUploadPath(),
"正文插图要走模块真实前缀 /campusnotice(且不是附件的 FileUpload)");
assertEquals("/campusnotice/api/file/Preview?id={id}", content.getRichTextPreviewPath(),
"图片预览地址模板同样要带前缀,{id} 留给前端替换");
FormField deptId = field(resp, "deptId");
assertEquals("select", deptId.getWidget(), "发文部门应是下拉,不该让用户手填部门编号");
@@ -300,9 +305,13 @@ class IntentDirectFormTest {
assertTrue(resp.getForm().isWrite(), "是写操作,前端要二次确认");
assertEquals("/list/new", resp.getForm().getLandingPath());
// 标题 + 正文 + 通知类型(下拉)+ 发文部门(下拉)
// 标题 + 正文(富文本,模块页 NewNotice.vue 用 wangEditor)+ 通知类型(下拉)+ 发文部门(下拉)
assertEquals("text", field(resp, "title").getWidget());
assertEquals("textarea", field(resp, "content").getWidget());
FormField content = field(resp, "content");
assertEquals("richtext", content.getWidget(), "正文用富文本编辑器(模块页 wangEditor)");
assertEquals("/dormnotice/api/file/UploadImage", content.getRichTextUploadPath(),
"正文插图走模块真实前缀 /dormnotice");
assertEquals("/dormnotice/api/file/Preview?id={id}", content.getRichTextPreviewPath());
FormField noticeType = field(resp, "noticeType");
assertEquals("select", noticeType.getWidget(), "通知类型应是下拉,不该让用户手填编号");
@@ -336,9 +345,14 @@ class IntentDirectFormTest {
assertTrue(resp.getForm().isWrite());
assertEquals("/list/new", resp.getForm().getLandingPath());
// 活动表单:标题 + 说明(多行)+ 分类(下拉)+ 主办部门(下拉)+ 地点 + 起止时间
// 活动表单:标题 + 说明(**富文本**,模块页 PublishActivity.vue 用 wangEditor)+
// 分类(下拉)+ 主办部门(下拉)+ 地点 + 起止时间
assertEquals("text", field(resp, "title").getWidget());
assertEquals("textarea", field(resp, "content").getWidget());
FormField content = field(resp, "content");
assertEquals("richtext", content.getWidget(), "活动说明用富文本编辑器(模块页 wangEditor)");
assertEquals("/familyactivity/api/file/UploadImage", content.getRichTextUploadPath(),
"正文插图走模块真实前缀 /familyactivity(不是附件的 FileUpload)");
assertEquals("/familyactivity/api/file/Preview?id={id}", content.getRichTextPreviewPath());
FormField category = field(resp, "category");
assertEquals("select", category.getWidget());