chore: 重构前基线快照(工作区既有改动 + 重构计划文档)

This commit is contained in:
oneao committed 2026-08-28 14:14:03 +08:00
1 parent 35ca496b5f
commit 61ac01075b
9 files changed
+1474 -238

No files matched your search

@@ -0,0 +1,303 @@
# `/system/module-management` 代码重构计划(还债专项)
> 本文是**纯代码重构**计划:不改任何行为、UI 文案、API 调用和保存协议。
> 与 `docs/module-management-refactor-plan.md`(功能改造历史记录)无关,请勿混淆。
> 执行前请先通读本文第 1、2 节和第 8 节(禁改清单)。
## 1. 背景与问题
模块管理功能已基本完成,但代码存在三类结构性债务:
1. **巨型文件**:`ModuleFormEditPanel.vue` 3709 行、`ModuleTableEditPanel.vue` 2962 行、
`index.vue` 1522 行,全模块约 1.2 万行。
2. **三处复制的"同源"逻辑**:分组树推导在面板与结构树组件间整段复制(注释自述
"与面板 bandMeta 同源");两个编辑面板共享约 20 个同名函数。靠人肉同步,改一处漏
一处就是 bug。
3. **index.vue 职责过重**:树管理、草稿加载、保存编排、删除级联、i18n 协调、确认弹窗
全部平铺在一个 `<script setup>` 里,`saveModule` 单函数约 120 行。
目标:在不改变任何外部行为的前提下消除上述债务,让后续功能开发有清晰的落点。
## 2. 当前代码范围(行数为 2026-08 时点,执行时以符号名为准)
```text
src/views/system/module-management/
index.vue 1522 容器:树 + 草稿 + 保存编排
common.js 6
components/
ModuleFormEditPanel.vue 3709 主表表单设计器
ModuleTableEditPanel.vue 2962 子表表格设计器
ModuleBasicPanel.vue / ModuleFieldsPanel.vue / ModuleListConfigPanel.vue
ModuleQueryConfigPanel.vue / ModuleAutoCodePanel.vue / ModulePowersPanel.vue
ModuleI18nPanel.vue / MenuDetailPanel.vue / I18nQuickSetModal.vue / IconSelect.vue
structure-tree/
ModuleStructureTree.vue 963 专用结构编辑树(拖拽/右键/重命名)
StructureTreeNode.vue 584 树节点(递归渲染)
tests/unit/
module-table-edit-panel-dnd.spec.js 443 拖拽语义测试(重构的安全网)
tree-external-drag.spec.js
utils.spec.js
```
现有 utils/composables 层(新代码遵循同样习惯):
`@/utils/tree.js`(buildTree/sortTree)、`@/utils/dataChanges.js`(diffRows/tableChange/
allocateTemporaryIds/remapForeignKeys)、`@/utils/i18n.js`、`@/utils/tempId.js`、
`@/composables/useHotkeys.js`。
## 3. 同源复制清单(重构的核心对象)
### 3.1 分组树推导:`ModuleTableEditPanel.vue` ⟷ `structure-tree/ModuleStructureTree.vue`
以下符号在两个文件中各有一份、语义一致(面板版 bandMeta 额外计算 maxLevel):
| 符号 | 作用 |
| --- | --- |
| `sortGroupNodesByXh` | 按 b_xh 排序分组节点(注意两处都注释了 toSorted 必须回写原数组) |
| `buildGroupTree` | 分组扁平数组 → 树 |
| `groupTree` / `groupNodeMap` | 树 computed / gid→节点 Map |
| `groupColumnsMap` | gid→直属列 Map + 未分组列数组 |
| `bandMeta` | 层级/ownCount/span/children 元信息(面板版含 maxLevel) |
| `isDescendantOf` / `isDescendantGroup` | 判断分组祖先-子孙关系(防拖入自身子树成环) |
| `FIELD_TYPE_LABELS` + `fieldNameMap` / `fieldMap` / `fieldName` / `controlTypeLabel` / `groupIdOfRow` | 字段查询与标签 |
### 3.2 两个编辑面板的共享函数:`ModuleFormEditPanel.vue` ⟷ `ModuleTableEditPanel.vue`
同名(或仅改名)且核心语义一致的符号:
`onEditModeChange`、`emitUpdate`、`onRowOrderChange`、`renumberRows`、
`groupIdOfRow`/`groupIdOf`、`fieldNameMap`、`fieldMap`、`fieldOf`、`fieldName`、
`controlTypeLabel`、`FIELD_TYPE_LABELS`、`libraryFields`、`isFieldAdded`、`nextGroupXh`、
`selectedRow`、`selectRow`、`createEditRow`、`insertField`、`selectField`、
`copyRowProps`、`pastePropsToSelected`、`toggleInspector`/`toggleRowFlag`、
`syncToList`、`syncToQuery`、`scrollToRowCell`/`scrollToRowCard`
**注意:同名不等于同体。** 例如 `createEditRow` 在表格面板写 `b_width`/`b_height`,
在表单面板写 `b_colspan` 等;`insertField` 的落点参数也不完全相同。抽取前必须逐对
diff 函数体,按第 5.2 节的三条规则归类,禁止强行合并。
## 4. 总体约束(每个阶段都适用)
1. **纯重构**:不改行为、不改 UI 结构与文案、不改 props/emits 契约(子面板对父级的
接口保持不变)、不改 API 调用与保存请求结构、不引入任何新依赖(包括拖拽库)。
2. **现有测试是安全网**:`tests/unit/module-table-edit-panel-dnd.spec.js` 及其它测试
必须全程绿;除 import 路径外不允许修改测试断言。若某步导致断言失败,说明该步
改变了行为,应回退重做。
3. **每阶段一个独立 commit**,commit message 注明阶段号。开始前若工作区有未提交
改动,先提交或暂存,保证基线干净。
4. **保持注释纪律**:项目注释是"为什么"型中文注释;搬运代码时注释随行;删除复制
体后,同步删除"与面板 bandMeta 同源"这类描述复制关系的注释(已不成立)。
5. **不重写响应式模型**:`localEdit` 本地副本 + watch props、`localGroups` 绕过回流
等模式虽然别扭但经过调试,本次只搬运不重新设计。
6. 每阶段结束运行:`pnpm test && pnpm lint`(最终再跑一次 `pnpm check`)。
## 5. 阶段任务
### 阶段一:抽取分组树推导 composable(风险最低、收益最大,先做)
新建 `src/views/system/module-management/composables/useGroupTree.js`:
```text
useGroupTree(groupsRef, rowsRef)
入参:groups 的 ref/computed(面板传 props 派生,结构树传 localGroups),
rows 的 ref/computed(编辑行数组)
返回:groupTree、groupNodeMap、groupColumnsMap、bandMeta(统一含 maxLevel)、
isDescendantGroup(ancestorGid, gid)、groupIdOfRow(row)
纯函数另行导出:buildGroupTree(groups)、sortGroupNodesByXh(nodes)
```
改造点:
- `ModuleTableEditPanel.vue`:删除本地 `sortGroupNodesByXh`/`buildGroupTree`/
`groupTree`/`groupNodeMap`/`groupColumnsMap`/`bandMeta`/`isDescendantOf`,改用
composable。`bandMeta` 统一含 `maxLevel` 后,`bandRows` 等消费处照常解构。
- `structure-tree/ModuleStructureTree.vue`:同样替换;其 `bandMeta` 消费处不使用
maxLevel,多余字段无害。
- **保持 watch 注册顺序**:`ModuleTableEditPanel.vue` 中 `watch(() => props.editConfig, …,
{ immediate: true })` 必须位于 bandMeta 等 computed 定义之后(原文件有 TDZ 注释,
搬运时不得把 watch 上移)。
验收:`pnpm test` 全绿;两个文件合计减少约 250 行;拖拽/右键/分组增删改手工冒烟
(见第 7 节清单)无变化。
### 阶段二:抽取字段查询与共享纯函数
1. 新建 `src/views/system/module-management/composables/useFieldLookup.js`:
```text
useFieldLookup(fieldsRef)
返回:fieldNameMap、fieldMap、fieldOf(row)、fieldName(row)、controlTypeLabel(row)
常量:FIELD_TYPE_LABELS
```
2. 新建 `src/views/system/module-management/editPanelShared.js`(纯函数,无 Vue 依赖):
```text
renumberRows(rows) // (i+1)*10 重排
buildClipboardProps(row, fieldNameOf) // 复制列属性的键集合(两面板 diff 后统一)
applyClipboardProps(row, props)
buildVisibilityMap(rows) // syncToList/syncToQuery 共用的可见性 Map
```
3. 两个面板中 3.2 节列出的每个函数:先逐对 diff 函数体,再归三类处理——
- **完全相同** → 移入共享文件,原位改为导入;
- **语义相同、细节不同**(如 `createEditRow` 的默认值差异)→ 抽参数化工厂
(如 `createEditRowFactory(defaults)`),两面板各自传参;
- **本质不同** → 保留各自实现,不强行合并,在 PR 说明中列出。
4. `syncToList`/`syncToQuery` 结构高度一致(仅目标数组与写回字段不同):用
`buildVisibilityMap` 共享取数部分,emit 与提示文案留在各自面板。
验收:`pnpm test` 全绿;两面板各减约 150–250 行;数据视图"同步到列表/查询配置"
行为与提示文案不变。
### 阶段三:拆分 index.vue
目标:index.vue 只保留模板、状态接线和面板编排(预期 ≤ 700 行)。抽出的
composables 放 `src/views/system/module-management/composables/`:
```text
useModuleTree.js 树状态与操作:moduleTreeData/Loading、moduleKeyword/ExpandedKeys、
unsavedModuleIds、loadModuleTree、markCategoryNonLeaf、moduleTypeIconMap、
filterTree/filteredModuleTree、collectAllKeys、expandAncestors、
findModuleNode、insertModuleNode、removeModuleNode、
hasSavedModuleDescendant、collectUnsavedModuleIds、
buildBreadcrumb/moduleBreadcrumb、addModule、deleteModule
useModuleConfig.js 草稿加载与脏检查:configLoading、originals/drafts、loadedModuleId、
loadModuleConfig、hasModuleChanges、reloadModule、
watch(moduleSelectedId) 及 skipModuleLoad 约定、onModuleSelect
useModuleSave.js 保存编排:saveModule、validateAutoCode、subTables 常量、
临时 id 分配与外键重映射、i18n reconcile 调用
useConfirm.js 两套确认弹窗:通用 showConfirm/onConfirmOk/confirmState 与
切换保护 switchConfirm/onSwitch*
```
要求:
- composable 返回 index.vue 模板所需的最小集合;跨 composable 共享的状态
(如 `selectedModule`)由 index.vue 组装后传入,避免 composable 互相 import。
- `saveModule` 依赖 `basicPanelRef`/`autoCodePanelRef`/`i18nPanelRef` 与
`finalI18nItems`,以参数(refs + getter)注入 `useModuleSave`,不移动模板。
- `onFieldsUpdate`/`applyDisabledFieldDefaults`/`onAutoCodeUpdate` 属于草稿派生
联动,随 `useModuleConfig` 或留在 index.vue 均可,以"内聚"为准并在 commit 说明。
验收:`pnpm test` 全绿;保存(含跨表外键重映射)、重载、切换未保存保护、删除模块
(含 i18n 孤儿键清理)手工冒烟无变化。
### 阶段四:编辑器目录化拆分(本计划的维护性核心,目标单文件 ≤ 700 行)
两个编辑面板各 3000+ 行、script/template/style 混在一个文件,是维护痛点的主要
来源。本阶段按"职责切分、只搬运不重写"把两个编辑器重组为目录:
```text
components/table-edit/
TableEditPanel.vue 壳:视图切换(设计/数据)、编辑方式切换(主表/子表)、布局编排 ≤ 300
FieldLibrary.vue 字段库:搜索 + 字段列表 + 拖出源(application/fms-field) ≤ 250
TablePreview.vue 多级表头预览表:band 行渲染、mock 行、预览拖拽落点指示 ≤ 500
TableInspector.vue 属性面板:列属性 / 分组带属性 / 空态三态 ≤ 400
useTablePreviewDrag.js 预览表拖拽:onPreviewDragStart/Over/Drop、落点指示状态 ≤ 180
useColumnResize.js 列宽拖拽:pointer capture 三件套 + 40–600 边界 ≤ 100
components/form-edit/
FormEditPanel.vue 壳:视图切换、编辑方式切换、布局编排 ≤ 300
FormCanvas.vue 画布:分组区块 + 字段卡片 + 24 栅格行 + 插入线 ≤ 700
FormInspector.vue 属性面板:栅格 / textarea 高度 / 状态开关等 ≤ 450
useFormDrag.js 拖拽:跟手卡片、insert line 计算、落点提交(updateDragIntent 等) ≤ 600
useFormGrid.js 栅格分配纯逻辑:collectLineIndexes/rebalanceInsert/distributeLine ≤ 200
components/edit-panels/
EditDataView.vue 两编辑器共享的数据视图:工具栏 + FmsTable + 行拖拽排序 ≤ 150
```
切分规则:
1. **壳组件保留对外契约**:`TableEditPanel.vue` / `FormEditPanel.vue` 对 index.vue 的
props/emits 完全不变,index.vue 只改 import 路径。
2. **状态归属**:`localEdit`、`localGroups`、选中态(selectedNodeKey/selectedRow)
留在壳组件,子组件通过 props/事件交互;`useXxx` composable 接收壳的 refs、返回
handlers 与状态,不自带数据源。
3. **样式随组件迁移**(scoped),BEM 类名前缀(`mm-fdesign-` / `mm-tdesign-`)保持
不变,避免测试选择器与深层样式选择器失效;全局样式(如 `.mst-drag-badge`)留在
原文件并保留注释说明。
4. `FieldLibrary` 两编辑器都需要但文案/点击行为有差异:优先做成参数化共享组件放在
`edit-panels/` 下;若 diff 后发现耦合过深,允许 form-edit 下保留一份薄的私有实现,
在交付说明中说明选择。
5. 原 `components/ModuleTableEditPanel.vue` / `ModuleFormEditPanel.vue` 删除,由
壳组件顶替(路径变化后同步更新 index.vue import 与测试中的 import 路径,仅此
一处允许改测试文件,断言不动)。
6. `structure-tree/` 已是独立目录,本阶段不动。
停损:`FormCanvas` + `useFormDrag` 是全计划最大风险点(表单画布模板与拖拽状态机
交织最深)。若拆 FormEditPanel 时出现难定位的行为差异,允许降级交付:完成
table-edit/ 目录重组 + form-edit 仅抽 `useFormGrid.js`(纯逻辑无 UI 搬迁,风险低),
其余记录到交付说明留待下轮。
验收:`pnpm test` 全绿(dnd spec 通过 StubDraggable 定位 `.mst-node`,不受影响);
所有新文件 ≤ 700 行(参考目标);设计/数据视图切换、属性面板编辑、列宽拖拽、
字段库点击/拖出手工冒烟无变化。
### 阶段五:收尾与小修
1. 硬编码颜色收敛到 token:`ModuleTableEditPanel.vue` 中 `#d95757`(危险红)、
`#fafbfc`(列头底色)、`#e7f6ed`/`#146c41`(已加入勾选)等,替换为
`--fms-*` 变量(项目已有 `--fms-danger` 类 token,先查 `src/styles/` 再定值;
没有合适 token 则新增,不硬编码第二次)。
2. `index.vue` 的 `deleteModule` 中 i18n 孤儿键清理 SQL 拼接(`b_key LIKE
'module.${code}.%'` 等)抽为 `@/utils/i18n.js` 的
`buildModuleI18nCleanupFilter(moduleType, moduleCode)`,并把"前缀匹配安全性依赖
`.` 与 `_` 不互撞"这一约定写成函数注释。调用处行为不变。
3. 检查全模块不再有"`module-management` 内重复定义 `FIELD_TYPE_LABELS`"等残留
(grep 确认单一定义点)。
### 阶段六:补测试
1. `tests/unit/module-form-edit-panel.spec.js`:参照
`module-table-edit-panel-dnd.spec.js` 的手法(StubDraggable、
`document.elementFromPoint` mock、defineProperty 注入坐标)覆盖 FormEditPanel 的
核心纯逻辑:栅格 rebalance(`collectLineIndexes`/`rebalanceInsert`/`distributeLine`)、
行内移动 `moveSelectedRow`、复制/粘贴属性。
2. 为 `composables/useGroupTree.js` 写直接单测:band 的 span/ownCount/层级计算、
参差深度、空分组占位、`isDescendantGroup` 防环。
3. 若阶段三已把保存编排中的纯计算(subTables diff 收集、外键重映射输入输出)
拆为可测函数,为其补 spec;否则跳过不强求。
验收:新增测试与既有测试全部通过;`pnpm check`(lint + fmt:check + build)通过。
## 6. 建议的执行顺序与停损点
- 顺序即阶段编号:一 → 二 → 三 → 四 → 五 → 六,每阶段独立 commit。
- 阶段四风险最高(模板与拖拽状态机大搬家),其内部自带停损规则(见阶段四末尾);
最低交付线为 table-edit/ 重组 + useFormGrid 抽取。阶段一~三、五不允许跳过。
- 任何阶段若发现测试失败且 30 分钟内无法定位根因,回退该阶段 commit 并在交付
说明中记录现象。
## 7. 手工冒烟清单(每阶段结束过一遍,重点阶段一、三、四)
```text
模块树:搜索展开/收起、右键新增三种模块、未保存节点置灰不可作父级、删除级联
草稿:修改后切换模块弹三选一(保存并切换/放弃/取消)、重载确认、Ctrl/Cmd+S
表格设计器:拖列换位/入组/出组、拖分组嵌套与排序、拖入自身子树被禁止、
右键增删改分组、分组内联重命名、列宽拖拽与步进、Ctrl+C/X/V、Delete、
未分组悬浮落点(无未分组列时)、字段库点击加入/定位
表单设计器:字段拖入任意位置、插入线、栅格自动分配(12→6/6)、跨组移动、
textarea 高度、删除与恢复、同步到列表/查询
保存:新增字段+新增分组后保存一次成功(验证临时 id 分配与 b_field_id/b_group_id 重映射)
```
## 8. 明确不要做的事情
- 不改 `props/emits` 契约、不改保存请求结构、不改 API 参数。
- 不重写 `localEdit`/`localGroups` 的响应式数据流,不把双快照(originals/drafts)
模式换成其它脏检查方案。
- 不引入 TS、不引入新依赖、不做大规模重命名(`b_` 前缀数据库字段命名保持)。
- 不把 `ModuleFormEditPanel` 与 `ModuleTableEditPanel` 合并为一个编辑器组件
(允许共享叶子组件如 `EditDataView`/`FieldLibrary`,但两个编辑器本体各自独立)。
- 不删除任何"为什么"型注释;不修改既有测试的断言。
- 不顺手修 UI 细节/交互问题(发现则记录到交付说明,另开任务)。
## 9. 可直接交给其他 AI 的执行指令
在 `fms-vue` 项目中按照 `docs/module-management-code-refactor-plan.md` 执行代码重构。
先完整阅读该计划、`src/views/system/module-management/` 下的 index.vue、两个编辑面板、
structure-tree/ 两个组件以及 `tests/unit/module-table-edit-panel-dnd.spec.js`,再按
阶段一至阶段六逐步执行:每阶段一个 commit,阶段内先 diff 再抽取(同名函数不等于
同体),全程保持行为不变。每阶段结束运行 `pnpm test && pnpm lint` 并按第 7 节清单
手工冒烟;最终运行 `pnpm check`。交付时说明:各阶段修改的文件与行数变化、抽取的
共享符号清单、哪些同名函数被判定为"本质不同"而未合并、测试与构建结果、以及执行
过程中发现但未处理的问题。