20260903221055

This commit is contained in:
oneao committed 2026-09-03 22:10:55 +08:00
1 parent 3d4e2c7413
commit 0928437c67
46 files changed
+2121 -1643

No files matched your search

@@ -0,0 +1,129 @@
---
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: 确认无旧名/旧路径残留,通过引用搜索验证改造完整性