Files
workspace/code/fms/.codebuddy/plans/module-management-index-重构-面板自治_3c0e8f17.md
T
2026-09-04 17:31:36 +08:00

266 lines
17 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: module-management-index-重构-面板自治
overview: 重构 fms-vue 模块管理页:将 index.vue(1600 行)中散落的面板数据回写回调与业务逻辑下放到各面板组件,index 只持有 drafts/originals 两份数据,通过 v-model 与面板双向绑定,保存时统一 diff。目标是将 index.vue 精简到 500 行以内。
todos:
- id: diff-foundation
content: 改造 utils/dataChanges.js:diffRows 增加剔除 _ / v_ 前缀字段,抽出 stripInternalKeys 供 diffRows 与 hasUnsavedChanges 共用
status: completed
- id: model-table-form-panels
content: 模式 A/C 改造表格类与表单类面板:ModuleFieldsPanel、ModuleQueryConfigPanel、ModuleBasicPanel、ModuleAutoCodePanel 改 defineModel,去掉 local 副本、watch 同步与虚拟字段剥离
status: completed
dependencies:
- diff-foundation
- id: model-designers
content: 用 [subagent:code-explorer] 与 [skill:lsp-code-analysis] 定位并改造 FormDesignEditor、TableDesignEditor 及壳组件 ModuleListConfigPanel、ModuleEditPanel 为 defineModel,useStructureEditing 只换写入口
status: completed
dependencies:
- model-table-form-panels
- id: index-rewire
content: 重接 index.vue:全部面板改 v-model 双向绑定,删除 4 个 proxy 回调,新增分组引用清理、自动编码只读、i18n reconcile 三个跨面板 watch
status: completed
dependencies:
- model-designers
- id: sync-fields-down
content: 将 buildSyncPlan、applySyncPlan、syncFields 迁入 ModuleFieldsPanel,同步按钮移至字段面板工具栏,index 相应删除调用与按钮
status: completed
dependencies:
- index-rewire
- id: extract-module-tree
content: 新建 useModuleTree.js 抽出模块树的加载、新增、删除、拖拽排序与右键菜单逻辑,index.vue 改为解构接入
status: completed
dependencies:
- sync-fields-down
- id: verify-regression
content: 编译校验与关键流程回归:新建模块保存、字段同步、分组增删、自动编码、i18n、模块拖拽排序、切换模块脏检查
status: completed
dependencies:
- extract-module-tree
---
## 用户要求
对 `fms-vue/src/views/module/module-management/index.vue`(1600 行)进行职责解耦重构:
- **index.vue 只保留两个核心状态**:`drafts`(草稿数组对象)与 `originals`(基线数组对象),保存时执行一次 diff 即可。
- **各面板自治**:字段、列表、编辑、查询、自动编码、多语言、基础信息各自的编辑逻辑与数据写入,下沉到各自组件内部,通过 `v-model` 双向绑定自己那份数据,不再由父级中转。
## 产品概述
模块管理页面(左侧模块树 + 右侧分节配置面板)的内部架构重构,属纯前端逻辑层改造,页面功能与视觉表现保持不变,仅"同步字段"按钮从顶部通栏移入字段定义面板工具栏。
## 核心特性
- **数据所有权下沉**:7 个配置面板通过 `defineModel` 直接读写 `drafts` 中属于自己的那一份数据,父级不再提供 proxy 回调。
- **保存时统一 diff**:index.vue 保留 `drafts` / `originals` 两份快照,`handleSave` 时按主表 `diffRow`、子表 `diffRows`、i18n `buildI18nChangeRequest` 生成差异请求。
- **跨面板联动改为响应式**:分组删除清理引用、自动编码字段置只读、受管 i18n 键补齐,由事件回调改为父级 `watch` 幂等联动。
- **字段同步内聚**:`syncFields` / `buildSyncPlan` / `applySyncPlan` 整体移入 `ModuleFieldsPanel`,同步按钮随行迁至字段面板工具栏。
- **树逻辑外移**:模块树的加载、新增、删除、拖拽排序、右键菜单(约 250 行)抽为 `useModuleTree.js`。
## 预期效果
`index.vue` 由 1600 行降至 500 行以内,仅剩编排职责(树 + 草稿加载 + 保存编排 + 3 个跨面板 watch + 模板接线);面板组件各自内聚、可独立测试。
## 技术栈
沿用项目现有技术栈,不引入新依赖:
- Vue 3 `<script setup>` + `defineModel`(Vue 3.4+ 稳定 API,项目中 `ModuleI18nPanel.vue` 已在使用)
- Vite + `@/components/ui` 自建组件库(`FmsTable` / `Form` / `Modal` 等)
- 数据层:通用 API `loadDataApi` / `saveObjectApi` / `nextIdApi` / `describeApi`
- 差异计算:`@/utils/dataChanges`(`diffRow` / `diffRows` / `allocateTemporaryIds` / `remapForeignKeys`)
## 实现思路
### 探索验证结论(决定方案可行性的 4 个事实)
1. **改造是封闭的**:`ModuleBasicPanel` / `ModuleFieldsPanel` / `ModuleListConfigPanel` / `ModuleEditPanel` / `ModuleQueryConfigPanel` / `ModuleAutoCodePanel` / `ModuleI18nPanel` / `TableDesignEditor` / `FormDesignEditor` 的全部引用点都在 `module-management/index.vue` 内,无外部调用者。改组件签名不会破坏其他页面。
2. **`cloneData` 是 JSON 深拷贝**,故 `drafts` 与 `originals` 完全独立,组件就地修改 `drafts` 的行对象不会污染基线,保存时 diff 依然准确。
3. **`useStructureEditing`(33KB 内核)无需任何改动**:它采用依赖注入,`localRows` / `localGroups` 副本 + `watch(getRows)` 同步 + `applyRows` / `applyGroups` 两个写入口均由宿主提供。改造只需把 getter 指向 model、把两个写入口改为写 model,其 `if (val === localRows.value) return` 防环逻辑天然继续生效,零循环风险。
4. **表格渲染不能依赖深度侦听**:`ModuleQueryConfigPanel` 注释明确 "stk-table 对同一引用数组的 push 不敏感,必须换新引用才会渲染新行"。故约定 **行内字段就地 patch + 提交时换新数组引用**。
### 架构设计
```mermaid
flowchart TB
subgraph IDX["index.vue(编排层 ~500 行)"]
direction TB
TREE["useModuleTree.js<br/>树加载/新增/删除/拖拽排序/右键菜单"]
ST["状态:originals + drafts"]
LOAD["loadModuleConfig"]
SAVE["handleSave<br/>validate → diffRow/diffRows<br/>→ 分配真实ID → 跨表重映射 → reload"]
W1["watch: i18n reconcile(防抖300ms)"]
W2["watch: 分组删除 → 清理引用"]
W3["watch: 自动编码字段 → 置只读"]
end
B["ModuleBasicPanel"] <-->|"v-model"| ST
F["ModuleFieldsPanel<br/>含 syncFields"] <-->|"v-model × 6"| ST
L["ModuleListConfigPanel"] <-->|"v-model × 2"| ST
E["ModuleEditPanel"] <-->|"v-model × 4"| ST
Q["ModuleQueryConfigPanel"] <-->|"v-model"| ST
A["ModuleAutoCodePanel"] <-->|"v-model"| ST
I["ModuleI18nPanel"] <-->|"v-model:rows"| ST
L --> TDE["TableDesignEditor"]
E --> TDE
E --> FDE["FormDesignEditor"]
TDE --> USE["useStructureEditing<br/>(内核不改,仅换写入口)"]
FDE --> USE
ST --> SAVE
```
### 三种改造模式
**模式 A · 表格类面板**(`ModuleFieldsPanel` / `ModuleQueryConfigPanel`)
删除 local 副本与 `watch(props)` 同步,直接以 model 作为 `FmsTable` 数据源;行内编辑就地 patch,提交时换新数组引用触发渲染。
**模式 B · 设计器**(`TableDesignEditor` / `FormDesignEditor`)
保留 `useStructureEditing` 的 local 副本机制,仅替换注入的 getter 与两个写入口。这是本次改动中风险最低、收益最高的一处。
**模式 C · 表单类面板**(`ModuleBasicPanel` / `ModuleAutoCodePanel`)
`defineModel` 拿到对象引用后直接改属性即可(无需 emit,因为 `drafts.module` 与 `originals.module` 已是两份深拷贝)。
### 关键决策与权衡
| 决策 | 选择 | 理由 |
| --- | --- | --- |
| 是否抽 `useModuleTree` | **抽** | 250 行树逻辑与配置编辑零耦合;作为独立最后阶段,可单独验证与回退 |
| 同步字段按钮位置 | **移入 `ModuleFieldsPanel` 工具栏** | 符合"谁的数据谁负责";若留顶部通栏需额外暴露 ref,反而增加耦合 |
| 步骤 1 是否拆分 | **拆成 1a 表格类 / 1b 设计器与表单类** | `FormDesignEditor` 96KB 为最大改动块,拆分降低单次回归风险 |
| 分组删除清理引用 | **改父级 `watch` 差集推断** | 原 `applyGroups(groups, removedIds)` 携带的 removedIds 不再上抛;watch 版幂等且同时覆盖 `listConfig` / `editConfig`,比原回调更不易漏 |
| 自动编码只读联动 | **改父级 `watch`** | 逻辑幂等;需回归验证 `syncFields` 批量改 `editConfig` 时的行为 |
| i18n reconcile | **保留在 index**(不下放) | 其依赖横跨 module/fields/groups;且保存前需同步执行一次,`v-show` 隐藏时防抖未完成会漏。保留纯函数 `applyManagedI18n()` 由 index 直接调用 |
## 实现要点(防回归)
### 1. 地基:`diffRows` 必须补剔除逻辑(否则字段面板的 `__form/__list/__query` 会进保存请求)
`diffRow`(主表)已有剔除 `_` / `v_` 前缀的逻辑,`diffRows`(子表)缺失,需对齐。改造后 `ModuleFieldsPanel` 不再逐行剥离虚拟字段,因此这一步是前置依赖。
### 2. `stk-table` 渲染约定(最容易踩的坑)
所有表格面板提交时必须换新数组引用,不能只就地改行对象:
```js
function commit() {
rows.value = [...rows.value] // 新数组引用,行对象引用保持不变
}
```
### 3. `ModuleAutoCodePanel` 改为惰性建行(顺带修掉一个既有缺陷)
现状 `watch(immediate)` 在面板挂载时即 `createDraft()` 造一行,导致用户从未配置自动编码也会 diff 出一条无意义 insert。改为:仅当用户实际编辑时才建行,`rows.value[0]` 不存在时以 `computed` 返回 null 渲染空态。
### 4. 保存编排链路不可下放
`beforeSave`(跨面板 `validate` + 依赖校验 + i18n 键物化)→ `buildSaveData`(diff)→ `prepareSaveReqs`(`allocateTemporaryIds` + `remapForeignKeys` 跨表重映射)→ `afterSave`(重载回灌真实 ID)必须留在 index。临时负数 ID → 真实雪花 ID 的重映射依赖 `drafts` 与待提交请求共享同一批行对象引用,**改造后此特性必须保持**(`defineModel` 共享引用天然满足)。
### 5. 中间态说明
步骤 1a / 1b 改完组件契约后、步骤 2 完成 index 接线前,页面处于不可用中间态。因此 1a、1b 须连续执行,不可中途停手。
## 目录结构
```
fms-vue/src/
├── utils/
│ └── dataChanges.js # [MODIFY] diffRows 增加剔除 _ / v_ 前缀字段(与 diffRow 对齐);
│ # 新增 stripInternalKeys 供 diffRows 与 index 的 hasUnsavedChanges 共用
└── views/module/module-management/
├── index.vue # [MODIFY 主体] 1600 行 → ~500 行。
│ # 删除:4 个 proxy 回调(onModuleUpdate / onFieldsUpdate /
│ # onAutoCodeUpdate / onGroupsUpdate)、syncFields 三件套、树逻辑、moduleRowOf
│ # 保留:originals / drafts、loadModuleConfig、保存编排四步、脏检查、快捷键、模板与样式
│ # 新增:3 个跨面板 watch、useModuleTree 接入、全部面板 v-model 接线
├── useModuleTree.js # [NEW ~250 行] 模块树逻辑:loadModuleTree / addModule / deleteModule /
│ # onModuleDrop / buildModuleContextMenu / onModuleContextMenuClick /
│ # findNodePosition / renumberSiblings / hasSavedModuleDescendant /
│ # collectUnsavedModuleIds / markCategoryNonLeaf / moduleRowOf /
│ # unsavedModuleIds 集合。导出响应式状态与方法供 index 解构
├── ModuleBasicPanel.vue # [MODIFY] :data + @update → defineModel 默认 model(模式 C);
│ # setField / onCodeInput / onModuleTypeChange 直接改属性,不再 emit
├── ModuleFieldsPanel.vue # [MODIFY 职责最重] defineModel × 6(fields / editConfig / listConfig /
│ # queryConfig / i18n / module);模式 A 去掉 local 副本与虚拟字段剥离;
│ # 承接从 index 迁入的 buildSyncPlan / applySyncPlan / syncFields,
│ # 并在面板工具栏新增「同步字段」按钮
├── ModuleListConfigPanel.vue # [MODIFY] 壳组件:defineModel × 2(listConfig / groups);
│ # 向下 TableDesignEditor 透传 v-model
├── ModuleEditPanel.vue # [MODIFY] 壳组件:defineModel × 4(editConfig / listConfig / groups / editMode);
│ # 向 FormDesignEditor / TableDesignEditor 透传 v-model
├── ModuleQueryConfigPanel.vue # [MODIFY] defineModel(模式 A);去掉 localQuery 副本与 watch 同步
├── ModuleAutoCodePanel.vue # [MODIFY] defineModel(模式 C);改为惰性建行,未配置时不产生脏行
├── ModuleI18nPanel.vue # [不改] 已使用 defineModel('rows'),作为本次改造的契约样板
├── utils.js # [不改] i18n 纯函数(reconcileI18nRows / buildI18nChangeRequest 等)
├── components/fieldDefaults.js # [不改] 行默认值工厂
└── components/design-editor/
├── TableDesignEditor.vue # [MODIFY] rows / groups / editMode 改 defineModel(模式 B);
│ # applyRows / applyGroups 从 emit 改为写 model
├── FormDesignEditor.vue # [MODIFY 96KB 最大块] 同上;需用 LSP 定位全部 emit('update') /
│ # emit('updateGroups') / emit('updateListConfig') 调用点集中替换
└── common/useStructureEditing.js # [不改] 依赖注入式内核,写入口由宿主提供,本次仅换调用侧
```
## 关键代码结构
### 1. `useStructureEditing` 宿主注入契约改造(模式 B 的核心,33KB 内核零改动)
```js
// TableDesignEditor.vue / FormDesignEditor.vue —— 改造后
const rows = defineModel('rows', { type: Array, default: () => [] })
const groups = defineModel('groups', { type: Array, default: () => [] })
const editMode = defineModel('editMode', { type: String, default: 'form' })
const editing = useStructureEditing({
moduleId: () => props.moduleId,
getRows: () => rows.value, // 原:() => props.rows
getGroups: () => groups.value, // 原:() => props.groups
createRow: props.createRow,
applyRows: (next) => { rows.value = next }, // 原:(r) => emit('update', r)
applyGroups: (next) => { groups.value = next }, // 原:(g, ids) => emit('updateGroups', g, ids)
requestRename: (gid) => structureTreeRef.value?.startRename(gid),
fieldNameOf: (row) => fieldName(row),
getField: (fieldId) => fieldMap.value.get(fieldId),
})
```
### 2. index.vue 三个跨面板 watch 的签名(幂等,替代原 proxy 回调)
```js
// 分组删除 → 清理列表/编辑配置中的 b_group_id 引用
watch(() => drafts.value?.groups, (next, prev) => { /* 差集推断 removedIds,置 null */ })
// 自动编码字段 → 在表单中置只读;换字段时还原旧字段
watch(
() => (Number(drafts.value?.autoCode?.[0]?.b_canuse) === 1
? String(drafts.value.autoCode[0].b_field_id ?? '') : ''),
(nextId, prevId) => { /* 遍历 editConfig 幂等置位 */ },
)
// 受管 i18n 键补齐(保留在 index,防抖 300ms;保存前由 beforeSave 同步直调 applyManagedI18n 兜底)
watch(managedI18nItems, () => { /* 防抖后 reconcile */ })
```
## 执行约束
- 每次提交后页面须可运行:步骤 1a、1b 连续执行不可中断;步骤 2 / 3 / 4 各自完成后独立可验证。
- 不改动 `useStructureEditing.js`、`utils.js`、`fieldDefaults.js`、`tree.js` 及 `table/` `form/` 下的受控展示子组件。
- 复用既有 `Message` 提示风格与 `_` / `v_` 前缀字段约定,不新增全局约定。
## Agent Extensions
### SubAgent
- **code-explorer**
- 用途:在 `FormDesignEditor.vue`(96KB / 约 2500 行)中精确定位全部 `emit('update')` / `emit('updateGroups')` / `emit('updateListConfig')` / `emit('update:editMode')` 调用点及其所在函数,避免肉眼遗漏导致写入口漏改
- 预期产出:完整的调用点清单(行号 + 所属函数 + 上下文),据此集中替换为 model 写入
### Skill
- **lsp-code-analysis**
- 用途:改造前后对各面板的 props / emits 做符号级引用导航(find references / call hierarchy),确认无遗漏调用点,并复核 `useStructureEditing` 的 `applyRows` / `applyGroups` 注入点
- 预期产出:引用点完整性校验报告,确保契约变更后无悬挂引用