diff --git a/code/fms/.trae/documents/删除规则一期实现方案.md b/code/fms/.trae/documents/删除规则一期实现方案.md
new file mode 100644
index 00000000..c8ed53be
--- /dev/null
+++ b/code/fms/.trae/documents/删除规则一期实现方案.md
@@ -0,0 +1,164 @@
+# 删除规则一期(配置侧)实现方案
+
+## Context
+
+「删除规则引擎」的设计已定稿([FMS删除规则引擎设计.md](file:///d:/workspace/code/fms/FMS删除规则引擎设计.md)),但代码侧**一期尚未开工**:`s_rule` 表不存在、`s_relation` 没有 `b_on_delete`、模块管理里没有规则 tab、后端无 `/data/deleteobj`。
+
+本次做**一期:配置侧的元数据落地**(前端 + SQL 脚本,不含后端引擎)。完成后 IT 就能把规则配起来,但**不改变现有删除行为**——没有引擎去读这些配置。二期再写检查器/执行器与 `saveobjt` 接入。
+
+职责划分(这是本次的设计主线):
+
+| | 性质 | 落哪 | 配置入口 |
+| --- | --- | --- | --- |
+| 结构性删除动作(连带删除/连带归档) | 无条件,跟"谁引用谁"绑定 | `s_relation.b_on_delete` | 模块关联面板 |
+| 引用检查(有引用时拒绝删除) | 无条件(存在引用即拒) | `s_rule`,`scope_type='relation'` | 模块关联面板勾选 |
+| 业务条件(审核中/已销账/核销金额>0…) | 带条件、长尾 | `s_rule`,`scope_type='module'` | 模块管理「删除规则」tab(勾条件 / 写 SQL) |
+
+关系表只说结构与结构性动作,**条件一律不进关系表**。
+
+## 与设计文档的偏离(3 处,均为落地适配)
+
+1. **`s_rule` 用业务联合主键** `(b_scope_type, b_scope_id, b_code)`,砍掉 `b_id`(bigint 雪花) 与 `ux_s_rule_code`。理由:全仓元数据表都用业务键/复合键,[开发规范.md](file:///d:/workspace/code/fms/开发规范.md#L187) 第 6 条要求配置表用业务键、不用雪花 ID;且文档的 `unique(b_code)` 与"规则可复制到其他模块"互相矛盾,**scope 内唯一**才能复制。
+2. **`b_scope_id` 用 `varchar(250)`**:模块是编码 `varchar(50)`,关系是 4 列各 50 的复合键(拼串最长约 203),文档里的 `bigint` 装不下(这一点文档 §6.1 与 §6.2 本身不自洽)。
+3. **DSL 的 `expr` 直接复用现有查询 AST**:`{"kind":"dsl","expr":buildAdvancedAst({nodes,groups})}`,即 `{type:'group',children:[{type:'condition',field,operator,value,join}…]}`。前端已有 `astToSql` 能把同构 AST 翻成 SQL 片段,避免自造第二套语法(文档 §6.3 的 `and/in` 写法是同一信息的另一种表述)。
+
+另外遵循文档定案:**规则类型不单独存列**,由 `b_predicate` 推断——空 → 引用规则;可解析为 JSON → 勾条件;否则 → SQL 文本。`b_hook` 一期固定 `'delete.pre'`。`b_archived/b_archive_datetime` 一期**不落列**(引擎启用时才校验)。
+
+## SQL(2 个文件)
+
+`sql/fms_core.sql`(全新安装,紧接 §9 关系段之后新增第 9.1 节,并在 s_relation 建表段补列):
+
+```sql
+alter table dbo.s_relation add b_on_delete varchar(20) not null default 'none' -- none/cascade/archive
+```
+
+```sql
+create table dbo.s_rule (
+ b_scope_type varchar(20) not null, -- module / relation
+ b_scope_id varchar(250) not null, -- module=模块编码;relation=源模块|源字段|目标模块|目标字段
+ b_code varchar(50) not null, -- 规则编码(scope 内唯一,复制/导入导出的稳定标识)
+ b_name nvarchar(100) not null, -- 规则名(如"审核中不可删")
+ b_hook varchar(30) not null, -- delete.pre(save.pre 留位)
+ b_predicate nvarchar(max) null, -- 空=引用规则;JSON=勾条件;其余=SQL 文本
+ b_message nvarchar(500) null, -- 提示文案(写 SQL 必填)
+ b_canuse tinyint not null default 1,
+ b_xh int not null default 0,
+ b_created_by varchar(50) null, b_created_at datetime2 null, -- 前端不提交,二期由服务端维护
+ b_updated_by varchar(50) null, b_updated_at datetime2 null,
+ primary key (b_scope_type, b_scope_id, b_code)
+);
+create index ix_s_rule_scope on dbo.s_rule (b_scope_type, b_scope_id, b_hook, b_canuse, b_xh);
+```
+
+- `b_on_delete` 用 `not null default 'none'`:存量行 alter 时回填 none,新增行与存量行取值一致 → 打开即不脏。
+- 新增增量脚本 `sql/fms_delete_rule.sql`(沿用 [fms_rename_othercompany_category.sql](file:///d:/workspace/code/fms/sql/fms_rename_othercompany_category.sql) 的风格:头部注释写背景与前置核查「确认无 s_rule、s_relation 无 b_on_delete」,正文 `alter table` + `create table/index`,GO 分批)。已有库执行走 `fms-api/tools/migration/RunSqlFile.java`(GO 分批、单事务、失败整体回滚),先 `--dry-run` 再实跑。
+- 后端**零改动**:`loaddata/saveobjt` 无表白名单,复合主键用逗号拼 `key_field`,deletes 按行内列值 AND 匹配。
+
+## 前端改动
+
+### 新增 3 个文件
+
+**`src/views/module/module-management/ruleUtils.js`**
+`relationScopeId(row)`(四列 `|` 拼串,与主键同序)/ `parseRulePredicate(row)`(三态)/ `ruleSummary(row, itemByField, t)`(dsl 走 `describeAdvancedQuery`,sql 取单行化前 80 字)/ `nextRuleCode(rules)`(`rule_{最大序号+1}`,可改)/ `createRuleRow(moduleId, seq)` / `createRefRuleRow(scopeId)`(`b_code='referenced'`,一个关系最多一条)/ `buildRuleQueryItems(fields, querySchema)`。
+
+**`ModuleRulePanel.vue`**(样板:[ModuleRelationPanel.vue](file:///d:/workspace/code/fms/fms-vue/src/views/module/module-management/ModuleRelationPanel.vue))
+- props:`moduleId / fields / querySchema / relations / refRulesReverse / loading`;`defineModel` 行数组 `rules`;`defineExpose({ validate })`。
+- 列:`b_name` / `_typeText`(条件|SQL)/ `_summaryText`(条件中文摘要或 SQL 片段)/ `b_canuse`(checkbox 行内编辑)/ `b_xh`(拖拽重排 `(i+1)*10`)/ `actions`(编辑、删除)。派生列在 `watch(rules, deep)` 里就地补(`_` 前缀不落库、不判脏),补完换数组引用触发表格刷新。
+- 工具栏:新增规则、批量删除。行点击/编辑 → 打开编辑弹窗。
+- 只读区「来自模块关联」:列出 `refRulesReverse`(关系上的引用规则),列 源模块/源字段/目标模块/目标字段/说明;操作「查看」→ 当前模块的规则 `emit('navigate')` 由父级切到关系 tab,其它模块走 `onModuleSelect`(带未保存守卫)。
+- `validate()`:规则名非空;`b_code` 非空、合法标识符、scope 内不重复;dsl 行至少一个有效条件;sql 行 `b_message` 非空。
+
+**`ModuleRuleEditModal.vue`**(受控弹窗,参考 [UnsavedChangesModal.vue](file:///d:/workspace/code/fms/fms-vue/src/components/UnsavedChangesModal.vue) + `ui/Modal`)
+- props `open / row / moduleId / items / existingCodes`;emits `update:open / ok`。
+- 表单:规则名、规则编码(默认自动生成,可改)、类型 radio(勾条件 / 写 SQL)、启用、提示文案(sql 必填)。
+- 勾条件分支:显示条件摘要 + 「编辑条件」→ 以**嵌套抽屉**打开 `FmsAdvancedQueryPanel`(`v-model:open` + `:items` + `v-model` 条件状态;Modal 与 Drawer 的滚动锁是引用计数、z-index 同层后挂载,已确认安全),`@apply` 回写状态与摘要。
+- ok 时组装:dsl → `{"kind":"dsl","expr":buildAdvancedAst(state)}`;sql → 文本。
+
+### 复用契约:`items` 怎么造(这是接条件面板的关键)
+
+照抄 [FmsModuleListPage.vue](file:///d:/workspace/code/fms/fms-vue/src/components/fms-module-list/FmsModuleListPage.vue#L315-L334) 的组装方式:先 `buildQueryRender(query_schema)`(`@/components/fms-module-common/schemaRender`)取到 `conditions`(含 `operator/component/options/defaultValue`),与当前模块 `fields` 合并,产出
+
+```js
+{ config: { b_field, b_operator, b_component, b_default_value }, field, optionConfig, options }
+```
+
+`b_component` 缺省按字段类型推导(select→select、date/datetime→date-picker、number/money→number、checkbox→checkbox、其余 input);select 字段若查询配置里没有 options,降级为 input(提示直接填值),避免空下拉死路。**首版先做到这一步**——字段权限裁剪(查询侧有 `loadFieldPower`)本次不接,规则面板直接列全部启用字段。
+
+### `queryUtils.js` 增一个反向函数
+
+`src/components/fms-module-common/queryUtils.js` 已有 `buildAdvancedAst`(界面 → AST)但**没有反向**,回显已保存规则需要:
+
+```js
+export function advancedQueryFromAst(ast, { idFactory } = {})
+// → { nodes, groups, degraded }
+```
+
+`condition` → node(`join` 缺省 'and');`group` → 组;组内再出现 group(>1 层)时把内层 children 拍平进该组并置 `degraded: true`,面板据此提示一次"原条件含多层分组,已按单层展开,请确认后保存"。
+
+### `ModuleRelationPanel.vue` 加两列
+
+- `b_on_delete`「删除连带」:select,选项 不连带/连带删除/连带归档;`many_to_one` 行 `disabled`,且 `onCellChange` 把关系类型切成 many_to_one 时自动置回 `none` 并 warning(防脏数据绕过校验)。
+- `_hasRefRule`「有引用时拒绝删除」:checkbox(`_` 前缀,不落 s_relation、不参与关系行的 diff)。勾选/取消 → 在 `refRules` 草稿里增/删一条规则行(`scope_type='relation'`、`scope_id=relationScopeId(row)`)。
+- **主键列变化处理**:关系行的源字段/目标模块/目标字段一旦改动,`scope_id` 就变了 → 自动取消勾选并删除对应规则行,提示"关系主键已变化,请重新勾选引用保护"。避免规则挂在旧键上产生孤儿行。
+- props 增加 `v-model:refRules`(父级持有 `refRules/refRules_org` 成对草稿,走正常 diff 与脏判定,不需要任何 `_` 前缀的脏判定特例)。
+
+### `index.vue` 接入点(9 处)
+
+1. import `ModuleRulePanel`;`configGroups` 的 capability 组追加 `{ key: 'rule', label: '删除规则', icon: CircleSlash }`(已确认 @lucide/vue 有该图标)。
+2. 数据对:`rules / rules_org`(scope=module)、`refRules / refRules_org`(scope=relation,源侧)、`refRulesReverse`(只读)。
+3. `clearModuleData` 清空上述 ref;面板 ref `rulePanelRef`。
+4. 模板挂载(relation 面板之后):``,`onRuleNavigate` 切 `activeSection` 或 `onModuleSelect`。
+5. `loadModuleConfig` 加第 7 张表:`s_rule`,条件 `b_scope_type='module' AND b_scope_id = <模块编码>`,排序 `b_xh, b_code`;关系侧规则**必须第二次查询**(规则表没有模块列,deletes 也不支持 LIKE 前缀):关系加载完后用双向关系键集合查 `b_scope_type='relation' AND b_scope_id IN (…)`,按 `scope_id` 属于源侧/目标侧拆成 `refRules_org` 与 `refRulesReverse`(无关系时跳过这次查询)。
+6. `buildSaveData` 在 s_relation 段之后加两段(都先物化 scope 两列):
+
+```js
+for (const row of rules.value) { row.b_scope_type = 'module'; if (row.b_scope_id !== moduleId) row.b_scope_id = moduleId }
+const ruleReq = tableChange('s_rule',
+ diffRows(stripInternalKeys(rules.value), stripInternalKeys(rules_org.value),
+ { keyFields: ['b_scope_type', 'b_scope_id', 'b_code'] }),
+ ['b_scope_type', 'b_scope_id', 'b_code'])
+if (hasChanges(ruleReq)) saveReqs.push(ruleReq)
+
+for (const row of refRules.value) { row.b_scope_type = 'relation'; row.b_hook = 'delete.pre' }
+const refRuleReq = tableChange('s_rule',
+ diffRows(stripInternalKeys(refRules.value), stripInternalKeys(refRules_org.value),
+ { keyFields: ['b_scope_type', 'b_scope_id', 'b_code'] }),
+ ['b_scope_type', 'b_scope_id', 'b_code'])
+if (hasChanges(refRuleReq)) saveReqs.push(refRuleReq)
+```
+
+7. `beforeSave`:接 `rulePanelRef.value?.validate?.()`,失败则 `activeSection='rule'` + 首条文案。
+8. `hasUnsavedChanges` 的 pairs 追加 `[rules, rules_org]` 与 `[refRules, refRules_org]`。
+9. `deleteModule`:追加模块级清理 `{ table:'s_rule', key_field:'b_scope_type,b_scope_id', deletes:[{ b_scope_type:'module', b_scope_id:id2 }] }`;关系级需**先查一次** `s_relation`(双向)拿到关系键,再一条批量 delete;确认文案补「删除规则」。
+ - 顺带:在关联面板删掉某条关系并保存时,规则行由 `refRules` 的 diff 自然删除(勾选态随行消失),无需额外逻辑。
+
+## 边界与风险
+
+- **many_to_one 禁连带**:面板 disabled + 改类型自动清 + `validate()` 三层拦截。
+- **归档列校验点**:一期前端查不了 `sys.columns`,选「连带归档」时给一次性 `Message.warning('连带归档要求目标表含 b_archived / b_archive_datetime 列,引擎启用时会校验')`,真正的校验留在二期服务层。
+- **打开即干净**:`b_on_delete` 非空默认值 + 派生列一律 `_` 前缀 + `refRules` 走常规 pairs(无脏判定特例)。
+- **删除规则 tab 只对 data 模块显示**(与自动编码/权限/模块关联一致)。
+
+## 验证
+
+手工(`pnpm dev` → `/module/module-management`,需 g3soft 账号):
+1. `RunSqlFile --dry-run` 后实跑增量脚本,核对 `s_rule` 与 `s_relation.b_on_delete`。
+2. 关联面板新增 one_to_many 关系选「连带删除」→ 保存 → 库里 `b_on_delete='cascade'`。
+3. 改成 many_to_one → 下拉只剩「不连带」且原值被清。
+4. 勾「有引用时拒绝删除」→ 保存 → `s_rule` 出现 `scope_type='relation'`、`scope_id` 为四段竖线串的一行;取消勾选再保存 → 该行删除。
+5. 改该关系的目标模块 → 勾选自动取消并提示;保存后库里无旧键孤儿规则。
+6. 「删除规则」tab 新增勾条件规则(两条件 and/or 混用)→ 保存 → `predicate` 为 `{"kind":"dsl",…}`;重开模块条件与摘要一致。
+7. 新增 SQL 规则不填提示文案 → 被拦。
+8. 打开模块立即保存 → 提示「没有需要保存的变更」(不脏)。
+9. 删除该模块 → `s_rule`/`s_relation` 残留为 0(含关系级规则)。
+
+测试(一源文件一 spec,见 [tests/README.md](file:///d:/workspace/code/fms/fms-vue/tests/README.md)):
+- 改 `tests/unit/query-ast.spec.js`:`advancedQueryFromAst` 空 AST / 单层往返 / 二层拍平置 `degraded`。
+- 改 `tests/unit/module-relation-panel.spec.js`:`b_on_delete` 加载不置脏、many_to_one disabled 与自动清、cascade+many_to_one 被 validate 拦、勾选增删 `refRules` 行、改主键自动取消勾选。
+- 新增 `tests/unit/module-rule-panel.spec.js`:回填只补 `_` 键、dsl 无条件被拦、sql 缺文案被拦、编码重复被拦、只读区渲染与 navigate。
+- 新增 `tests/unit/module-rule-utils.spec.js`:scopeId 往返、predicate 三态(含非法 JSON 降级为 sql)、`nextRuleCode` 递增、items 推导。
+- 改 `tests/views/module-unsaved-after-save.spec.js`:加载 7 张表 + 二次规则查询、`buildSaveData` 产出两条 `s_rule` 请求、`deleteModule` 的模块级与关系级清理。
+
+## 明确不做(一期之外)
+
+后端检查器/执行器与连带展开、`POST /data/deleteobj`(预览/错误列表)、`saveobjt` deletes 分支接入、`b_archived` 归档列、规则复制到其他模块、系统管理全局规则列表页、「值 vs 另一字段」比较操作符、条件面板的字段权限裁剪、`rule.edit` 权限点(页面已由 `meta.adminOnly` 兜底)。
\ No newline at end of file
diff --git a/code/fms/.trae/documents/模块关联配置面板实现方案.md b/code/fms/.trae/documents/模块关联配置面板实现方案.md
new file mode 100644
index 00000000..a595abc1
--- /dev/null
+++ b/code/fms/.trae/documents/模块关联配置面板实现方案.md
@@ -0,0 +1,197 @@
+# 模块关联(s_relation)配置面板实现方案
+
+## Context
+
+`s_relation` 是模块关系表,用于表达主子表、引用关系:
+
+- 建表脚本:[fms_core.sql](file:///d:/workspace/code/fms/sql/fms_core.sql#L202-L225),7 列 + 4 列复合主键(`b_source_module_id, b_source_field, b_target_module_id, b_target_field`),另有 `b_relation_type / b_canuse / b_xh` 与两个索引;无种子数据。
+- 设计定义:[FMS新系统核心表结构设计.md](file:///d:/workspace/code/fms/FMS新系统核心表结构设计.md#L519-L548) 第 9 节,`one_to_one / one_to_many / many_to_one`,不提供 `many_to_many`。
+- 现状:`fms-vue/src` 与 `fms-api/src` **零引用**,模块管理 8 个配置面板里没有入口 → 数据库有表、设计有定义,但用户无法配置。
+- 附带缺陷:删除模块的级联清理([index.vue](file:///d:/workspace/code/fms/fms-vue/src/views/module/module-management/index.vue#L715-L745))漏了 `s_relation`,删模块会残留悬空关系行。
+
+本次目标:在「模块管理 → 业务能力」下新增「模块关联」面板,读写现有 7 列,并补齐删除清理与临时 id 重映射。
+
+## 已确认的决策
+
+1. **双向可见、源侧可编辑**:上区「本模块 → 其他模块」(`b_source_module_id` = 当前模块)可编辑;下区「其他模块 → 本模块」(`b_target_module_id` = 当前模块)只读,避免对方配置的引用完全不可见。
+2. **目标模块**用 `ui/Select`,选项来自一次性加载的全量模块;停用/已删模块降级显示裸编码。
+3. **导航位置**:「业务能力」分组,与自动编码、权限并列;仅 data 模块显示(复用 `isDataModule` 门控)。
+4. **不含 `b_on_delete`**(属删除规则引擎规划,SQL 里尚无此列)。
+5. **不拦截被引用模块的删除**:连带清理双向关系行即可(`s_relation` 属配置元数据)。
+6. **后端零改动**:`DataSaveService` 已支持逗号拼接复合 key,`DbUtils` 用「行内列值 AND」生成 DELETE 条件,`s_relation` 可直接读写。
+
+## 数据流
+
+```
+loadModuleConfig(id) ── 1 条查询 b_source_module_id = id OR b_target_module_id = id
+ ├─ relations (source = id) ← 可编辑,参与 diff 与脏判定
+ ├─ relations_org (source = id) ← 基线快照
+ └─ reverseRelations (source ≠ id) ← 只读展示,不参与 diff / 脏判定
+buildSaveData() ── 回写 b_source_module_id = 当前模块编码 → diffRows(4 列主键) → tableChange
+beforeSave() ── 面板 validate() + 目标模块必须已落库
+hasUnsavedChanges() ── pairs 追加 [relations, relations_org]
+deleteModule() ── 追加两条按 b_source_module_id / b_target_module_id 的清理
+afterSave() ── 临时 id → 真实编码时重映射三份数据
+```
+
+## 改动清单
+
+### 1. 新增 `ModuleRelationPanel.vue`
+
+路径:`fms-vue/src/views/module/module-management/ModuleRelationPanel.vue`
+
+**接口**
+
+- props:`moduleId: String`(当前模块编码,父级 `String(maindata.b_id || '')`)、`fields: Array`(当前模块字段,源字段选项)、`modules: Array`(全量模块,含停用项,用于选项与名称解析)、`reverseRows: Array`(反向只读行)、`loading: Boolean`
+- model:`const relations = defineModel({ type: Array, default: () => [] })`
+- expose:`validate(): string[]`(父级只看 `errors?.length` 与首条文案)
+
+**内部状态**:`activeKey`、`tableRef`、`selectedRowKeys`;模块编码 → 字段选项缓存 `fieldOptionsCache`(`reactive(new Map())`)+ 进行中的 `inflightFields`(Set,防重复请求)。
+
+**行本地键**:`watch(relations, ..., { deep: true, immediate: true })` 逐行补 `_rowKey = createTempId()`。`_` 前缀不参与 diff,加载回填不会置脏(同 ModulePowerPanel 手法)。
+
+**列定义**(`shallowRef`,参照 [ModuleQueryConfigPanel.vue](file:///d:/workspace/code/fms/fms-vue/src/views/module/module-management/ModuleQueryConfigPanel.vue#L209-L242))
+
+| dataIndex | 标题 | 宽 | editor |
+| --- | --- | --- | --- |
+| `b_source_field` | 源字段 | 190 | `select`,options = 当前模块字段(停用标注「(停用)」) |
+| `b_target_module_id` | 目标模块 | 220 | `select`,options = 启用模块「名称(编码)」 |
+| `b_target_field` | 目标字段 | 190 | `select`,**options 按行取该行目标模块的字段**(见改动 2) |
+| `b_relation_type` | 关系类型 | 130 | `select`:一对一 / 一对多 / 多对一 |
+| `b_canuse` | 启用 | 70 | `checkbox` |
+| `b_xh` | 排序号 | 80 | 无(拖拽重排后回写) |
+| `actions` | — | 60 | `actions:[{ type:'delete', label:'删除', onClick: removeRow }]` |
+
+**交互**(照搬 ModuleQueryConfigPanel 的既有约定)
+
+- `addRow()`:换新数组引用(stk-table 对同引用 push 不敏感)→ 新行 `b_source_module_id = props.moduleId`、其余键为 null、`b_relation_type = 'many_to_one'`、`b_canuse = 1`、`b_xh = (length + 1) * 10`、`_rowKey = createTempId()` → `nextTick` 后 `tableRef.editRow(key, 'b_source_field')`
+- `removeRow()` / `batchDelete()`:同步清 `activeKey`、`selectedRowKeys`
+- `onRowOrderChange(rows)`:`rows.map((r, i) => ({ ...r, b_xh: (i + 1) * 10 }))`
+- `onCellChange(row, col)`:就地 patch 行对象 → 换新数组引用触发渲染;**当 `b_target_module_id` 变化时**:清空该行 `b_target_field`,并 `void ensureTargetFields(code)`,完成后换引用并 `editRow(row._rowKey, 'b_target_field')`
+- 惰性加载:`ensureTargetFields(code)` 查缓存 / in-flight 直接返回,否则 `loadDataApi('s_field', \`b_module_id = ${sqlStringLiteral(code)}\`, 'b_xh ASC, b_field ASC')` 写缓存;反向下只读行的源模块用同一缓存预热(去重、失败静默降级为裸编码)
+
+**只读区**:用 `editable=false` 的 FmsTable,列不含 editor;展示文案由 computed 派生行(模块名 / 字段名 / 关系类型中文 / 启用状态)预先算成普通字段,命中缓存后随响应式刷新,未命中回退裸编码。
+
+**validate()**:逐行校验 `b_source_field / b_target_module_id / b_target_field` 非空、`b_relation_type` 取值合法、4 列组合唯一(用 `|` 拼接的 key,与 `diffRows` 行身份一致)。空列/重复必须拦——`diffRows.keyOf` 对空值用空串占位,两行未填完的新行会撞成同一身份,落库直接主键冲突。
+
+**样式**:scoped;flex 链(`flex: 1` + `min-height: 0`)撑高,不写 `height: 100%`;表格用 FmsTable 默认 `bordered` 外框。
+
+### 2. FmsTable 编辑器小扩展:`select.options` 支持按行解析
+
+`src/components/fms-table/utils/createEditableCellRenderer.js` 三处(改动 3 行,向后兼容——现有 `options: () => fieldOptions.value` 忽略入参):
+
+```js
+function resolveOptions(editor, row) {
+ return typeof editor.options === 'function' ? editor.options(row) : (editor.options ?? [])
+}
+// displayValue(row, col, editor) 内:resolveOptions(editor, row)
+// createSelect(row, col, editor, ctx) 内:resolveOptions(editor, row)
+```
+
+这是给组件库补能力(而非用选择器权重绕过),后续「目标字段」等按行联动的下拉都可复用。
+
+### 3. 导出 SQL 字面量助手
+
+`module-management/utils.js:451` 的 `sqlStringLiteral` 加 `export`,供面板拼 `s_field` 查询条件(它已是本地函数,属第二个真实调用方)。`index.vue:94` 的 `sqlText` 保持不动(不在本次范围)。
+
+### 4. `index.vue` 十处改动
+
+1) import:`ModuleRelationPanel from './ModuleRelationPanel.vue'`;图标 `Waypoints`(@lucide/vue 已确认存在)。
+2) `configGroups`(L270-279)的 `isDataModule` 分支 items 追加 `{ key: 'relation', label: '模块关联', icon: Waypoints }`。
+3) 数据对:`const relations = ref([])` / `const relations_org = ref([])` / `const reverseRelations = ref([])`。
+4) `relationModules` computed:递归扁平 `moduleTreeData`(保留停用项用于名称解析),排除 `unsavedModuleIds` 与 `/^-\d+$/` 草稿 id;零额外请求(树已加载)。
+5) `const relationPanelRef = ref(null)`。
+6) 模板挂载(放在 power 面板之后,样板同 L1191-1202):
+
+```html
+
+
+
+```
+
+7) `clearModuleData()`:三份数据一并清空。
+8) `loadModuleConfig` 的 `Promise.all` 追加第 6 项(与 auto-code 同 `type === 'data'` 门控),并在 `maindata_org` 赋值段后拆分:
+
+```js
+const rows = relationRes.data || []
+const isSource = (r) => String(r.b_source_module_id ?? '').trim() === id
+relations_org.value = cloneData(rows.filter(isSource))
+reverseRelations.value = cloneData(rows.filter((r) => !isSource(r)))
+relations.value = cloneData(relations_org.value)
+```
+
+竞态沿用既有守卫(同一 `Promise.all` 内,无需新增)。只把 source 侧放入 `relations`,diff 的 deletes 不会误删反向行。
+9) `buildSaveData` 的 `if (isDataModule.value)` 块内(权限之后):
+
+```js
+for (const row of relations.value) {
+ if (String(row.b_source_module_id ?? '').trim() !== moduleId) row.b_source_module_id = moduleId
+}
+const relationReq = tableChange(
+ 's_relation',
+ diffRows(relations.value, relations_org.value, {
+ keyFields: ['b_source_module_id', 'b_source_field', 'b_target_module_id', 'b_target_field'],
+ }),
+ ['b_source_module_id', 'b_source_field', 'b_target_module_id', 'b_target_field'],
+)
+if (hasChanges(relationReq)) saveReqs.push(relationReq)
+```
+
+10) 其余四处:
+- `beforeSave`:`relationPanelRef.value?.validate?.()` 有错则 `activeSection = 'relation'` + `Message.warning`;追加守卫 `relations.value.some((r) => unsavedModuleIds.has(String(r.b_target_module_id ?? '')))` → 「目标模块尚未保存,请先保存目标模块」。
+- `hasUnsavedChanges` 的 `pairs`(L967-972)追加 `[relations, relations_org]`(`reverseRelations` 不参与)。
+- `deleteModule` 的 `reqs` 追加两条,并把确认文案(L709)补上「模块关联」:
+
+```js
+{ table: 's_relation', key_field: 'b_source_module_id', deletes: [{ b_source_module_id: id2 }] },
+{ table: 's_relation', key_field: 'b_target_module_id', deletes: [{ b_target_module_id: id2 }] },
+```
+
+- `afterSave`:与 `remapDraftParentId(treeId, savedKey)` 并列调用 `remapRelationModuleIds(treeId, savedKey)`,把 `relations` / `relations_org` / `reverseRelations` 三份数据中的源侧与目标侧临时 id 改为真实编码(幂等;主路径仍是保存后按真实编码重载)。
+
+## 边界与约定
+
+| 场景 | 处理 |
+| --- | --- |
+| 改主键列(源字段 / 目标模块) | diff 判为 delete + insert,可接受(同事务、无子引用、不涉雪花回灌);仅要求换目标模块时清空 `b_target_field` |
+| 目标模块是未落库草稿 | 选项里不出现 + `beforeSave` 拦截(双保险) |
+| 停用 / 已删模块、已删字段 | 选项缺失时 Select 回退显示裸值;文案统一「名称(编码)」 |
+| 四列为空 / 组合重复 | 面板 `validate()` 拦截 |
+| 删除被引用模块 | 不拦截,双向连带清理 |
+| `_rowKey` | `_` 前缀,不参与 diff / 脏判定,不提交 |
+
+## 验证
+
+按 [开发规范.md](file:///d:/workspace/code/fms/开发规范.md) 第 2 节,改业务代码时不主动执行构建/测试;以下供确认后执行。
+
+手工验证:`pnpm dev` → `/module/module-management`(`meta.adminOnly`,需 g3soft 账号)→ 选一个 data 模块 → 业务能力 →「模块关联」
+
+1. 上区新增一行 → 选源字段、目标模块(目标字段下拉需在选完目标模块后可选)、关系类型 → 保存 → 刷新确认落库
+2. 切到目标模块,下区应出现该行(只读,含模块名 / 字段名 / 类型)
+3. 停用或删除目标模块后重开面板:选项缺失但显示完整、行不丢
+4. 拖拽排序 + 启用开关 → 保存后 `b_xh` / `b_canuse` 更新
+5. 新建模块未填编码时选目标模块 → 保存被拦;填编码保存后 `s_relation.b_source_module_id` 是真实编码
+6. 删除被引用模块 → 双向关联行随之消失
+7. 反复切换树节点 / 打开关闭面板,不应误报「未保存」
+
+测试(约定见 [tests/README.md](file:///d:/workspace/code/fms/fms-vue/tests/README.md),每个源文件最多一个 spec):
+
+- 新增 `tests/unit/module-relation-panel.spec.js`:增行、换目标模块清空目标字段、`validate()` 空列与重复拦截、`_rowKey` 不置脏
+- 页面级断言(`table: 's_relation'`、`key_field` 为 4 列、删除模块含两条 `s_relation`):复用 [module-unsaved-after-save.spec.js](file:///d:/workspace/code/fms/fms-vue/tests/views/module-unsaved-after-save.spec.js) 的装配(在 `buildDb()` 补 `s_relation: []`),或按「一源文件一 spec」新建 `tests/views/module-management.spec.js`
+- 跑法:`pnpm test:file tests/unit/module-relation-panel.spec.js` → `pnpm test` → `pnpm test:coverage`
+
+## 明确不做
+
+- `s_relation.b_on_delete`(属删除规则引擎规划,SQL 尚无此列)
+- 模块下拉的服务端搜索(库内 select 编辑器固定 `filterable: false`;如需,后续对该列用 `customCell` + `ui/Select(filterable)`)
+- 字段重命名(`b_field`)后重映射关联行
+- 被引用模块的删除拦截
+- 反向只读区的编辑 / 删除入口
\ No newline at end of file
diff --git a/code/fms/.workbuddy-ai/memory/2026-09-17.md b/code/fms/.workbuddy-ai/memory/2026-09-17.md
index 3a4a4d08..fdf8aac9 100644
--- a/code/fms/.workbuddy-ai/memory/2026-09-17.md
+++ b/code/fms/.workbuddy-ai/memory/2026-09-17.md
@@ -12,6 +12,83 @@
全套 10 个用例通过;oxlint 干净(oxfmt 全仓库既有 format issues,未动)。
- 已核对仓库内另 3 处 `pageDataApi` 调用(FileListPanel / useLookupOptions / FmsModuleListPage)都传了 orderBy。
+## 新增:UI 组件库全局配置(uiConfig)
+
+- 背景:组件库此前**没有任何全局配置层**(`ui/index.js` 只是具名导出,无 `install`/`app.use`;
+ 组件间只有 form/form-group/radio-group/tree/dropdown 五个局部 provide)。Form 的
+ `layout` 等默认值硬编码在 prop 里,想改只能逐处传。
+- 新增 `fms-vue/src/components/ui/config.js`:`uiConfig`(reactive)+ `setUIConfig(patch)`(按组件维度浅合并)
+ + `resetUIConfig()`;三者也从 `@/components/ui` 具名导出。
+- 回落顺序统一为 **自身 prop > 全局配置 > 组件内置默认**。凡要支持全局覆盖的 prop,
+ `default` 必须改成 `undefined` 且 validator 放行 `undefined`(Vue 会校验 default 值)。
+- Form(`form/form.vue`):`layout`/`labelAlign`/`labelWidth`/`validateTrigger` 改为经
+ `resolvedXxx` computed 回落,provide 下发的是**回落后的最终值**,FormItem 不用再管全局配置;
+ FormItem 独立使用(无 Form 包裹)时也读一次 `uiConfig`。
+- 语言联动写在 `src/App.vue`:`watch(() => i18nStore.locale)` → `layout = /^zh/i.test(locale) ? 'horizontal' : 'vertical'`
+ (中文横排、其他语言竖排),`immediate: true` 覆盖持久化 locale 的刷新场景。
+- 存量 8 处 `layout="vertical"` 已全部删除(login、base/othercompany、system/user、
+ menu-management/MenuBasicPanel、module-management 的 Basic/AutoCode/FormPreview、FmsModuleEditModal),
+ 统一跟随语言;`ui/demo/form.vue` 保留(它是组件展示页,故意演示各布局)。
+ 风险点:FmsModuleEditModal(columns=24 网格)与 FormPreview(columns=48)在中文下变横排可能拥挤,待目测。
+- 测试:`tests/components/form.spec.js` 新增 5 例(全局默认、prop 覆盖、局部合并、validateTrigger、独立 FormItem),
+ 15 例全通过;oxlint `--deny-warnings` 干净;`vite build` 通过。
+- 已知非本次引入:`tests/views/login.spec.js` 套件级失败(favicon.svg URL 解析,环境问题,收集 0 用例);
+ `module-list-components.spec.js` 的 `.filter-joiner`/`.filter-row` 断言是过期基线。
+
+## 修复:顶栏语言切换不生效(i18n 只有 locale,没有译文)
+
+- 根因:`setMessages` 在 `src/` 下**从无调用点** → `messagesRef` 永远为空,`t(key, fallback)` 恒返回中文兜底;
+ 顶栏按钮只是把 `locale` 在 zh-CN/en-US 间翻一下,界面毫无变化。
+ 另查库:`s_i18n_type` 有 zh-CN(默认)/en-US;`s_i18n` **134 行全是 zh-CN,en-US 为 0 行**。
+- 改动:
+ - `stores/i18n.js` 新增 `loadMessages(locale, force)`:查 `s_i18n`(`b_canuse = 1 and b_locale = N'..'`,
+ `sqlQuote` 转义单引号),按 locale 缓存 + 并发 Promise 复用 + 失败静默;`applyMessages` 只认**当前** locale
+ (防止请求回来时语言已被切走导致串台),合并顺序 **前端内置 < 库表译文**;`clearMessages` 顺带清缓存。
+ - 新增 `src/i18n/builtin-messages.js`:内置 en-US 的 `common.*`(45 条,代码里已用 `t('common.x','中文')` 约定)。
+ 中文不内置(fallback 即中文)。库里登记同名 key 可覆盖内置。
+ - `App.vue` 的 locale watcher 增加 `i18nStore.loadMessages(locale)`;`router/index.js` 守卫在
+ `loadLanguages()` 后再 `loadMessages()` 兜首次进入。
+ - 顶栏:按钮改为 Dropdown,语种取自 `s_i18n_type` 清单(不再硬编码互切),当前项用 `shortcut: '✓'` 标出
+ (Dropdown item 没有 active 态)。
+- 测试新增 12 例:`tests/stores/i18n-messages.spec.js`(7)、`tests/components/app-topbar-language.spec.js`(3)、
+ `tests/unit/app-locale-sync.spec.js`(2)。全量 497 通过 / 13 失败(**均为既有基线**:
+ login 套件 + router-auth 3 例都是 `favicon.svg` 环境问题——`views/login/index.vue:98` 的 `
`;
+ 其余 9 例是查询面板改版后的过期断言)。
+- 注:`vite build` 冒烟本次被用户拒绝执行(含 find -delete),改用 vitest 挂载 App/AppTopbar 验证编译与行为。
+
+## 查询区改用 Form/FormItem(跟随语言布局)
+
+- 现象:切中文后查询条件区仍是上下结构。原因:`FmsQueryToolbar.vue` **没用 Form**,是自己写的
+ `