Files
workspace/code/fms/.codebuddy/plans/fms-vue表单保存流程规范重构_087f3fa9.md
T
2026-09-03 22:10:55 +08:00

129 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: fms-vue表单保存流程规范重构
overview: 按根目录《开发规范.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 等调用方与相关测试。
todos:
- id: refactor-edit-modal
content: 重构 FmsModuleEditModal:新增 origin 快照与编辑态 diff、拆分 handleSave/beforeSave/buildSaveData/afterSave、prop beforeSave 更名 onBeforeSave,无变化时不调保存接口;同步 i18n-type/index.vue 的 prop 绑定
status: completed
- id: refactor-menu-save
content: 用 [skill:lsp-code-analysis] 定位引用后,将 menu-management/index.vue 的 saveMenu 重构为内联五段式并补 s_menu/s_i18n diff 与无变化跳过,同步模板/hotkey/onSwitchSave
status: completed
- id: refactor-module-save-i18n
content: 将 module-management/index.vue 的 saveModule 重构为内联五段式,并用 [subagent:code-explorer] 复核后把 i18n 基线/diff 统一上移、ModuleI18nPanel 改为受控展示组件、迁移 applySyncPlan 联动
status: completed
- id: sync-tests-quality
content: 删除引用缺失 composable 的 module-save-orchestration.spec.js 并清理残留引用,核对 module-dirty-sections.spec.js,运行 oxlint/oxfmt 与受影响单测
status: completed
dependencies:
- refactor-edit-modal
- refactor-menu-save
- refactor-module-save-i18n
---
## 用户需求
将 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` 的外部 prop `beforeSave` 改名(建议 `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: 确认无旧名/旧路径残留,通过引用搜索验证改造完整性