266 lines
17 KiB
Markdown
266 lines
17 KiB
Markdown
---
|
||
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` 注入点
|
||
- 预期产出:引用点完整性校验报告,确保契约变更后无悬挂引用 |