- 同步仓库 CI 配置 .gitea/workflows/build.yml - 纳入 src/public/config/scripts 等工程文件 - .gitignore 增加 .scratch/.backup/.trash,排除本地临时产物
172 KiB
通知公告前端改造成「校园通知」执行计划任务方案(v2 · 2026-09-13 检查修订)
工程:前端
E:\YHXT\view\CampusAnnouncement(Vue3 + iview + alova + vite);后端E:\YHXT\java\CampusNotice参照物:http://localhost:8101/api-test.html(校园通知测试台,6 视图信息架构) 决策基线:对齐 api-test 能力 + 全站「通知」术语;保留 iview 左侧菜单框架;接口契约以后端 Controller 实际定义为准。本版用途:v1 方案(信息架构/术语/API 扩展/页面清单)已大面积落地。本轮对已改造部分做全面错误检查——进度盘点、错误清单 E1–E8、修复计划与回归清单;修复结果回填 §七。
一、改造进度盘点(对照 v1 差距清单)
| v1 待办项 | 状态 | 说明 |
|---|---|---|
| 术语全站切换 | ✅ 完成 | views/components 零残留;name 为准 |
| 菜单契约风险(GetUserMod 数据不可控) | ⚠️ 定论已被 v10 推翻 | 原方案:layout.vue MENU_NAME_MAP 展示层改名 + utils/menuRoute.ts 解析器(path 精确→模块名别名→外链→modId 兜底),配错显式提示不静默跳默认页。现方案见 v10:改名层移除,仅保留 menuRoute 解析器(仍按原始 name/modId 兜底) |
| 草稿箱独立页 | ✅ 完成 | DraftList.vue(搜索/续编/提交审批弹窗/删除);菜单由 layout 注入(id 3100,挂「已发管理」后) |
| API 扩展 SaveDraft/DraftList/Submit/ScopeTree/ScopePreview/ReadReceipt/Approve+opinion | ✅ 完成 | notice.ts 已封装,类型对齐后端 |
| 写通知:三态动作区/回执勾选/推送范围树+预估/多步审批流 | ✅ 基本完成 | PushScopePanel + ApprovalFlowEditor(审批人池数据源、会签/或签、上移下移);教师接收名单缺失 → E3 |
| 已发管理:状态/类型/部门/日期筛选、审批进度、定时中标注、驳回重提、阅读回执弹窗 | ✅ 完成 | NoticeList.vue |
| 审批中心:四 Tab/待审角标/多步进度 | ✅ 完成 | layout 60s 轮询 ApprovalCount 角标;审批意见列死字段 → E6 |
| 我的通知:未读已读分段/类型/搜索/置顶重要徽标/回执提示 | ✅ 完成 | NoticeAccept.vue |
| 详情:审批轨迹+每步流程进度+四态徽标 | ✅ 完成 | NoticeDetails.vue;三处字段后端不返回 → E4/E5 |
| 审核设置(审批人池 CRUD) | ⚠️ 有错误 | 页面完成,列表接口路径写错 → E1 |
| 旧路径兼容/重定向 | ✅ 完成 | routes.ts 别名段保留 |
编译级验证:npm run build 通过;vue-tsc --noEmit 与本机 Node/TS 版本不兼容无法运行(工具链问题,非代码错误),类型契约以下一轮人工比对为准。
二、错误检查方法
- 编译层:vite build;
- 契约层:逐接口比对
NoticeController/DictController/BaseApiController/FileController的 @Mapping 与 @RequestParam/@RequestBody 参数名 vs 前端api/module/notice.ts的 URL/方法/参数; - 数据层:比对
NoticeVo/NoticeDetailVo/SubmitRequest/FlowStepItem实际返回字段 vs 页面渲染引用与types/notice.ts声明(前端类型里存在而后端不返回的字段=死引用); - 交互层:对照 api-test.html 六视图的表单字段与操作语义,找缺失能力。
三、检出错误清单
| # | 位置 | 现象 | 根因 | 修复 |
|---|---|---|---|---|
| E1 | notice.ts approvers() |
审核设置页审批人列表加载失败 | 调 dict/Approvers,后端为 dict/Approver(单数) |
前端路径改 /Approver |
| E2 | NewNotice.vue buildBody |
「存为草稿」丢失已配置审批流 → 草稿箱「提交审批-沿用草稿已存流程」必失败 | flow 仅提交审批动作携带,draft 分支不带(后端 SaveDraft 支持存流程定义) | draft 分支同样携带 flow |
| E3 | NewNotice.vue |
无「教师接收名单」:不能只通知教师;ScopePreview 教师数恒 0;发布被强制要求选学员范围 | 参照 api-test cReceivers 面板未实现(后端 receiveList 支持逗号串/JSON/ALL) |
新增表单项;buildBody 带 receiveList;校验改「范围或名单二选一」;联动 PushScopePanel 预估;编辑回显(ALL 或 JSON 数组型) |
| E4 | NoticeDetails.vue 驳回/通过 Alert |
审批人/时间/意见显示空 | 用了后端不存在的 approvalBy/approvalTime/approvalRemark |
改从 approvalRecords 取末条 action=3/2(operatorName/actionTime/opinion) |
| E5 | NoticeDetails.vue 接收班级标签 |
恒显「全部班级」 | 用了不存在的 receiveText |
按 pushScopes 名称拼接;ALL→全体教师;旧链路兜底;标签改「接收范围」 |
| E6 | ApprovalList.vue 审批意见列 |
恒显「—」 | NoticeVo 无 approvalRemark(意见仅详情轨迹返回) |
删除该列;types 同步清理死字段 |
| E7 | 后端 DictController |
「发送短信提醒」勾选永不出现(404 被 catch 静默) | 前端约定 dict/Sync;后端 service 有 getSchModuleIsSms() 但控制器漏暴露 |
后端补 @GetMapping("Sync")(3 行;需重启生效;现实现固定 false 属平台占位,UI 行为不变) |
| E8 | ClassCheckInput.vue/GradeClassSel.vue |
无运行影响 | 死代码,引用后端不存在的 base/GetClassTree |
仅登记,后续清理删除 |
已知限制(不列入本轮修复):旧链路班级通知编辑回显依赖的 detail.receiveType 后端 Detail VO 未返回,旧数据续编范围不回填;如需支持由后端补字段。
四、修复执行顺序
- 前端小修:E1(1 行)→ E2(1 行)→ E6(删列)+ types 死字段清理;
- 前端中修:E4/E5(NoticeDetails computed)→ E3(NewNotice 教师名单 + 校验 + 回显 + 预估联动);
- 后端补口:E7(DictController.Sync)+
mvn compile验证(JAVA_HOME=D:/Program Files/Java/jdk-1.8,mvn=D:/Program Files/Java/maven/apache-maven-3.6.1/bin/mvn.cmd); - 回归:
npm run build重跑 + §五清单自检,结果回填 §七。
五、回归验证清单
- E1:审核设置页列表加载/新增/停用/删除正常
- E2:写通知→配两步审批→存为草稿→草稿箱提交审批(不重选人)→ 已发管理进度「第1/2步…」
- E3:只填教师名单不选范围可发布;范围+名单预估合计正确;ALL 全员;草稿续编回显
- E4:已驳回单详情意见/人/时间正确;通过单显示最后通过人
- E5:范围单「接收范围:xx、xx」;教师单「全体教师」;旧单「全部班级」
- E6:审批中心四 Tab 正常、无死列
- E7:重启后
GET /api/dict/Synccode=0 - 旧链路回归:收件箱/一键已读/撤回/置顶/转发/附件/打印 不受影响
npm run build通过;主链路冒烟(Create direct/approve、Submit、多步 Approve、Reject、Delete)
六、风险与边界
- E7 需 IDEA 重启服务生效(服务跑在 Debug 模式,脚本不代重启),本轮仅保证编译通过;
- E3 回显保守:仅
ALL或 JSON 数组型 receiveList 回填,防旧班级 id 串误显; - 全部修复不动鉴权/布局/alova 基建;types 仅删后端证实不返回的字段。
七、修复完成记录(实施时回填)
| 项 | 状态 | 备注 |
|---|---|---|
| E1 | ✅ 2026-09-13 | notice.ts approvers() → dict/Approver |
| E2 | ✅ 2026-09-13 | NewNotice.vue draft 分支 buildBody 携带 flow |
| E3 | ✅ 2026-09-13 | 教师名单表单项 + receiveList 入 body + 「范围/名单二选一」校验 + 预估联动 + 编辑回显(ALL 或 JSON 数组型) |
| E4 | ✅ 2026-09-13 | NoticeDetails.vue 驳回/通过 Alert 改取 approvalRecords 末条记录 |
| E5 | ✅ 2026-09-13 | 「接收范围」标签按 pushScopes/ReceiveList 计算(receiveLabel) |
| E6 | ✅ 2026-09-13 | 审批中心删除审批意见死列;types 清 approvalRemark/approvalBy/approvalTime/receiveText |
| E7 | ✅ 2026-09-13 | DictController 补 GET Sync,mvn compile 通过;需 IDEA 重启服务生效(当前 service 固定返回 false,勾选仍隐藏属平台占位预期) |
| E8 | 📝 登记 | ClassCheckInput/GradeClassSel 死代码,后续清理删除 |
| 构建回归 | ✅ 2026-09-13 | npm run build 通过(仅 chunk 体积警告,非错误);死字段引用清零 |
v3(2026-09-14)· 写通知业务逻辑迁移三项
需求(参照 api-test 写通知视图):
- 推送范围(学员)接口更换,并显示学员选中人数;
- 去除教师接收名单(撤销 v2-E3 在写通知中的名单输入,仅移除发布表单入口,旧数据详情展示不动);
- 选择审批人接口更换:由「审批人池(dict/ApproverPool 账号路径)」改为「教师树(base/getTeaTree 教师ID路径)」。
一、契约核对(后端实况,均已确认支持)
| 能力 | api-test 用法 | 后端契约 | 前端现状(待改) |
|---|---|---|---|
| 学员范围树 | GET base/getStuTree?toUser=false,节点 {id,name,type(1组/2叶子),children} 自底向上归类 scopeType(叶=4 班级→父=3 年级→再上=2 学部→根强制=1 全校) |
BaseApiController.getStuTree(SchTreeNodeDto 树) |
PushScopePanel 走 notice/ScopeTree(后端已拼好树,但需求要求统一换 getStuTree 口径) |
| 选中学员数 | GET notice/ScopePreview?scopes=… → studentCount |
NoticeController.scopePreview(scopes, receiveList?),receiveList 可空 |
已有预估行「学员/教师/合计」,含教师定向口径 |
| 审批人选人树 | GET base/getTeaTree?toUser=true,叶子 id=teaId |
BaseApiController.getTeaTree |
ApprovalFlowEditor 走 dict/ApproverPool + UserCheckModal 账号 |
| 流程提交 | 每步 {name,vote,approverTeaIds:[…]} |
FlowStepItem.approverTeaIds 与 approvers 二选一;resolveRequestFlow 同时服务于 Create.flow 与 Submit.steps;服务端 bindApprovers 按校教师名单绑定姓名 |
前端每步传 approvers:[账号] |
| 记忆带入 | notice/ApproverSuggest → teaIds;notice/ApproverResolve?ids= → {id,name} 姓名解析 |
两接口均存在 | Suggest 结果靠池内 teaId 映射账号;Resolve 未封装 |
| 编辑回显 | 详情 approvalFlow[].approverTeaIds/approverNames/vote |
ApprovalFlowVo 四字段齐备 |
回显走 approvers 账号列表 |
二、修改计划
- API 层
api/module/notice.ts:baseApi增getStuTree/getTeaTree;noticeApi删scopeTree()、增approverResolve(ids);approvalSettingApi删approverPool()(仅编辑器使用;审核设置管理页走approvers()不受影响);scopePreview保留(receiveList 变可选、不再传)。 - 类型
types/notice.ts:新增SchTreeNode(getStuTree/getTeaTree 节点);NoticeRequest.flow项改{name?,vote,approverTeaIds};ScopeTreeNode注释改为「面板视图节点(由 getStuTree 归类而来)」。 PushScopePanel.vue:数据源换base/getStuTree?toUser=false+toScopeNode归类(与 api-test 同算法,toUser=false 无学生叶子、防御性跳过 type≠1);删receiveListprop 及预估联动;预估改为「已选学员 N」(N=ScopePreview.studentCount)。NewNotice.vue:删「教师名单」表单项、form.receiveList、buildBody receiveList、编辑回显 ReceiveList 块;校验恢复「必须选择推送范围」;PushScopePanel 去 prop;审批流回显改approverTeaIds/approverNames;编辑态 Submit steps 改传approverTeaIds。ApprovalFlowEditor.vue(重写选人):内联教师树选择面板(搜索过滤+命中自动展开、组节点不可勾、教师叶子可多选、同教师跨组出现勾选态同步、完成选择回填当前步);步骤模型{vote, teaIds, names};勾选启用时/手动按钮带入 ApproverSuggest 记忆(经 ApproverResolve 解析姓名,未命中降级 ID);对外 v-model flow=有效步{vote, approverTeaIds}。DraftList.vue:提交审批 steps 改传approverTeaIds;「沿用草稿已存流程」逻辑不变(不勾选=steps 缺省)。- 回归:
npm run build;对照 api-test 人工走读契约。
已知限制:旧「账号路径」草稿/驳回单(approvalFlow 仅有 approvers 无 teaIds)在新编辑器中不回显,保存后流程按新 selections 覆盖(api-test 同口径);如需兼容由后端补 teaId 反查。
三、回归验证清单(v3)
- 写通知:范围树来自 getStuTree,勾选全校/学段/年级/班级出 chips,「已选学员 N」随勾选变化
- 写通知:无「教师名单」输入;不选范围直接发布/提交审批被拦截
- 写通知:勾选审批→教师树选人(搜索/多选/多组同步);带入上次审批人经 Resolve 显示姓名
- Create/Edit/Submit 请求体 flow/steps 为
{name,vote,approverTeaIds};提交后审批中心可正常审批 - 编辑回显:教师树审批流+姓名正确;草稿箱提交审批(重选/沿用)正常
- 旧数据:详情「接收范围/全体教师」标签不受影响;审核设置页 CRUD 不受影响
npm run build通过
四、实施记录(v3 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| 1 API 层 | ✅ 2026-09-14 | baseApi.getStuTree/getTeaTree;noticeApi 删 scopeTree()、增 approverResolve(ids);删 approverPool();scopePreview 保留(receiveList 缺省空) |
| 2 类型层 | ✅ 2026-09-14 | 新增 SchTreeNode;NoticeRequest.flow 与 SubmitRequest.steps 改 {name?,vote,approverTeaIds};receiveList 字段标注「发布表单已下线」 |
| 3 PushScopePanel | ✅ 2026-09-14 | 数据源 base/getStuTree?toUser=false + toScopeNode 自底向上归类(同 api-test);删 receiveList prop;预估区改「已选学员 N」(ScopePreview.studentCount,防抖 350ms,无勾选不显示) |
| 4 教师名单去除 | ✅ 2026-09-14 | NewNotice 表单项/form.receiveList/buildBody 组装/编辑回显(ALL·JSON 串回填)全删;校验恢复「请选择推送范围!」;详情 receiveLabel 与旧数据展示未动 |
| 5 ApprovalFlowEditor | ✅ 2026-09-14 | 重写为教师树路径:Modal 内联树(搜索命中过滤+自动展开、组不可勾、叶子多选、同教师跨组按 teaId 同步、完成选择回填当前步);步骤模型 {vote,teaIds};带入上次=ApproverSuggest→ApproverResolve(未命中降级 ID);对外 flow={name,vote,approverTeaIds} |
| 6 NewNotice 审批流 | ✅ 2026-09-14 | 回显改 approvalFlow[].approverTeaIds/approverNames;编辑态 Submit steps 传 approverTeaIds;Create/Edit flow 体同步 |
| 7 DraftList | ✅ 2026-09-14 | 提交审批弹窗 steps 传 approverTeaIds;不勾选=steps 缺省沿用草稿已存流程(不变) |
| 构建回归 | ✅ 2026-09-14 | npm run build 通过(40.2s,仅 chunk 体积警告);残留引用清零(approverPool/scopeTree/form.receiveList 无引用);ApproverResolveVo{id,name,photo}/ApproverSuggestVo{teaIds} 字段人工比对一致 |
待人工冒烟(需登录环境):§三 v3 清单逐条过;后端 notice/ScopeTree 接口本轮起前端不再调用(保留未删,其他端或仍用)。
v4(2026-09-14)·「审批列表菜单消失」排查登记(平台侧问题,前端零改动)
一、现象与结论
- 现象:左侧菜单无「审批中心」「审核设置」入口,其余菜单正常。
- 结论:非代码问题。基础平台用户中心对该校全员不再返回模块 id=5(审批列表)与 id=6(审核设置),
GetUserMod透传后前端菜单随之消失。
二、排查证据链
| # | 层面 | 核查结果 |
|---|---|---|
| 1 | 前端路由/菜单 | /approval(modId 5)、/approval-setting(modId 6) 路由、menu.ts 兜底、MENU_NAME_MAP(审批列表→审批中心)、图标角标逻辑全部完好;v3 改造未触碰菜单/layout |
| 2 | 后端 CampusNotice | BaseApiController.GetUserMod 仅透传 YschBaseOpenApi.getUserModule(userId, schId),无过滤 |
| 3 | 远程用户中心实况 | 直查 base.yunhaixingtong.com/userbase /oapi/user/modules(复刻平台签名,脚本 .scratch/oapi-call.mjs):云海中学(schId=3) 全部已知账号(lijing=10008、王虎=10007、陈星1=10006、测试教师2=10045、ceshiteacher=125287)均只返回 1/2/3/21/4 五个模块,5/6 全员缺失 |
| 4 | 时间线线索 | 平台 2026-09-10 新建「校园公告」应用(appKey 100006),疑似其后有人调整过应用/模块/角色配置导致丢失 |
三、修复方案(平台侧操作,前端不用发版)
- 基础平台管理后台 → 应用管理 → 「通知公告」应用 → 模块管理:确认两模块存在且启用——审批列表(path
/approval,前端显示「审批中心」)、审核设置(path/approval-setting); - 角色权限中为对应角色(老师/管理员)重新勾选这两个模块;
- 恢复授权后菜单即时回来(前端已有名称映射+路径解析兜底)。
备选:平台侧无法及时修改时,可仿「草稿箱」由 layout 前端注入这两个菜单项临时兜底(偏离“菜单以平台授权为准”契约,需产品确认后实施)。
四、验证方式
恢复后执行 node .scratch/oapi-call.mjs "/oapi/user/modules" "userId=10008&schId=3",确认返回列表包含 id=5、id=6;或直接刷新前端页面观察左侧菜单。
五、实施记录
| 项 | 状态 | 备注 |
|---|---|---|
| 排查登记 | ✅ 2026-09-14 | 本节;前端/后端零代码改动 |
| 平台侧重新授权 | ⏳ 待平台管理员操作 | 模块 5/6 挂回应用并授权角色 |
| 复验 | ⏳ 待平台操作后 | 按 §四 验证 |
v5(2026-09-14)· 发公告「推送范围」改为「接收班级」二选一(全部班级 / 指定班级)
需求(参照截图,旧链路 new_notice 样式):发公告表单的范围选择由「全校/学段/年级/班级」多级树面板(PushScopePanel)改为:
- 单选 全部班级 / 指定班级;
- 指定班级 → 只读输入框(placeholder「选择通知的班级」)+「+」按钮打开选择用户弹窗(左侧年级→班级树勾选、右侧已选列表全选/计数、提交/取消),垃圾桶按钮清空;
- 表单项 label 由「推送范围」改为「接收班级」。
一、契约核对(后端实况,均以源码确认)
| # | 结论 | 依据 |
|---|---|---|
| 1 | 后端零改动:全部班级 → pushScopes=[{scopeType:1, scopeId:0, scopeName:"全部班级"}];指定班级 → [{scopeType:4, scopeId:classId, scopeName:className}…] |
PushScopeTypeEnum:SCHOOL(1) 走 getSchStuList(schSerId) 全校(scopeId 不参与解析,api-curl-test.sh 亦用 scopeId=0 验证);CLASS(4) 走 getClaArrStuList 展开 |
| 2 | 弹窗班级树数据源 = base/getStuTree?toUser=false,前端自底向上归出「年级→班级」两层(学校/学段层丢弃) |
后端无 base/GetClassTree(v2-E8 登记属实,ClassCheckInput 的旧数据源不可用);BaseApiController.getStuTree 返回组节点树 |
| 3 | mock 缺 getStuTree(v3 换源后未补),本轮一并补 mock(学校→学段→年级→班级),保持 mock 模式可用 |
mock/noticeapi.ts 仅有 GetClassTree/GetSchoolAll |
| 4 | 编辑回显兼容:pushScopes 中 type=2/3(旧多级面板选的学部/年级)在树中展开为其下属班级;type=5/6 等无法映射项原样保留随表单提交,防丢范围 | 对齐 PushScopePanel.applyScopes「树外项保留」口径 |
| 5 | 旧链路数据(receiveType=class):tlist 班级 id 串 → type=4 项(树加载后解析真实班级名);tlist="all" → 全部班级 | NewNotice 既有映射逻辑延续 |
二、修改计划
- 新建
src/components/ClassScopeInput.vue:v-model=PushScopeItem[];radio 全部班级(默认)/指定班级 + 只读输入框 + 加号(开 UserCheckModal,数据源 getStuTree 自归类的年级→班级树)/垃圾桶(清空指定班级);弹窗提交空选=回落全部班级(旧链路口径);指定班级下方保留「已选学员 N 人」预估(ScopePreview,防抖,v3 能力不回退);暴露setScopes()供编辑回显(含 type 2/3 展开与名称解析,树未就绪时缓冲)。 NewNotice.vue:PushScopePanel → ClassScopeInput,label 改「接收班级」;回显三分支改走 setScopes(pushScopes / receiveType=class 班级串 / tlist=all→全部班级);发布校验文案改「请选择接收班级!」。mock/noticeapi.ts:补/api/base/getStuTree(toUser 参数按组节点树返回,由 gradeClasses 派生)。- PushScopePanel.vue 退役(保留文件不删,文档登记);ClassCheckInput/GradeClassSel 死代码登记维持 v2-E8 口径。
- 回归:
npm run build+ 本节清单人工冒烟。
三、回归验证清单(v5)
- 新建公告:默认「全部班级」,直接发布成功,详情接收范围显示「全部班级」
- 切「指定班级」自动开弹窗:年级树勾选级联、右侧全选/计数、搜索班级名可用;提交回填班级名顿号串
- 指定班级清空后发布被拦(「请选择接收班级!」);弹窗提交空选回落全部班级
- 编辑回显:v3 单(全校/年级/班级混合)→ 对应 radio 态与班级名正确;旧链路班级串单 → 指定班级 + 真实班级名;tlist=all 单 → 全部班级
- Create/Edit 请求体 pushScopes 为 scopeType 1/4 组合,后端详情/收件范围/应读人数正确
- mock 模式(isMock)下弹窗树可加载
npm run build通过;其余表单项(附件/审批/定时/草稿)不受影响
四、实施记录(v5 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| 1 ClassScopeInput | ✅ 2026-09-14 | 新建 components/ClassScopeInput.vue:radio 全部班级(默认)/指定班级 + 只读框 + 加号弹窗/垃圾桶清空;树=base/getStuTree 自归类「年级→班级」;空选提交回落全部班级(旧链路口径);setScopes 含 type 2/3 展开为班级、type 5/6 原样保留(extras)、树未就绪缓冲;「已选学员 N 人」预估保留(ScopePreview) |
| 2 NewNotice 接入 | ✅ 2026-09-14 | PushScopePanel → ClassScopeInput;label 改「接收班级」;回显三分支(pushScopes 原样 / receiveType=class 班级串→type4+树解析真实班级名 / tlist=all→全部班级);转发空范围默认全部班级;校验文案「请选择接收班级!」 |
| 3 mock getStuTree | ✅ 2026-09-14 | 补 /api/base/getStuTree(学校→学段→年级→班级组节点树,由 gradeClasses 派生);已知问题登记:mock 仍缺 base/getTeaTree(v3 审批选人换源后未补),mock 模式下审批流选人不可用,后续补 |
| 4 PushScopePanel 退役登记 | ✅ 2026-09-14 | 文件保留未删(无任何 import 引用,不参与构建产物);如需恢复多级树面板可直接复用 |
| 构建回归 | ✅ 2026-09-14 | npm run build 通过(15.68s,仅 chunk 体积警告,与历次一致);PushScopePanel/推送范围 残留引用仅剩注释与退役文件本身 |
待人工冒烟(需登录环境):§三 v5 清单逐条过(重点:真实 getStuTree 数据下年级归类是否正确、后端 Create/Edit 全部班级与指定班级的应读人数)。
v6(2026-09-14)· 审批人选人弹窗改用「选择用户」穿梭式(UserCheckModal)
需求(参照截图):发送通知「审批 → 选择审批人」当前用的是 v3 重写的单栏内联教师树 Modal(标题「选择审批人(可多选)」)。改为复用
UserCheckModal.vue的「选择用户」双栏穿梭式弹窗:左侧组织树(带复选框、父子级联)+ 中间箭头 + 右侧已选列表(全选/计数/删除选中)+ 顶部名字搜索 + 提交/取消。审批人数据源不变,仍走教师树base/getTeaTree?toUser=true(叶子 id=teaId)。多步审批、或签/会签、记忆带入、编辑回显等能力不回退。
一、契约核对
| # | 结论 | 依据 |
|---|---|---|
| 1 | UserCheckModal 可直接复用:props sources:[{name,loader}](loader 返回 ztree 树)、userids:number[](回显勾选);emits on-submit 返回 [{id,name,group}](id=真实 teaId,经 getSplitId 还原) |
组件已用于「接收班级」弹窗,formatTree 无 isParent 时按有无 children 判组/叶子(教师叶子无 children→type 2) |
| 2 | 单数据源(教师树)→ sources.length===1,Tabs 不渲染,与截图一致(截图无 tab 栏,仅搜索框+双栏) |
UserCheckModal 模板 v-if="sources.length>1" |
| 3 | 提交后按步回填:steps[editingStep].teaIds = 返回 ids,姓名写入 teaNames 供 chip 展示;对外 flow 仍 {name,vote,approverTeaIds} |
现 collectFlow/emitChange 不变 |
| 4 | 依赖 jQuery zTree + requireImg 已由「接收班级」弹窗全局引入,无需新增依赖 |
UserCheckInput/ClassScopeInput 已用 |
| 5 | 记忆带入(ApproverSuggest→ApproverResolve)、编辑回显(setFlow+names 兜底)、多步增删/上下移、dropApprover 逐 chip 删除——均与选人弹窗解耦,保持不动 | 现组件逻辑 |
| 6 | 已知限制延续 v5:mock 缺 base/getTeaTree,mock 模式下审批选人弹窗树加载失败(真实环境正常);本轮不扩范围补该 mock |
v5 §四登记 |
二、修改计划(仅 ApprovalFlowEditor.vue)
- 引入
UserCheckModal;模板中删除内联<Modal title="选择审批人(可多选)">…</Modal>整块,替换为<UserCheckModal ref="pickerRef" :sources="pickerSources" :userids="editingTeaIds" @on-submit="onPickerSubmit" />。 pickerSources = [{ name: "教师", loader: () => baseApi.getTeaTree(true).send() }];editingStep由let改ref(0);editingTeaIds = computed(() => steps[editingStep].teaIds)。openPicker(i):editingStep=i后pickerRef.value.show();onPickerSubmit(arr):回填当前步 teaIds、写 teaNames、emitChange()(UserCheckModal 提交后自关)。- 删除仅供内联树使用的死代码:
rawTree/rows/rowByKey/treeLoading/treeError/expanded/keyword/pickerOpen、buildRows/ensureTree/hitKeys/visibleRows/toggle/isTeacherChecked/onTeacherCheck/closePicker/renderFromSteps,及SchTreeNode若不再引用则删;删除.tea-tree/.picker-search等相关 scoped 样式。 - 保留:
teaNames/nameOf/steps/enabled/addStep/removeStep/move/dropApprover/collectFlow/emitChange/validFlow/validate/setFlow/loadSuggest/bindStepByTeaIds/watch(enabled)/onMounted/defineExpose。 - 回归:
npm run build。
三、回归验证清单(v6)
- 勾选审批→点「选择审批人」弹出「选择用户」双栏弹窗(无 tab),左=教师组织树可勾选级联,右=已选列表带全选/计数/删除选中,顶部搜索教师名可定位勾选
- 提交后当前步生成教师 chips(姓名正确);再次打开回显已勾教师
- 多步:每步独立选人互不串;上移/下移/删除步、逐 chip ×删除、垃圾桶清空 均正常
- 「带入上次所选审批人」经 ApproverResolve 显示姓名(池外降级 ID)
- 编辑回显:教师树审批流+姓名正确还原,弹窗内可再编辑
- Create/Edit/Submit 请求体 flow/steps 仍为
{name,vote,approverTeaIds} npm run build通过;接收班级弹窗(同组件另一实例)不受影响
四、实施记录(v6 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| 1 弹窗替换 | ✅ 2026-09-14 | 模板删除内联 <Modal title="选择审批人(可多选)"> 整块,改 <UserCheckModal ref="pickerRef" :sources="pickerSources" :userids="editingTeaIds" @on-submit="onPickerSubmit" />;单数据源「教师」→ 无 tab,与截图一致 |
| 2 数据源 | ✅ 2026-09-14 | pickerSources=[{name:"教师",loader:()=>baseApi.getTeaTree(true).send()}],叶子 id=teaId 由 UserCheckModal getSplitId 还原为真实 teaId |
| 3 按步回填 | ✅ 2026-09-14 | editingStep 改 ref;editingTeaIds=computed(steps[editingStep].teaIds) 回显;openPicker(i) 设步后 pickerRef.show();onPickerSubmit(arr) 回填 st.teaIds=arr.map(id)、写 teaNames、emitChange() |
| 4 死代码清理 | ✅ 2026-09-14 | 删 rawTree/rows/rowByKey/treeLoading/treeError/expanded/keyword/pickerOpen + buildRows/ensureTree/hitKeys/visibleRows/toggle/isTeacherChecked/onTeacherCheck/closePicker/renderFromSteps + SchTreeNode import + .tea-tree/.picker-search 等 scoped 样式;保留多步/或签会签/记忆带入/回显/校验/defineExpose |
| 构建回归 | ✅ 2026-09-14 | npm run build 通过(17.51s,仅 chunk 体积警告,与历次一致);残留引用清零(grep 内联树符号无匹配);flow 对外契约 {name,vote,approverTeaIds} 不变 |
| 5 单栏收敛 | ✅ 2026-09-14 | 追加需求「不需要双栏,只保留左树」:UserCheckModal 新增 showSelected prop(默认 true,接收班级弹窗不变),false 时隐藏中间箭头 + 右侧已选栏(全选/计数/删除选中),树面板满宽、弹窗宽 420px;勾选状态仍由内部 tempUserList 承载,submit 返回逻辑不变;审批弹窗传 :show-selected="false"。npm run build 通过(17.89s) |
待人工冒烟(需登录环境):§三 v6 清单逐条过(重点:真实 getTeaTree 数据下双栏弹窗勾选/搜索/全选/计数、多步互不串、编辑回显再编辑、提交后审批中心可正常审批)。已知限制延续:mock 缺 getTeaTree,mock 模式审批选人弹窗树加载失败(真实环境正常)。
v7(2026-09-14)· 审批中心与审核设置:接口与绑定改造(审批人池升级为教师白名单)
核心问题:v3–v6 将写通知审批人选人切换到教师树路径(
base/getTeaTree→ teaId),但「审核设置」维护的仍是账号路径的sys_approver池(account 主键,teaId 字段存在但从未写入)。两条链路完全脱节——审核设置里配的名单写通知选人不消费,任何本校教师都能被选为审批人,池形同虚设;后端bindApprovers(teaId 路径)只校验教师名单不查池,bindAccounts(账号路径)查池但前端已不再触发。改造方向(已确认):池升级为教师白名单——审核设置新增审批人改为「从本校教师中选择」,一次落 account/name/deptId/teaId 四件套;写通知选人时教师树只显示池中 enabled=1 的教师;后端
bindApprovers送审时增加池校验。审批中心增加「当前审批人」列。
一、现状盘点
1.1 审批中心(views/notice/ApprovalList.vue,路由 /approval,modId 5)
| 维度 | 现状 |
|---|---|
| 接口 | GET /api/notice/ApprovalList(参数 pageIndex/pageSize/status/keyword/scope);POST /api/notice/Approve(form serid/opinion);POST /api/notice/Reject(form serid/remark);GET /api/notice/ApprovalCount(layout 60s 轮询角标) |
| Tab | pending 待我审批(默认)/ approved 已通过 / rejected 已驳回 / all 全部 |
| 表格列 | 序号、标题、类型、来源部门、发布人、提交时间、审批进度(approvalStatusDesc Tag)、操作 |
| 操作 | 查看通知(跳详情 type=0);canApprove && pending → 通过/驳回弹窗;pending 但非当前审批人 → 灰字「等待他人审批」 |
| 参数契约 | 前端传 status(旧名),后端主参数 auditState(新名),Controller L359-360 做别名归一——能跑但不对齐新契约 |
| 缺失 | 无「当前审批人」列(卡在谁那里不可见);无「我提交的」视角(发布人无法跟踪自己的单子) |
1.2 审核设置(views/notice/ApprovalSetting.vue,路由 /approval-setting,modId 6)
| 维度 | 现状 |
|---|---|
| 接口 | GET /api/dict/Approver(含停用全量);POST /api/dict/AddApprover(JSON {account,name,deptId});POST /api/dict/UpdateApprover(JSON {id,name?,deptId?,enabled?});POST /api/dict/DelApprover(form id);GET /api/dict/List?type=1(部门下拉) |
| 列表列 | 姓名、账号、所属部门、状态(启用/停用 Tag)、操作(停用/启用、编辑、删除) |
| 弹窗字段 | 姓名(必填)、账号(可选,手填)、所属部门(Select 必填) |
| 核心缺陷 | ① 无 teaId 绑定入口 → sys_approver.tea_id 永远为空;② 账号手填易错且与用户中心不一致;③ 池无消费者——写通知选人走教师树全量,不查池 |
1.3 写通知选人链路(components/ApprovalFlowEditor.vue,v6 现状)
| 维度 | 现状 |
|---|---|
| 数据源 | UserCheckModal,pickerSources=[{name:"教师", loader:()=>baseApi.getTeaTree(true).send()}]——全量教师树,不过滤池 |
| 提交契约 | 每步 {name?, vote, approverTeaIds:[teaId]}(教师ID路径) |
| 后端校验 | bindApprovers(NoticeService L931-969):teaId 按本校教师名单(classService.getSchTeaList)绑定姓名;approverGroupId>0 时加资格组校验(当前=0 未启用);不查 sys_approver 池 |
| 记忆带入 | ApproverSuggest→teaIds;ApproverResolve→姓名(未命中降级 ID) |
1.4 后端审批人池接口(DictController + DictService)
| 接口 | 返回字段 | teaId 支持 |
|---|---|---|
GET dict/ApproverPool(仅启用) |
id, name, account, deptName, teaId, isParent |
✅ 已返回 teaId(L294) |
GET dict/Approver(含停用) |
id, account, name, deptId, deptName, enabled |
❌ 不返回 teaId(L302-316) |
POST dict/AddApprover |
body Map<String,String>{account,name,deptId} |
❌ 不接 teaId(L320-337) |
POST dict/UpdateApprover |
body Map<String,Object>{id,name,deptId,enabled} |
❌ 不接 teaId(L341-355) |
POST dict/DelApprover |
form id |
— |
1.5 后端送审校验双轨(NoticeService.buildSteps L790-839)
| 路径 | 触发条件 | 校验方法 | 校验强度 |
|---|---|---|---|
| 账号路径 | FlowStepItem.approvers 非空 |
bindAccounts(L845-874):查 sys_approver 池 enabled=1,池外/停用报错 |
✅ 强(池约束) |
| 教师ID路径 | FlowStepItem.approverTeaIds 非空 |
bindApprovers(L931-969):查本校教师名单 getSchTeaList,不在名单报错;approverGroupId>0 时加资格组 |
❌ 弱(不查池,任何本校教师均可) |
v3 后前端只走教师ID路径 → 池校验被完全绕过。
1.6 saveFlowDefinition(L765-783)与草稿
saveFlowDefinition(草稿/编辑态存流程定义)也调用 buildSteps(L775)→ 若直接在 bindApprovers 加池校验,草稿保存也会被拦。需区分:草稿暂存不校验池,正式送审(Submit/Create approve)才校验。
二、问题清单
| # | 层面 | 问题 | 影响 | 优先级 |
|---|---|---|---|---|
| P1 | 架构 | 审核设置池(sys_approver)与写通知选人(教师树)完全脱节,池无消费者 | 审核设置形同虚设,配了也不约束选人 | P0 |
| P2 | 后端 | bindApprovers(teaId 路径)不查 sys_approver 池,任何本校教师可被选为审批人 |
审批人不受管控 | P0 |
| P3 | 后端 | DictService.addApprover/updateApprover 不接 teaId;getApproverList 不返回 teaId |
sys_approver.tea_id 永远为空,池与教师树无法关联 | P0 |
| P4 | 前端 | 审核设置弹窗账号手填,无「从教师选择」入口 | 易输错、与用户中心不一致、无法落 teaId | P0 |
| P5 | 前端 | ApprovalFlowEditor 选人数据源为全量教师树,不过滤池 |
可选到池外教师(后端也不拦) | P0 |
| P6 | 前端 | notice.ts 的 approverPool() 在 v3 被删除,前端无法拉池 |
写通知选人无法做池过滤 | P1 |
| P7 | 前端 | ApproverVo 类型缺 teaId 字段 |
审核设置列表/弹窗无法展示和编辑 teaId | P1 |
| P8 | 后端 | NoticeVo 无「当前步待签人姓名」字段 |
审批中心无法显示「当前审批人」列 | P1 |
| P9 | 前端 | 审批中心 ApprovalQuery.status 未升级为后端新名 auditState |
能跑(后端别名兼容)但契约不对齐 | P2 |
| P10 | Mock | mock 定义 /api/dict/Approvers(复数)vs 实际 /api/dict/Approver(单数);缺 getTeaTree/ApproverSuggest/ApproverResolve;审批人无 teaId |
mock 模式下审核设置 404、审批选人不可用 | P2 |
| P11 | 后端 | saveFlowDefinition(草稿)也走 buildSteps→bindApprovers,加池校验会误拦草稿 |
需区分草稿/送审校验强度 | P1 |
三、修改计划
Phase 1 · 后端:审批人池 teaId 化 + 送审池校验 + 列表待签人字段
工程:
E:\YHXT\java\CampusNotice;编译:JAVA_HOME=D:/Program Files/Java/jdk-1.8,mvn=D:/Program Files/Java/maven/apache-maven-3.6.1/bin/mvn.cmd;服务跑 IDEA Debug + devtools(小改热重启,大改需手动重启)。
| # | 文件 | 改动 |
|---|---|---|
| 1.1 | DictService.java L302-316 getApproverList() |
返回 Map 增加 item.put("teaId", approver.getTeaId()) |
| 1.2 | DictService.java L320-337 addApprover(...) |
签名扩为 addApprover(String account, String name, String deptId, Integer teaId);teaId 非空时落库;teaId 非空且 account 为空时,按 teaId 查本校教师名单(classService.getSchTeaList)自动解析 account/name(教师登录账号),解析失败报「所选教师无登录账号,无法作为审批人」 |
| 1.3 | DictService.java L341-355 updateApprover(...) |
签名扩为 updateApprover(Integer id, String name, String deptId, Integer enabled, Integer teaId);teaId 非 null 时更新(含清空传 0→null 语义可选) |
| 1.4 | IDictService.java L94/L99 |
接口签名同步 1.2/1.3 |
| 1.5 | DictController.java L104-107 addApprover |
body 改 Map<String,Object>(兼容 teaId 数字),取 teaId = body.get("teaId") 转 Integer 传 service |
| 1.6 | DictController.java L112-119 updateApprover |
body 取 teaId 转 Integer 传 service |
| 1.7 | NoticeService.java L931-969 bindApprovers(...) |
增加重载 bindApprovers(Integer schId, List<Integer> teaIds, boolean validatePool);validatePool=true 时:拉 sysApproverRepository.findBySchIdAndEnabledOrderByNameAsc(schId,1) 建 teaId Set,teaIds 中不在 Set 的报「审批人不在本校审批人名单或已停用(姓名),请在「审核设置」中维护」;原签名委托 validatePool=false 保持兼容 |
| 1.8 | NoticeService.java L790 buildSteps(...) |
增加 boolean validatePool 参数,透传 bindApprovers;账号路径 bindAccounts 本身已查池不受影响 |
| 1.9 | NoticeService.java L765 saveFlowDefinition(...) |
调 buildSteps(entity, flow, legacyTeaIds, **false**)——草稿/编辑暂存不校验池 |
| 1.10 | NoticeService.java L490 doSubmitApproval(...) |
调 buildSteps(entity, flow, legacyTeaIds, **true**)——正式送审校验池 |
| 1.11 | NoticeVo.java L58-75 |
增加字段 private List<String> pendingApproverNames;(当前步待签人姓名;非 pending 态为 null) |
| 1.12 | NoticeService.java L1081 fillApprovalFields(...) |
装配 pendingApproverNames:定位当前步(stepNo==approvalLevel+1 且 status==ACTIVE),取 approverNames JSON 列表,减去本轮本步已处理人(notice_approval_record round+stepNo 命中者),余下即待签人;复用 toFlowVos 中 pending 计算逻辑(L1204+) |
| 1.13 | 编译验证 | mvn compile -q;devtools 热重启或 IDEA 手动重启 |
兼容性说明:
- 存量
sys_approver记录 teaId 为 null → 池过滤时这些记录不会出现在教师树白名单(teaId null 无法匹配树叶子),需在审核设置中逐条「编辑→选择教师」补绑 teaId;方案提供批量补绑脚本可选(§五)。 - 存量待审批单(status=2)的
approverTeaIds快照已落库,Approve/Reject 按快照走不重新校验池(requireStepApproverL672 只比对快照),不受影响。 bindAccounts(账号路径)保持不变,api-test.html 测试台账号路径仍可用。
Phase 2 · 前端:审核设置改造(从教师选择 + teaId 绑定)
工程:
E:\YHXT\view\CampusAnnouncement。
| # | 文件 | 改动 |
|---|---|---|
| 2.1 | types/notice.ts ApproverVo(L504-511) |
增加 teaId?: number | null |
| 2.2 | api/module/notice.ts approvalSettingApi |
addApprover 入参增 teaId?: number;updateApprover 入参增 teaId?: number;恢复 approverPool(): GET /api/dict/ApproverPool(v3 删除,Phase 3 选人过滤要用),返回类型 ApproverPoolItem{id,name,account,deptName,teaId,isParent} |
| 2.3 | ApprovalSetting.vue 弹窗 |
① 增加「选择教师」按钮 → 打开 UserCheckModal(数据源 baseApi.getTeaTree(true),单选模式:新增 multiple prop 默认 true,此处传 false,选新自动取消旧);② 选中回调 [{id:teaId,name,group}] → 自动填 form.name、form.teaId,account 由后端按 teaId 解析(前端 account 字段改只读展示,placeholder「选择教师后自动填充」);③ 编辑态回显:row.teaId 非空时按钮文案「重新选择教师」,空时「选择教师」;④ form 增 teaId 字段,saveApprover 请求体带 teaId |
| 2.4 | ApprovalSetting.vue 列表 |
可选增加「教师ID」列(teaId,width 90,排查用;teaId 为空显示「未绑定」灰色 Tag 提示需编辑补绑) |
| 2.5 | ApprovalSetting.vue Alert 文案 |
更新为:「这里维护可被选为审批人的教师名单(白名单)。发布通知选择审批人时,只能从此处启用的教师中选择;停用后不出现在选人列表,已有审批流程不受影响。」 |
| 2.6 | UserCheckModal.vue |
新增 multiple?: boolean prop(默认 true);false 时勾选新叶子自动取消其他已选(单选语义),submit 仍返回数组(长度≤1);接收班级/审批流弹窗不传该 prop 行为不变 |
Phase 3 · 前端:写通知选人改为池白名单过滤
| # | 文件 | 改动 |
|---|---|---|
| 3.1 | ApprovalFlowEditor.vue pickerSources |
loader 改为:并行拉 approvalSettingApi.approverPool() + baseApi.getTeaTree(true);池为空 → Message.warning("请先在「审核设置」中添加审批人") 并返回空树;否则建 allowedTeaIds = Set(pool.filter(p=>p.teaId!=null).map(p=>p.teaId)),递归过滤教师树:叶子 id∈Set 保留,组节点无保留叶子则剪枝 |
| 3.2 | 新增工具函数 filterTreeByTeaIds(tree, idSet) |
放 utils/index.ts 或组件内:递归 SchTreeNode[],type===2(叶子)按 id∈idSet 过滤,type===1(组)过滤 children 后空则丢弃;返回新树不改原数据 |
| 3.3 | 记忆带入兼容 | ApproverSuggest 返回的 teaIds 若含池外教师(已被停用/删除),bindStepByTeaIds 解析姓名后标注「(已不在审批人名单)」灰色 chip,提交时后端池校验会拦截并提示——前端不静默丢弃,让用户可见可删 |
| 3.4 | 编辑回显兼容 | 存量单 approvalFlow[].approverTeaIds 含池外教师时,回显 chip 正常展示姓名(ApproverResolve 不依赖池),但重新打开选人弹窗时该教师不在过滤后树中 → 勾选态丢失;保存时若步骤含池外 teaId 后端报错,前端 catch 提示「审批人 XX 已不在名单,请重新选择」。已知边界,登记不阻塞 |
Phase 4 · 前端:审批中心增强
| # | 文件 | 改动 |
|---|---|---|
| 4.1 | ApprovalList.vue columns |
「审批进度」列后增加「当前审批人」列:key:"pendingApproverNames",render 逗号顿号拼接,空显示「—」;width 160,tooltip |
| 4.2 | types/notice.ts NoticeVo |
增加 pendingApproverNames?: string[] |
| 4.3 | types/notice.ts ApprovalQuery |
status 字段改名 auditState(保留 status? 为 deprecated 别名可选);ApprovalList.vue 的 useRequest 参数与 RadioGroup v-model 同步改 auditState |
| 4.4 | api/module/notice.ts approvalList |
参数序列化改传 auditState(后端别名兼容期结束后可删 status 支持,本轮不动后端) |
Phase 5 · Mock 修复(P2,可与 Phase 1-4 并行或延后)
| # | 文件 | 改动 |
|---|---|---|
| 5.1 | mock/noticeapi.ts |
补 GET /api/dict/Approver(单数,含停用,返回增 teaId);现有 /api/dict/Approvers(复数)保留或删除 |
| 5.2 | mock/noticeapi.ts |
审批人种子数据增加 teaId 字段(与 getTeaTree mock 教师 id 对齐) |
| 5.3 | mock/noticeapi.ts |
补 GET /api/base/getTeaTree(v5 登记缺失):学校→部门→教师组节点树,叶子 id=teaId |
| 5.4 | mock/noticeapi.ts |
补 GET /api/notice/ApproverSuggest、GET /api/notice/ApproverResolve |
| 5.5 | mock/noticeapi.ts AddApprover/UpdateApprover |
入参处理 teaId;ApprovalList 返回行增加 pendingApproverNames |
Phase 6 · 回归验证
| # | 项 |
|---|---|
| 6.1 | 后端 mvn compile -q 通过;重启后 GET /api/dict/Approver 返回含 teaId;POST AddApprover 带 teaId 落库 |
| 6.2 | 前端 npm run build 通过 |
| 6.3 | 审核设置:新增→选择教师→自动填姓名/账号→提交→列表 teaId 列有值;编辑→重新选择教师→更新;停用/启用/删除正常 |
| 6.4 | 写通知:勾选审批→选人弹窗教师树只显示池内启用教师;池为空时提示;选池外教师(记忆带入残留)提交被后端拦截并提示 |
| 6.5 | 送审:Submit/Create(approve) 时池校验生效(池外 teaId 报错);SaveDraft/Edit 暂存不校验(草稿可含池外教师) |
| 6.6 | 审批中心:表格「当前审批人」列显示待签人姓名;通过后流转下一步姓名更新;末步通过→发布→列显示「—」 |
| 6.7 | 存量兼容:改造前提交的待审批单可正常 Approve/Reject(按快照走不重校验);存量池记录 teaId=null 不出现在选人树,编辑补绑后恢复 |
| 6.8 | 旧链路回归:收件箱/一键已读/撤回/置顶/转发/附件/打印/接收班级弹窗 不受影响 |
四、实施顺序
Phase 1(后端)──→ Phase 2(审核设置)──→ Phase 3(选人过滤)──→ Phase 4(审批中心)
│ │
└────────────── Phase 5(Mock,可延后)─────────────────────────┘
↓
Phase 6(回归)
- Phase 1 先行:后端 teaId 化 + 池校验是前端改造的前提(前端选人过滤依赖 ApproverPool 返回 teaId)。
- Phase 2/3 可并行:审核设置弹窗改造与选人过滤互不依赖(都消费 Phase 1 产物)。
- Phase 4 独立:仅依赖 Phase 1.11/1.12(NoticeVo 新字段)。
- Phase 5 不阻塞真实环境联调,可最后补。
- 每个 Phase 完成后回填 §六实施记录。
五、风险与边界
| # | 风险 | 应对 |
|---|---|---|
| R1 | 存量 sys_approver 记录 teaId=null,改造后这些审批人不出现在选人树(白名单按 teaId 过滤) |
审核设置列表「教师ID」列显示「未绑定」Tag 提示逐条编辑补绑;可选提供一次性脚本:按 account 匹配本校教师名单反查 teaId 批量回填(UPDATE sys_approver a SET tea_id=(SELECT user_tea_ser_i_d FROM ...) WHERE ...,需人工核对) |
| R2 | 池为空时写通知无法选审批人 | 前端 loader 检测空池 → warning 提示引导去审核设置;不降级为全量教师树(保持白名单语义) |
| R3 | 草稿含池外教师,提交时被拦 | 预期行为(草稿宽松、送审严格);前端 catch 后端报错并高亮问题步骤 |
| R4 | 教师离职/调岗后池内 teaId 失效 | 停用时池过滤自动排除;Approve/Reject 按已落快照走不受影响;建议定期核对池与教师名单 |
| R5 | UserCheckModal 新增 multiple prop 影响现有调用方 |
默认 true,接收班级/审批流弹窗不传行为不变;仅审核设置传 false |
| R6 | addApprover 按 teaId 解析 account 依赖 getSchTeaList 返回登录账号字段SchTeaList 无登录账号字段(仅 teaName/userTeaSerID/teaNo/teaTel);改走用户中心 YschBaseOpenApi.getUserById(teaId).getLoginName() 解析 account(currentAccount() L155 同口径),解析失败报错提示手填 |
实施按核实结论执行 |
| R7 | devtools 热重启对 DictService/NoticeService 签名变更可能不生效 | Phase 1 完成后 IDEA 手动重启服务再联调 |
五-B、v7 补充(2026-09-14 · 旧系统截图对照)
补充资料:旧系统「审批列表」「公告详情」截图两张。对照结论:
| # | 截图证据 | 结论 / 范围补充 |
|---|---|---|
| S1 | 旧审批列表列序:序号/标题/来源部门/发布人/提交时间/审批状态/审批意见/操作;待审批行审批意见恒「—」 | 补充 4.5:恢复「审批意见」列(v2-E6 曾以死字段删除)。后端 NoticeVo 增 lastOpinion(最新轮末条处理记录意见:驳回行=驳回意见、通过行=末步通过意见、pending/none=null),fillApprovalFields 从已批量加载的 records 装配;前端列 render row.lastOpinion || "—" |
| S2 | 旧详情审批流面板:每步「审批人:…/待其签:刘文娟/朱皓东 通过·时间·意见」 | 确认 4.1「当前审批人」列口径=当前步待签人姓名(与详情 approvalFlow[].pending 同源);详情渲染已具备,无需改 |
| S3 | 旧列表操作列:非本步审批人显示灰字「等待他人审批」,本步待签人显示通过/驳回图标 | 与现 canApprove 逻辑一致,不改 |
补充实施项(并入 Phase 1 / Phase 4):
| # | 文件 | 改动 |
|---|---|---|
| 1.11b | NoticeVo.java |
增 private String lastOpinion;(最新轮末条处理意见;pending/none=null) |
| 1.12b | NoticeService.fillApprovalFields |
装配 lastOpinion:status∈{rejected, approved(带步骤)} 时取最新轮按 id 末条 record(record 表 action=1 通过/2 驳回)的 opinion |
| 4.5 | ApprovalList.vue + types/notice.ts |
增「审批意见」列(lastOpinion,width 160,tooltip,空显「—」),置于「当前审批人」列后;NoticeVo 类型增 lastOpinion?: string | null |
六、实施记录(2026-09-14 实施回填)
| 项 | 状态 | 备注 |
|---|---|---|
| 1.1 getApproverList 返 teaId | ✅ | DictService 返回 Map 增 teaId |
| 1.2 addApprover 接 teaId + 自动解析 account | ✅ | account 空且 teaId>0 时 YschBaseOpenApi.getUserById(teaId).getAccount() 解析(R6 核实:SchTeaList 无账号字段、BoaUser 用 getAccount 非 getLoginName);解析失败报「请手动填写账号」 |
| 1.3 updateApprover 接 teaId | ✅ | teaId>0 更新绑定、=0 清空(null 不改) |
| 1.4 IDictService 签名同步 | ✅ | 含 javadoc(teaId=0 清空语义) |
| 1.5 DictController.addApprover body 改 Map<String,Object> | ✅ | 新增 asString helper 防 String.valueOf(null)="null" |
| 1.6 DictController.updateApprover 取 teaId | ✅ | 复用 toInteger |
| 1.7 bindApprovers 增 validatePool 重载 | ✅ | 池校验抽为 requireInApproverPool helper(池外/停用/未绑定 teaId 报「请在「审核设置」中维护」) |
| 1.8 buildSteps 增 validatePool 参数 | ✅ | 教师ID路径两处 bindApprovers 透传;补充:草稿已存定义复用早退分支也按 validatePool 校验(堵「沿用草稿流程」绕过池校验漏洞) |
| 1.9 saveFlowDefinition 传 false | ✅ | 草稿/编辑暂存不校验池 |
| 1.10 doSubmitApproval 传 true | ✅ | 正式送审校验池 |
| 1.11 NoticeVo 增 pendingApproverNames | ✅ | 另增 lastOpinion(1.11b,审批意见列) |
| 1.12 fillApprovalFields 装配待签人 | ✅ | 新增 pendingNamesOf(账号路径按账号比对/教师ID路径按姓名快照比对、仅最新轮)与 lastOpinionOf;顺带修复 toFlowVos 待签恒空缺陷(教师ID路径无账号快照 + 旧轮记录计入已处理,详情「待其签」恢复,对齐旧系统截图 S2) |
| 1.13 mvn compile 通过 | ✅ | 首轮报 BoaUser.getLoginName 不存在→改 getAccount 后 EXIT=0 |
| 2.1 ApproverVo 增 teaId | ✅ | 另增 ApproverPoolItem 接口 |
| 2.2 notice.ts 恢复 approverPool + 入参增 teaId | ✅ | approverPool(): GET dict/ApproverPool 恢复(v3 曾删) |
| 2.3 ApprovalSetting 弹窗选择教师 | ✅ | 「选择教师/重新选择教师」按钮 + UserCheckModal 单选教师树;选中自动填姓名+teaId;账号已选教师时禁用(后端解析);「账号或教师二选一」前置校验 |
| 2.4 列表教师ID列 | ✅ | 空显灰色「未绑定」Tag |
| 2.5 Alert 文案更新 | ✅ | 白名单语义 |
| 2.6 UserCheckModal multiple prop | ✅ | 新增 enforceSingle:单选模式组节点勾选回退+提示、叶子勾选取消其他已选(callbackFlag=false 防递归);接收班级/审批流弹窗不传行为不变 |
| 3.1 pickerSources 池过滤 | ✅ | loader 改 loadPickerTree(教师树 ∩ approverPool 的 teaId 集合);空池 warning「请先在「审核设置」中添加并启用审批人」返回空树,不降级 |
| 3.2 filterTreeByTeaIds 工具函数 | ✅ | 组件内 filterTreeByPool(叶子按集合保留、空组剪枝) |
| 3.3 记忆带入池外标注 | ✅ | 池外教师 chip 姓名追加「(不在审批人名单)」,提交由后端池校验拦截 |
| 3.4 编辑回显池外兼容 | ✅ | 回显 chip 走 names 兜底不受池影响;重开弹窗池外教师不在树中勾选态丢失(登记边界);保存含池外 teaId 后端报错由页面 catch 提示 |
| 4.1 审批中心当前审批人列 | ✅ | pendingApproverNames 顿号拼接、空显「—」 |
| 4.2 NoticeVo 类型增字段 | ✅ | pendingApproverNames/lastOpinion |
| 4.3 ApprovalQuery status→auditState | ✅ | 旧名保留 deprecated 别名 |
| 4.4 approvalList 参数改 auditState | ✅ | ref/模板 RadioGroup/run() 同步改名 |
| 4.5 审批意见列(截图 S1 补充) | ✅ | lastOpinion 空显「—」,置于「当前审批人」列后 |
| 5.1-5.5 Mock 修复 | ✅ | 种子补 teaId(对齐 getTeaTree 教师 id 1/2/3/4/7/11);Approvers→Approver 单数;ApproverPool 补 teaId;Add/UpdateApprover 处理 teaId(account 空按教师树 mock 解析);ApprovalList 兼容 auditState;toVo 两处补 pendingApproverNames/lastOpinion;补 ApproverSuggest/ApproverResolve 端点(getTeaTree 已由 v8 补齐) |
| 6.1 后端编译 | ✅ | mvn compile EXIT=0 |
| 6.2 前端构建 | ✅ | npm run build 44.55s 通过(仅 chunk 体积警告) |
| 6.3-6.8 人工冒烟(需登录环境) | ⏳ | 待冒烟:审核设置选教师落库/列表 teaId 列;选人树仅池内教师+空池提示;送审池校验拦截/草稿暂存不拦;审批中心两列显示;存量待审批单 Approve/Reject 不受影响;旧链路回归 |
v8(2026-09-14)· 修复 v6 审批人教师树「显示异常」(对齐 v5 指定班级的样式与接口口径)
现象:发公告→审批→「选择审批人」弹窗内教师树不显示(面板空白,控制台报 TypeError)。 要求:参考 v5「指定班级」选择弹窗的样式与接口口径重做。
一、根因(源码级确认)
| # | 位置 | 问题 |
|---|---|---|
| R1 | UserCheckModal.formatTree |
if (typeof element.children !== "undefined" && element.children.length > 0):typeof null === "object",而 getTeaTree/getStuTree 的人员叶子 children 显式为 null(SchTreeNodeDto 注释「叶子为 null」;后端 application.yml 未配 jackson.default-property-inclusion: non_null,Jackson 输出 "children":null)→ null.length 抛 TypeError → createTree 建树中断,弹窗树整体空白。v6 §一.1 假设「教师叶子无 children」不成立 |
| R2 | UserCheckModal.currentTreeFlatten |
解构默认值 children = [] 同样不拦截 null → flatten(null) 抛错,搜索框一输入即炸 |
| R3 | UserCheckModal.createTree 回显 |
getNodesByFilter(userids.includes(id)) 未限定叶子:teaId 与组节点 id 撞号时整组被误勾(教师ID/组ID 同空间风险) |
| R4 | ApprovalFlowEditor |
直接把 getTeaTree(toUser=true) 原始树塞给弹窗(未像 v5 班级树那样前端归一化);且 :show-selected="false" 单栏窄树与指定班级样式不一致;teaNames 仅靠弹窗提交回填 → 编辑回显 chip 只能显示「ID xxx」 |
| R5 | mock | 缺 base/getTeaTree(v5/v6 两轮登记、v7 Phase 5.3 排期),mock 下无法验证审批选人 |
二、修复计划
UserCheckModal.vue(通用防御,所有调用方受益):formatTree统一children || []归一(叶子删除 children 键,避免 zTree 内部同样踩 null);currentTreeFlatten空值兜底;回显勾选限定type === 2叶子节点。既有调用方(接收班级/应用设置/联系组,叶子本就无 children 键)行为不变。ApprovalFlowEditor.vue(对齐指定班级口径):- 教师树 loader 归一化(同
ClassScopeInput.loadTree/toGradeTree模式):缓存getTeaTree?toUser=true结果 → 重建纯净节点(叶子不带 children 键、isParent显式标注、无教师的空组分支剪除),弹窗侧与姓名解析共用一次请求; - 弹窗改穿梭式(去掉
:show-selected="false",720px + 右侧已选列表/全选/计数),与指定班级同款(撤销 v6-5 的单栏收敛,仅审批弹窗恢复,showSelectedprop 保留供其他场景); - 树加载后用叶子回填
teaNames,编辑回显 chip 直接显示教师姓名(ApproverResolve/详情 names 仍作池外兜底)。
- 教师树 loader 归一化(同
mock/noticeapi.ts:补GET /api/base/getTeaTree(学校→部门组→教师,节点结构对齐 SchTreeNodeDto:type/isParent/children:null)——同时作为 R1 归一逻辑的回归载体;提前完成 v7 Phase 5.3。- 回归:
npm run build+ 人工冒烟。与 v7 关系:本轮只修「显示」,v7 的池白名单过滤(Phase 3)在归一化 loader 上追加过滤即可,不冲突。
三、回归验证清单(v8)
- 选择审批人弹窗:教师树正常渲染(组📁/教师👤图标区分、展开折叠、搜索教师名命中并勾选)
- 穿梭式:勾选教师进右侧已选列表(组级联全选),全选/计数/删除选中可用;提交后当前步 chips 显示姓名
- 编辑回显:已提交审批流 chips 直接显示姓名(不再「ID xxx」);重开弹窗已选教师自动打勾
- teaId 与组 id 撞号时不误勾整组
- 接收班级弹窗(v5)、应用设置/联系组等其他 UserCheckModal 调用方行为不变
- mock 模式:审批选人弹窗可加载教师树(含 children:null 叶子,验证归一化)
npm run build通过
四、实施记录(v8 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| R1-R3 UserCheckModal | ✅ 2026-09-14 | formatTree 改 Array.isArray(children) ? children : [] 归一、叶子 delete children 键(防 zTree 二次踩 null);currentTreeFlatten 改 `(data |
| R4 ApprovalFlowEditor | ✅ 2026-09-14 | 新增 toPickerTree()(对齐 ClassScopeInput.toGradeTree 口径:叶子重建为 {id,name,isParent:false}、空组分支剪除、顺带回填 teaNames)+ loadTeaTree() 缓存 promise(失败置空可重试);弹窗去掉 :show-selected="false" 恢复穿梭式(showSelected prop 保留供其他场景);setFlow 末尾 warmTree() 补齐回显姓名 |
| R5 mock getTeaTree | ✅ 2026-09-14 | 补 GET /api/base/getTeaTree(学校→部门组→教师,人员叶子 children:null 复刻真实序列化,兼作 R1 回归载体);v7 Phase 5.3 提前完成 |
| 根因验证 | ✅ 2026-09-14 | .scratch/verify-tree.mjs 对照跑旧/新建树逻辑:旧实现抛 TypeError: Cannot read properties of null (reading 'length')(复现显示异常);新实现组 type=1、教师 type=2、叶子无 children 键;teaId 与组 id 撞号时回显命中数 旧 1(误勾整组)→ 新 0 |
| 构建回归 | ✅ 2026-09-14 | npm run build 通过(31.85s,仅 chunk 体积警告);其他 UserCheckModal 调用方(接收班级/应用设置/联系组/死代码 ClassCheckInput)叶子本就无 children 键,归一逻辑对其为空操作,行为不变 |
待人工冒烟(需登录环境):§三 v8 清单逐条过——重点真实教师树(多组重复教师、深层部门)渲染与搜索、穿梭式勾选/全选/计数、编辑回显 chip 姓名。与 v7 衔接:Phase 3 池白名单过滤在 toPickerTree 前追加 filterTreeByTeaIds 即可,无需再改弹窗。
v9(2026-09-14)· 应用标题「校园通知」→「校园公告」
需求 1:产品对外名称改为「校园公告」。仅文案层,不改路由、接口与模块 modId。
一、标题来源盘点
| # | 位置 | 作用 |
|---|---|---|
| T1 | vite.config.ts → createHtmlPlugin.injectOptions.data.title |
浏览器标签页 <title>(index.html 里是 <%- title %> 占位,改模板无效) |
| T2 | src/App.vue layoutData.appName |
侧边栏顶部 <h4> + 面包屑首项(layout.vue 第 251 / 312 行消费);App.vue 不调 GetLayout,故这就是真实显示值 |
| T3 | src/components/layout.vue appName computed 兜底 |
props 缺省时的兜底文案 |
| T4 | src/assets/mock/noticeapi.ts base/GetLayout 的 topName |
mock 平台应用名(保持与线上一致口径) |
| T5 | README.md 主标题 |
仓库文档标题 |
不改动:代码注释里的历史称谓「校园通知改造 / 校园通知测试台」(指向 v1–v8 方案条目,改动会破坏可追溯性);菜单项「通知统计」「已收到/待收到」等业务文案;后端 E:\YHXT\java\CampusNotice 无标题字段,平台侧模块名由用户中心库表配置(如需同步改名在平台管理后台处理)。
二、修改计划
T1–T5 五处字面量替换为「校园公告」;dist/ 为构建产物,随下次 npm run build 覆盖,不手改。
三、回归验证清单(v9)
- 浏览器标签页标题显示「校园公告」
- 侧边栏顶部应用名、面包屑首项显示「校园公告」
- mock 模式下布局正常
npm run build通过且产物index.html标题为「校园公告」
四、实施记录(v9 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| T1–T5 | ✅ 2026-09-14 | 五处字面量替换完成;改动前原文件备份至 .backup/20260914-v9-title/(两工程均非 git 仓库) |
| 构建回归 | ✅ 2026-09-14 | npm run build 通过(29.76s,仅 chunk 体积警告);产物 dist/index.html 的 <title> 已为「校园公告」 |
未改动的同名串(有意保留):源码注释里的历史称谓;mock 内非标题测试数据(附件名 校园通知附件N.png、占位图文字 校园通知 Mock 图片)——仅 dev:mock 下可见,不属「标题」范围,如需一并换名后续追加。
v10(2026-09-14)· 左侧栏目名与 GetUserMod.name 绑定(撤销前端改名覆盖)
需求:左侧菜单栏目名称直接绑定接口
base/GetUserMod返回的name,平台改什么前端显示什么。 决策(已确认):完全按接口 name 显示,生效范围 = 侧边栏 + 顶部面包屑;页面内部标题(routes.tsmeta.title、各视图自身文案)不变。
一、现状盘点
| # | 位置 | 现状 | 结论 |
|---|---|---|---|
| M1 | src/App.vue L29 |
baseApi.getUserMod() → layoutData.modList,并透传 children |
数据源已是接口,无需改 |
| M2 | src/components/layout.vue displayModList / menuArr |
侧边栏与面包屑同源消费 normalizeModList(modList) |
改一处即两处同时生效 |
| M3 | src/components/layout.vue MENU_NAME_MAP(L31–49) |
展示层强制改名:已收到→我的通知、已发送→已发管理、发公告→写通知、公告列表→全部通知、审批列表→审批中心、公告统计→通知统计 |
本次移除(v3 结论「前端映射覆盖」被本次需求推翻) |
| M4 | src/utils/menuRoute.ts NAME_ALIASES / ROUTE_BY_TITLE |
按原始 name(映射前)解析路由,仅作为 path 配错时的兜底 | 保留,与显示解耦,不受影响 |
| M5 | normalizeModList 内草稿箱注入(modId 31) |
平台无草稿箱模块 | 保留,仍用前端字面量「草稿箱」 |
| M6 | src/assets/config/menu.ts |
getMenuList() 无人引用(死代码) |
不动,仅在 README 说明其为历史兜底 |
二、修改计划
layout.vue删除MENU_NAME_MAP常量及其在normalizeModList父/子两级中的引用,name直接沿用接口值;注释同步改为「栏目名以平台GetUserMod.name为准」。- 路由解析继续用接口原始
name+modId兜底(M4 逻辑不变)——即显示名与跳转目标彻底解耦,平台改名不会导致菜单跳错页。 - 连带影响(预期行为,非缺陷):若平台库表仍配旧叫法(如
已收到),侧边栏/面包屑即显示旧叫法,需由平台管理后台改name才显示新名;mock 数据(已收到/已发送/审批列表)会直接呈现这一变化。 - README 菜单说明补一句「栏目名不做前端改名」。
三、回归验证清单(v10)
- 侧边栏每项文字 == 接口
GetUserMod.name(mock 下应为 已收到/已发送/全部通知/审批列表/审核设置/通知统计/应用设置 + 注入的草稿箱) - 面包屑模块层级名与侧边栏同源一致;详情/写通知等子页仍取
meta.title - 平台名与内部新名不一致时点击菜单仍跳正确路由(
page失效时NAME_ALIASES/modId兜底仍生效) - 审批中心角标、菜单图标、
isAlone判定不受影响 npm run build通过
四、实施记录(v10 回填)
| 项 | 状态 | 备注 |
|---|---|---|
M3 移除 MENU_NAME_MAP |
✅ 2026-09-14 | layout.vue 删映射表及父/子两处 name 覆盖,normalizeModList 只做「path 归一 + 外链标记 + 草稿箱注入」;全站 grep MENU_NAME_MAP 零残留 |
| README 菜单说明 | ✅ 2026-09-14 | 改为「栏目名直接显示接口 name,前端不改名」,并注明 menu.ts 为无引用历史文件 |
| v2 盘点表定论修正 | ✅ 2026-09-14 | 「菜单契约风险=前端映射覆盖」标注为已被 v10 推翻,防后续会话按旧结论回滚 |
| 备份 | ✅ 2026-09-14 | 改动前 layout.vue / README.md 备份至 .backup/20260914-v10-menu-name-bind/(工程非 git 仓库) |
| 构建回归 | ✅ 2026-09-14 | npm run build 通过(17.90s,仅 chunk 体积警告) |
| 路由解析回归(改名安全性) | ✅ 2026-09-14 | .scratch/verify-menu-name-bind.mjs(esbuild 现场打桩 menuRoute.ts 后 Node 直跑)14/14 PASS:平台旧叫法 + 正确 path 全部命中内部路由;平台改名(重要通知/我发出的/完全没听过的名)时仍按 path 剥离 → 名称别名 → modId 兜底正确解析;外链模块保持整页跳转 |
结论:显示名与跳转目标彻底解耦,平台改 name 即时反映到侧边栏与面包屑;因平台库表仍配旧叫法,当前线上/mock 会显示「已收到 / 已发送 / 审批列表 / 通知统计」等旧名,需平台管理后台把模块 name 改为新叫法(我的通知 / 已发管理 / 全部通知 / 审批中心 / 审核设置 / 通知统计 / 应用设置)后才呈现新文案。草稿箱为前端注入项,名称仍由 layout.vue 字面量决定。
v11(2026-09-14)· 左侧栏目顺序按 GetUserMod.orderNumber 排序
需求:菜单顺序不再依赖平台/SDK 返回行序,直接按接口字段
orderNumber升序排。与 v10 同构:name定名、orderNumber定序,平台侧改完前端刷新即生效。
一、现状盘点
| # | 位置 | 现状 | 结论 |
|---|---|---|---|
| O1 | 后端 UserCenterService.buildModuleTree L265 |
注释「保持 SDK(用户中心)返回顺序,不再按 orderNumber 重排」,roots 与 children 均按扁平列表插入序 |
维持后端不动(本次为前端排序;如需全平台统一口径再改此处的 node.put("orderNumber", …) 消费点) |
| O2 | 后端 L227–228 javadoc | 仍写「子节点按 orderNumber 排序」,与 L265 实现自相矛盾 | 登记为后端文档缺陷(本次不改后端,留待后续单独修) |
| O3 | 后端 L245 | orderNumber = BoaModule.sortNo,null → 0 |
排序键口径:数值,0 可能表示「未配置」 |
| O4 | 前端 App.vue L32 |
layoutData.modList = modList 原样接收,未排序 |
无需改 |
| O5 | 前端 layout.vue normalizeModList |
只做 path 归一 + 外链标记 + 草稿箱 splice,完全不管顺序;children 顺序同样沿用接口 |
本次加排序 |
| O6 | 前端 mock base/GetUserMod |
已带 orderNumber: 1..7,且数组顺序恰好一致 → 现状看不出问题 |
验证需构造乱序输入 |
二、修改计划
src/utils/menuRoute.ts新增并导出sortByOrderNumber(list):以Number(orderNumber)为键升序稳定排序;orderNumber缺失/非数值(NaN、null、undefined)视为未参与比序,统一沉到末尾并保持相互间原序;同值(含全 0)保持接口原行序。放这里与resolveMenuPage同属「平台模块配置 → 前端菜单」的解析层,便于脚本直测。src/components/layout.vuenormalizeModList:顶层与children两级在进入 path 归一之前先过sortByOrderNumber(先定序再做草稿箱splice,草稿箱继续固定紧跟「已发管理」/list,不参与平台序号)。 2-B. 实施时新增的重构:normalizeModList整体从layout.vue下沉到utils/menuRoute.ts(layout 只保留注释与消费),否则「排序 + 归一 + 草稿箱注入」这条管线在 SFC 内无法脚本直测,v11 只能验到一半。- README「菜单说明」补第 3 条:栏目顺序按接口
orderNumber升序。 - 不改后端、不改
routes.ts、不改 mock 数据(mock 的 orderNumber 与数组序一致,排序后无变化,符合预期)。
三、回归验证清单(v11)
- 乱序
orderNumber输入 → 侧边栏按升序输出,且各项page/路由解析结果与顺序无关(.scratch/verify-menu-order.mjs) children子项同样升序;父项顺序决定分组显示顺序- 缺失 / 非数值
orderNumber沉底且不相互打乱;全 0 或全相同 → 与接口原序一致(稳定性) - 草稿箱仍紧跟
/list,即使/list被平台排到中间或末尾 - v10 路由解析回归(
verify-menu-name-bind.mjs)与npm run build仍通过
四、实施记录(v11 回填)
| 项 | 状态 | 备注 |
|---|---|---|
sortByOrderNumber |
✅ 2026-09-14 | utils/menuRoute.ts 导出;键取 Number(orderNumber)(字符串数字可比,如 "2" < "10"),缺失/非数值沉底、同值保持接口原序;显式带原始下标决胜,不依赖引擎 sort 稳定性,且不就地修改入参 |
normalizeModList 下沉(2-B) |
✅ 2026-09-14 | 由 layout.vue 整体搬入 utils/menuRoute.ts:orderNumber 定序(顶层 + children 两级)→ resolveMenuPage 归一/外链标记 → 排序后 splice 注入草稿箱;DRAFTS_MENU 提为常量。layout.vue 只留一行注释说明管线落点,导入改 { resolveMenuPage, normalizeModList }(resolveMenuPage 仍被 flaten 使用,避免 noUnusedLocals 报错) |
| README 菜单说明 | ✅ 2026-09-14 | 「两层兜底」改为「三层处理」,新增第 3 条 orderNumber 升序口径 |
| 备份 | ✅ 2026-09-14 | 改前 layout.vue / menuRoute.ts / README.md → .backup/20260914-v11-menu-order/(基线为 v10 完成后状态) |
| 排序回归(v11 清单) | ✅ 2026-09-14 | .scratch/verify-menu-order.mjs 11/11 PASS:乱序序号升序、children 升序、缺失/非法沉底且互保原序、全 0 不改序、字符串数字参与比序、入参不被修改、undefined/空数组安全、/list 排首位与末位时草稿箱均紧跟其后(splice 不越界) |
| v10 回归 + 构建 | ✅ 2026-09-14 | verify-menu-name-bind.mjs 14/14 仍 PASS;npm run build 通过(33.50s,仅既有 CSS * 兼容写法警告与 chunk 体积警告) |
后端零改动:UserCenterService.buildModuleTree 继续透传用户中心行序(L265),排序完全由前端消费层负责,因此其他 GetUserMod 消费方(若有)口径不变。遗留缺陷 O2(L227–228 javadoc 与 L265 实现矛盾)本次未动,建议后续把该 javadoc 改为「保持 SDK 返回顺序,排序由调用方决定」。
v12(2026-09-14)· 界面文案「通知」→「公告」
需求:把界面上的「通知」字样统一改为「公告」,例如「暂无通知」→「暂无公告」。 已确认范围(三项决策):① 只改界面可见文案,源码注释 / JSDoc / 类型契约注释 / README 不动;②
routes.ts的meta.title一起改,同时在menuRoute保留旧叫法兜底;③ 侧边栏栏目名不改前端(v10 定论),只输出平台后台改名清单。
一、现状盘点
全站 src 内「通知」共 150 处,按是否影响显示分为四类:
| # | 类别 | 数量/位置 | 本次处理 |
|---|---|---|---|
| C1 | 界面可见文案(按钮、表单标签、占位符、弹窗标题、Message/MyConfirm 提示、空态文案、Alert 说明) |
约 30 处,分布在 11 个视图/组件 | 改为「公告」 |
| C2 | routes.ts meta.title |
我的通知 / 写通知 / 全部通知 / 通知详情×2 / 通知统计 | 改为「公告」;同时是面包屑子页显示名与 ROUTE_BY_TITLE 匹配键 |
| C3 | 源码注释与 JSDoc(api/module/notice.ts、types/notice.ts、mock/noticeapi.ts、各视图头部说明、App.vue 布局注释) |
约 100 处 | 不动(保留 v1–v11 历史称谓可追溯性,且不影响显示) |
| C4 | 非界面数据/死代码(mock 种子标题与 mock 菜单 name、assets/config/menu.ts 无引用兜底表、ClassCheckInput.vue/UserCheckInput.vue/PushScopePanel.vue 未被 import 的旧链路组件) |
其余 | 不动(仅 dev:mock 或不可达路径可见) |
特殊保留:NoticeAccept.vue 回执提示中的「请确认已通知到学员/家长」是动词用法,只把同句主语的「本通知」改为「本公告」,动词不动。
二、修改计划(C1 + C2 逐处)
router/routes.ts:我的通知→我的公告、写通知→写公告、全部通知→全部公告、通知详情→公告详情(两条详情路由)、通知统计→公告统计;已发管理、草稿箱、审批中心、审核设置、应用设置本就不含「通知」,不动。utils/menuRoute.ts:NAME_ALIASES已含写通知/我的通知/全部通知/通知统计旧名 → 路由的映射,保持不动即为旧兜底;新叫法(我的公告/写公告/全部公告/公告统计)在改名后由ROUTE_BY_TITLE直接命中,无需新增条目。本次仅在该表补注释说明「C2 改名后旧平台名依赖本表兜底,勿删」。- 视图与组件文案:
NoticeList.vue:获取列表失败提示、删除/撤回确认文案、操作列「查看通知/删除通知」、写通知按钮NoticeDetails.vue:加载失败提示、待审批 Alert、空态「通知不存在或已被删除」NoticeAccept.vue:加载失败提示、左列表空态与右侧空态「暂无通知」、回执 Alert 主语句DraftList.vue:写通知按钮、提交审批弹窗「通知:标题」NewNotice.vue:标题超长校验、正文占位符、加载失败提示、删除弹窗标题、「通知标题:」标签与其占位符ApprovalList.vue:通过成功提示、操作列「查看通知」、通过/驳回弹窗的「通知标题」标签、驳回通知弹窗标题、会签或签提示ApprovalSetting.vue:白名单 Alert「发布通知选择审批人时…」NoticeSetting.vue:部门排序 Alert 中的「写通知」NoticeStats.vue:「通知统计功能开发中…」ClassScopeInput.vue:占位符「选择通知的班级」
- 不改后端、不改接口路径与
modId契约、不改dist/产物。
三、回归验证清单(v12)
grep -rn "通知" src/views src/components src/router剩余命中全部为注释/未引用组件/mock 之外的白名单项- 面包屑:
/list/new显示「已发管理>写公告」,详情页显示「…>公告详情」 - 平台模块名仍为旧叫法(
已收到/已发送/全部通知/审批列表/通知统计)时,菜单点击仍解析到正确路由(verify-menu-name-bind.mjs全绿) - 「请确认已通知到学员/家长」动词未被误改
npm run build通过
四、实施记录(v12 回填)
| 项 | 状态 | 备注 |
|---|---|---|
C2 routes.ts meta.title |
✅ 2026-09-14 | 6 条改名:我的通知→我的公告、写通知→写公告、全部通知→全部公告、通知详情→公告详情(modId 23/24 两条)、通知统计→公告统计;已发管理/草稿箱/审批中心/审核设置/应用设置 不含「通知」,未动 |
menuRoute.ts 兜底加固 |
✅ 2026-09-14 | NAME_ALIASES 条目一字未改(旧名即本次兜底),仅补注释说明「ROUTE_BY_TITLE 不再注册旧标题,旧平台名依赖本表,勿删」 |
| C1 界面文案(10 个在用文件) | ✅ 2026-09-14 | NoticeList(6)、NoticeDetails(3)、NoticeAccept(4)、DraftList(2)、NewNotice(6)、ApprovalList(6,含「通知标题」标签 2 处)、ApprovalSetting(1)、NoticeSetting(1)、NoticeStats(1)、ClassScopeInput(1),共 31 处 |
| 动词用法保留 | ✅ 2026-09-14 | NoticeAccept.vue L253 改为「本公告需学员阅读确认,请确认已通知到学员/家长。」——句内动词「通知」原样保留 |
| C4 的一处例外 | ✅ 2026-09-14 | ClassCheckInput.vue L9 占位符(旧链路组件、全工程无 import)一并改为「选择公告的班级」,避免其在被引用的 ClassScopeInput 口径不一致;该文件改前基线也补入备份目录 |
| 备份 | ✅ 2026-09-14 | 14 个文件(含 执行计划任务方案.md)→ .backup/20260914-v12-notice-to-announce/(保持 src/... 相对目录结构;工程非 git 仓库) |
| 菜单解析回归 | ✅ 2026-09-14 | 重新 esbuild 打桩 .scratch/menuRoute.bundle.mjs 后跑 verify-menu-name-bind.mjs 14/14 PASS、verify-menu-order.mjs 全部通过;另补测新/旧叫法各 5+4 项:新名(我的公告/写公告/全部公告/公告统计/公告详情)经 ROUTE_BY_TITLE 命中,旧名(我的通知/写通知/全部通知/通知统计)经别名命中,同一路由不分裂 |
| 构建 + 产物抽查 | ✅ 2026-09-14 | npm run build 通过(19.38s,仅既有 chunk 体积警告);dist 内命中「暂无公告/写公告/公告详情/选择公告的班级」,旧串「暂无通知 / >写通知< / 通知不存在」零残留 |
结论与遗留:
- 侧边栏栏目名本次未动前端(决策③)。改名后
meta.title只影响面包屑子页与标题匹配,侧边栏与面包屑的模块层级名仍等于平台GetUserMod.name。要让左侧显示「公告」口径,需在平台用户中心后台把模块name改为:我的通知→我的公告、全部通知→全部公告、通知统计→公告统计(已发管理、审批中心、审核设置、应用设置无需改;若平台当前仍配旧叫法已收到/已发送/审批列表,一并改为我的公告/已发管理/审批中心)。前端路由解析对新旧叫法均已兜底,平台何时改、改不改都不会跳错页。 - 代码注释/JSDoc/
README/mock 种子数据仍写「通知」(决策①),其中routes.tsL19/L33–34、NoticeStats.vueL5 等注释直接引用了已改名的显示文案,后续若要做全量口径统一可按 C3 单独立项。 - mock(
dev:mock)下左侧菜单仍显示「全部通知/通知统计」,属平台数据镜像,非缺陷。
v13(2026-09-14)· 审批中心:tab 显示统计数字 + 「待我审批」改名「待审批」+ 修复待审批恒空缺陷
一、需求与现状盘点
用户三点需求:① 待审批/已通过/已驳回/全部四个标签页显示各自条数;② 「待我审批」文案改「待审批」;③ 待审批 tab 未显示应看到的单子(缺陷)。
| 现状 | 位置 | 说明 |
|---|---|---|
| 四 tab 仅文字无数字 | ApprovalList.vue L186–191 RadioGroup |
数字来源缺失:后端只有 GET /api/notice/ApprovalCount(单一「待我审批」数,供菜单角标) |
| 文案「待我审批」 | 同上 L187 | 需改「待审批」;左侧菜单「审批中心」角标继续用 ApprovalCount,不改口径 |
| pending 过滤 = 当前步审批人快照含我 | NoticeService.approvalList L1067–1070 + approverMatchPredicate L1102–1125 |
口径经确认维持不变(仍只列需我审批的单),空列表属匹配链缺陷 |
二、证据表(本地库 campus_notice 直查,2026-09-14)
| 编号 | 证据 | 结论 |
|---|---|---|
| S1 | notice 表 5 条 status=2 待审批单:approver_accounts 全为 NULL,approver_tea_ids 分别 [10007]/[10007]/[10008]/[10007,10008,10006]/[10007,10006,10008] |
v7 前端教师树选人只走教师ID路径,buildSteps L841–851 该路径从不写账号快照(只有 step 表 approver_names),账号匹配条件恒落空 |
| S2 | DefaultLoginUserProvider(yh-auth-spring-boot-starter 反编译):getLoginUser 用 token 串构造 LoginUser 并 setLoginName/setDisplayName;LoginUser(String) 单参构造把同一串写进 userId 字段 |
LoginUser.userId 实际是登录名(如 lijing),不是数字用户ID |
| S3 | NoticeService.currentTeaId() L170–180:Integer.valueOf(userId.trim()) 抛 NumberFormatException → 返回 null |
教师ID匹配条件也恒缺席 → approverMatchPredicate 的 cb.or() 无任何可命中谓词 → 待审批 tab 恒空;同一缺陷使 canApproveNow/requireStepApprover/requireApprover 全部失效(通过/驳回会报「您不是本步骤的审批人」) |
| S4 | YschBaseOpenApi.getUserInfo(account) 返回 BoaUser,有 getId()(用户中心ID,与教师树叶子 teaId 同空间)与 getAccount();工程内已有按账号反查用法(resolveUserName L2138) |
用登录名反查 teaId 的兜底通道可用 |
| S5 | 审批动作表 notice_approval 的 operator_account=lijing(登录名)与 S1 的 teaId 快照分属两个 ID 空间 |
印证修复需双路:读侧解析 teaId + 写侧补账号快照 |
三、错误清单与修复计划
| 编号 | 缺陷/需求 | 修复 | 落点 |
|---|---|---|---|
| E1 | 待审批恒空(S1–S3) | currentTeaId():userId 非数字时回退 YschBaseOpenApi.getUserInfo(currentAccount()).getId()(异常/查无 → null,保持现语义) |
NoticeService.currentTeaId |
| E2 | 教师ID路径缺账号快照,OpenAPI 故障时无法匹配 | buildSteps 教师ID路径与旧包装步:按 teaId getUserById(teaId).getAccount() 补 step.approverAccounts(查不到跳过),主表当前步快照随首步自动带上 |
NoticeService.buildSteps |
| E3 | 四 tab 无数字 | 新增 GET /api/notice/ApprovalTabCount → {pending,approved,rejected,all},口径与各 tab 列表谓词一致(不含 keyword);菜单角标仍用 ApprovalCount |
NoticeController + INoticeService + NoticeService.approvalTabCount + 新 VO ApprovalTabCountVo |
| E4 | 「待我审批」文案 | 改「待审批」;数字后缀显示,为 0 不显示 | ApprovalList.vue |
| E5 | 前端契约 | notice.ts 加 approvalTabCount();types/notice.ts 加 ApprovalTabCountVo;mock noticeapi.ts 补 /api/notice/ApprovalTabCount(同口径) |
前端三件套 |
四、回归验证清单(v13)
mvn compile通过(JAVA_HOME=jdk-1.8,BUILD SUCCESS)- lijing(10008) 登录后「待审批」可见单 3/4/5(或新送审单),且能实际通过/驳回(待浏览器验证)
- 四 tab 数字与各自列表条数一致;0 值不显示数字(待浏览器验证)
- 「审批中心」菜单角标仍为待我审批数(
ApprovalCount端点与layout.vue未改) npm run build通过(18.87s,仅既有 chunk 体积警告)
五、实施记录(v13 回填)
| 项 | 状态 | 备注 |
|---|---|---|
E1 currentTeaId() 回退 |
✅ 2026-09-14 | NoticeService:userId 非数字时 YschBaseOpenApi.getUserInfo(登录名).getId() 反查;异常/查无仍返回 null。读侧 approvalList/ApprovalCount/fillApprovalFields/canApproveNow/approve/reject 全部受益 |
| E2 教师ID路径补账号快照 | ✅ 2026-09-14 | buildSteps 教师ID分支与旧包装步调用新私有方法 bindAccountsByTeaIds(逐个 teaId getUserById().getAccount(),失败跳过,全空保持 null);主表当前步快照随首步自动带上账号 |
E3 ApprovalTabCount |
✅ 2026-09-14 | 新 VO model/notice/ApprovalTabCountVo;tab 谓词从 approvalList 抽为共用方法 approvalStatePredicates(语义不变);countApprovalState 只加多租户过滤、不带 keyword;Controller 加 GET 端点 |
| E4 前端 tab | ✅ 2026-09-14 | ApprovalList.vue:「待我审批」→「待审批」,四标签渲染 (n) 后缀、0 不显示;run() 并行 loadTabCounts()(进入/翻页/审批操作后自动刷新,统计接口异常静默保留上次数字) |
| E5 前端契约 | ✅ 2026-09-14 | notice.ts(api) 加 approvalTabCount();types/notice.ts 加 ApprovalTabCountVo;mock 补 /api/notice/ApprovalTabCount 并把 mock 列表 pending/all 口径对齐真实后端 |
| 备份 | ✅ 2026-09-14 | 前端 4 文件 + 后端 3 文件 → 两工程各自 .backup/20260914-v13-approval-tab-count/(非 git 仓库) |
遗留与说明:
- 存量单不回补账号快照:E2 只作用于新送审/重提的单;存量(
approver_accounts=NULL)靠 E1 的 teaId 读侧解析匹配,用户中心不可用时这批老单仍会漏列。如需彻底补齐可一次性脚本回写,暂不立项。 - 存量脏数据:
notice 3步骤表存在同 step_no 重复行(改造前遗留),fillApprovalFields/canApproveNow按findByNoticeIds顺序取首个审批中步,展示可能抖动;notice 1/2无步骤行走approveLegacy旧或签兜底,本轮不清洗。 - 会签+教师ID路径:
approve()的会签步完结判定按stepAccounts比对(E2 补上账号后对新单生效);存量教师ID会签单仍会卡在会签判定,属旧缺陷,建议单独立项回归。 - IDEA Debug 实例带 devtools:
mvn compile已更新target/classes,一般会自动热重启;若未生效请在 IDEA 里 Build 一次或重启实例(重启前先kill-port.bat 8101)。
v14(2026-09-14)· 写公告页底部按钮:保存→存为草稿 + 新增直接发布 + 固定排列
需求(截图:编辑态底部为
保存 / 提交审批 / 定时发送 / 删除):① 「保存」改「存为草稿」;② 增加「直接发布」;③ 排列顺序直接发布 → 存为草稿 → 提交审批 → 定时发送(删除仍居末)。
一、接口口径证据(决定改法,2026-09-14 实测/读码)
| 编号 | 证据 | 结论 |
|---|---|---|
| S1 | 后端 NoticeController 全量 mapping 无 Publish;发布只有 Create(publish→publish())、SaveDraft、Edit、Submit |
不存在「发布一条已存在记录」的接口 |
| S2 | NoticeService.edit() L1555–1615:只更新内容/范围/附件/流程定义,全程不改 status、不调 doRealPublish |
编辑态点任何按钮都不会让草稿变成已发布 |
| S3 | NoticeService.publish() L204–249 按 publishType 分流(draft/approve/direct),saveDraft() L254 与 publish() 均 entity.setId(null) |
「直接发布」的正确落点是 Create + publishType:"direct",且必然产生新记录 |
| S4 | api-test.html submitCompose('direct') → body.publishType='direct' 且 delete body.flow → doSubmitCompose 在 mode==='edit' 时只调 /notice/Edit |
测试台编辑态本就没有直接发布按钮(只有 保存修改/提交审批/删除),本次是前端新增能力,契约沿用 Create |
| S5 | NoticeRepository.findDueTimingPublish L107 谓词 n.status = 1 and timingSendTime<=now and timingPublished=0 |
编辑态「定时发送」今天只 Edit 存了时间、状态仍为草稿 → 到点不会被调度(现存缺陷) |
| S6 | doSubmitApproval L496–498:步骤为空即抛「请选择审批人」 |
无法借 Submit 实现无审批发布 |
二、已确认决策
- 「直接发布」调
Create(publishType:"direct",不带 flow),成功后删除原草稿记录,避免同内容两条(决策来源:用户指定按 api-test 的直接发布接口实现)。 - 已驳回(status=3)不给发布类按钮:只能改对内容后重新「提交审批」;其「存为草稿」= 仅保存内容,状态保持已驳回。
- 编辑态「定时发送」一并改走
Create+timingSendTime,真正排期(修 S5 缺陷)。 - 已勾选审批流程且已选审批人时,点「直接发布/定时发送」拦截并提示走「提交审批」;该拦截同时作用于新建态(新建态原行为是静默跳过审批直接发布,属同类绕过,一并按决策口径收紧)。
三、按状态分化的按钮矩阵(目标态)
| 页面状态 | 判定 | 按钮(左→右) |
|---|---|---|
| 新建 / 转发 | !isEdit |
直接发布 · 存为草稿 · 提交审批 · 定时发送 |
| 草稿续编 | isEdit && editStatus===0 |
直接发布 · 存为草稿 · 提交审批 · 定时发送 · 删除 |
| 已驳回重提 | isEdit && editStatus===3 |
存为草稿 · 提交审批 · 删除 |
| 旧链路已发布修订 | isEdit && editStatus===1 |
保存 · 提交审批 · 定时发送(Edit 语义,按钮不叫「存为草稿」以免名实不符;带 pushScopes 的已发布记录后端本就拒绝编辑) |
四、修改计划(仅 src/views/notice/NewNotice.vue)
- 新增 computed
publishViaCreate = !isEdit || editStatus===0(发布类动作可用)与draftBtnText(isEdit && editStatus===1→ 「保存」,否则「存为草稿」)。 doAdd:- 入口新增审批拦截——
act!=='draft' && act!=='approve'且flowModel.enabled && flow.length→Message.info("已配置审批流程,请点击「提交审批」发布")并 return; - 编辑分支拆两路:
draft/approve仍走sendEdit(approve追加sendSubmit,逻辑不变);direct/isTime且publishViaCreate→sendCreate({...buildBody(false), publishType:"direct", [timingSendTime]}),成功后sendDelete(serid),删除失败只Message.warning提示去草稿箱手动删除(发布已成,不回滚);direct/isTime且editStatus===1→ 维持原sendEdit行为; - 成功文案:定时 → 「已设置定时发送,到点自动发布」,直接 → 「发送成功」,跳转
/list;draft文案按状态区分「草稿已保存」/「已保存,状态仍为已驳回」。 Create路径不带sourceId(避免新公告挂在将被删除的草稿上形成断链血缘)。
- 入口新增审批拦截——
- 模板
bottom-btn:直接发布v-if="publishViaCreate";草稿按钮改用{{ draftBtnText }}并去掉v-if="!isEdit";定时发送v-if="publishViaCreate || editStatus === 1";提交审批、删除维持;顺序按第三节矩阵排列。 - 注释「三态动作区」同步为四态口径。
五、回归验证清单(v14)
- 新建态:
直接发布 / 存为草稿 / 提交审批 / 定时发送顺序与文案正确 - 草稿续编:五按钮齐全;直接发布 →
/notice/Create新记录 status=1 且原草稿从草稿箱消失(代码路径已落地,待联调实测) - 草稿续编:定时发送 → 新记录
status=1 + timing_published=0 + timing_send_time到点被TimingPublishJob触发(S5 缺陷修复,待联调实测) - 草稿续编:存为草稿 → 仍为草稿、留在草稿箱
- 已驳回:无「直接发布」「定时发送」;存为草稿后状态仍为已驳回(留在已发管理,未回流草稿箱);提交审批可走通
- 勾选审批并选人后点直接发布/定时发送 → 被拦截提示
- 删除按钮仍只在草稿/已驳回出现
npm run build通过
六、实施记录(v14 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| 新增 computed | ✅ 2026-09-14 | publishViaCreate = !isEdit || editStatus===0、draftBtnText(isEdit && editStatus===1 → 保存,否则 存为草稿),置于 editStatus 声明之后 |
doAdd 审批拦截 |
✅ 2026-09-14 | 接在既有「勾了审批未选人」校验之后:act==="direct" && flowModel.enabled && flow.length → 提示「已配置审批流程,请点击「提交审批」发布」并 return;isTime 归一为 direct,定时发送同样被拦 |
doAdd 编辑态分流 |
✅ 2026-09-14 | 新增 else if (act==="direct" && publishViaCreate):publishType="direct" → sendCreate → sendDelete(serid)(失败仅 Message.warning("原草稿删除失败,请到草稿箱手动删除"),不回滚已发布结果)→ 文案按 isTime 区分 → 跳 /list;不设 data.sourceId(避免新公告血缘指向待删草稿)。原 else 保留 Edit(draft / approve / 已发布修订),draft 提示按状态拆为「草稿已保存」/「已保存,状态仍为已驳回」 |
| 模板按钮区 | ✅ 2026-09-14 | 固定顺序 直接发布(v-if=publishViaCreate) → 存为草稿/保存({{draftBtnText}} 常显) → 提交审批 → 定时发送(v-if=publishViaCreate || editStatus===1) → 删除(v-if=isEdit && (editStatus===0||3));已驳回态无主按钮时草稿按钮提为 primary(:type="publishViaCreate ? 'default' : 'primary'") |
| 注释同步 | ✅ 2026-09-14 | doAdd JSDoc 由「三态」改「四态」并写清分流;动作区注释改为矩阵口径 |
| 备份 | ✅ 2026-09-14 | 改前 NewNotice.vue → .backup/20260914-v14-compose-buttons/(工程非 git 仓库) |
| 构建 + 产物校验 | ✅ 2026-09-14 | npm run build 通过(40.46s,仅既有 chunk 体积警告);NewNotice-*.js 内 9 个新串(直接发布/存为草稿/提交审批/定时发送/已配置审批流程/已设置定时发送/状态仍为已驳回/原草稿删除失败/保存成功)全部命中 |
| 待人工联调(8101 + CAS 登录) | ⏳ | ① 草稿续编点直接发布后 notice 表新增 status=1 行、原草稿不再出现在草稿箱;② 定时发送后 timing_published=0,到点由 TimingPublishJob 圈人发布(本次修复的 S5);③ 已驳回单「存为草稿」后 status 仍为 3 |
结论与提示:
- 后端零改动。编辑态「直接发布」按 api-test 契约落
Create,必然生成新公告 ID(原草稿被删除)——这是现有接口能力下唯一正确解。若希望「同一条草稿原地发布、保留 ID 与审批历史」,需后端补notice/Publish?serid=(复用publish()的 direct 尾段 +doRealPublish),届时前端把该分支换回单次调用即可。 - 行为变更提醒:新建态此前允许「勾了审批、选了人,仍点直接发布」静默绕过审批,本次按决策一并拦截(新建 + 编辑一致)。若只想约束编辑态,给该守卫条件加
isEdit.value即可。 - 与并行的 v13(审批中心 tab 数字)无文件交集:v14 只改
NewNotice.vue。
v15(2026-09-14)· 审批中心改造为「已收到」左右分栏 + 审批人名/教师ID 口径
需求四点:① 通过/驳回按钮显示在公告详情里;② 审批详情按钮为
上一篇 / 下一篇 / 通过 / 驳回 / 打印;③ 审批详情显示审批轨迹与审批流程;④ 所有轨迹与流程显示人名,数据库保存同步教师ID(用户中心 teaId,非本项目表自增 id)。
一、现状与接口证据(2026-09-14 读码 + 本地库直查)
| 编号 | 证据 | 结论 |
|---|---|---|
| S1 | NoticeController L73–77 Detail→detailMap、L84–87 GetDetail→getDetailMap;NoticeService.detailMap L2633–2641 两种 mode 都转 getDetailMap |
审批详情应走 GetDetail(不标读),避免审批人点开就把阅读统计搅浑 |
| S2 | getDetailMap L2549–2562 已装配 approvalRecords(loadApprovalRecords L2585)+ approvalFlow(toFlowVos L1402,含每步 records/pending)+ canApprove;isMobiRequest() 不返回 |
需求③后端数据已具备,属前端展示活 |
| S3 | 前端 types/notice.ts L245–290 已定义 approvalRecords/approvalFlow,但 ApprovalRecordVo 缺后端已下发的 assigneeTeaIds |
补一个可选字段即可 |
| S4 | 本地库 notice_approval 6 行 operator_account=operator_name=lijing;notice_approval_record 1 行 approver_name=lijing;notice.publisher_name=lijing |
需求④是真缺陷:凡取自登录态的“姓名”都落成了登录名 |
| S5 | NoticeService.currentName() L161–164 直接返回 user.getDisplayName();v13 已证明 DefaultLoginUserProvider 用 token 串同时喂 loginName/displayName/userId |
根因:displayName 恒等于登录名且无回退 |
| S6 | notice_approval_step:approver_tea_ids=[10007,10006,10008] 正确、approver_names 有真名,但 approver_accounts 全 NULL,历史行含 ["测试教师2","ceshiteacher","李 Jing"](账号被当姓名兜底) |
审批人快照的 teaId 口径本就正确(sys_approver.id 从不下发:SysApprover L33–51,DictService L290/308 仅池列表 UI 用);缺的是“操作人”侧 |
| S7 | NoticeApproval / NoticeApprovalRecord 实体均无 teaId 列;application.yml L24 ddl-auto: update |
加 operator_tea_id / approver_tea_id 两列由 JPA 自动建,无需手写 ALTER |
| S8 | 批量人名通道 classService.getSchTeaList(schId) → SchTeaList{teaName, userTeaSerID}(无 account 字段);resolveUserName(account) L2204 逐条外呼 |
teaId→姓名可批量;account→姓名只能逐条,需去重缓存 |
| S9 | approve(serid, opinion?) L328、reject(serid, opinion?/remark?) L339 无审批人入参;requireStepApprover L681、canApproveNow L1298 判定“我是本步待签人” |
详情区按钮直接复用现有接口,签名不动 |
二、已确认决策
- 左列用已收到同款卡片:标题 +(重要/置顶/状态徽标)+「部门·时间」+ 副行审批进度与当前审批人;顶部保留四 tab(含 v13 条数)与关键字搜索,
pageSize=50滚动。 - 通过/驳回仍弹窗填意见(通过默认“同意”、驳回必填),仅触发点从表格操作列移到详情按钮行。
- 需求④做到写入链 + 表结构层:修
currentName()、两张留痕表补 teaId 列并写入、历史数据出回填脚本。 上一篇 / 下一篇在当前筛选结果列表内移动(与左列选中联动),不复用GetNextPrev(它按已发/已收全局序列,会跳出当前 tab)。
三、修改计划
前端 src/views/notice/ApprovalList.vue(整页重构)
- 布局改
.accept-wrap:左.title-list(tab + 搜索 + 卡片 + 分页)+ 右.detail-content;样式从NoticeAccept.vue移植并补审批块样式(scoped,不外溢)。 - 详情源换
noticeApi.getNoticeDetails(serid);渲染:标题头 + 按钮行(上一篇/下一篇/通过/驳回/打印)+ 元信息(来源部门/发布人/发布时间/接收范围)+ 审批状态 Alert + 审批流程(approvalFlow每步:步名·或签/会签·状态·审批人姓名·每人结论与时间·意见·当前待签人)+ 审批轨迹(approvalRecords时间线)+ 正文 + 附件(预览/下载/全部下载)。 - 交互:
selected索引驱动左列高亮;上一篇/下一篇在page.content内移动并加载详情;通过/驳回成功后同时刷新列表与当前详情(本步流转后进度与待签人会变)。 canApprove=false或状态非 pending 时不显示通过/驳回,改为“等待他人审批 / 该单已处理”文字提示。types/notice.ts的ApprovalRecordVo补assigneeTeaIds?: number[]。
后端 E:\YHXT\java\CampusNotice(需求④)
currentName():displayName为空或等于loginName时回退用户中心真实姓名(异常保持原值),加请求内去重缓存避免多次外呼。NoticeApproval加operator_tea_id、NoticeApprovalRecord加approver_tea_id;saveApprovalRowL1533、saveRecordRowL934 写入currentTeaId()。- 读取兜底:
loadApprovalRecords、toFlowVos在姓名缺失或姓名==账号时补名——先按 teaId 走getSchTeaList批量 map,仍缺的按去重账号逐个resolveUserName。 - 历史回填脚本
.scratch/backfill-approver-name-teaid.mjs:经.scratch/oapi-call.mjs拉本校教师(account↔teaId姓名),生成UPDATE notice_approval / notice_approval_record / notice / notice_approval_step语句,先预览后执行、可重复跑。
四、回归验证清单(v15)
- 审批中心为左右分栏,左列卡片含进度与当前审批人,四 tab 数字仍正确
- 点卡片右侧出详情;
上一篇/下一篇在当前 tab+关键字结果内移动,首/末条置灰 - 详情按钮行含
通过/驳回/打印;非当前审批人只见打印与等待提示 - 通过弹窗默认“同意”、驳回意见必填;提交后列表与详情同时刷新
- 审批流程区显示每步 步名/或签会签/审批人人名/状态/意见;轨迹时间线显示提交与各级处理人人名(存量数据由读取兜底 + 回填双路保障,不再依赖
lijing) - 新送审与新审批动作后,
operator_tea_id/approver_tea_id落用户中心 teaId,姓名落真实姓名(需 8101 + CAS 登录实测) - 回填任务对历史 6 条
notice_approval、1 条notice_approval_record、notice.publisher_name生效且幂等(需带开关重启实测) mvn compile(JAVA_HOME=jdk-1.8)与npm run build均通过
五、实施记录(v15 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| 前端整页重构 | ✅ 2026-09-14 | ApprovalList.vue 由「表格 + 跳详情页」改为 .accept-wrap 左右分栏:左列 tab(含 v13 条数)+搜索+卡片(标题/置顶/重要徽标 + 部门·时间 + 审批进度 + 当前审批人)+ Page simple 分页;右列详情。删除 Table/MyConfirm/useRouter 依赖,详情源改 getNoticeDetails(GetDetail,不标读) |
| 详情按钮行 | ✅ 2026-09-14 | 上一篇 ‹ / 下一篇 › / 通过 / 驳回 / 打印;goStep(±1) 在 page.content 内移动并联动左列高亮,首末置灰(prevDisabled/nextDisabled);通过/驳回 仅 canAct = approvalStatus==='pending' && canApprove 时出现,否则 pending 态显示「等待他人审批」文字 |
| 审批流程 + 轨迹 | ✅ 2026-09-14 | 流程块渲染 approvalFlow[](步名/或签·会签/步骤状态色/审批人姓名/每人 records 结论+时间+意见/pending 待签);轨迹块渲染 approvalRecords[] 时间线(提交·通过·驳回 + 人名 + 意见 + 提交行「选定审批人 N 人」);统一走 personName(name, account),缺值显示「—」而非账号 |
| 动作后刷新 | ✅ 2026-09-14 | run(keepSelected=true):重拉列表与 tab 数,优先保持原公告选中(该单已流转出当前筛选时回落首条),列表与详情同步刷新 |
| 前端类型 | ✅ 2026-09-14 | types/notice.ts 的 ApprovalRecordVo 补 assigneeTeaIds?: number[](后端早已下发) |
| 后端写入链 | ✅ 2026-09-14 | currentName():displayName 空或等于 loginName → cachedUserName(account)(用户中心真名,ConcurrentHashMap 进程内缓存,>2000 清空防无界);saveApprovalRow 落 operator_tea_id、saveRecordRow 落 approver_tea_id(均取 currentTeaId(),签名不变) |
| 后端表结构 | ✅ 2026-09-14 | NoticeApproval.operatorTeaId → operator_tea_id、NoticeApprovalRecord.approverTeaId → approver_tea_id;ddl-auto: update 自动建列,无需手写 ALTER |
| 后端读取兜底 | ✅ 2026-09-14 | loadApprovalRecords 改 for 循环 + nameMissing() 判定 + 懒加载 teaNameMap(schId)(一次教师名单外呼)→ displayApproverName()(teaId → 账号缓存 → 原值);toFlowVos 同步处理步骤审批人姓名快照与每步 records 姓名,teaId 仅在长度与姓名列表一致时用于对齐 |
| 历史回填(方案变更) | ✅ 2026-09-14 | 计划里的 .scratch/backfill-approver-name-teaid.mjs 未采用:实测猜 /oapi/user/* 路径全 404,而应用内已有签名客户端,Node 侧硬编 SDK 路径太脆。改为 INoticeService.backfillApproverIdentity() + job/ApproverIdentityBackfillRunner(ApplicationRunner,@ConditionalOnProperty(campus-notice.backfill-approver-identity=true) 才执行):按账号去重解析后回填 notice_approval(姓名+teaId)、notice_approval_record(姓名+teaId)、notice_approval_step(按 teaId 用教师名单权威姓名覆盖快照)、notice.publisher_name,返回 {trail,record,step,notice,unresolved} 统计;只改需改的行,可重复执行 |
| mock 对齐 | ✅ 2026-09-14 | assets/mock/noticeapi.ts 详情补 approvalRecords: approvalTrailOf(n)(提交行 + 各步 records,按时间升序),此前 dev:mock 无轨迹数据可渲染 |
| 备份 | ✅ 2026-09-14 | .backup/20260914-v15-approval-detail/:ApprovalList.vue、types/notice.ts 改前副本(NoticeService.java 基线为其时磁盘状态) |
| 构建与载荷验证 | ✅ 2026-09-14 | mvn -q compile BUILD SUCCESS(含并行 v16–v18 改动后复验);npm run build 通过(44.21s,仅既有 *zoom SCSS 警告与 chunk 体积警告);dist 内 ApprovalList chunk 命中 15 个新串;起 dev:mock 实测 ApprovalList?auditState=pending/approved/rejected/all 行数 1/2/1/3、ApprovalTabCount {1,2,1,3}、GetDetail?serid=3/5/8 的 approvalFlow(步名/会签·或签/状态/人名/pending/records)与 approvalRecords(提交+通过+驳回,均为人名)字段与模板消费口径一致,验完已关停 8077 端口进程 |
结论与遗留:
- 与并行会话的交叠(重要):本次整页重写落地后,v19/v19.1 在同一文件的左列 tab 样式上继续改造(
nowrap+ 等宽 + 去掉库分段控件的box-shadow/伪元素),v16 扩展了本次新增的cachedUserName(教师ID形态回退getUserById)并加了displayPublisherName,v18 下线「审核设置」、v20 改了NewNotice.applyTiming。当前磁盘状态是这些改动的叠加结果,mvn compile与npm run build均在叠加后复验通过;后续再动ApprovalList.vue/NoticeService.java前必须先重读磁盘。 - 需求④的显示侧已闭环(新数据写入真名 + 存量数据读取兜底 + 可重跑回填);
notice_read.reader_name与notice_approval_step.approver_accounts(历史全 NULL)未纳入本次回填范围——前者由阅读回执展示层的账号→姓名映射兜底,后者 v13-E2 已在写入侧补齐,存量不影响展示(姓名快照走 teaId 路径)。 - 待人工实测两项(需 IDEA 起 8101 + CAS 登录):新送审/新审批动作后两列 teaId 与真名落库;带
-Dcampus-notice.backfill-approver-identity=true重启一次后历史行被改写、二次重启无新增(幂等)。 - 本机无 headless 浏览器(未装 puppeteer/playwright),左右分栏的真实字体渲染与选中态视觉需在
#/approval人工过一眼。
六、v16:发布人显示人名(不再显示教师ID/账号)(2026-09-14)
6.1 需求与错误清单
| 编号 | 现象 | 期望 |
|---|---|---|
| E1 | 列表页「发布人」列、详情页 TeaName、统计图例按发布人分布,显示的是账号(如 lijing)或教师ID(如 10008) |
一律显示教师真实姓名 |
6.2 根因证据(用户中心 oapi 实测,账号 lijing→id 10008)
| 编号 | 证据 | 结论 |
|---|---|---|
| S1 | /oapi/user/byLoginName?loginName=lijing → {id:10008, account:"lijing", name:"李晶"};/oapi/user/byId?id=10008 → 同 name:"李晶" |
姓名可从用户中心解析 |
| S2 | /oapi/user/byLoginName?loginName=10008 → code:0 且 data 空 |
按数字教师ID走 byLoginName 查不到,必须走 byId |
| S3 | 本地 notice.publisher_name 6 行全为 lijing(=publisher_account) |
v15 前旧代码只存 displayName(=登录名)落库,快照即账号 |
| S4 | NoticeService.currentName() 的 cachedUserName 仅调 getUserInfo(String)(=byLoginName) |
当发布人落库值是数字教师ID时解析失败→回退ID本身,即用户所述「数据库保存的是教师id 不是账号」 |
6.3 修复实施(仅后端 NoticeService.java,前端与契约不动)
cachedUserName:byLoginName 查不到且值纯数字时,回退YschBaseOpenApi.getUserById再解析(覆盖教师ID形态)。写侧新发布快照随之落真实姓名。- 新增
displayPublisherName(snapshot, account):快照已是正常人名直返;快照==账号 或 快照为数字ID 时经用户中心(byLoginName→byId 兜底)解析,解析失败保持原值。 toVo统一调用displayPublisherName归一publisherName——列表页与详情页(getDetail/getDetailMap→TeaName)共用toVo,存量错误数据无需回填脚本即刻显示人名。- 统计
byPublisher分组 key 同经displayPublisherName解析,图例不再出现账号/ID。
6.4 回归清单(v16)
- 列表/详情/统计三处「发布人」均显示真实姓名,不再出现
lijing或数字ID - 存量 6 条历史数据在未回填情况下前端即显示人名
- 新发布公告
publisher_name落真实姓名;publisher_account仍为登录标识 - 用户中心不可达时优雅回退原值、不抛异常、有
cachedUserNamewarn 日志 mvn compile(JAVA_HOME=jdk-1.8)通过(已验证 BUILD SUCCESS)
v17(2026-09-14)· 「已收到」三 tab 显示数量(新增后端统计接口)+ 详情 上一篇/下一篇/打印
需求两点,均落在「已收到」
NoticeAccept.vue左右分栏:① 左列全部 / 未读 / 已读三个 tab 显示条数,后端补统计接口;② 右侧公告详情加上一篇 / 下一篇 / 打印。 与 v15(审批中心左右分栏)无文件交集:v17 只动NoticeAccept.vue、新抽的打印工具、NoticeDetails.vue(去重复)、mock、后端Notice*。v15 的「上下篇在当前 tab 结果内移动」是审批中心口径,本页口径不同(见决策 2)。 行号按本轮读码记录;v15/v16 并行会话在NoticeService头部(currentName/cachedUserName/toVo区)持续改动会使命名行号漂移,以方法名为准。
一、现状与接口证据(2026-09-14 读码)
| 编号 | 证据 | 结论 |
|---|---|---|
| S1 | NoticeAccept.vue L189-193 RadioGroup v-model="readType"(0 全部/1 未读/2 已读)三个 Radio 只有文字;L183 顶部「未读 N 条」取 page.unreadCount;L236-239 详情 .ops 只有「打印」 |
tab 无数字、详情无上下篇,属功能缺失而非样式问题 |
| S2 | NoticeController L49-54 List → userList,@SortDefault({"topmost","createTime"} DESC);NoticeService.userList(L1905-1929);userSpec(L1969-1991)已实现 readType=0/1/2 三分支 |
三个 tab 的条数 = 同一 Specification 在 readType=0/1/2 下 count 三次,可直接复用,无需另立查询口径 |
| S3 | 现有 unreadCount(L1921-1922)= count(visibleSpec(account,true)) − noticeReadRepository.countByReaderAccount(account),再 clamp≥0 |
口径不同源:减数不带 schId/状态/范围可见性过滤,读者曾读过但已删除/已撤回/跨校的通知会长期虚减未读数(clamp 把负数掩盖成 0)。新接口以 userSpec 为准,并顺手让 List.unreadCount 复用同一结果 |
| S4 | NoticeService.getNextPrev(L2355-2381)type=1(已收)分支只读 noticeReadRepository.findNoticeIdsByReaderAccount() 再 reverse |
序列只含已读公告;未读公告 indexOf = −1 → prevId/nextId 恒 0,即「未读公告点上下篇没反应」。且不按 status/schId/范围可见性与 keyword/type 过滤,也不含 topmost 排序 |
| S5 | NoticeQuery.orderByTime(L47)后端从未参与排序(appendCommonPredicates L2054-2072 只处理 keyword/type/deptId/时间;PageUtils.toPageable 不读它) |
列表实际顺序恒为 topmost DESC, createTime DESC;相邻查询按同键 + id DESC 做稳定 tie-break;不顺手“修”orderByTime(登记遗留 E2) |
| S6 | 全工程 grep EntityManager 0 命中(Service 只用 Repository);Notice.content 为 LONGTEXT 普通字段(L36-38,非懒加载) |
为拿两个邻居而 findAll(spec, Sort) 会整表加载正文(现有 type=0 分支正是这个反模式)→ 本次用「count 判定 + size=1 定向分页」,每次只落 1 行实体 |
| S7 | 前端 noticeApi.getNextPrevNotice(serid, type) + NextPrevVo 已存在,NoticeDetails.vue L19-20/L38-40/L182-183 已接(navType 随视角取 0/1) |
契约与调用位就绪,v17 只扩展可选筛选参数并保持不传参时的行为可用 |
| S8 | 打印两份逐字重复实现:NoticeAccept.vue L144-164、NoticeDetails.vue L111-131(只有 标题 + 来源/发布人/时间 + 正文) |
缺接收范围、附件清单、回执提示,且两处会持续漂移 → 抽公共工具,两页共用 |
| S9 | 同类范式已在 v13 落地:ApprovalTabCount(Controller L379-381 + ApprovalTabCountVo + ApprovalList.vue L28-38 tabCounts/tabCountText(为 0 不显示)/异常静默保留上次) |
ReadTabCount 的命名、VO 形态、前端刷新与静默策略全部对齐 v13 |
| S10 | /api/* 受 CAS 拦(curl 不可直测),手工联调入口 resources/static/api-test.html(收件箱块 L735-830 已调 List/Detail/GetNextPrev,L273-275 三个 tab 按钮) |
顺带在 api-test 收件箱 tab 上渲染数量,让新接口可在控制台自证 |
| S11 | mock:src/assets/mock/noticeapi.ts L722 /api/notice/List(本地算 unreadCount)、L1006 /api/notice/ApprovalTabCount |
不同步补 mock,则 .env.mock 模式下新 tab 数字空白、上下篇跳不动 |
二、已确认决策(本轮提问回复)
- 数量口径 = 随筛选联动:
ReadTabCount接收与List相同的 keyword / type(另支持 deptId、时间范围)过滤,readType 不作为入参(它就是要统计的三个桶),恒满足全部 = 未读 + 已读。搜索、切类型、标读、一键已读后刷新。 - 上下篇序列 = 后端「我可见的已收公告」序列,沿用当前 keyword / type 筛选,但不吃 readType。
关键理由:在未读 tab 点开第一条即被标读,若序列带 readType=1,当前 serid 已不在该集合内 → 判定「当前公告不在筛选集」→ 上下篇立刻全灰、用户卡在原地。不吃 readType 后,未读/已读两个 tab 共用同一条「全部可见」序列,跳得动。
跨 tab 目标不在左列当前结果内时:若
readType!==0先把左列切回「全部」重载再定位高亮;仍找不到则不高亮(selected=-1),不擅自改用户的关键字/类型筛选。 - 打印抽公共工具并完善:新增
src/utils/noticePrint.ts,纯函数buildPrintHtml(detail)+ 薄封装printNotice(detail);打印件 = 标题 / 来源部门·发布人·发布时间 / 接收范围 / 正文(原样富文本)/ 附件文件名与大小清单 / 需回执时一行提示 / 页脚打印时间;带@media print与 A4 边距,不含按钮等界面元素。NoticeDetails.vue一并换用,消除 S8 重复。
三、修改计划
后端 E:\YHXT\java\CampusNotice
| # | 文件 | 改动 |
|---|---|---|
| B1 | model/notice/ReadTabCountVo.java(新增) |
int all / unread / read,javadoc 写明「口径 = GET /api/notice/List 同过滤下 readType=0/1/2 的 count;不含 readType 入参」 |
| B2 | service/INoticeService.java |
加 ReadTabCountVo readTabCount(NoticeQuery query);getNextPrev 签名扩为 (Integer serid, String type, NoticeQuery query) |
| B3 | service/impl/NoticeService.java |
① userSpec 增加 readTypeOverride 重载,原三参版委托 query.getReadType()(单一口径,不复制过滤逻辑);② 新增 readTabCount:readIds/receiverNoticeIds 各取一次 → 三次 noticeRepository.count(spec);③ userList 的 unreadCount 改取同一次 readType=1 的 count(修 S3,并省掉一次全表 count);④ 重写 getNextPrev 的 type=1 分支:当前公告须落在筛选集内否则 0/0;prev = 排序键 (coalesce(topmost,0), createTime, id) 大于当前者中最小(PageRequest.of(0,1, ASC)),next = 小于当前者中最大(DESC);NULL topmost 用 cb.coalesce 归 0 与列表 @SortDefault 对齐;type=0 分支本次不动(遗留 E1) |
| B4 | controller/api/NoticeController.java |
新增 @GetMapping("ReadTabCount")(绑定 NoticeQuery);GetNextPrev 加 NoticeQuery query 绑定(参数可选,旧调用不传照旧工作) |
| B5 | resources/static/api-test.html |
收件箱三 tab 渲染数量(调 ReadTabCount,随 keyword/type 刷新);详情弹窗 GetNextPrev 补传当前筛选 |
前端 src/
| # | 文件 | 改动 |
|---|---|---|
| F1 | types/notice.ts |
加 ReadTabCountVo { all; unread; read }(就近放在 NoticePageResult 之后) |
| F2 | api/module/notice.ts |
新增 readTabCount(params) → GET /api/notice/ReadTabCount;getNextPrevNotice(serid, type, filters?) 把 keyword/type 拼进 query |
| F3 | views/notice/NoticeAccept.vue |
① Radio 文本改 全部 (n)(n=0 只显示 全部,静默策略同 v13),loadTabCounts() 与 run() 一起触发(搜索/切类型/标读/一键已读后刷新);② 详情 .ops 行改 上一篇 / 下一篇 / 打印,prevId/nextId=0 置灰,点击走 loadDetail(id, true) 并按决策 2 定位左列高亮;③ run() 后按 detail.serid 重算 selected(修「列表重载后高亮错位」,上下篇联动必需);④ onPrint 改调 noticePrint |
| F4 | utils/noticePrint.ts(新增) |
buildPrintHtml(detail) / printNotice(detail)(弹窗被拦截返回 false,调用方提示)+ receiveScopeLabel(detail)(pushScopes → ReceiveList 的 ALL/[/兜底三态,与 NoticeDetails 现口径一致) |
| F5 | views/notice/NoticeDetails.vue |
onPrint 换用 printNotice;receiveLabel computed 换用共享 receiveScopeLabel |
| F6 | assets/mock/noticeapi.ts |
补 /api/notice/ReadTabCount(与 /List 共用同一过滤函数算三桶);/api/notice/GetNextPrev 改为「可见已收序列(含未读)+ keyword/type 筛选」,忽略 readType |
遗留登记(不在 v17 修)
| 编号 | 内容 |
|---|---|
| E1 | GetNextPrev?type=0(已发序列)仍是「全量加载实体 + 无筛选」老实现,NoticeDetails 管理视角受其影响;可与本轮同法修,待单独排期 |
| E2 | NoticeQuery.orderByTime 后端从未生效(S5),前端固定传 "1" 掩盖了它——要么后端接上排序,要么删字段,需与 api-test 契约一起定 |
| E3 | InitializeRed(一键已读)返回条数与改口径后的 List.unreadCount 是否一致,回归时确认 |
四、回归验证清单(v17)
- 「已收到」左列
全部 / 未读 / 已读带条数且全部 = 未读 + 已读;输入关键字、切「普通/重要」后数字随列表变化 - 点开未读公告(自动标读)后 tab 数字与顶部「未读 N 条」同步 −1;「一键已读」后未读归 0、全部不变
- 详情右上依次
上一篇 / 下一篇 / 打印;首条「上一篇」置灰、末条「下一篇」置灰 - 未读 tab 第一条点开后上下篇仍可跳(决策 2 核心用例);跳到不在当前左列的已读公告时,左列自动切「全部」并高亮该条
- 列表重载后高亮跟随
detail.serid,不错位到相邻条目 - 打印件含 标题/来源部门/发布人/发布时间/接收范围/正文/附件文件名清单(无附件不出现该段)/需回执时一行提示;无按钮等界面元素,A4 样式生效
NoticeDetails.vue的打印与接收范围文案与「已收到」一致(同一工具函数)- 后端:
GET /api/notice/ReadTabCount三值与GET /api/notice/List?readType=0|1|2的total两两相等 mvn compile(JAVA_HOME=jdk-1.8)BUILD SUCCESS 与npm run build(43.32s)均通过;.scratch/verify-accept-print.mjs22/22 全绿(其余项需登录环境人工回归,见五、实施记录)- 改前文件已备份至
.backup/20260914-v17-accept-count-nav/(含 java 源码树快照 be-src-full.tgz;工程非 git 仓库)
五、实施记录(v17 回填)
待实施后回填。
| 项 | 状态 | 备注 |
|---|---|---|
B1 ReadTabCountVo |
✅ | 新增 model/notice/ReadTabCountVo.java(all/unread/read),javadoc 锁死「与 List 同过滤、readType 非入参、all = unread + read」 |
B3① userSpec 重载 |
✅ | 四参版 (query, readIds, receiverNoticeIds, int readType) 承载全部过滤逻辑,三参版只委托 query.getReadType();新增阅读桶常量 ALL/UNREAD/READ 替换裸 0/1/2 |
B3② readTabCount |
✅ | readIds/receiverNoticeIds 各取一次,三次 noticeRepository.count(userSpec(...)) |
| B3③ 未读数口径统一 | ✅ | 新增 countUnreadForCurrentUser();userList.unreadCount 与 statistics.unreadForCurrentUser 都改走它,删除 S3 的「可见总数 − 全部已读记录」相减(并清掉 statistics 里失效的 account 局部变量) |
B3④ getNextPrev 已收分支 |
✅ | 新增 adjacentReceivedId(base, current, newer) + noticeKeySidePredicate:先 count(base ∧ id=current) 判定当前条是否在集合内(不在则 0/0),再按排序键 (coalesce(topmost,0), createTime, id) 三档字典序 OR 取相邻;每次只 PageRequest.of(0,1,sort) 拉 1 行。旧实现(只遍历 notice_read、未读公告跳不动、为取邻居整表加载 LONGTEXT 实体)删除 |
| B4 Controller | ✅ | 新增 GET notice/ReadTabCount(绑定 NoticeQuery);GetNextPrev 加 noticeType + NoticeQuery 绑定,由控制器把 noticeType 写进 query.setType(...)、并把 readType 置 null |
| B5 api-test.html | ✅ | 三个阅读状态按钮加 <span class="seg-cnt"> + .seg .seg-cnt 样式;loadInboxCounts() 调 ReadTabCount(随 keyword/type 刷新),在 loadInbox 内非阻塞调用;详情弹窗 GetNextPrev 补传 keyword/noticeType(仅收件箱视角) |
| F1–F2 类型与接口封装 | ✅ | types/notice.ts 加 ReadTabCountVo 并入头契约清单;api/module/notice.ts 加 readTabCount() 与 getNextPrevNotice(serid, type, filters?: NextPrevFilter) |
| F3 NoticeAccept | ✅ | tab 文案 全部 (n)(0 不显示、异常静默);详情操作行 上一篇 / 下一篇 / 打印(prevId/nextId=0 走 .op.disabled 置灰,goNav 内部再短路一次);reposition() + ensureHighlighted() 让高亮跟随 detail.serid,目标不在当前结果时自动切「全部」重载、仍找不到则不高亮;左列条目函数式 ref + scrollIntoView 滚进可视区;allRead 改 await 串行刷新 |
F4 utils/noticePrint.ts |
✅ | 新增 buildPrintHtml(纯函数)/printNotice(弹窗被拦返回 false;onload + 1.5s 兜底触发打印)/receiveScopeLabel;自由文本统一 HTML 转义、正文按原样富文本;A4 @page 样式 + 附件清单 + 回执行 + 打印时间 |
| F5 NoticeDetails | ✅ | 本地重复的 onPrint 与 receiveLabel computed 换用公共工具(两处文案口径从此同源) |
| F6 mock | ✅ | 抽 inboxVisible(query) 供 List/ReadTabCount/GetNextPrev 共用;顺带补上 mock 此前完全缺失的 readType 分桶,List.unreadCount 改为全局未读(对齐后端),GetNextPrev 按 type 分流(此前忽略 type) |
| 备份 | ✅ | .backup/20260914-v17-accept-count-nav/:前端 5 文件(types.notice.ts/api-notice.ts 前缀区分同名)+ 后端 4 文件 + be-src-full.tgz(java 源码树快照,供与并行会话交叉时回滚) |
| 构建验证 | ✅ | mvn -o compile BUILD SUCCESS;npm run build 通过(43.32s,仅既有 chunk 体积/CSS 告警);产物字符串命中:ReadTabCount→index-*.js、noticeType + 打印窗口被浏览器拦截→NoticeAccept-*.js、打印工具独立分片 noticePrint-*.js(两页共用的实证) |
| 打印回归脚本 | ✅ | .scratch/verify-accept-print.mjs 22/22 通过(要素齐全 / 条件段落 / 转义安全 / 接收范围三态 / 无界面文字混入);打桩命令 npx esbuild src/utils/noticePrint.ts --bundle --format=esm --platform=node --external:*.vue --tsconfig=tsconfig.json --outfile=.scratch/noticePrint.bundle.mjs |
关键纠偏与踩坑:
type参数名冲突(本轮最重要的发现):GetNextPrev的type一直是「序列类型(0 已发/1 已收)」。若按原计划让NoticeQuery直接绑定请求参数,type=1会被 Spring 顺手绑进NoticeQuery.type(公告重要程度),于是每条已收公告的跳篇序列都会被误筛成「普通」。改为新增noticeType显式入参、由控制器写回query.setType(...);前端NextPrevFilter类型也只暴露noticeType,从签名上堵住再传type的路;api-test.html 同步。- 序列刻意不带 readType 的实证价值:最初按「与列表完全同一条序列」实现,未读 tab 里点开第一条即标读 → 该条离开未读集合 →
count(集合 ∧ id=当前)=0→ 上下篇双双置灰、用户卡死。这正是决策 2 预判的坑,落到代码后确认必须忽略 readType,控制器再兜一层setReadType(null)。 - 本次自引入的编译错误:
findAll(Specification, Pageable)返回Page<Notice>而非List<Notice>(首次 compile 报「不兼容的类型」),已改为Page+getContent()。教训:定向取 1 行也要按 Spring Data 的返回类型写。 - 与并行会话交叉:
NoticeService.java在本轮期间被 v15/v16 会话连续改写,两次触发 Edit 的「文件已修改」守卫(须重读并在 grep 里重新定位——目标行一度从 2499 漂到 2466);期间 compile 还出现过buildSteps参数不匹配的两条 ERROR,属对方 v15 送审改造的中间态,非本次改动引入,本轮未介入;最终 compile 仅剩我那条类型错误并已修。 vue-tsc本机不可用(既有环境缺陷),类型核对仍以npm run build+ 与后端 Controller 人工契约比对为准。
遗留与更新:
- E3 部分收敛:
List.unreadCount、Statis.unreadForCurrentUser已与 tab 统计同源;但initializeRed()仍用visibleSpec(account, **false**)(不带学校过滤)批量补阅读记录,理论上会给跨校可见的已发布通知记账,与 tab 统计口径不严格一致 → 保留登记,待单独处理。 - E1(
GetNextPrev?type=0已发序列仍全量加载实体 + 无筛选)、E2(orderByTime后端从未生效)原样保留。 - 侧边栏栏目名仍取平台
GetUserMod.name(若仍显示旧叫法,须平台后台改名,与本轮无关)。
待人工联调(8101 + CAS 登录):
- 「已收到」三 tab 数字与
List?readType=0/1/2的total三者两两相等;输入关键字 + 选「重要」后数字随列表变化 - 未读 tab 第一条点开即标读,上下篇仍可继续跳;跳到已读公告时左列自动切「全部」并高亮、滚动到该条
- 序列首条「上一篇」置灰、末条「下一篇」置灰;当前公告不在筛选结果内时两键置灰而非乱跳
- 两处打印入口的打印件都含接收范围与附件清单;含图公告的图片能出现在纸上(1.5s 兜底);浏览器拦弹窗时出现提示文案
- 一键已读后未读归 0、全部不变;
InitializeRed返回条数与未读数是否一致(E3) - mock 模式(
npm run dev:mock)下三 tab 数字与 readType 分桶生效(此前 mock 根本不按 readType 过滤)
v18(2026-09-14)· 去掉「审核设置」功能;写公告审批人选择直显教师树
一、需求与现状
用户要求:① 去掉审核设置功能;② 写公告页「审批 → 选择审批人」弹窗直接显示教师树接口(base/getTeaTree?toUser=true,含教师叶子)。
现状证据:
| 编号 | 证据 |
|---|---|
| S1 | ApprovalFlowEditor.vue v7 起把选人树过滤为「教师树 ∩ 审批人池(dict/ApproverPool 启用者 teaId)」;池为空时 loadPickerTree 返回 [] 并 warning「请先在审核设置中添加审批人」→ 弹窗左右两栏全空白(用户截图即此形态) |
| S2 | 审核设置页 views/notice/ApprovalSetting.vue(路由 /approval-setting,modId 6)维护 sys_approver 白名单;NoticeService.buildSteps(validatePool=true)(L636 正式送审)经 requireInApproverPool 拦截池外教师,报错文案引导去「审核设置」 |
| S3 | 菜单真实来源=平台 GetUserMod(modId 6=审核设置);前端仅删路由/别名不够——normalizeModList 解析失败仍会保留原始 page 渲染菜单项,点击跳空路由。须在菜单管线剔除 modId 6 |
| S4 | 前端只发 approverTeaIds(教师ID路径);FlowStepItem.approvers(账号路径→bindAccounts 池校验)新版前端与 api-test 均不使用,属遗留兼容,本轮不动 |
二、改动清单
前端 src/(工程非 git,改前备份 .backup/20260914-v18-drop-approval-setting/)
| # | 文件 | 改动 |
|---|---|---|
| F1 | components/ApprovalFlowEditor.vue |
删 v7 池过滤整块(poolTeaIds/loadPool/filterTreeByPool/loadPickerTree);pickerSources loader 改直连 loadTeaTree()(全量教师树,含教师叶子);bindStepByTeaIds 去「(不在审批人名单)」标注与 loadPool 调用;删 approvalSettingApi/ApproverPoolItem import;注释同步 |
| F2 | views/notice/ApprovalSetting.vue |
删除(移入备份目录) |
| F3 | router/routes.ts |
删 /approval-setting 路由;头注释 modId 对照表去掉 6=审核设置 |
| F4 | assets/config/menu.ts |
删本地兜底菜单 id 6 条目 |
| F5 | utils/menuRoute.ts |
删 审核设置/审批设置 别名;normalizeModList 顶层+children 剔除 modId===6 模块(平台仍授权时也不显示,前端契约兜底) |
| F6 | components/layout.vue |
getMenuIcon 删 /approval-setting 行 |
| F7 | api/module/notice.ts |
删 ApprovalSettingApi 类与 approvalSettingApi 导出、ApproverVo/ApproverPoolItem import |
| F8 | types/notice.ts |
删 ApproverVo、ApproverPoolItem 接口 |
| F9 | assets/mock/noticeapi.ts |
删 dict/ApproverPool、Approver、AddApprover、UpdateApprover、DelApprover mock 与 GetUserMod 中 id 6 模块;approvers 数据保留(mock 审批流/记忆仍消费) |
后端 E:\YHXT\java\CampusNotice(git 仓库,diff 可回滚)
| # | 文件 | 改动 |
|---|---|---|
| B1 | service/impl/NoticeService.java |
去池白名单校验:buildSteps 删 validatePool 参数与「沿用草稿定义时的池校验」块;bindApprovers 合并为单签名(不校验池);删 requireInApproverPool;bindAccounts(账号路径遗留)与 sysApproverRepository 保留不动 |
| B2 | controller/api/DictController.java 等审批人维护接口 |
不删(表与接口保留,前端不再调用;彻底下线另行排期) |
三、回归清单(v18)
- 写公告/草稿箱提交审批弹窗:勾选审批 →「选择审批人」打开穿梭式弹窗,左栏显示完整教师树(组→教师叶子可勾),不再出现空树与「请先在审核设置…」提示
- 选任意本校教师送审成功,后端不再报「不在本校审批人名单」
- 侧边栏不再出现「审核设置」菜单(mock 与平台仍返回 modId 6 两种来源均验证)
- 直接访问
#/approval-setting落到 404/兜底,不白屏崩溃 - ApproverSuggest 记忆带入、编辑回显 chip 姓名正常
npm run build、后端mvn compile通过;.scratch/verify-menu-order.mjs、verify-menu-name-bind.mjs重打桩后全绿
四、实施记录(v18 回填)
| 项 | 状态 | 备注 |
|---|---|---|
F1 ApprovalFlowEditor.vue |
✅ 2026-09-14 | 池过滤整块(poolTeaIds/loadPool/filterTreeByPool/loadPickerTree)删除;pickerSources loader 直连 loadTeaTree()(base/getTeaTree?toUser=true 归一化全量教师树,含教师叶子);bindStepByTeaIds 去「(不在审批人名单)」标注;approvalSettingApi/ApproverPoolItem import 清除 |
F2 ApprovalSetting.vue |
✅ | 已移入系统回收站;副本存 .backup/20260914-v18-drop-approval-setting/views/ |
F3 routes.ts |
✅ | /approval-setting 路由删除;头注释 modId 对照表登记「6 已于 v18 下线」 |
F4 menu.ts |
✅ | 本地兜底菜单 id 6 条目删除 |
F5 menuRoute.ts |
✅ | 审核设置/审批设置 别名删除;normalizeModList 新增 DROPPED_MOD_IDS={6} 剔除(顶层+children,按 modId ?? id 判定)——平台仍授权该模块时侧边栏也不出现,且不点不进 |
F6 layout.vue |
✅ | getMenuIcon 去 /approval-setting 行 |
F7 api/module/notice.ts |
✅ | ApprovalSettingApi 类、approvalSettingApi 导出、ApproverVo/ApproverPoolItem import 删除 |
F8 types/notice.ts |
✅ | ApproverVo、ApproverPoolItem 接口删除 |
| F9 mock | ✅(偏差) | dict/ApproverPool、Approver、AddApprover、UpdateApprover、DelApprover mock 端点与随之失效的 approverSeq/approverPoolAccounts 已删;GetUserMod 中 id 6「审核设置」条目保留(偏差①:故意模拟「平台仍授权」场景,端到端验证 F5 剔除链路)。approvers 演示数据保留(mock 审批流/ApproverSuggest 仍消费) |
B1 NoticeService.java |
✅ | buildSteps 去 validatePool 参数与「沿用草稿定义时池校验」块;bindApprovers 合并为单签名(仅按本校教师名单+资格组校验);requireInApproverPool 删除。bindAccounts(账号路径遗留)与 sysApproverRepository 保留 |
| B2 后端 dict 维护接口 | ✅ 未动 | 按计划保留(表/接口在,前端不再调用) |
| 附带 | ✅ | UserCheckModal.vue multiple prop 注释去「审核设置」引用(prop 本身保留为通用能力) |
验证结果(2026-09-14):
npm run build通过(exit 0);后端mvn compile通过(exit 0,JAVA_HOME=jdk-1.8).scratch/verify-menu-order.mjs重打桩后 13 项全绿(含新增「children 中 modId 6 剔除」断言,测试①期望已改为无审核设置).scratch/verify-menu-name-bind.mjs14 项全绿(/approval-setting期望改为解析空串,即路由/别名彻底下线)- 残留引用全局 grep:仅剩注释与 mock GetUserMod 兜底条目(有意保留)
遗留登记:
| 编号 | 内容 |
|---|---|
| E1 | 平台用户中心仍授权 modId 6「审核设置」模块(GetUserMod 照常返回);前端已剔除显示,如需彻底回收授权请在平台管理后台取消模块授权 |
| E2 | 后端 sys_approver 表、dict/Approver* 接口与 bindAccounts 账号路径成为无入口的死代码,彻底下线另行排期 |
| E3 | 人工冒烟(需登录环境):教师树选任意教师 → 直接发布/提交审批全链路;已驳回单重提;草稿箱提交审批弹窗同样直显教师树 |
v19(2026-09-14)· 审批中心状态 tab 改为一行显示
一、需求与现状
用户要求:审批列表左列的四个状态 tab「待审批 (1) / 已通过 (1) / 已驳回 (1) / 全部 (2)」在一行内显示,不再折行。
| 编号 | 证据 |
|---|---|
| S1 | ApprovalList.vue 左列 .title-list 宽 360px(与「已收到」同款),.filter-box 左右 padding 12px + 1px 边框 → tab 行可用宽 ≈ 334px |
| S2 | v13 给 tab 文字加了条数后缀 tabCountText(),同时把 :deep(.ivu-radio-group) 设为 display:flex; flex-wrap:wrap; gap:6px —— 换行是当时的兜底,正是本需求的直接成因 |
| S3 | view-ui-plus 实测 CSS(dist/styles/viewuiplus.css):.ivu-radio-group-button.ivu-radio-group-small .ivu-radio-wrapper{height:24px;padding:0 12px;font-size:14px},且 .ivu-radio-group-button 父级 font-size:0 不作用于 wrapper。按 14px 字号估宽:待审批 (1) ≈ 3×14 + 「 (1)」≈18 + 左右 padding 24 ≈ 84px,四个 ≈ 330px + 3 个 gap 18px ≈ 348px > 334px → 必然折行 |
| S4 | tab 文字来自前端字面量,条数为 0 时不显示后缀(tabCountText),故最宽形态是「三位数条数」;DOM 结构为 div.ivu-radio-group.ivu-radio-group-button.ivu-radio-group-small > label.ivu-radio-wrapper,flex 项是 wrapper 而非 group |
二、改动计划(仅 src/views/notice/ApprovalList.vue 样式层)
| # | 位置 | 改动 |
|---|---|---|
| F1 | .filter-box :deep(.ivu-radio-group) |
flex-wrap: wrap → nowrap + width:100%,保留 6px 间距观感 |
| F2 | 新增 .filter-box :deep(.ivu-radio-group-button .ivu-radio-wrapper) |
flex:1 1 0; min-width:0; box-sizing:border-box; padding:0 4px; font-size:12px; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis —— 四项等宽铺满(约 79px/项),文字最宽形态约 62px,留有余量;极端条数(四位数)时省略号兜底而非撑破 |
| F3 | 注释 | 更新 v13 折行兜底说明为 v19 一行等宽口径 |
不动:tab 文案、tabCountText 规则、接口契约、右列与「已收到」页样式。
三、回归清单(v19)
- 四个 tab 同一行等宽显示,无折行、无横向溢出
- 条数为 0 时仍只显示「待审批」等裸文案;条数为两位数时仍不换行
- 选中态(蓝底)与 hover 态样式正常,切换 tab 触发
doSearch正常 - 窄屏(左列固定 360px,不随窗口变化)下不出现溢出
npm run build通过(SCSS + scoped:deep编译无误)
四、实施记录(v19 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| F1 group 改 nowrap | ✅ 2026-09-14 | flex-wrap: wrap → nowrap,补 width:100% 让四项铺满 334px 内容宽;6px gap 保留 |
| F2 wrapper 等宽收紧 | ✅ | 新增 :deep(.ivu-radio-group-button .ivu-radio-wrapper):flex:1 1 0(每项 ≈79px)+ min-width:0 + box-sizing:border-box + padding:0 4px + font-size:12px + 居中 + white-space:nowrap; overflow:hidden; text-overflow:ellipsis 兜底极端长文案 |
| F3 注释 | ✅ | 折行兜底说明替换为 v19 一行等宽口径,并记录库默认盒模型与宽度算式 |
| 备份 | ✅ | .backup/20260914-v19-approval-tabs-one-line/views/ApprovalList.vue(工程非 git) |
| 未动 | — | tab 文案、tabCountText、接口契约、右列样式;「已收到」页 .filter-box 未改(其 3 tab + 类型 Select 估宽约 297px < 328px,一行放得下) |
验证结果(2026-09-14):
npm run build通过(exit 0,仅既有 chunk >1000kB 提示)- 编译产物核对(
dist/static/css/ApprovalList-*.css):.title-list .filter-box[data-v-888bcda5] .ivu-radio-group{display:flex;flex-wrap:nowrap;gap:6px;width:100%}与... .ivu-radio-group-button .ivu-radio-wrapper{flex:1 1 0;...}均正确落位;选择器特异性(4 class + 1 属性)高于库默认.ivu-radio-group-button.ivu-radio-group-small .ivu-radio-wrapper(3 class),字号/内边距覆盖生效 - 宽度核算:12px 字号下
待审批 (1)≈ 53px 文字 + 8px 内边距 ≈ 61px < 单项 79px;两位数条数仍在余量内,四位数将走省略号而非撑破 - 遗留(需登录环境人工冒烟):本机无 headless 浏览器(未装 puppeteer/playwright),真实字体渲染宽度与选中态视觉需人工在
#/approval页确认一次
五、v19.1 缺陷返工(用户截图反馈「存在样式问题」)
| 编号 | 缺陷 | 根因(库 CSS 证据) |
|---|---|---|
| D1 | 选中 tab 左侧出现游离蓝色短竖线 | .ivu-radio-wrapper-checked{box-shadow:-1px 0 0 0 #2d8cf0} —— 该阴影是为分段控件「补上被 border-left:0 省掉的左边界」,v13 的 gap:6px 把分段拆成独立 chip 后,它画到了 chip 左侧 1px 外的空隙里 |
| D2 | 选中 tab 外圈多出一层淡蓝光晕,看起来像双边框 | .ivu-radio-wrapper-checked.ivu-radio-focus{box-shadow:...0 0 0 2px rgba(45,140,240,.2)} + ::after{left:-3px;top:-3px;height:28px;opacity:1};点击后 radio 常驻 focus,光晕不会消失 |
| D3 | 首项右两角、末项左两角是方角(chip 形态下应四角全圆) | :first-child{border-radius:3px 0 0 3px} / :last-child{border-radius:0 3px 3px 0} 只在相接时成立 |
| D4 | 非首项左边缘本无 border(靠 ::before 1px 分隔线补位),chip 之间留缝时左边缘缺线 |
.ivu-radio-wrapper{border-left:0} + ::before{left:-1px;width:1px;background:#dcdee2} |
取舍(F4):与其逐条对抗库的分段假设(还要自己补圆角、四边边框、隐藏伪元素),不如回到库原生的无间隙分段控件形态 —— 与「已收到」页观感一致,自定义量最小。
| 项 | 状态 | 改动 |
|---|---|---|
| F4 | ✅ 2026-09-14 | :deep(.ivu-radio-group) 去掉 gap:6px(保留 nowrap + width:100%);wrapper 上 &:before,&:after{display:none};新增 :deep(.ivu-radio-group-button .ivu-radio-wrapper-checked) 与 ...checked.ivu-radio-focus 两条 box-shadow:none。等宽 79px/项、12px 字号、nowrap+ellipsis 不变 → 选中态只剩蓝边蓝字,无光晕、无游离竖线、圆角由 first/last 原生规则正确切分 |
验证(2026-09-14):npm run build 通过;dist/static/css/ApprovalList-BBUuPZPq.css 中四条覆盖规则均按预期落位(.title-list .filter-box[data-v-c541a2a1] .ivu-radio-group-button .ivu-radio-wrapper:before,:after{display:none}、...checked, ...checked.ivu-radio-focus{box-shadow:none}),特异性 5 class + 1 属性 > 库默认 3 class。构建期 11 条 Expected identifier but found "*" 警告来自既有 src/assets/styles/utils/_utils.scss 的 *zoom 老式 hack,与本次无关。
v20(2026-09-14)· 修复「写公告→提交审批」被过期定时时间卡死
一、现象与根因
| 编号 | 证据 |
|---|---|
| S1 | 用户 #/list/new?serid=4 点「提交审批」弹「请选择大于当前的时间!」,无法送审 |
| S2 | 库表 notice id=4:status=3(已驳回)、timing_send_time=2026-09-11 15:00:00(早于当前 2026-09-14) |
| S3 | NewNotice.vue onMounted 用 detail.DsfsTime 回填 timeForm.date1(L163);驳回态按钮只有「存为草稿+提交审批」(publishViaCreate=false 且 editStatus!==1,L446-449),页面上根本没有定时发送入口可改这个过期时间 |
| S4 | doAdd('approve') → applyTiming(data,false) → 编辑分支 t<now 抛错并 return false(L274-282),把送审整条链路掐断 |
定性:编辑态沿用记录里已有的定时时间时,把「过期排期」当成硬校验失败。对直接发布/存草稿/提交审批都应视为失效排期静默丢弃,而不是阻断。真正需要「必须大于当前时间」校验的只有用户主动选时间的「定时发送」弹窗(fromModal 分支,本就独立校验,保持不动)。
二、改动(仅 src/views/notice/NewNotice.vue)
| 项 | 改动 |
|---|---|
| F1 | applyTiming 编辑分支:去掉 t<now 的 return false;仅当 timeForm.date1 解析为未来时间才写 data.timingSendTime,过期即不写(后端 edit() 仅在 getTimingSendTime()!=null 时更新,不写=保留原值但驳回重提走的是送审、且后端 isTiming 把过期时间判为立即发布,无副作用);恒 return true |
| F2 | fromModal(定时发送弹窗提交)分支的「请选择发送日期/时间」「大于当前时间」校验原样保留 |
三、回归清单(v20)
#/list/new?serid=4(已驳回+过期定时)点「提交审批」不再弹「请选择大于当前的时间」,正常进入送审- 同上「存为草稿」不被过期时间阻断
- 草稿续编(status=0)带未来定时时间,「直接发布」仍按该时间排期(携带
timingSendTime);带过期定时时间,直接发布改为立即发布不报错 - 「定时发送」弹窗:空日期/空时间/过去时间三种仍分别弹对应提示(行为不变)
npm run build通过
四、实施记录(v20 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| F1 applyTiming 编辑分支 | ✅ 2026-09-14 | 去掉 t<now 的 Message.info+return false;改为 t>now 才写 data.timingSendTime,过期静默丢弃,末尾恒 return true;补 5 行根因注释 |
| F2 fromModal 分支 | ✅ 未动 | 定时发送弹窗的「请选择发送日期/时间」「大于当前时间」三条校验原样保留 |
| 备份 | ✅ | .backup/20260914-v20-submit-approve-timing/views/notice/NewNotice.vue(工程非 git) |
验证(2026-09-14):npm run build 通过(exit 0,43.87s,仅既有 chunk >1000kB 提示,NewNotice chunk 已重命名为 NewNotice-BfdcofXZ.js)。人工冒烟(需登录环境):#/list/new?serid=4 点「提交审批」不再弹过期时间提示、正常送审;「定时发送」弹窗三种校验不变。
v21(2026-09-14)· 审批中心右列内容过长被裁切,无滚动条
一、现象与根因
用户截图:#/approval 右列详情中「审批流程 / 审批轨迹」把面板撑满,底部「公告内容」只露半行即被切断,整个右列没有任何滚动条,正文与附件无法查看。
| 编号 | 证据 |
|---|---|
| S1 | .detail-content 为 display:flex; flex-direction:column; overflow:hidden,子块自上而下:.notice-title(标题+操作+元信息)、Alert、.flow-block、.track-block、.notice-content |
| S2 | 全列唯一滚动区是 .notice-content{flex:1; overflow-y:auto};前四块未设 flex:none,但作为普通块其内容高度不可压缩,flow/track 条目一多就把 flex:1 的正文区挤到 0 高 |
| S3 | 超出容器高度的部分被 overflow:hidden 直接裁掉 → 既无外层滚动,正文滚动区又高 0,形成「内容过长没有伸缩框」 |
| S4 | 对照「已收到」NoticeAccept.vue:同款结构但其右列没有 flow/track 两块固定内容,正文永远占大头,故未暴露此问题 |
定性:滚动职责放错了层级——应让「Alert + 审批流程 + 审批轨迹 + 正文 + 附件」整体成为一个滚动区,标题与操作按钮行(上一篇/下一篇/通过/驳回/打印)保持常驻。
二、改动(仅 src/views/notice/ApprovalList.vue)
| 项 | 改动 |
|---|---|
| F1 | 模板:在 .notice-title 之后新增包裹层 <div class="detail-scroll">,把 Alert、.flow-block、.track-block、.notice-content 一并纳入(内部不重排缩进,减小 diff) |
| F2 | 样式:.notice-title{flex:none};新增 .detail-scroll{flex:1; min-height:0; overflow-y:auto};.notice-content 去掉 flex:1; overflow-y:auto(改为随包裹层自然流式) |
不动:左列(.notice-items 本就是 flex:1; overflow-y:auto 的正常滚动区)、按钮行为、接口契约、「已收到」页。
三、回归清单(v21)
- 审批流程/轨迹条目很多时,右列出现一条纵向滚动条,可滚到「公告内容」末尾与附件区
- 标题 + 上一篇/下一篇/通过/驳回/打印 按钮行始终常驻可见,不随滚动消失
- 内容不超时(如截图第一条 2 步流程)布局与改前一致,无双滚动条
- 「暂无公告/加载中」空态仍居中
npm run build通过
四、实施记录(v21 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| F1 模板包裹层 | ✅ 2026-09-14 | .notice-title 之后插入 <div class="detail-scroll">,Alert×3 / flow-block / track-block / notice-content 全部纳入;内部未重排缩进 |
| F2 样式 | ✅ | .notice-title{flex:none}、.detail-scroll{flex:1;min-height:0;overflow-y:auto};.notice-content 去掉 flex:1; overflow-y:auto 仅留 padding-top:10px |
| 备份 | ✅ | .backup/20260914-v21-approval-detail-scroll/views/notice/ApprovalList.vue(工程非 git) |
验证(2026-09-14):npm run build 通过(exit 0,18.18s)。编译产物 dist/static/css/ApprovalList-CTyoM2OJ.css 核对:.detail-scroll[data-v-26beade0]{flex:1;min-height:0;overflow-y:auto}、.notice-content[data-v-26beade0]{padding-top:10px}(flex/overflow 已移除)、.notice-title[data-v-26beade0]{flex:none} 均落位。遗留(需登录环境人工冒烟):本机无 headless 浏览器,多步流程长单滚动到底、按钮行常驻需人工在 #/approval 确认一次。
v22(2026-09-14)· 审批:驳回后「审批轨迹」无记录 + Approve 误报「已离开待审批状态」
一、现象与证据
现场 URL:GET /api/notice/Approve?serid=9&opinion=同意;notice 9 报「该通知已离开待审批状态,无需重复处理」,且此前该单被驳回后「审批轨迹」无任何处理记录。
DB 取证(campus_notice,serid=9):
| 证据 | 数据 |
|---|---|
S1 notice |
status=2(PENDING)、approval_level=0 —— 仍处「待审批」,Approve 却报错 |
S2 notice_approval |
仅 1 行 id=7 action=1(提交) lijing;无 action=2/3(通过/驳回) 行 |
S3 notice_approval_record |
空 —— 多步 approve/reject 只写这张表,S2 因此缺处理留痕 |
S4 notice_approval_step |
两条 step_no=1:id=10 status=0(待流转)、id=11 status=1(审批中),同为 lijing/[10008] |
二、根因
问题1(审批轨迹无驳回记录):NoticeService.reject() 的多步分支(L793–809)只调 saveRecordRow(...) 写 notice_approval_record(驱动「审批流程」approvalFlow),从不写 notice_approval;而前端「审批轨迹」= approvalRecords = loadApprovalRecords() 只读 notice_approval(L2924)。∴ 多步驳回(及中间多步通过)在「审批轨迹」里完全不留痕。(旧单步 legacy 分支正常写 saveApprovalRow,故只有走步骤表的单子复现。)
问题2(Approve 误报已离开待审批):approve()/reject() 用 steps.get(Math.min(level, size-1)) 按数组下标定位当前步。S4 里靠前的 id=10 是陈旧 WAITING 脏行,get(0) 命中它,status(0)≠STATUS_ACTIVE(1) → 进 L690/L795 抛 alreadyHandledError()。该方法又只查 notice_approval 的通过/驳回行(S2 为空)→ 落到兜底文案「该通知已离开待审批状态」。真正待办的 id=11(ACTIVE) 永远取不到,审批卡死。
三、修复方案(仅 NoticeService.java)
| 项 | 改动 | 位置 |
|---|---|---|
| F1 | 多步 reject():在 saveRecordRow(... ACTION_REJECT ...) 后补 saveApprovalRow(serid, ACTION_REJECT, meAccount, meName(meAccount), opinion, null),使「审批轨迹」显示驳回,并让 alreadyHandledError 能给出「已被X驳回」正解文案 |
reject 多步分支 |
| F2 | 新增 resolveCurrentStep(steps, level):优先返回 status==STATUS_ACTIVE 的步骤,无则回退 steps.get(min(level,size-1));approve()/reject() 的下标取步改用它,规避陈旧/重复步骤行 |
approve/reject + helper |
| F3 | 新增 normalizeSteps(raw):按 step_no 去重(每步留 id 最大=最近提交写入行,丢弃陈旧脏行)再升序;approve()/reject()/getDetailMap() 三处查询包裹它。修复脏数据下 steps.get(level+1)/steps.size() 误把同一步脏行当「下一步」、前端流程渲染重复步 |
三处 + helper |
不改:legacy 分支、publishAfterFinalApproval(末步已写 APPROVE 轨迹行,不重复写)、alreadyHandledError 本身、submit() 的删旧建新逻辑、前端。
四、回归清单(v22)
- 多步审批流驳回后,「审批轨迹」出现「驳回」条目(含处理人姓名+意见)
- 再次点 Approve:提示为「该通知已被X于…驳回」(不再是「已离开待审批状态」)
- notice 9 这类带陈旧 WAITING 步骤脏数据的单,Approve 能命中 ACTIVE 步正常通过并流转/发布
- 正常新建→提交→逐步通过→发布链路不回归
mvn -q compile通过
五、实施记录(v22 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| F1 多步驳回补写审批轨迹 | ✅ 2026-09-14 | reject() 多步分支 saveRecordRow(...REJECT...) 后新增 saveApprovalRow(serid, ACTION_REJECT, meAccount, meName(meAccount), opinion, null);轨迹出现驳回条目,alreadyHandledError 给出「已被X驳回」正解 |
| F2 当前步定位健壮化 | ✅ | approve()/reject() 取步由 steps.get(level) 改 resolveCurrentStep()(优先 STATUS_ACTIVE) |
| F3 步骤列表去重 | ✅ | normalizeSteps() 按 step_no 去重升序,approve()/reject()/getDetailMap() 三处包裹 |
| 编译 | ✅ | mvn -q compile EXIT=0(JAVA_HOME=jdk-1.8,maven-3.6.1) |
对 notice 9 脏数据(step_no=1 两行 id=10 WAITING / id=11 ACTIVE):normalizeSteps 归一为单条 ACTIVE → resolveCurrentStep 命中 → level+1 !< size 走 publishAfterFinalApproval 正常发布;旧脏行无需手工清库即在运行时被读侧/动作侧忽略。重复行属更早构建遗留,未动 submit() 删旧建新;若复发需另查提交侧 delete 未生效的具体路径。
生效方式:后端经 IDEA Debug + devtools 运行,命令行 mvn compile 产物落 target/classes,须让 IDEA「Build → 重新编译触发 devtools 热重启」或重启后端实例(先 scripts/kill-port.bat 8101 释放端口)才被加载。浏览器 #/approval 待人工冒烟:驳回后审批轨迹可见 + Approve 不再误报。
v23(2026-09-14)· 审批中心:状态提示条常驻 + 「查看审批」折叠/展开审批信息
一、需求(用户截图标注)
#/approval 右列详情,用户圈定两点:
- 提示条「该公告正在审批中:第2/2步 第1步·或签」固定——不随右列滚动而消失,始终常驻在标题/元信息之下。
- 新增「查看审批」功能——在提示条右侧放一个可点开关,用于折叠 / 展开审批信息(即「审批流程」+「审批轨迹」两块)。
二、现状与根因
| 编号 | 证据 |
|---|---|
| S1 | v21 把 Alert + .flow-block + .track-block + .notice-content 一并塞进唯一滚动区 .detail-scroll,故 Alert 会随滚动被推走,不满足「固定」 |
| S2 | 现 Alert 仅三态文案(pending/rejected/approved),无任何折叠交互;审批信息恒显示,无法收起 |
| S3 | 工程无 icon-down/up/zhankai 类字形(icons.css 缺),chevron 不能硬引 iconfont,须用 CSS 绘制或文字符号 |
定性:把 Alert 从滚动区里提出来做成常驻条(flex:none,位于 .notice-title 与 .detail-scroll 之间);在其右侧挂一个开关,用 approvalOpen 控制 .flow-block/.track-block 的显隐(仍在滚动区内,随展开/收起出现或消失)。
三、改动(仅 src/views/notice/ApprovalList.vue)
| 项 | 改动 |
|---|---|
| F1 | 脚本:新增 approvalOpen(默认展开)、hasApprovalInfo(流程或轨迹有数据)、alertType/alertText/showAlert(合并原三态 Alert 判定) |
| F2 | 模板:.notice-title 之后、.detail-scroll 之前插入常驻 .approval-bar(内含单一 <Alert> + 右侧「查看审批/收起审批」开关,chevron 用 CSS 边框绘制并随开合旋转);.detail-scroll 内原 Alert×3 移除,.flow-block+.track-block 包进 <template v-if="approvalOpen"> |
| F3 | 样式:.approval-bar{flex:none}(常驻不滚);.alert-row flex 两端对齐;.alert-toggle 手型 + hover 变色;.chevron 旋转动画 |
不改:左列、按钮行为(上一篇/下一篇/通过/驳回/打印)、正文/附件、接口契约、其它页面。
四、回归清单(v23)
- 右列内容较长时上下滚动,黄色「该公告正在审批中…」提示条始终固定在标题下方不动
- 提示条右侧「查看审批」可点:收起后审批流程 + 审批轨迹两块隐藏、只剩正文可滚;再点展开恢复
- 无审批流程/轨迹数据的公告不显示「查看审批」开关(避免点了没反应)
- 通过/驳回/已发布三态提示条文案与配色不变(warning/error/success)
npm run build通过
五、实施记录(v23 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| F1 脚本状态 | ✅ 2026-09-14 | approvalOpen 默认 true(保持既有「审批信息可见」口径);hasApprovalInfo 门控开关显隐 |
| F2 模板 | ✅ | Alert 提出滚动区成 .approval-bar 常驻;flow/track 包 v-if="approvalOpen" 留在 .detail-scroll 顶部 |
| F3 样式 | ✅ | .approval-bar{flex:none};chevron 用 CSS 边框绘制(工程无对应 iconfont 字形),开合旋转 180° |
| 备份 | ✅ | .backup/20260914-v23-approval-view-toggle/views/notice/ApprovalList.vue(工程非 git) |
验证:待 npm run build + 人工在 #/approval 冒烟(提示条常驻、查看审批折叠/展开)。
v24(2026-09-14)· 移除审批列表左列顶部「共 X 条 / X 条待我处理」统计行
一、需求(用户截图圈红)
#/approval 左列顶部的 .total 行(共 {page.total} 条 + 右侧 {tabCounts.pending} 条待我处理)用户圈红并明确要求移除。
二、现状与影响面
| 编号 | 证据 |
|---|---|
| S1 | 模板 ApprovalList.vue L298–301:.title-list 内首个子节点 .total,含 .num(总数)与 .tip-text(待我处理)两个 span |
| S2 | 样式 L542–556:.total{flex/space-between} 及其内 .num、.tip-text |
| S3 | page.total 另用于分页(L334 v-if、L338 :total),tabCounts.pending 另用于「待审批」tab 计数(L300),二者均不可随本行删除 |
定性:仅删展示层 .total 节点与其配套样式;数据源 page.total/tabCounts 仍被分页与 tab 引用,保留不动,删除后无死变量。
三、改动(仅 src/views/notice/ApprovalList.vue)
| 项 | 改动 |
|---|---|
| F1 | 模板:删除 .title-list 下整个 .total div,.filter-box(状态 tab 行)上移为首个元素 |
| F2 | 样式:删除 .total{...} 块(含内嵌 .num、.tip-text) |
不改:状态 tab、搜索框、列表项、右列详情、分页、接口契约、tabCounts/page.total 的其它引用。
四、回归清单(v24)
- 左列不再显示「共 X 条 / X 条待我处理」,状态 tab 行成为顶部第一行
- 「待审批」等四个 tab 的条数仍正常显示(
tabCounts未受影响) - 底部翻页控件在结果超一页时仍正常(
page.total未受影响) npm run build通过,产物ApprovalList-*.js不再命中「条待我处理」
五、实施记录(v24 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| F1 模板 | ✅ 2026-09-14 | 删除 .total div |
| F2 样式 | ✅ | 删除 .total 及内嵌 .num/.tip-text |
| 构建验证 | ✅ | npm run build EXIT=0(43.72s,仅既有 chunk 体积告警);grep 条待我处理 dist/.../ApprovalList-*.js 无命中;page.total/tabCounts.pending 仍分别在分页与 tab 处引用 |
| 备份 | ✅ | .backup/20260914-v24-drop-approval-total/views/notice/ApprovalList.vue(工程非 git) |
验证:待人工在 #/approval 冒烟确认左列首行即为状态 tab、无残留空白。
v25(2026-09-14)· 审批详情伸缩框扩大为「标题以下整块」
一、需求(用户截图圈红)
#/approval 右列,用户圈红标题以下整块(元信息 + 提示条 + 正文),要求把「伸缩框」从 v23 的「仅折叠审批流程/轨迹」改为包含标题外整个详情区。经确认取「扩大折叠区但不动开关」:折叠开关仍留在常驻提示条内。
二、现状与取舍
| 编号 | 证据 / 决策 |
|---|---|
| S1 | v23:approvalOpen 仅包 .flow-block+.track-block(在 .detail-scroll 内),元信息(.title-content)与正文(.notice-content)恒显示 |
| S2 | 折叠开关 .alert-toggle 位于常驻 .approval-bar(提示条)内,非在 .detail-scroll 内 |
| D1 | 若把提示条也纳入折叠区,收起后开关随之消失、无法再展开 → 定:提示条作为伸缩框「把手」保持常驻,折叠区 = 元信息 + 审批 + 正文附件(标题以下除提示条外的全部) |
| D2 | 「不动开关」按位置理解:开关位置与文案(查看审批/收起审批)均不改 |
三、改动(仅 src/views/notice/ApprovalList.vue)
| 项 | 改动 |
|---|---|
| F1 | .title-content(元信息)加 v-if="approvalOpen" |
| F2 | .detail-scroll 整块加 v-if="approvalOpen",并删除其内部原只包审批信息的 <template v-if="approvalOpen">(flow/track 与正文现同属该折叠块) |
| F3 | 更新 approvalOpen 注释:语义由「审批信息展开」扩为「标题以下整块详情展开」 |
不改:.notice-title(标题行)、.approval-bar(常驻提示条 + 开关)、hasApprovalInfo 门控、开关文案、接口与其它页面。
收起态视觉:仅标题行 + 提示条(含「查看审批」把手);展开态:标题 + 元信息 + 提示条 + 审批流程/轨迹 + 正文附件。
四、回归清单(v25)
- 点提示条右侧「查看审批/收起审批」,收起后元信息 + 正文 + 审批一起隐藏、只剩标题行与提示条;再点全部恢复
- 收起后开关仍在提示条上可见可点(不会把自己收进去)
- 默认进入详情为展开态(
approvalOpen=true) npm run build通过
五、实施记录(v25 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| F1 元信息折叠 | ✅ 2026-09-14 | .title-content 加 v-if="approvalOpen" |
| F2 滚动区折叠 | ✅ | .detail-scroll 加 v-if="approvalOpen",移除内层 <template> 包裹 |
| F3 注释 | ✅ | approvalOpen 语义更新为整块详情 |
| 构建验证 | ✅ | npm run build EXIT=0(20.19s,仅 _utils.scss 既有 *zoom 告警);ApprovalList-*.js 正常产出 |
| 备份 | ✅ | .backup/20260914-v25-approval-fold-whole-detail/views/notice/ApprovalList.vue(工程非 git) |
验证:待人工在 #/approval 冒烟(收起只剩标题行+提示条、展开恢复整块、开关常驻可点)。
v26(2026-09-14)· 已发送/全部公告:标题列不显示 + 超 8 字截断加略号
一、需求(用户截图)
#/list(已发送)与 #/noticelist(全部公告)共用 NoticeList.vue。用户截图反馈两点:①标题列不显示内容(只见「…」);②要求标题超过 8 个字时截断并在末尾显示略号。
二、根因盘点
| 编号 | 证据 / 结论 |
|---|---|
| S1 | 后端 NoticeVo.java 有 title 字段,DB notice.title 均有值(如「这是一条公告」)→ 数据侧无问题 |
| S2 | 端口探测:8101=后端 IDEA Debug;4173=vite preview(PID 13552)在跑,未起 dev(8077) |
| S3 | vite preview 载的是旧 dist/,旧「标题」列 {key:"title", tooltip:true} 无 width,被定宽列挤压塌成窄列渲染成「…」,且不含截断逻辑 → 截图现象由此而来 |
| D1 | 源码侧改「标题」列为自定义 render:t.length>8 ? slice(0,8)+"…" : t,并加 minWidth:200 防塌、原生 title 属性悬浮看全文;改完必须 npm run build 重打 dist,preview 才会反映 |
三、改动(仅 src/views/notice/NoticeList.vue)
| 项 | 改动 |
|---|---|
| F1 | 「标题」列由 key+tooltip 改为 render:按字符数截断(>8 取前 8 字 + …),minWidth:200,attrs.title 存全文供悬浮 |
两页共用同一组件,一处改动同时覆盖「已发送」与「全部公告」。
四、回归清单(v26)
#/noticelist、#/list标题列正常显示文字(不再是空「…」)- 标题 >8 字显示前 8 字 +
…;≤8 字原样 - 鼠标悬停标题单元格可看到完整标题
npm run build通过
五、实施记录(v26 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| F1 标题 render | ✅ 2026-09-14 | slice(0,8)+"…",minWidth 200,native title 全文 |
| 构建 | ✅ | npm run build EXIT=0(41.51s);NoticeList-MS8GxHwa.js 内含 a.length>8?\${a.slice(0,8)}…`:a与attrs:{title:a}` |
| 备份 | ✅ | .backup/20260914-v26-notice-title-ellipsis/views/notice/NoticeList.vue(工程非 git) |
验证:用户当前跑 vite preview(4173),须硬刷新(Ctrl+Shift+R)加载新 dist;反复调样式建议改 npm run dev(8077) 免每次 build。
v27(2026-09-14)· 审批详情伸缩框含提示条整块收起,折叠开关上移标题行
注:编号顺延——v26 已被并行会话(已发送/全部公告标题列)占用,本节记 v27。承接并修订 v25:v25 让提示条常驻作把手,本次按用户新截图(红框把黄色提示条也框进折叠区)改为提示条一并收起。
一、需求(用户截图圈红)
#/approval 右列,红框 = 标题以下整块(元信息 + 黄色提示条 + 审批流程/轨迹 + 正文附件),要求伸缩框覆盖到含提示条的全部;即收起后只剩标题行。
二、冲突与决策
| 编号 | 证据 / 决策 |
|---|---|
| S1 | v25 折叠开关 .alert-toggle 长在提示条 .approval-bar 内,且提示条常驻作「把手」 |
| S2 | 若把提示条纳入折叠区而开关仍在其内,则收起后开关随之消失、无法再展开(v25 遗留的取舍点) |
| D1 | 定:折叠开关上移到标题行(.title-header 内 .detail-toggle,常驻可见),提示条并入折叠区 v-if="showAlert && approvalOpen" |
| D2 | 开关文案由「查看审批/收起审批」改为「展开/收起」——现折叠的是整块详情而非仅审批信息 |
| D3 | hasApprovalInfo(原门控旧开关显隐)失去唯一消费者,删除;标题行开关恒可见 |
三、改动(仅 src/views/notice/ApprovalList.vue)
| 项 | 改动 |
|---|---|
| F1 | 模板:.title-header 内新增 .title-left(.tt + .detail-toggle),开关 @click="approvalOpen=!approvalOpen",chevron 随开合旋转 |
| F2 | 模板:.approval-bar 的 v-if 由 showAlert 改为 showAlert && approvalOpen,并删除其内部 .alert-toggle(只留状态文案) |
| F3 | 脚本:删 hasApprovalInfo;更新 approvalOpen 注释为「控制标题以下整块」 |
| F4 | 样式:.title-header 加 .title-left/.detail-toggle(含 chevron 边框绘制);删 .alert-row 内已无引用的 .alert-toggle 块 |
折叠区(approvalOpen)现覆盖:.title-content(元信息)+ .approval-bar(提示条)+ .detail-scroll(审批 + 正文附件)。不改:标题行、ops(上一篇/下一篇/通过/驳回/打印)、接口与其它页面。
收起态:仅标题行(标题 + 展开 + ops);展开态:标题 + 元信息 + 提示条 + 审批 + 正文附件。
四、回归清单(v27)
- 点标题行「收起」→ 元信息、黄色提示条、审批、正文一并隐藏,只剩标题行;点「展开」全部恢复
- 收起后「展开」开关仍在标题行可见可点(不再随提示条消失)
- 默认进入详情为展开态
- 通过/驳回/打印等 ops 与提示条文案配色不受影响
npm run build通过
五、实施记录(v27 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| F1 标题行开关 | ✅ 2026-09-14 | .title-left 包 .tt+.detail-toggle,文案「展开/收起」 |
| F2 提示条并入折叠 | ✅ | v-if="showAlert && approvalOpen",移除内嵌开关 |
| F3 脚本清理 | ✅ | 删 hasApprovalInfo(grep 确认无其它引用) |
| F4 样式 | ✅ | 加 .title-left/.detail-toggle+chevron,删 .alert-toggle |
| 构建验证 | ✅ | npm run build EXIT=0(18.25s,仅 _utils.scss 既有 *zoom 告警);ApprovalList-*.js/.css 正常产出 |
| 备份 | ✅ | 承接 v25 备份链(.backup/20260914-v25-approval-fold-whole-detail/ 为改前态,工程非 git) |
验证:待人工在 #/approval 冒烟(收起只剩标题行、展开恢复整块含提示条、开关常驻标题行可点)。
v28(2026-09-14)· 公告详情「上一篇/下一篇」点击后不跳转(goNav 竞态)
一、需求(用户反馈)
#/noticelist/detail?serid=…&type=0(及 /list/detail)点「下一篇」报「接口错误」,用户贴出请求 Detail?serid=1&type=0。
二、排查
| 编号 | 证据 / 结论 |
|---|---|
| S1 | 后端 NoticeVo/checkDetailAccess:serid=1 为 lijing 本人待审批单,本人可访问;notice_approval_step 无行、数据干净 → Detail?serid=1&type=0 正常应 200 |
| S2 | 端口:4173=vite preview(载 dist)、8101=后端。curl 打 4173 /api 返回 302→SSO(无会话),证明 preview 已代理 /api(继承 server.proxy),代理链路正常 |
| S3 | 无法用无会话 curl 复现「接口错误」(后端 302 由 alova 层转跳 SSO,不弹错);需用户登录态才能抓具体响应 |
| D1 | 定位到明确代码缺陷:goNav 在 await 前同步调用 load(),而 router.replace 是异步提交 → load() 读到旧 serid,点「下一篇」实际重拉当前公告、序列不前进(表现为「点了没反应/异常」) |
三、改动(仅 src/views/notice/NoticeDetails.vue)
| 项 | 改动 |
|---|---|
| F1 | import 补 watch |
| F2 | 新增 watch(() => route.query.serid, id => Number(id) && load()):加载统一由路由变化驱动(兼顾上一篇/下一篇 + 浏览器前进后退) |
| F3 | goNav 去掉同步 load(),只 router.replace 改地址,避免读到未提交的旧 serid |
四、回归清单(v28)
- 详情页点「下一篇/上一篇」能真正切换到相邻公告、URL 与内容一致
- 浏览器前进/后退改变 serid 时详情随之刷新
- 首屏进入仍正常加载(onMounted 的 load 与 watch 不重复触发)
npm run build通过
五、实施记录(v28 回填)
| 项 | 状态 | 备注 |
|---|---|---|
| F1/F2/F3 | ✅ 2026-09-14 | 竞态修复:watch 驱动 load,goNav 只导航 |
| 构建 | ✅ | npm run build EXIT=0(18.26s) |
| 备份 | ✅ | .backup/20260914-v27-detail-nav-race/views/notice/NoticeDetails.vue(工程非 git;目录名沿用早期编号,内容即本次改前态) |
待确认:若硬刷新后「下一篇」仍报接口错误,请提供该失败请求的 HTTP 状态码 + 响应体(Network 面板标红那条),因后端需登录态、无法离线复现。