彻底对齐各模块「新建 / 发布」页里的两类控件(此前只有通知公告搬到位):
正文 → 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:
@@ -369,6 +369,39 @@ public class ToolParam {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 声明为「班级范围」自绘控件(widget=scopepicker)。
|
||||
*
|
||||
* <p>与 {@link #scopes(int)} <b>提交口径完全相同</b>({@code [{scopeType,scopeId,scopeName}]}),
|
||||
* 区别只在**呈现**:这个控件是校园公告模块页 {@code ClassScopeInput} 的同款
|
||||
* —— radio「全部班级 / 指定班级」+ 只读回显 + 弹窗班级树(左树右已选)。
|
||||
* {@code scopes} 是普通多选下拉,模块页早就不长那样了。
|
||||
*
|
||||
* <p>弹窗的树由 {@link #pickerSource} 下发(校园公告用的是
|
||||
* {@code /api/base/getStuTree?toUser=false},前端自底向上归成「年级→班级」两层)。
|
||||
*
|
||||
* @param type 具体班级节点的 scopeType:1 全校 / 2 学部 / 3 年级 / 4 班级
|
||||
*/
|
||||
public ToolParam scopePicker(int type) {
|
||||
this.widget = "scopepicker";
|
||||
this.scopeType = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 声明为「接收宿舍」自绘控件(widget=roompicker)。
|
||||
*
|
||||
* <p>宿舍通知模块页那个自写的「选择接收宿舍」弹窗(左楼栋列表 / 右按楼层分组的房间勾选 /
|
||||
* 底部已选间数与住宿生人数),提交房间 id 逗号串,或 {@link #allOption} 的全选哨兵。
|
||||
*
|
||||
* <p>弹窗要**两个**数据源,按顺序用 {@link #pickerSource} 下发:
|
||||
* ① 楼栋列表;② 房间列表(地址里可带 {@code {buildingId}} 占位,前端切楼栋时替换后再请求)。
|
||||
*/
|
||||
public ToolParam roomPicker() {
|
||||
this.widget = "roompicker";
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 声明为「审批流」控件(校园公告的 {@code flow})。
|
||||
*
|
||||
|
||||
@@ -22,6 +22,12 @@ public final class ActivityTools {
|
||||
/** 活动分类字典的 type 值(/api/dict/List?type=2) */
|
||||
private static final Object DICT_TYPE_CATEGORY = 2;
|
||||
|
||||
/** 活动说明富文本里「插入图片」的上传地址(模块页 NewActivity.vue 的 editorConfig.MENU_CONF.uploadImage)。 */
|
||||
private static final String RICHTEXT_UPLOAD_PATH = "/api/file/UploadImage";
|
||||
|
||||
/** 正文内图片的预览地址模板 —— 必须是 {@code Preview}(inline 输出图片)。 */
|
||||
private static final String RICHTEXT_PREVIEW_PATH = "/api/file/Preview?id={id}";
|
||||
|
||||
private ActivityTools() {
|
||||
}
|
||||
|
||||
@@ -110,8 +116,10 @@ public final class ActivityTools {
|
||||
"POST_JSON", "/api/activity/Create")
|
||||
.p(ToolParam.required("title", "string", "活动标题(50 字以内)")
|
||||
.label("活动标题").eg("秋季亲子运动会"),
|
||||
ToolParam.required("content", "string", "活动说明(纯文本,对应模块页的富文本编辑器)")
|
||||
.label("活动说明").as("textarea").eg("为增进家校互动,学校拟举办秋季亲子运动会……"),
|
||||
ToolParam.required("content", "string", "活动说明(富文本 HTML,对应模块页的 wangEditor)")
|
||||
.label("活动说明").as("richtext")
|
||||
.richText(RICHTEXT_UPLOAD_PATH, RICHTEXT_PREVIEW_PATH)
|
||||
.eg("为增进家校互动,学校拟举办秋季亲子运动会……"),
|
||||
ToolParam.required("category", "string", "活动分类:**分类名称**(模块页存的是名称,不是 id)")
|
||||
.label("活动分类")
|
||||
.optionsFrom("activity.categoryDic").optionsKeys("v", "v"),
|
||||
@@ -168,8 +176,9 @@ public final class ActivityTools {
|
||||
.label("活动"),
|
||||
ToolParam.required("title", "string", "活动标题")
|
||||
.label("活动标题"),
|
||||
ToolParam.required("content", "string", "活动说明")
|
||||
.label("活动说明").as("textarea"))
|
||||
ToolParam.required("content", "string", "活动说明(富文本 HTML)")
|
||||
.label("活动说明").as("richtext")
|
||||
.richText(RICHTEXT_UPLOAD_PATH, RICHTEXT_PREVIEW_PATH))
|
||||
.fix("receiveType", "class")
|
||||
.fix("receiveList", "ALL")
|
||||
.write("/list"));
|
||||
|
||||
@@ -13,6 +13,22 @@ public final class CampusNoticeTools {
|
||||
|
||||
public static final String MODULE = "campusnotice";
|
||||
|
||||
/** 公告正文富文本里「插入图片」的上传地址(模块页 NewNotice.vue 的 editorConfig.MENU_CONF.uploadImage)。 */
|
||||
private static final String RICHTEXT_UPLOAD_PATH = "/api/file/UploadImage";
|
||||
|
||||
/** 正文内图片的预览地址模板 —— 必须是 {@code Preview}(inline 输出图片);
|
||||
* {@code Down} 是 attachment,放进 {@code <img src>} 显示不出来。 */
|
||||
private static final String RICHTEXT_PREVIEW_PATH = "/api/file/Preview?id={id}";
|
||||
|
||||
/**
|
||||
* 「接收班级」弹窗的数据源。
|
||||
*
|
||||
* <p>逐项对齐模块页 {@code ClassScopeInput.vue}:它调的正是
|
||||
* {@code baseApi.getStuTree(false)}({@code GET /api/base/getStuTree?toUser=false}),
|
||||
* 前端自底向上归成「年级→班级」两层树。后端**没有** GetClassTree,别写那个地址。
|
||||
*/
|
||||
private static final String[] PICKER_SOURCE_CLASS = {"班级", "/api/base/getStuTree?toUser=false"};
|
||||
|
||||
private CampusNoticeTools() {
|
||||
}
|
||||
|
||||
@@ -148,8 +164,10 @@ public final class CampusNoticeTools {
|
||||
List<ToolParam> ps = new ArrayList<>();
|
||||
ps.add(ToolParam.required("title", "string", "公告标题(200 字以内)")
|
||||
.label("公告标题").eg("关于国庆假期安排的通知"));
|
||||
ps.add(ToolParam.required("content", "string", "公告正文(纯文本,对应模块页的富文本编辑器)")
|
||||
.label("公告正文").as("textarea").eg("各位老师、同学:经学校研究决定……"));
|
||||
ps.add(ToolParam.required("content", "string", "公告正文(富文本 HTML,对应模块页的 wangEditor)")
|
||||
.label("公告正文").as("richtext")
|
||||
.richText(RICHTEXT_UPLOAD_PATH, RICHTEXT_PREVIEW_PATH)
|
||||
.eg("各位老师、同学:经学校研究决定……"));
|
||||
ps.add(ToolParam.optional("type", "integer", "重要程度:1 普通 / 2 重要")
|
||||
.label("重要程度").dft("1")
|
||||
.options(new String[]{"1", "2"}, new String[]{"普通", "重要"}));
|
||||
@@ -160,10 +178,13 @@ public final class CampusNoticeTools {
|
||||
ps.add(ToolParam.optional("deptName", "string", "发文部门名称(由所选部门自动带出,无需填写)")
|
||||
.label("发文部门名称")
|
||||
.mirrorLabelOf("deptId"));
|
||||
// ★ 接收班级 = 模块页 ClassScopeInput 的同款自绘控件(radio 全部/指定 + 弹窗班级树),
|
||||
// 提交口径不变:仍拼成 [{scopeType,scopeId,scopeName}],只是不再用普通多选下拉。
|
||||
ps.add(ToolParam.optional("pushScopes", "array", "接收班级:留空或含全部 = 全部班级;也可多选具体班级")
|
||||
.label("接收班级")
|
||||
.optionsFrom("notice.stuTree").optionsKeys("id", "name")
|
||||
.scopes(4).allOption("ALL", "全部班级"));
|
||||
.scopePicker(4).allOption("ALL", "全部班级")
|
||||
.pickerSource(PICKER_SOURCE_CLASS[0], PICKER_SOURCE_CLASS[1]));
|
||||
ps.add(ToolParam.optional("topmost", "integer", "是否置顶")
|
||||
.label("置顶").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"}));
|
||||
|
||||
@@ -29,6 +29,26 @@ public final class DormNoticeTools {
|
||||
/** 发文部门字典的 type 值(/api/dict/List?type=dept) */
|
||||
private static final String DICT_TYPE_DEPT = "dept";
|
||||
|
||||
/** 通知正文富文本里「插入图片」的上传地址(模块页 NewNotice.vue 的 editorConfig.MENU_CONF.uploadImage)。 */
|
||||
private static final String RICHTEXT_UPLOAD_PATH = "/api/file/UploadImage";
|
||||
|
||||
/** 正文内图片的预览地址模板 —— 必须是 {@code Preview}(inline 输出图片)。 */
|
||||
private static final String RICHTEXT_PREVIEW_PATH = "/api/file/Preview?id={id}";
|
||||
|
||||
/**
|
||||
* 「接收宿舍」弹窗的两个数据源,**顺序即角色**(与模块页 NewNotice.vue 的
|
||||
* {@code openRoomPicker()} + {@code loadRooms()} 一一对应):
|
||||
* <ol>
|
||||
* <li>楼栋列表 —— {@code dormApi.getBuildingList()}(含房间数/床位数/入住数/宿管);</li>
|
||||
* <li>房间列表 —— {@code dormApi.getRoomList(buildingId)}(**buildingId 可缺省**:
|
||||
* 模块页回显时就是不带参数调的,一次取回全部房间用于补全箱名/床位数)。
|
||||
* 前端切楼栋时自行追加 {@code ?buildingId=},所以这里只给裸地址。</li>
|
||||
* </ol>
|
||||
* 同样只写模块内相对路径,前缀由编排层拼。
|
||||
*/
|
||||
private static final String[] PICKER_SOURCE_BUILDING = {"楼栋", "/api/dorm/BuildingList"};
|
||||
private static final String[] PICKER_SOURCE_ROOM = {"房间", "/api/dorm/RoomList"};
|
||||
|
||||
private DormNoticeTools() {
|
||||
}
|
||||
|
||||
@@ -105,8 +125,10 @@ public final class DormNoticeTools {
|
||||
"POST_JSON", "/api/notice/Create")
|
||||
.p(ToolParam.required("title", "string", "通知标题(50 字以内)")
|
||||
.label("通知标题").eg("关于宿舍熄灯时间调整的通知"),
|
||||
ToolParam.required("content", "string", "通知正文(纯文本,对应模块页的富文本编辑器)")
|
||||
.label("通知正文").as("textarea").eg("各位家长:自本周起,宿舍熄灯时间调整为 22:30……"),
|
||||
ToolParam.required("content", "string", "通知正文(富文本 HTML,对应模块页的 wangEditor)")
|
||||
.label("通知正文").as("richtext")
|
||||
.richText(RICHTEXT_UPLOAD_PATH, RICHTEXT_PREVIEW_PATH)
|
||||
.eg("各位家长:自本周起,宿舍熄灯时间调整为 22:30……"),
|
||||
ToolParam.required("noticeType", "string", "通知类型,来自 dorm.noticeTypeDic 的 k 字段")
|
||||
.label("通知类型")
|
||||
.optionsFrom("dorm.noticeTypeDic").optionsKeys("k", "v"),
|
||||
@@ -132,9 +154,15 @@ public final class DormNoticeTools {
|
||||
ToolParam.optional("formType", "string", "回执单据类型(receiptMode=fill 时必填),来自 dorm.formTemplates 的 type 字段")
|
||||
.label("回执单据")
|
||||
.optionsFrom("dorm.formTemplates").optionsKeys("type", "name"),
|
||||
// ★ 接收宿舍 = 模块页那个自写的「选择接收宿舍」弹窗同款控件
|
||||
// (左楼栋列表 / 右按楼层分组的房间勾选 / 底部已选间数与住宿生人数)。
|
||||
// 提交口径不变:房间 id 逗号串,或全选哨兵 "ALL"。
|
||||
ToolParam.optional("receiveList", "array", "接收宿舍:房间 id 列表;留空或含 ALL = 全部宿舍")
|
||||
.label("接收宿舍")
|
||||
.optionsFrom("dorm.rooms").optionsKeys("roomId", "buildingName+roomNo")
|
||||
.roomPicker()
|
||||
.pickerSource(PICKER_SOURCE_BUILDING[0], PICKER_SOURCE_BUILDING[1])
|
||||
.pickerSource(PICKER_SOURCE_ROOM[0], PICKER_SOURCE_ROOM[1])
|
||||
.join(",").allOption("ALL", "全部宿舍"),
|
||||
ToolParam.optional("needSupervisor", "integer", "是否需要宿管到场配合执行(仅需回执时有意义)")
|
||||
.label("宿管到场配合").dft("0")
|
||||
@@ -164,8 +192,9 @@ public final class DormNoticeTools {
|
||||
.label("通知"),
|
||||
ToolParam.required("title", "string", "通知标题")
|
||||
.label("通知标题"),
|
||||
ToolParam.required("content", "string", "通知正文")
|
||||
.label("通知正文").as("textarea"))
|
||||
ToolParam.required("content", "string", "通知正文(富文本 HTML)")
|
||||
.label("通知正文").as("richtext")
|
||||
.richText(RICHTEXT_UPLOAD_PATH, RICHTEXT_PREVIEW_PATH))
|
||||
.fix("receiveType", "room")
|
||||
.fix("receiveList", "ALL")
|
||||
.write("/list"));
|
||||
|
||||
@@ -34,6 +34,13 @@ public final class HomeworkTools {
|
||||
*/
|
||||
private static final String UPLOAD_PATH = "/api/file/Upload";
|
||||
|
||||
/** 作业内容富文本里「插入图片」的上传地址(模块页 NewHomework.vue 的 editorConfig.MENU_CONF.uploadImage)。
|
||||
* 注意与附件的 {@link #UPLOAD_PATH} **不是同一个接口**:正文图走 file/UploadImage。 */
|
||||
private static final String RICHTEXT_UPLOAD_PATH = "/api/file/UploadImage";
|
||||
|
||||
/** 正文内图片的预览地址模板 —— 必须是 {@code Preview}(inline 输出图片)。 */
|
||||
private static final String RICHTEXT_PREVIEW_PATH = "/api/file/Preview?id={id}";
|
||||
|
||||
private HomeworkTools() {
|
||||
}
|
||||
|
||||
@@ -159,8 +166,10 @@ public final class HomeworkTools {
|
||||
ps.add(ToolParam.required("classIds", "array", "接收班级:班级 id 列表(仅本人任教班级,可多选)")
|
||||
.label("接收班级")
|
||||
.optionsFrom("homework.classTree").optionsKeys("id", "name"));
|
||||
ps.add(ToolParam.optional("description", "string", "作业内容(纯文本,对应模块页的富文本编辑器)")
|
||||
.label("作业内容").as("textarea").eg("完成课本 P32 第 1~5 题,写出解题过程。"));
|
||||
ps.add(ToolParam.optional("description", "string", "作业内容(富文本 HTML,对应模块页的 wangEditor)")
|
||||
.label("作业内容").as("richtext")
|
||||
.richText(RICHTEXT_UPLOAD_PATH, RICHTEXT_PREVIEW_PATH)
|
||||
.eg("完成课本 P32 第 1~5 题,写出解题过程。"));
|
||||
ps.add(ToolParam.optional("deadlineAt", "string", "截止时间 yyyy-MM-dd HH:mm:ss(留空=不设截止)")
|
||||
.label("截止时间").eg("2026-10-20 22:00:00"));
|
||||
ps.add(ToolParam.optional("submitType", "array",
|
||||
|
||||
@@ -182,8 +182,13 @@ public final class ProfessionTools {
|
||||
ps.add(ToolParam.optional("ifOpen", "integer", "是否公开:1 公开 / 0 不公开")
|
||||
.label("是否公开").as("radio").dft("1")
|
||||
.options(new String[]{"1", "0"}, new String[]{"公开", "不公开"}));
|
||||
ps.add(ToolParam.optional("remark", "string", "正文内容(对应模块页的富文本编辑器)")
|
||||
.label("正文内容").as("textarea").placeholder("请输入正文内容…"));
|
||||
// ★ 富文本但**不带图片上传**:模块页 RecordEdit.vue 的 editorConfig 只有 placeholder
|
||||
// (toolbarConfig = {}),并没有配 MENU_CONF.uploadImage —— 该模块的 file 域只暴露了
|
||||
// Upload / Preview / Down(见 profession.ts),**没有** UploadImage。
|
||||
// 所以这里不调 .richText(...):上传地址为空时前端会把工具栏的「图片」组去掉,
|
||||
// 而不是留一个点了必失败的按钮。
|
||||
ps.add(ToolParam.optional("remark", "string", "正文内容(富文本 HTML,对应模块页的 wangEditor;该模块不支持正文插图)")
|
||||
.label("正文内容").as("richtext").placeholder("请输入正文内容…"));
|
||||
// 栏目动态字段的打包结果:由表单取回字段定义、渲染后自行生成,用户不直接填
|
||||
ps.add(ToolParam.optional("contents", "string", "栏目动态字段的取值(JSON 数组,由表单自动生成)")
|
||||
.label("栏目动态字段").hide());
|
||||
|
||||
Reference in New Issue
Block a user