彻底对齐各模块「新建 / 发布」页里的两类控件(此前只有通知公告搬到位):
正文 → 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:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user