13 KiB
13 KiB
name, overview, todos
| name | overview | todos | |||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| fms-vue表单保存流程规范重构 | 按根目录《开发规范.md》的「详细表单页面设计」与「表单数据与变更检测」章节,将 fms-vue 三处详细表单保存主流程(module-management/index.vue、menu-management/index.vue、FmsModuleEditModal.vue)统一重构为 handleSave/beforeSave/buildSaveData/afterSave 五段式结构,补齐 origin 基线 + diff 保存 + 成功后重载;ModuleI18nPanel 退化为受控数据展示组件,i18n diff 上移到 index.vue 统一编排;并同步 i18n-type 等调用方与相关测试。 |
|
用户需求
将 fms-vue 中「详细表单页面」的代码结构与命名统一重构,使其完全符合根目录 开发规范.md 中「详细表单页面设计」(52-62 行)与「表单数据与变更检测」(64-108 行)两条规范,消除"相似但不一致"的实现差异。
重构范围(用户已确认)
- 仅三处表单主流程:
module-management/index.vue、menu-management/index.vue、FmsModuleEditModal.vue,及调用方i18n-type/index.vue的同步。 - 列表/内联保存保留专用名,不改:
saveToggle、saveChanges、deleteSelected、persistRowOrder、saveColumnSettings、菜单/模块树的拖拽即时保存与启用切换。 - module-management 保存编排保持内联在 index.vue(不新建 composable)。
- ModuleI18nPanel 一并纳入统一:i18n 的 origin/当前数据与 diff 由 index.vue 统一持有与计算,面板退化为受控数据展示组件。
beforeSave名称冲突处理:FmsModuleEditModal的外部 propbeforeSave改名(建议onBeforeSave,模板:on-before-save),组件内部标准方法使用规范名beforeSave()。
目标统一形态
每个入口遵循固定流程与命名:handleSave()(入口)→ beforeSave()(校验/字段同步/数据准备,false 终止)→ buildSaveData()(组装请求,返回 null 表示无变化不调接口)→ saveObjectApi() → afterSave()(成功后的 reload/状态更新)。编辑态必须有 origin 基线、只提交 diff 出的变化、无变化时不调用保存接口、成功后重新从服务端加载再更新基线。
技术栈
Vue 3 <script setup> + Pinia + 现有组件库(ui/Modal/Form/FmsTable)+ 既有工具 @/utils/dataChanges(cloneData/diffRows/tableChange/hasChanges/allocateTemporaryIds/remapForeignKeys)与 @/utils/common(createTempId)。纯前端重构,不涉及后端接口与数据库。
实现方案
1. FmsModuleEditModal.vue(含调用方 i18n-type/index.vue)
- 新增 origin 基线:
loadConfig()中buildFormData()之前对props.record深拷贝为originRef(ref)。新增模式originRef = null。弹窗复用(重新打开)自动重建基线。 - 保存流程拆五段:
beforeSave():现有必填校验(268-276 行逻辑迁入)+ 调用改名后的外部钩子props.onBeforeSave(完整行, ctx)(返回 false 终止;返回{ extraUpdates }暂存到局部变量)。外部钩子继续接收完整合并行(含未变字段),保证 i18n-type 等钩子判断rowData.b_default等字段语义不变。buildSaveData():构造完整行(现 collectRowData 逻辑)后做 diff —— 新增模式返回inserts=[完整行];编辑模式将完整行与"由 originRef 重建的初始 formData 对应字段"逐字段归一化比较(checkbox 0/1、'' 与 null、number 空值),只提交有变化的字段 + 主键;若表单无字段变化且无extraUpdates返回null(无变化不调接口)。afterSave():Message.success、emit('saved', ...)、emit('update:open', false)。handleSave():按规范骨架编排并捕获错误提示,返回 boolean 保持现语义。- prop 改名:
beforeSave→onBeforeSave(props 声明、JSDoc 用法注释 12-21 行、模板、i18n-type/index.vue的:before-save="beforeModalSave"同步为:on-before-save)。页面私有函数名beforeModalSave保留。 - 行为兼容注意:仅当提交字段无任何 diff 且钩子未返回 extraUpdates 时才跳过接口并提示"没有需要保存的变更";disabled/readonly 字段依旧不参与收集与比较。
2. menu-management/index.vue
saveMenu()重构为内联五段式(页面 script 内同文件拆分,不抽 composable),改名handleSave(),模板@click、useHotkeys、onSwitchSave(await 返回 boolean)三处调用点同步。beforeSave():迁入表单validateAll、菜单类型/必填/上级不能是自己等校验、类型规则清理(b_route/b_module_id 置 null)、b_code trim、menuData.b_i18n物化等"校验 + 字段同步 + 数据准备"。buildSaveData():组包全部迁入并补 diff:- s_menu 主表:编辑态将(beforeSave 规范化后的)
menuData与originals.menu逐字段比较,仅不同才推updates(不再无条件整行提交); - s_i18n:与
originalI18nValues比较,非默认语言值无任何差异时不生成"先删后插"请求; - s_power 访问/操作权限:保留现有基于
powersBaseline的手动 diff 与目录清理逻辑; - 全部无变化时返回
null(handleSave 不调接口)。 afterSave():unsavedMenuIds.delete、树节点同步(含 markDirectoryNonLeaf)、await loadMenuConfig(menuId)、成功提示。configSaving状态归handleSave管理;onSwitchSave依赖的返回语义(false 留在当前 / true 继续切换)保持不变。
3. module-management/index.vue:保存五段式 + i18n 统一(含 ModuleI18nPanel)
saveModule()重构为内联五段式,改名handleSave(),同步模板 /useHotkeys/onSwitchSave调用点。beforeSave():迁入基础面板与自动编码面板校验(basicPanelRef/autoCodePanelRef.validate)、validateAutoCode()跨面板校验、i18n 键物化(syncModuleItemI18nKeys+moduleData.b_i18n赋值,属"字段同步/数据准备")。buildSaveData():迁入主表 JSON 比较 + 六张子表diffRows/allocateTemporaryIds/remapForeignKeys/s_autocode 剥离等现有编排,并用统一 i18n diff 取代i18nPanelRef.buildSaveRequest/applyInsertedIds;无任何请求时返回null(保留"没有需要保存的变更"提示语义)。afterSave():unsavedModuleIds.delete、树节点更新、await loadModuleConfig(moduleId)(先 reload 覆盖 origin/draft 再更新基线)、恢复activeSection、成功提示。- i18n 上移设计(ModuleI18nPanel 受控化):
- index.vue 将现有
rawI18nRows(扁平行)扩展为完整基线存originals.i18n,并新增草稿态drafts.i18n(扁平行:b_id 真实/临时、b_key、b_locale、b_value、b_module_id、b_canuse;临时 id 用 createTempId 负数规则由现有实现定义)。加载(loadModuleConfig / addModule / watch 切回未保存模块)时同步初始化。 - ModuleI18nPanel 改为受控组件:props 收
flatRows(=drafts.i18n)、module/fields/groups/languages/loading;不再自持 localRows/originalsFlat,不再自动 reconcile;表格透视展示改为对 flatRows 的 computed 派生(b_key 分组、每语言一列);编辑行为全部emit上行:update-value(key, locale, value)、add-row、remove-row(keys)、编辑态/选中态等本地 UI 状态保留面板内(activeKey/selectedRowKeys 仅为交互态,不入草稿)。 - reconcile(受管键补齐、默认语言跟名、失效键清理,原 109-179 行)逻辑从面板上移:抽为
module-management/utils.js纯函数或在 index.vue 内联,触发点改为 index.vue 的 watch(module/fields/groups 草稿变化防抖 300ms,与现面板行为一致)与保存前buildSaveData()调用;跟名写入drafts.i18n(不写 originals),与 originals.i18n 的 diff 语义保持不变。 - diff 核心(原 buildSaveRequest 285-329 行:namespace→b_module_id、cellKey seen 判定、deletes 兜底)迁入
buildSaveData()统一编排;保存成功后 reload 重建全部 i18n 状态,applyInsertedIds不再需要。 - 联动迁移:
applySyncPlan中i18nPanelRef.removeByManagedIds(约 922 行)改为 index.vue 直接按_managedId语义(field:key)过滤删除drafts.i18n中的失效受管扁平行;删除模块/菜单的 s_i18n 清理路径保持不变。
4. 测试与质量
tests/unit/module-save-orchestration.spec.js:import 的composables/useModuleSave在 src 中不存在(已确认目录下无该文件),且保存编排已明确保持内联 → 删除该文件,并全局搜索确认无其它引用残留(遵守"先做引用清理")。tests/unit/module-dirty-sections.spec.js:先读文件确认依赖与断言目标,若引用被重构方法/状态则同步调整,保证改造后通过。- 自查
oxlint/oxfmt,运行npm test中受影响用例。
风险与注意
- 行为保持优先:三处入口的切换未保存确认(UnsavedChangesModal)、快捷键保存、返回 boolean 语义、错误 Message 提示均不得因重构改变;
beforeSave()阶段可修改草稿(b_i18n 物化等),但不得调用保存接口。 - diff 归一化:编辑态比较前统一 checkbox 0/1、''→null、number 空值规则,避免"打开即脏"误报;i18n-type 的默认语言保护钩子依赖完整行判断,外部钩子必须收到完整合并行而非仅差异子集。
- i18n 上移是最大风险点:实现前先以 [subagent:code-explorer] 复核 ModuleI18nPanel 全部引用点与 index.vue 的 i18n 联动(loadModuleConfig/addModule/watch/applySyncPlan/删除),防止透视展示与跟名行为回退;面板受控化后 UI 交互(行内编辑、受管行禁用、排序、行选)需逐项验证。
- 性能:子表与 i18n 行数均为页面级(数十至数百行),diff 采用单次遍历 + Map 索引(沿用 diffRows 模式),无新增热点;reconcile 保留 300ms 防抖避免高频输入触发全量重建。
目录结构
fms-vue/src/components/fms-module-edit/
└── FmsModuleEditModal.vue # [MODIFY] prop beforeSave→onBeforeSave;新增 origin 快照与编辑 diff;
# 保存流程拆 handleSave/beforeSave/buildSaveData/afterSave;无变化不调接口
fms-vue/src/views/module/
├── i18n-type/
│ └── index.vue # [MODIFY] :before-save → :on-before-save(业务钩子函数名 beforeModalSave 保留)
├── menu-management/
│ └── index.vue # [MODIFY] saveMenu→handleSave 五段式内联;s_menu/s_i18n 补 diff 与无变化跳过;
│ # 同步模板/hotkey/onSwitchSave 调用点
└── module-management/
├── index.vue # [MODIFY] saveModule→handleSave 五段式内联;i18n origin/draft/diff 统一上移;
│ # applySyncPlan 联动改直改 drafts.i18n;同步三处调用点
├── ModuleI18nPanel.vue # [MODIFY] 退化为受控展示:props flatRows+派生透视、编辑事件 emit 上行、
│ # 移除 buildSaveRequest/applyInsertedIds/removeByManagedIds/自持 reconcile
└── utils.js # [MODIFY·视需要] 若 reconcile/受管键补齐抽为纯函数则放此(按目录归属规则)
fms-vue/tests/unit/
├── module-save-orchestration.spec.js # [DELETE] import 不存在的 composables/useModuleSave,保存编排确定内联
└── module-dirty-sections.spec.js # [MODIFY·视需要] 核对依赖,随重构同步
Agent Extensions
SubAgent
- code-explorer
- Purpose: 在实现前复核 ModuleI18nPanel 全部引用点、module-management/menu-management/index.vue 内被重构方法的所有调用点(模板/hotkey/onSwitchSave)、以及各测试对方法名/状态的依赖,保证影响面清单完整
- Expected outcome: 输出精确的"方法/状态/事件契约"清单,避免重构遗漏调用方或破坏联动行为
Skill
- lsp-code-analysis
- Purpose: 对 FmsModuleEditModal、saveModule、saveMenu、ModuleI18nPanel 做符号级引用与定义查找,校验重命名后的残留引用
- Expected outcome: 确认无旧名/旧路径残留,通过引用搜索验证改造完整性