34 KiB
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_orgref,面板用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"); 与之绑定的editModeref /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/下 greptree|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 <sha>/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/*是散列写法,均已归并):{ 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违反 vueno-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 新增
loadingprop,内部渲染.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替换式,已一并统一为遮罩)。
- 60% 白遮罩),并留
-
虚拟根选中约定: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:<script setup>禁止 ESM 导出, 此坑在本日更早一轮已踩过并记录)。
踩到的一个真 bug(我的回归,被测试抓到):
effectiveSelectedKeys 一开始写成读 props.selectedKeys,但 FmsTree 没有声明该 prop
(它走 $attrs 透传),于是 props.selectedKeys 永远 undefined,非空选中项被覆盖成
虚拟根 → 表现为「选中任意分类,高亮的却是全部分类」。改用 useAttrs() 取值后修复。
写成了一个单测(选中业务节点时虚拟根不高亮)锁住。
顺带修掉的既有回归(来自本日第 4 节的抽取):
othercompany/index.vue::onCategoryDrop 里写了 updatedRows 但没解构
(const { updatedRows } = result 漏了),try/catch 把 ReferenceError 吞成
「排序更新失败」,拖拽分类实际完全不落库。被 tests/views/othercompany.spec.js
的「跨级拖入时同批落库」用例抓到,已补上解构。教训:抽取后可运行的用例比肉眼比对可靠。
验证:新增 tests/components/fms-tree.spec.js 的 11 个用例(选中约定 6 + loading 4 + 插槽 1)。
FmsTree 28/28、othercompany 视图 9/9、utils 35/35、tree-external-drag 3/3、othercompany-utils 4/4
全绿;改动 8 文件 lint 0 warnings 0 errors;vite build 通过。
全量 384 passed / 13 failed,失败项仍全是既有问题(query-UI / router-auth / login / scratch repro)。
8. collectDescendantIds 抽取到 @/utils/tree(已改)
用户:「这个需求可能以后还比较多」→ 抽成公用。
先查了有没有现成的:components/ui/tree/utils.js 已有 collectDescendants(rootKey, childrenMap),
但它作用于已构建好的嵌套树 + 预计算 childrenMap(供勾选联动用),
而业务页面从库里拿到的是扁平行(b_parent_id 邻接表),两者不能混用。
所以新增的是扁平行版本,放进 @/utils/tree.js,与 buildSortedTree / applyTreePosition 同族。
签名:collectDescendantIds(rows, rootId, { idKey = 'b_id', parentKey = 'b_parent_id' } = {})
返回「自身 + 全部下级」的 ID 数组(统一转字符串)。JSDoc 里写明了与 ui/tree 那个的区别,防混用。
刻意保留的两个语义(抽取不等于顺便改行为):
visited兜环:b_parent_id 无外键约束,脏数据成环不能死循环;- rootId 不在 rows 里时仍返回
[rootId]:拼成IN ('不存在')自然查不到数据,语义安全, 调用方不必额外判空。这条写进 JSDoc 并配了单测。
新增 8 个用例(含数值/字符串 ID 混用、成环、自引用、字段名覆盖)。
othercompany 改为 collectDescendantIds(categories.value, selectedCategoryKey.value)。
utils 43/43、othercompany 视图 9/9 全绿,lint 0,vite build 通过。
未动的一处同类逻辑(留给用户决定):otherdata/index.vue 的 isDescendantOfRoot
用 b_path 前缀匹配判断子孙(且排除根自身),与本次按 b_parent_id 展开是两种思路。
按 FMS新系统核心表结构设计.md,b_parent_id 才是权威关系、b_path 是派生字段,
理论上应该统一到新函数;但那会改动一个没坏的功能,按「只改必需文件」没动。
⚠️ 并发编辑警告(重要,影响后续所有验证)
本轮发现有另一个会话在同时改这个工作区:src/router/routes.js(19:47)与
src/views/system/user/index.vue(19:32)都不是本会话改的(对应本文件第 6 节的「用户管理路由占位」)。
它把路由分组节点的 redirect 去掉了,并改了文件头注释:
「分组节点不配 redirect:分组只是归类,不该自带默认落地页语义 —— 直接访问 /base、/module
这类裸前缀时交给表尾的 /:pathMatch(.) 兜到工作台」。
于是 /module 现在 → /dashboard,而 tests/unit/router-nested-groups.spec.js:30
仍断言旧的 /module/module-management,该用例随之失败(全量 13 → 14 failed)。
这是并发改动导致的陈旧测试,不是本轮回归(本轮没碰 routes.js)。未擅自修改: 该文件属于那个会话,且新行为看起来是有意设计。需要有人决定改测试还是改设计。
教训:本工作区当前不可假设「只有自己在改」,验证结论时要先确认失败项是不是自己引入的
(可用 find src tests -mmin -N 按 mtime 排查)。
后续处理(已改测试,未动 routes.js):确认工作区在 20:00 后无新并发改动后,
把 tests/unit/router-nested-groups.spec.js 对齐到新设计 —— 只改测试、不碰 routes.js,
避免与那个会话冲突:
- 文件头补上第二条分组约束(无 component 且无 redirect);
- 原用例「重定向到首个子页」改成「裸前缀兜到工作台」,
/module→/dashboard(用例本意「不白屏」保留,只是落点变了); - 新增一条断言锁住设计:解析
/module/i18n-type后matched[1]的 components 与 redirect 均为 falsy。 踩坑:/module嵌在 DefaultLayout children 里,顶层routes.find()找不到, 要改用router.currentRoute.value.matched[1]取分组记录。 结果:该 suite 4/4 通过,全量回到 13 failed / 395 passed(正是改动前既有基线),本轮零新增失败。
9. otherdata 子树裁剪统一到 collectDescendantIds(已改,收尾)
把第 8 节末尾「留给用户决定」的 otherdata/index.vue::isDescendantOfRoot 一并统一了。
动机是验证抽取真的可复用(用户说「这个需求可能以后还比较多」),且原实现有个自相矛盾:
同一个函数里「向下裁剪子树」用 b_path 前缀匹配,而紧接着「向上回溯祖先」用 b_parent_id,
两个权威源。按核心表结构文档 b_parent_id 才是权威、b_path 是派生,二者不一致时同函数会算出矛盾结果。
改法:
const scopedIds = root ? new Set(collectDescendantIds(all, ROOT_MODULE_ID)) : new Set()
scopedIds.delete(ROOT_MODULE_ID) // 根是本页入口,不进树
const scoped = all.filter((item) => scopedIds.has(String(item.b_id)))
做法:先针对旧实现写测试,再改。新建 tests/views/otherdata.spec.js 5 个用例,
先在未改动的 b_path 版本上跑通 4/4,改完再跑仍 4/4(行为一致),
然后补第 5 条锁住 b_path 与 b_parent_id 冲突时以 b_parent_id 为准 → 5/5。
这条经验可复用:重构前先给旧行为拍快照测试,比肉眼比对可靠(第 7 节那个漏解构的 bug 也是这么抓到的)。
自引入 2 个 lint warning(module_ 触发 no-underscore-dangle、.sort() 应 toSorted())已修,
oxfmt 格式化后 lint 0 warnings 0 errors。
最终验证:vite build --outDir dist-verify --emptyOutDir 通过(15.82s),dist-verify 已清理;
相关 5 个 spec 89/89 全绿(utils 43 / fms-tree 28 / otherdata 5 / othercompany-utils 4 / othercompany 9);
全量 13 failed / 400 passed(413 tests / 46 files),失败项仍是既有基线,通过数 395 → 400(+5 新用例)。
10. 用户拍板(2026-09-15 晚,后续照此执行)
- 设计器死代码 → 暂时保留,不删。
TableDesignEditor的subtable分支(isSubtable恒 false)与fieldDefaults.js::createListConfigRow(零调用点)都不要动。虽然开发规范.md有「没有调用点的代码必须删除」,但TableDesignEditor.vue:18的注释已把 subtable 标为「V2 移除 b_edit_mode 后已无宿主,暂未启用」—— 是前人有意为未来子表编辑留的骨架,不是无意遗留。再有人提删除时先回看这条。 - 13 个既有测试失败 → 暂时不管。非本轮引入,是改动前基线(query-UI / router-auth / login / scratch repro)。
collectDescendantIds抽取 → 用户确认没问题(已认可)。
otherdata 的页面定性(用户原话「比较特殊,只需要列下来就可以了,不参与编辑什么的」):
它是只读展示页——左侧 s_module 子树只负责列出来,不在这页增删/拖拽分类,分类结构一律去
「模块管理」维护。所以 computeTreeMove 对它不适用,只有 collectDescendantIds(读路径)相关。
这一点决定了改它的风险面:只在读路径,不写库。
关于 b_path / b_parent_id 潜在不一致(未验证,待用户决定):
第 9 节那处改动的前提是「存在 b_path 未同步的脏数据」。库里到底有没有,没查过 ——
远端生产库 118.89.70.199:1433 属外部系统,未经用户同意不应连接。
若要验证,只读校验 SQL 大致是(对比「父级 b_path + 自身 b_id」与自身 b_path):
SELECT c.b_id, c.b_parent_id, c.b_path, p.b_path AS parent_path
FROM s_module c LEFT JOIN s_module p ON p.b_id = c.b_parent_id
WHERE c.b_path <> ISNULL(p.b_path, '/') + CAST(c.b_id AS varchar(200)) + '/'
本地只有 config/dbconfigs/G3HD.properties 与 G3HY2025.properties,没有 FMS(新库)的配置,
要连得先补 FMS.properties 或用同服务器凭据直连。
顺带结论(本轮扫尾查证):树逻辑抽取已无遗漏 ——
全项目 b_path 前缀匹配找子孙的实现已清零(余下全是 ORDER BY b_path 与新增时派生赋值);
业务侧 3 处 drop 处理器(module-management / menu-management / othercompany)全部走 computeTreeMove;
components/ui/demo/tree.vue 的裸 drop 是组件库 demo(演示低层 API,不应改用业务工具),
FileListPanel.vue 的 handleDrop 是文件上传(dataTransfer.files),均与树无关。
11. 模块中心侧栏图标 Settings → Blocks(已改)
src/layouts/components/AppSidebar.vue:STATIC_MODULE_MENU.icon 由 Settings 改为 Blocks,
import 同步替换(Settings 该文件内仅此一处使用,改后无残留引用,删掉即可)。
要点备忘:
- 模块中心的图标不在
s_menu里,是代码写死的静态入口STATIC_MODULE_MENU(超管工具导航不进 s_menu,避免被「菜单管理」误删后无入口修复)。要改它的图标就改这里, 不要去数据库/菜单管理找。 - 同文件顶部还有
{ title: '工作台', icon: LayoutGrid }、{ title: '组件库', icon: Component }两个固定入口,以及icon: Settings之外的getIcon(node.b_icon)动态菜单图标解析 (走@/utils/lucideIcons.js,自动注册整个@lucide/vue,接受lucide:block等写法)。 - 新增图标只需从
@lucide/vueimport;Blocks已确认在node_modules/@lucide/vue/dist/lucide-vue.d.ts中存在。
验证:lint 0/0;menus.spec.js + router-menu-guard.spec.js + router-nested-groups.spec.js
共 11/11 通过(测试只断言入口存在与路由权限,不断言图标,所以换图标不影响用例);
vite build 通过(16.1s)。
12. 用户管理页(/system/user)落地:左部门树 + 右用户列表(已改)
用户需求:「在 http://localhost:5082/system/user,模块我还没创建,你看着办」。
先做的调研(结论决定了实现方式):
- 项目里已有 2 个同构先例:
base/othercompany(分类树+单位列表)、base/otherdata(模块树+数据列表)。 三者都是「左侧一棵树 + 右侧 FmsModuleListPage + 选中即过滤」,用户与部门的主从关系也属于这一类。 理由:b_user.b_dept_id→b_dept.b_id,「按部门筛人」「给部门加人」是最高频动作,同页体验最好。 - 决定性证据:
FMS业务表设计.md:235的视图里已经在left join dbo.b_dept d on d.b_id = u.b_dept_id, 说明 b_user / b_dept 是既有核心表(在sql/fms_core.sql建,不是本次要新建的)。
产出三个文件:
-
fms-vue/src/views/system/user/DeptTreePanel.vue(新建,约 450 行) 部门树面板,自管加载 / 增删改 / 搜索 / 拖拽(拖拽走computeTreeMove,与 othercompany 同构)。 选中结果用defineModel('selectedKey')抛给编排层;defineExpose({ reload })。 删除守卫两条:有子部门不让删;部门下有用户(查b_user.b_dept_id)不让删。 -
fms-vue/src/views/system/user/index.vue(占位页 → 实现) 编排层只做三件事:选中部门 →fixedSearchCondition(collectDescendantIds含全部下级)、 新增用户时补默认部门、用户增删改(FmsModuleEditModal+ 批量删除)。 按「面板式配置页」思路把树抽成独立组件,以后部门要是独立成页可直接搬走。 -
fms-vue/tests/views/system-user.spec.js(新建,9 用例) 锁三块行为:选中部门→查询条件(含全部下级 / 只含本分支 / 叶子只匹配自身 / b_path 与 b_parent_id 冲突时以 b_parent_id 为准)、新增补默认部门(4 种边界)、未选中不加条件。
FmsModuleEditModal 的钩子契约(踩坑,值得牢记):
onBeforeSave(row, { isNew, record }) —— 签名里没有 formData,第一个参数就是合并后的完整行,
且在组装 inserts 之前调用,所以直接改 row.b_dept_id 即可。
不要用 extraUpdates:那条路径只作用于 updates,新增场景(inserts)拿不到。
(FmsModuleListPage 的 reload / deleteSelected 均已 defineExpose,可直接调。)
测试踩的最大一个坑(已写进 MEMORY.md):
tests/helpers/app.js 的 mountPage 内部是 shallowMount,会打桩所有子组件。
给布局容器打桩时键名必须是 Vue 推断出的组件名 —— Splitter/SplitterPanel 的 SFC
文件名是 splitter.vue / panel.vue,推断名是 'splitter' / 'panel',不是 'SplitterPanel'。
写错键 → 匹配不上 → 容器成空壳 → 插槽不渲染 → 页面 depts 恒为空,
表现成「查询条件只剩自身」,很容易误判成业务逻辑 bug。
另:mountPage 的 global 选项会整体覆盖内部 stubs,别在 global 里再写 stubs(我调试时踩过)。
同时产出的 SQL 脚本 sql/fms_module_user.sql(需用户执行,我没有库凭据):
- 建查询视图
v_b_user(left join b_dept取v_dept_name派生列); - 注册模块
v_b_user(parentsystem,view_tablev_b_user,save_tableb_user,keyb_id); - 12 个
s_field;view / edit / query 三份s_module_schemaJSON; - 菜单仅在已存在同路由时修正,不擅自新建菜单行(s_menu 由「菜单管理」维护);
- 末尾带自检 select。
未验证的部分(要用户确认):
- 我没有连库(无 FMS 库凭据,且属远端生产环境
118.89.70.199)。所以:视图、模块注册、 b_user/b_dept 是否已真正建表,全部未验证;脚本只是按sql/fms_core.sql与文档写的。 - 页面在没有模块元数据时能打开(
/system/user无meta.moduleId,守卫不拦), 但右侧列表会因取不到s_module配置而空 —— 属预期,执行 SQL 后即可用。
验证:新增 2 文件 lint 0 warnings 0 errors;system-user.spec.js 9/9;
vite build --outDir dist-verify 通过(产物含 user-*.js,已确认页面进包);
全量 13 failed / 412 passed(425 tests / 47 files),失败项仍是既有基线,零新增失败。
(踩到的环境坑:构建第一次报错,原因是 --emptyOutDir 与我在同一时刻跑的手动清理
find dist-verify -delete 撞车;重跑即通过。教训:清理临时目录别与构建并行。)
收尾补的一个功能漏洞(自查发现):
onEdit 写好了但没有接线——列表没传 row-actions,导致用户只能新增/删除、永远不能编辑。
按 开发规范.md「没有调用点的代码必须删除」,这属于必须处理的死代码;
但正确做法是接线而非删除(编辑用户是必需功能)。
已补 rowActions(computed:edit 恒有 + delete 受 canDeleteRow 控制),
删除走 FmsTable 内置 confirm(与 i18n-type / contact 一致,不手工 Modal.confirm),
模板加 :row-actions 与 :inline-action-limit="2",工具栏按钮补 rowSaving 禁用守卫。
补 3 个用例(12/12):edit 操作存在且把该行交给弹窗、有删除权限时 keys = ['edit','delete']、
新增时清空 editingUser。
踩坑:断言 editingUser 用 toBe 会失败——它是 ref,读出来是响应式代理,
与传入的原始对象不同一(报错信息是误导性的 "serializes to the same string")。用 toStrictEqual。
最终:lint 0/0;system-user.spec.js 12/12;vite build 通过;
全量 13 failed / 415 passed(428 tests / 47 files),失败项仍是既有基线,零新增失败。
13. 用户授权连库 → sql/fms_module_user.sql 已实际执行并端到端验证
用户给了凭据路径 fms-api/config/dbconfigs/G3HD.properties(→ 118.89.70.199:1433 / FMS / sa)。
本机没有 sqlcmd / pymssql / pyodbc,但发现两条现成资源:
/c/Users/admin/.m2/repository/com/microsoft/sqlserver/mssql-jdbc/13.4.0.jre11/mssql-jdbc-13.4.0.jre11.jar
和 /d/devtool/jdk/jdk17/bin/java。于是写了个一次性 SqlRunner.java(按独立成行的 GO 分批执行 + 打印结果集)。
这个套路以后可直接复用(脚本已删,代码思路见下)。
执行前先探查,结果与我的草稿假设差很多(重要):
s_module主键就是b_id,没有b_module_code/b_route列;它本身也是一棵树 (b_parent_id/b_depth/b_path),另有b_module_type(取值module/data)、b_query_sql、b_config_json。我原稿里的b_module_code/b_route全是错的。s_field主键是(b_module_id, b_field),没有b_id;列为b_field/b_name/b_i18n/b_type/b_canuse/b_xh。s_module_schema主键是(b_module_id, b_schema_type)。b_user/b_dept确实已由fms_core.sql建好(2026-09-11 建的);b_user2 行(001、g3soft),b_dept0 行(还没建部门)。s_menu里已经有b_id = 'user'的 page 菜单(route/system/user),但没有s_menu_module绑定。
据此改了脚本(sql/fms_module_user.sql):
- 模块行
b_id = 'v_b_user'、b_parent_id = 'system'、b_depth = 1、b_path = '/system/v_b_user/',b_module_type = 'data',b_order_sql = null(跟随既有模块惯例,不写b_id ASC)。 - 补了原稿漏掉的
s_menu_module绑定(user→v_b_user)——这是让侧栏入口出现在角色 「菜单权限」树里、可被授权的关键。没有它,非超管看不到也授不了权。 - 保留「不新建菜单行」原则:菜单已存在,只做绑定 + 校正 route/path。
执行结果:12 个批次全绿,自检 6 项全对 —— 模块 1 / 字段 12 / 界面配置 3 / 菜单绑定 1 / 视图列 12 / 视图数据行 2。
端到端验证(走真实后端 8088,比看 SQL 结果强得多):
- 登录:
POST /api/auth/login,字段是小写orgid/userid/password(不是 orgId/username!),orgid必须传G3HD,否则报「机构码不能为空」。 POST /api/data/page:参数为view_name/order_by/search_columns(真 JSON 数组,不是字符串)/search_condition/page_no/page_size。用pageindex/pagesize/字符串化的 search_columns 都会报错。- 验证通过:
v_b_user返回 2 行且v_dept_name正常;b_dept树加载正常;[b_dept_id] IN ('d_x')这种collectDescendantIds拼出的条件是合法 SQL(返回 0 行)。 - 写路径也验了:
POST /api/data/saveobjt的 body 是数组、每项必须带key_field([{table, key_field, deletes, updates, inserts}],按 delete→update→insert 顺序执行)。 实测建部门d_test→ 建挂在它下面的用户u_test→ 查v_b_user确认v_dept_name联出「测试部门」。 验证后已把u_test/d_test全部删除,b_user恢复 2 行、b_dept恢复 0 行。