# 2026-09-15 ## 通读 fms-vue 的 views/module 与 views/base 纯代码阅读,无改动。结论要点: - `views/base` 是业务侧页面,几乎全部靠 `FmsModuleListPage` 按 `data-code` 自渲染,页面本身只留业务规则。 - `othercompany/index.vue`(768 行):唯一有左侧业务分类树的页面,分类树自己维护(增改删 + 拖拽), 选中分类 → 拼 `[b_cate_id] IN (...)` 作为 `fixedSearchCondition`(含全部下级分类)。 - `otherdata/index.vue`(479 行):左树是 `s_module` 子树(根 `base_otherdata`),**分类结构不在本页维护**, 去「模块管理」改。右侧按选中的 `data` 模块切换数据源,可编辑 + 行拖拽排序(`b_xh`)。 - `contact/index.vue`(130 行):最简形态,无左树,只有列表 + 权限按钮 + 行删除。 - `contacts/detail.vue`、`othercompany/detail.vue`:**两个都是空壳**(各 22 行,只有一个空 div + 注释), 注释明确写了「保存 / 重置 / 删除与表单均尚未实现」。新增/编辑跳转过去是死的。 - `views/module` 是配置中心(`meta.adminOnly`),三个页面: - `module-management/index.vue`(1316 行)编排层,严格按 `开发规范.md` 的「面板式配置页」模式: 8 对 `xxx` / `xxx_org` ref,面板用 `defineModel` 直读写,跨面板副作用走编排层 watch (自动编码字段在 edit schema 中自动只读)。拖拽排序即时落库并同步基线。 - `menu-management/index.vue`(1155 行)同类页面,已重构出 `MenuTreePanel / MenuBasicPanel / ModulePickerModal`, 但注释与 `ai-plans/TASK1.md` 显示还有 `MenuPowersPanel` 未落地(权限点已移到模块管理维护)。 - `i18n-type/index.vue`(214 行)最小样板:`FmsModuleListPage` + `FmsModuleEditModal` + 少量业务规则 (默认语言保护、行内切换自动保存)。**做新的简单字典页应以此页为模板。** - 设计器 `module-management/components/design-editor/` 是最大的一块(FormDesignEditor 2786 行、 useStructureEditing 1012 行)。`TableDesignEditor` 用 `mode` 参数化 `subtable` / `list` 两个场景; `schemaModel.js` 负责 schema JSON ↔ 编辑器行/分组 的双向转换。 ## 发现的问题(未修,仅记录) - `FormDesignEditor.vue` / `TableDesignEditor.vue` 的文件头注释仍写 `s_field_edit` / `s_field_view` / `s_field_group`(V1 已废弃的表,V2 统一为 `s_module_schema.b_schema_json`),与实际实现不符,属陈旧注释。 - `ModuleListConfigPanel` / `ModuleEditPanel` 只是 30~43 行的薄壳,真正的编辑器在 `components/design-editor/`。 ## 本轮改动 ### 1. 清理设计器子系统里 V1 表结构的陈旧注释(已改) 改了 7 个文件的注释:`TableDesignEditor.vue`、`FormDesignEditor.vue`、`useStructureEditing.js`、 `FormPreview.vue`、`fieldDefaults.js`、`dataChanges.js`、`schemaRender.js`。 统一改为「对外只读写 `s_module_schema.b_schema_json`,行与分组是编辑器内部草稿」。 顺带查实两条**代码事实**(不是注释问题,未改,需用户决定): - `TableDesignEditor` 的 `mode="subtable"` **已无宿主**(全项目只有 `ModuleListConfigPanel` 用 `mode="list"`); 与之绑定的 `editMode` ref / `onEditModeChange` / `EDIT_MODE_OPTIONS` / 头部 Segmented 全是死代码 (`editMode` 只在 `v-if="isSubtable"` 分支渲染,而 isSubtable 恒为 false)。 - `fieldDefaults.js` 的 `createListConfigRow` **无调用点**(只有 `listRowDefaults` / `queryRowDefaults` / `createQueryConfigRow` 在用)。按 `开发规范.md`「没有调用点的代码必须删除」应删。 ### 2. 删除 `ai-plans/TASK1.md`(已删,目录随之为空已移除) 无其他文件引用它。 ### 3. 树逻辑抽取调研(只出结论,未改代码) 全项目宿主树拖拽恰好 3 处,约 55 行核心算法**逐行一致**: `module-management/index.vue:651`(100 行)、`menu-management/index.vue:212`(107 行)、 `base/othercompany/index.vue:445`(63 行)。差异只有:树 ref 名、提示文案、类型守卫字段 (`b_module_type` / `b_menu_type`)、未保存集合、表名、基线同步、失败重载。 结论: - **该抽**,抽成 `@/utils/tree.js` 的纯函数(如 `computeTreeMove`),页面只留落库 + 基线同步 + 重载。 - **不放 FmsTree**:它 148 行纯展示(字段名映射 + i18n 标题 + 虚拟根),零 API 依赖; 且 6 个消费方里 3 个只读(`otherdata` / `ModulePickerModal` / `FileCategoryPanel`)。 - **不放 FmsModuleListPage**:`components/fms-module-list/` 下 grep `tree|b_parent_id|b_depth|b_path|dropPosition` 零命中,树与列表是并列关系(左树右表),塞进去是倒挂依赖。 - **不合并 ModuleTreePanel / MenuTreePanel**:两文件 477 行零相同行,且 `开发规范.md` 第 8 条 原文点名「模块树与菜单树先各自平铺,等共性真正清晰后再合并」。 抽取时要保留的行为差异:`othercompany` 失败也重载("以库里为准"),module/menu 只在失败时重载。 ### 4. 树拖拽抽取落地(已改,本轮核心) 在 `fms-vue/src/utils/tree.js` 末尾新增纯函数 **`computeTreeMove(nodes, options)`**(约 95 行含 JSDoc)。 一次调用完成:成环拦截 → 摘除 → 挂载 → 兄弟重排(`renumberSiblings`)→ 整树重建 `b_depth`/`b_path` (`rebuildTreePositions`)→ 变化时补写 `b_parent_id`。返回 `{ ok:true, updatedRows, moved, parentChanged }`,失败返回 `{ ok:false, reason, message }`, `reason ∈ self | notFound | cycle | notContainer | unsavedTarget`,文案由调用方经 `messages` 传入。 入参:`dragKey` / `dropKey` / `dropPosition('before'|'inside'|'after')`, 可选 `typeField` + `nonLeafValues`(inside 落点类型守卫)、`isUnsaved(id)`(草稿临时 id 不能当父级)。 **三个调用点已改写,行为保持不变**: - `module-management/index.vue::onModuleDrop`:100 → 约 50 行(`typeField: b_module_type`) - `menu-management/index.vue::onMenuDrop`:107 → 约 28 行(`typeField: b_menu_type`) - `base/othercompany/index.vue::onCategoryDrop`:63 → 约 18 行(无类型守卫,失败也重载) 三个文件的 `@/utils/tree` import 均已收敛:`addRowUpdate` / `findNodePosition` / `rebuildTreePositions` / `renumberSiblings` 已从页面移除(它们仍是 `computeTreeMove` 的内部实现)。 **踩坑**:`tree.js` 追加函数时多留了一个 `}`(第 412 行),导致 vite 报 `Failed to parse source for import analysis`,整个 `utils.spec.js` 无法运行。删掉即好。 ### 5. `computeTreeMove` 单测(已加) `tests/unit/utils.spec.js` 新增 `describe('utils/tree computeTreeMove:拖拽移动的守卫与落库行')`,12 个用例, 覆盖 5 条拒绝路径(self / notFound / cycle / notContainer / unsavedTarget)与 inside 换父、before/after 跨父与同级换序、同组索引修正、无变化时 `updatedRows` 为空。 写测试时踩到两个**自己对实现的误解**(代码是对的,断言错了): - `isLeaf` 只在 `dropNode.children` 为 falsy 时才补空数组并置 false;fixture 里给叶子写了 `children: []`(truthy)就不会走该分支。要测就造**没有 children 字段**的节点。 - 同级换序后若 `b_xh` 重排结果与原值相同,`renumberSiblings` 不会产生 update, 该节点在 `updatedRows` 里查不到(`rowById(...)` 返回 `undefined`),别断言它有值。 顺带清了该文件两处既有 lint 问题(unused `vi`、`Folder` 提升为模块级 `FolderIcon`), 测试 helper `makeMoveTree` / `rowById` 也提到模块级以满足 `consistent-function-scoping`。 ### 环境情况 - `pnpm lint`(123 warnings + 3 errors,`no-underscore-dangle`)与 `pnpm fmt:check`(155/293 文件) **均为既有失败**,不是本轮引入。 - `pnpm vitest run tests/unit`:10 failed / 144 passed,失败项全是既有问题: `module-list-utils.spec.js` 导入了不存在的 `@/components/fms-module-list/moduleListUtils`; `_scratch-freeze.spec.js` 是临时 repro;其余是查询条件 UI 测试(`fms-module-list-page` / `module-list-components` / `module-pref`)。树相关 4 个 suite 全绿(51 passed)。 - git 对象库损坏(`git stash` / `git diff` 报 `unable to read ` / `no initial commit`), 无法用 git 做前后对比,验证时改用「改前改后各跑一次测试」的方式。 - `vite build` 会被本机 safe-delete 守卫拦下(`SAFE_DELETE_BULK_CONFIRM_REQUIRED`,一次要删 `dist/assets` 下 672 个文件 > 阈值 50),与代码无关;要验证构建需换输出目录 (`--outDir dist-verify --emptyOutDir`)。 ### 6. 新增「系统管理 / 用户管理」路由占位(已改) - 新建 `fms-vue/src/views/system/user/index.vue`:**空壳占位页**(只有空 div + 注释), 待实现内容为左部门树(`b_dept`)+ 右用户列表(`b_user`)。 - `fms-vue/src/router/routes.js` 路由分组风格**统一为 /module 的写法**(本轮第二稿: 原先 `/system/user` 用绝对路径、`/base/*` 是散列写法,均已归并): ```js { path: '/base', children: [ { path: 'othercompany', name: 'base-othercompany', ... } ] } { path: '/module', meta: { adminOnly: true }, children: [ ... ] } { path: '/system', children: [ { path: 'user', name: 'system-user', ... } ] } ``` 三个分组节点一律:**不写 component、不写 redirect**,子页 path 写相对路径 (含 `'othercompany/detail'`、`'contact/detail'` 这种两级相对路径,vue-router 支持)。 完整路径(`/base/othercompany/detail` 等)与 `name` 全部保持不变 → 详情页内部的跳转、 `AppSidebar` 里写死的 `/module/*`、`tests/router-menu-guard.spec.js` 的断言都不用改。 去掉 `redirect` 的**理由**(原 `/module` 与 `/system` 都配了 redirect 到首个子页): 分组只是归类,不该自带「默认落地页」语义 —— 否则直接访问裸前缀 `/module`、`/base` 会静默落在某个业务页上,形成「菜单里没配、却能进」的第二入口。去掉后裸前缀交给表尾 `/:pathMatch(.*)*` 兜到工作台,业务入口一律由 `s_menu` + 菜单管理的「路由地址」维护。 - 未配 `moduleId`(`/system/user` 无对应 `s_module` 记录,配了会被权限守卫按无权限拦回工作台)。 - `/module` 的 `meta.adminOnly: true` **保留在分组节点上**(守卫按 `to.matched.some(r => r.meta.adminOnly)` 判断,分组节点带标记即可覆盖整组子页),`collectMenuRoutes` 也靠它整组排除。 踩到的要点(新增页面路由时需遵守): - `meta.componentName` 必填且全局唯一:页签标签同时是 keep-alive 的 `include` 名。 - `meta.title` 决定页签是否出现(`AppTabs` 只在有 title 时 `addVisitedTab`);三者缺一页签链路就断。 - **分组节点绝不能带 component**(哪怕只加一层无 router-view 的壳):`resolve()` 的 `matched` 会多出一条中间层记录(vue-router 会把中间节点的 path 推进 matched,即使它没有 components), 一旦该节点有 component,路由层级判断就会变。校验方式: `resolved.matched.filter(r => r.components).map(r => r.path)` 必须只有 `['/', '<页面完整路径>']`。 - 分组改造的**回归验证方式**:枚举所有页面 fullPath 跑 `router.resolve()`,断言 「路径仍能解析 + `name` 不变 + components 链只有 layout 与页面自身」, 再加一条「裸前缀不落在任何页面」(`matched.some(r => r.components) === false`)。 顺带修了两个**既有 lint error**(都不是本轮引入): - `src/components/fms-tree/FmsTree.vue:47`:`export const ALL_CATEGORY_KEY` 违反 vue `no-export-in-script-setup`;该常量**无任何外部引用**(注释已写明"调用方不需要也不应引用"), 去掉 `export` 即可。 - `tests/router-auth.spec.js:88`:`const pinia = createTestPinia()` 声明未使用, `createTestPinia()` 靠副作用生效 → 直接改成表达式语句(不要加 `_` 前缀,eslint 的 `no-unused-vars` 配置此处不认下划线豁免)。 - 剩余 1 个 error 是既有问题:`src/components/fms-table/utils/createCellRenderer.old.js:214` 未使用参数 `editingActive`(`.old.js` 文件本身疑似待删除的残留)。 ### 7. FmsTree 接管 loading 与「全部分类」虚拟根约定(已改) 用户提的两点:loading 收到 FmsTree 内部维护;`ALL_CATEGORY_KEY` 交给内部处理。 **结论与落地**: - **loading**:FmsTree 新增 `loading` prop,内部渲染 `.fms-tree__loading`(绝对定位居中 `Spin` + 60% 白遮罩),并留 `#loading` 插槽覆盖。**注意是覆盖层而非 v-else**:树始终渲染, 避免加载完成后尺寸跳动。5 个消费方各自那份 loading 标记与 CSS 全部删除: `othercompany`(`.oc-tree-panel__loading`)、`otherdata`(`.od-tree-panel__loading`)、 `FileCategoryPanel`(`.fc-panel__loading`)、`ModuleTreePanel`/`MenuTreePanel`(原 `.mm-tree-empty`「加载中...」纯文本,且是 `v-if/v-else` 替换式,已一并统一为遮罩)。 - **虚拟根选中约定**:FmsTree 内部完成 `[]` ⇄ 虚拟根的双向映射 —— 传入空选中键 → 渲染时补成 `[rootKey]` 让根高亮;`select` 事件里点中根 → 回传 `[]`。 调用方用「falsy 的选中键」表达「全部」,**不再需要 import 或书写 `__fms_tree_root__`**。 选这个方案(而不是 `v-model:selected-keys`)的依据:5 个消费方里已有 4 个本来就用 falsy 值表示「未选中」(`otherdata` 用 `''`、`FileCategoryPanel` 用 `''`、 两个 TreePanel 用 `null`),且都用同一个表达式 `x ? [String(x)] : []` 派生; 方案一只是把这个既有约定泛化,`v-model` 反而与两个纯展示型 TreePanel 的定位冲突。 常量随之改为 `FmsTree` 内部 local const(**不能 export**:`