cloud-web-corejs 1.0.54-dev.738 → 1.0.54-dev.739

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cloud-web-corejs",
3
3
  "private": false,
4
- "version": "1.0.54-dev.738",
4
+ "version": "1.0.54-dev.739",
5
5
  "scripts": {
6
6
  "dev": "vue-cli-service serve",
7
7
  "lint": "eslint --ext .js,.vue src",
@@ -122,6 +122,36 @@ f.enableCompareBySource(sourceId, { compareRowKey: 'uuid' });
122
122
  ```
123
123
  > 手动版:`f.loadOldData(sourceId).then(old => f.enableCompare(old, { compareRowKey:'uuid' }))`。
124
124
 
125
+ ### 模式 B 变体:搜索框选原单 → 点「对比」
126
+ 支持,**无需额外开发**。本质仍是模式 B,只是 `sourceId` 的来源从「变更单里存的原单引用」换成「搜索框(关联组件)当前选中的值」。
127
+
128
+ 关联组件(vabSearch / autocomplete 等)选中后,**原单 id 就存在它绑定的字段**(`formModel[该字段keyName]`,label 存在伴随字段)。只要「搜索框绑定的字段名」= 「对比按钮读取 sourceId 的字段名」(如都用 `f_source_id`),现有代码原样跑通:
129
+
130
+ ```js
131
+ // 「对比」按钮 onClick(GlobalConfig)
132
+ enableCompareBySource(that) {
133
+ let f = that.getFormRef();
134
+ let sourceId = that.formModel.f_source_id; // ← 搜索框绑定的字段,选中即有值
135
+ if (!sourceId) {
136
+ that.$message.warning('请先选择原单');
137
+ return;
138
+ }
139
+ if (f.isCompare) f.disableCompare(); // 换原单时先关掉上一轮对比,避免残留 hData
140
+ f.enableCompareBySource(sourceId, {
141
+ compareRowKey: compareRowKey,
142
+ excludeFields: excludeFields,
143
+ // sourceFormCode: '原单formCode', // 原单与变更单模板不同时必填
144
+ });
145
+ this.handleCompareButton(that, true);
146
+ }
147
+ ```
148
+
149
+ 注意事项:
150
+ 1. **切换原单**:重新选一张原单再点「对比」,`enableCompareBySource` 会重新拉数并覆盖 `oldFormData`、重做行快照。稳妥起见先 `disableCompare()` 再重新对比(如上),清掉上一轮遗留的行快照 `hData`。
151
+ 2. **模板不同必须传 `sourceFormCode`**:否则会用变更单模板去枚举子表发包,取不到原单数据。
152
+ 3. **搜索框字段本身要排除对比**:把它加入 `excludeFields`(如 `f_source_id`),否则它自己会被判为「变更」。
153
+ 4. 与模式 A 不冲突:新增时走 `getFormParam().sourceId` → `initChangeFromSource`;手选走本节。两条路最终都落到同一个原单引用字段。
154
+
125
155
  ### 其它按钮
126
156
  - 关闭对比:`this.getFormRef().disableCompare();`
127
157
  - 查看删除数据:`this.getFormRef().openCompareDelList('子表字段名');`(要中文列头传第二参 `[{field,title},...]`)
@@ -129,7 +159,18 @@ f.enableCompareBySource(sourceId, { compareRowKey: 'uuid' });
129
159
  ### 全局配置项(可选)
130
160
  - `formConfig.compareRowKey`:子表行匹配主键,默认 `uuid`;也可 `enableCompare(old,{compareRowKey})` 覆盖。
131
161
  - `formConfig.compareExcludeTypes` / `enableCompare(old,{excludeTypes})`:不参与对比的字段类型(附件/上传类已默认排除)。
132
- - `formConfig.compareExcludeFields` / `enableCompare(old,{excludeFields})`:按**字段名**不参与对比;`create_by/create_date/modify_by/modify_date/id/head_table_id/alias_id/object_foreign_id` 等系统字段已默认排除。
162
+ - **对比字段名单(主表 / 明细分开配置)**:
163
+
164
+ | | 黑名单(不对比) | 白名单(只对比) |
165
+ |---|---|---|
166
+ | 主表 | `excludeFields: ['f_type', ...]` | `includeFields: ['f_qty', ...]` |
167
+ | 明细 | `subExcludeFields: { 明细keyName: ['f_x', ...] }` | `subIncludeFields: { 明细keyName: ['f_qty', ...] }` |
168
+
169
+ - 都通过 `initChangeFromSource / enableCompareBySource / enableCompare` 的 options 传;兜底位 `formConfig.compareExcludeFields / compareSubExcludeFields / compareIncludeFields / compareSubIncludeFields`。
170
+ - **主表名单与明细名单互不干涉**:主表字段只看 `excludeFields/includeFields`,明细字段只看 `subExcludeFields[表]/subIncludeFields[表]`。主表名单**不会下渗到明细**(两者字段空间不同,同名字段语义也可能不同);明细字段要排除,就必须写进该表的 `subExcludeFields`。
171
+ - **白名单优先于黑名单**:某张表一旦配了白名单,该表的黑名单整体失效,别同时配。这个优先关系按表独立生效——主表用白名单、明细用黑名单是允许的。
172
+ - 系统字段(`id/create_by/create_date/modify_by/modify_date/head_table_id/alias_id/object_foreign_id` 等)、附件字段(`attachments_` 前缀)、`subSourceFields` 回填字段,**已默认排除**,不用写。
173
+ - 跨表场景(compareMapping)下白名单**自动推导**:主表白名单来自 `mapping.master[].to`,每张明细的白名单来自该表 `fields[].to` —— 主表与各明细互不串味,无需手写。
133
174
  - `sanitize.defaultFields`(主表)/ `sanitize.subDefaultFields`(明细表 `{keyName:[列prop]}`):这些字段**不吃原单值**,保留变更单自身值/默认值。
134
175
  - `sanitize.subExtraClearFields`:子表额外清空的外键字段,默认 `head_table_id/headTableId/alias_id/object_foreign_id`。
135
176
  - `sanitize.attachmentKeepFields`:附件文件对象保留字段白名单,默认 `name/thumbnail/extension/large/fileSize/domain/source/medium/url`(表头附件、data-table 单元格附件都按此精简;`sanitize.cleanAttachment:false` 可整体关闭)。
@@ -139,9 +180,13 @@ f.enableCompareBySource(sourceId, { compareRowKey: 'uuid' });
139
180
  - `{ fromHeader:'表头字段', to:'目标字段' }` → 原单表头字段回填
140
181
  - `[ {..}, {..} ]` → 多字段
141
182
  - `(row, header) => {...}` → 自定义函数(row 原明细行、header 原单表头;均为原始值,早于清 id/树表重生成执行)
183
+
184
+ > **回填字段自动排除对比**:回填出的字段(如 `f_source_item_id`)只存在于新单行,原单行没有 → 逐字段比较时是「空 → 有值」,会把整表行判成**变更行(黄)**。`sanitizeChangeData` 已自动把回填目标字段登记进 `compareAutoExcludeFields`(函数式配置也支持:通过比对函数执行前后新增的 key 捕获),无需手工配置。
185
+ > **但模式 A(打开已保存的变更单点「对比」)不会走 sanitize**,此时请把这些字段一并写进 `excludeFields`,例如 `excludeFields: ['f_type', 'f_source_id', 'f_source_item_id']`。
142
186
  - `sanitize.treeSubTables`:树表配置 `[{ name, idField, parentField }]`,id/parent_id 会重新生成并保留层级。
143
187
  - **保存**:提交时 `getReqFormData` 自动剥除对比快照字段 `hData`,无需业务处理。
144
- - **行级高亮**:变更行橙色、新增行绿色(各带左侧色条),新增行不逐格显示感叹号;系统字段/`compareExcludeFields`/附件列不参与行级判定。
188
+ - **行级高亮**:变更行橙色、新增行绿色(各带左侧色条),新增行不逐格显示感叹号;行级判定与单元格判定走**同一个** `isCompareExcludeField(字段名, 明细表名)`,名单口径完全一致。
189
+ > 附件恒不对比:新单附件已按 `attachmentKeepFields` 白名单精简(9 个字段),原单快照里是完整对象(含 id/createDate/sn…),直接比必然不等。故按 `attachments_` 前缀(设计器 `keyNamePrefixMap` 固定前缀)在行级判定里整体跳过。
145
190
 
146
191
  ### 完整接入示例(GlobalConfig)
147
192
  把对比相关逻辑收敛到 `getGlobalConfig()` 返回对象里,详情页脚本按需调用。
@@ -561,6 +606,153 @@ f.setOldSubData('子表字段名', 子表旧行); // 子表旧数
561
606
  4. ✅ `sanitizeChangeData`(系统字段/外键/自定义/默认值/树表 id 重生成)。
562
607
  5. ✅ 通用多包 `loadOldData`(读原单模板自动发包)+ 一行式 `initChangeFromSource` / `enableCompareBySource`。
563
608
 
609
+ ## 十·五、跨单据对比(场景 B:订单 → 变更单,不同数据表/不同字段)
610
+
611
+ > **状态:已落地。** 在原「同结构对齐」(源单与变更单同表同字段名)基础上,新增支持**源单与变更单是不同数据表、字段名不同**的场景。
612
+ >
613
+ > 落地方法(`form-render/indexMixin.js`):`normalizeBySourceMapping`(源→目标键空间归一化)、`applyCompareSourceTransform`(归一化 + `transformOldData` 钩子统一入口)、`getCompareRowKeyOf`(按子表行主键)、`isCompareSubTable`(未映射子表不参与对比);状态新增 `compareIncludeFields`(白名单)、`compareSubRowKeys`;`loadOldData` 改为「主表先查 → 用真实 id 查明细」并支持 `subParams/commonParams`;`enableCompare / enableCompareBySource / initChangeFromSource` 全部接入。
614
+
615
+ ### 10.5.1 业务前提(已确认)
616
+ | # | 事项 | 结论 |
617
+ |---|------|------|
618
+ | 1 | 单据关系 | **订单 → 变更单**。可能是同一张数据表,也可能**不是同一张表、字段名不同** |
619
+ | 2 | 明细行配对 | **有稳定业务键**可配对(如物料编码/行号) |
620
+ | 3 | 对比范围 | **白名单**:只比映射表里明确列出的关键字段 |
621
+ | 4 | 文案 | 「变更前」暂不改 |
622
+ | 5 | 搜索框绑定值 | **可能是编码,也可能是 ID**;业务侧不做反查 |
623
+ | 6 | 明细读脚本 | `getList`,**可能有额外查询参数** |
624
+ | 7 | 明细外键 | `head_table_id` → **主表的 `id` 字段** |
625
+
626
+ ### 10.5.2 核心思路
627
+ 从**「结构对齐」**(隐式按同名字段比)改为**「语义映射」**(显式声明源↔目标字段对应关系):
628
+
629
+ 1. 用 `sourceFormCode` 拉**源单模板**取数(已支持);
630
+ 2. 按 **`compareMapping`** 把源单数据**归一化到变更单的键空间**(字段名、子表 keyName、明细列、行匹配键);
631
+ 3. 归一化之后,现有的 diff / `hData` 快照 / 行级高亮 / 删除行逻辑**几乎不用改**——它们只认变更单的键名。
632
+
633
+ **映射表可选**:同表同字段时不配,自动退化为现在的「结构对齐 + 黑名单」模式,**完全向后兼容**。
634
+
635
+ | 是否配 `compareMapping` | 对齐方式 | 对比范围 |
636
+ |---|---|---|
637
+ | 否(同表) | 隐式同名对齐(现状) | 黑名单 `excludeFields` |
638
+ | 是(跨表) | 语义映射 | **白名单**(由 mapping 的 `to` 自动推导) |
639
+
640
+ ### 10.5.3 映射表形态
641
+ ```js
642
+ compareMapping: {
643
+ // 主表:源字段 → 变更单字段
644
+ master: [
645
+ { from: 'f_order_amount', to: 'f_amount' },
646
+ { from: 'f_delivery_date', to: 'f_ship_date' },
647
+ ],
648
+ // 明细:源子表 → 变更单子表
649
+ subs: [
650
+ {
651
+ from: 'order_item', // 订单明细 keyName(源)
652
+ to: 'change_item', // 变更单明细 keyName(目标)
653
+ rowKey: { from: 'f_material_code', to: 'f_mat_no' }, // 业务键,两边可不同名
654
+ fields: [
655
+ { from: 'f_qty', to: 'f_change_qty' },
656
+ { from: 'f_price', to: 'f_unit_price' },
657
+ ],
658
+ },
659
+ ],
660
+ }
661
+ ```
662
+ > **白名单自动推导**:所有 `to` 字段(主表 + 各明细列)自动组成对比白名单,业务无需再写一遍 `includeFields`。未列出的字段一律不参与对比、不显示感叹号、不参与行级高亮判定。
663
+
664
+ ### 10.5.4 行匹配(关键技巧:不改 `matchCompareRowKey`)
665
+ 归一化明细行时:
666
+ ```
667
+ 归一化旧行[变更单业务键字段] = 源行[源业务键字段] // 如 旧行.f_mat_no = 订单行.f_material_code
668
+ 归一化旧行[变更单列名] = 源行[源列名] // 按 fields 映射
669
+ ```
670
+ 再把该子表的 `compareRowKey` 设为 `f_mat_no`(**变更单侧**的业务键字段)。于是:
671
+ - 当前变更单明细行本来就有 `f_mat_no`;
672
+ - 归一化后的旧行也被写入了 `f_mat_no`;
673
+ - `matchCompareRowKey(o, row, 'f_mat_no')` → **直接匹配成功,现有逻辑零改动**。
674
+
675
+ > **多字段组合键**(如 物料+批次):归一化时生成合成键字段(如 `_cmpKey = 'M001|B01'`),并在开启对比时对**当前明细行也注入同样的合成键**,`compareRowKey='_cmpKey'`。单字段业务键则用不上。
676
+
677
+ ### 10.5.5 取数流程(必须「主表先查」)
678
+ **为什么**:明细外键 `head_table_id → 主表.id`,而搜索框存的**可能是编码**。若沿用现在的并行发包(主表和明细同时用搜索框的值查),**存编码时明细必然查空**——明细表里没有"编码"这个关联字段。
679
+
680
+ ```
681
+ 1. 按 sourceFormCode 拉源单模板
682
+ 2. 【串行】主表包:getOne, taBm=源单entity, data:{ [sourceKeyField]: 搜索框的值 }
683
+ ↓ sourceKeyField 默认 'id'(后端 getOne 只认 id 时,编码场景可配成 'f_order_no')
684
+ masterId = 主表响应.objx[sourceIdField] // sourceIdField 默认 'id'
685
+ ↓ 查不到主表 / 拿不到 id → 中止并提示
686
+ 3. 【并行】用 masterId 发:
687
+ - 表头附件包 × N:getList, taBm=附件字段keyName, data:{ id: masterId }
688
+ - 明细包 × N:getList, taBm=明细keyName, data:{ id: masterId, attachmentType, thirdtabm, ...subParams }
689
+ 4. 组装 → 归一化(compareMapping) → enableCompare
690
+ ```
691
+
692
+ **向后兼容**:搜索框存的就是 id 时,主表返回的 `objx.id` 就是同一个值,明细包参数与现在完全一致 —— 现有模式 A **行为不变**,只多一次串行等待(一个 RTT)。可用 `sourceIdIsKey: true` 保留并行快路径。
693
+
694
+ ### 10.5.6 明细额外查询参数
695
+ **为什么不能直接复用源模板的 `formScriptParam`**:那段脚本在**该 data-table 组件实例**上下文里执行(`this` = 表格组件)。对比取数时源单模板只是一份 JSON,**没有对应组件实例**,跨实例执行有风险。
696
+
697
+ **方案:显式配置(安全可控)**
698
+ ```js
699
+ subParams: {
700
+ '订单明细keyName': { f_status: 1, f_type: 'A' }, // 静态参数
701
+ '订单附件明细': (ctx) => ({ f_biz: ctx.masterData.f_biz }), // 函数:可读主表数据
702
+ },
703
+ commonParams: { ... }, // 所有明细包都带的公共参数
704
+ ```
705
+ merge 进明细包的 `data`(与 `{id, attachmentType, thirdtabm}` 合并)。
706
+ > 函数形态可拿到 `ctx.masterData`(第 2 步取回的主表数据)—— 这是「主表先查」顺带带来的能力。
707
+ >
708
+ > 兜底开关(可选):`useSourceScriptParam: true` 尝试在 form-render 上下文执行源模板 data-table 的 `formScriptParam`,`try/catch` 失败则忽略;不作为默认。
709
+
710
+ ### 10.5.7 配置全貌
711
+ ```js
712
+ formRef.enableCompareBySource(搜索框的值, {
713
+ sourceFormCode: '订单formCode', // ★ 跨表必填,否则按变更单模板枚举子表 → 明细取不到
714
+
715
+ // 主表定位(兼容编码/ID)
716
+ sourceKeyField: 'id', // 主表包用哪个字段查,默认 'id'
717
+ sourceIdField: 'id', // 从主表响应取真实主键的字段名,默认 'id'
718
+ // sourceIdIsKey: true, // 明确传的就是 id → 走并行快路径
719
+
720
+ // 明细额外查询参数
721
+ subParams: { '订单明细': { f_status: 1 } },
722
+ // commonParams: {...},
723
+
724
+ // 跨表映射(场景B)
725
+ compareMapping: { master: [...], subs: [...] },
726
+ // transformOldData: (oldData, sourceTpl) => 归一化后的oldData, // 值转换兜底
727
+ });
728
+ ```
729
+ 模式 A(`initChangeFromSource`)同样接收上述选项 —— 跨表时**填充变更单也必须走映射**,否则字段名对不上、数据填不进去。
730
+
731
+ ### 10.5.8 语义/边界
732
+ | 情况 | 处理 |
733
+ |---|---|
734
+ | 源单有、变更单没有的字段 | 映射表不写 → 白名单天然忽略 |
735
+ | 变更单有、源单没有的字段 | 不写就不比;写了 `to` 但源无 `from` → 旧值空 → 显示「变更前:(空)」 |
736
+ | 源明细有、变更单未匹配上 | → 进「查看删除明细」(源单有、当前未引用) |
737
+ | 变更单明细有、源单未匹配上 | → 标「新增行」(绿) |
738
+ | **未映射的子表** | **完全不参与对比**(不高亮、删除列表为空)——不做这条会**满屏绿** |
739
+ | 值需要转换(单位/币种/编码体系) | 用 `transformOldData` 钩子 |
740
+ | 清洗 `sanitizeChangeData` | 作用在**归一化后**的数据上,系统字段清理/树表 id 重生成/附件白名单照常工作;`subSourceFields` 回填订单明细 id 依然推荐(给出硬引用,将来行匹配可从业务键切到来源明细 id) |
741
+
742
+ ### 10.5.9 改动清单(8 项,已全部实现)
743
+ | # | 改动 | 落地方法 |
744
+ |---|------|----------|
745
+ | 1 | ✅ `compareMapping` + 源数据归一化到变更单键空间 | `normalizeBySourceMapping(srcData, mapping)` → `{ data, includeFields, rowKeyMap }` |
746
+ | 2 | ✅ 白名单对比(由 mapping 的 `to` 自动推导) | 状态 `compareIncludeFields`;`isCompareExcludeField` 改为「白名单优先:设了白名单则不在名单内一律排除,黑名单失效」。wrapper 的 `compareExcluded` 与行级 `isCompareRowValuesChanged` 都走这一个判定 |
747
+ | 3 | ✅ `compareRowKey` 按子表配置 | 状态 `compareSubRowKeys`(由 `rowKey.to` 自动推导,可被 `options.subRowKeys` 覆盖);新增 `getCompareRowKeyOf(subFormName)`,`applyCompareSubSnapshot / getCompareRowClass / getDelData` 全部改用它 |
748
+ | 4 | ✅ 未映射子表不参与对比 | 新增 `isCompareSubTable(subFormName)`(判据:旧数据里有无该子表数组);`getCompareRowClass` 返回 `''`、`getDelData` 返回 `[]`。单元格侧因 `hData` 缺失 → `compareRowIsNew` → 天然不显示感叹号 |
749
+ | 5 | ✅ `initChangeFromSource` 接入映射 | 先 `applyCompareSourceTransform` 归一化,再 `sanitizeChangeData` → `setFormData` → `enableCompare` |
750
+ | 6 | ✅ `transformOldData` 钩子 | `applyCompareSourceTransform` 里,归一化之后调用 `options.transformOldData(data, srcData)` |
751
+ | 7 | ✅ `loadOldData` 主表先查 → 用真实 id 查明细 | 串行:主表包 `data:{[sourceKeyField]: 入参}` → `masterId = objx[sourceIdField]` → 再并行发表头附件包/明细包。`sourceIdIsKey:true` 可走并行快路径;主表查不到或取不到主键则不发明细包 |
752
+ | 8 | ✅ 明细额外查询参数透传 | `commonParams` + `subParams`(对象或 `(ctx)=>对象`,`ctx.masterData/masterId`)merge 进明细包 `data` |
753
+
754
+ **向后兼容**:不配 `compareMapping` 时 `applyCompareSourceTransform` 原样返回,`compareIncludeFields=null` → 走黑名单,`compareSubRowKeys={}` → 走全局 `compareRowKey`,`isCompareSubTable` 对已有子表恒为 true。**同表场景(现有模式A/B)行为不变**,仅 `loadOldData` 多一次串行等待(可用 `sourceIdIsKey:true` 消除)。
755
+
564
756
  ## 十一、说明 / 注意事项
565
757
  - **对比图标注入点**:统一在 `form-item-wrapper.vue`,覆盖主表字段与三类表格容器的单元格(均经 field-widget + tableParam.row 渲染);仅对比态包裹 compareView,非对比态零改动。
566
758
  - **「查看删除数据」按钮**:未在容器工具栏内置,由业务按钮 onClick 调 `openCompareDelList('子表名')` 触发(更灵活)。
@@ -370,15 +370,22 @@ export default {
370
370
  },
371
371
 
372
372
  // ===================== 表单对比 =====================
373
+ // 所在明细表的属性名(keyName);主表字段为空。用于按表取黑/白名单
374
+ compareSubName() {
375
+ if (!this.tableParam || !this.tableParam.row) return "";
376
+ let opts = this.parentWidget && this.parentWidget.options;
377
+ if (!opts) return "";
378
+ return (opts.keyNameEnabled && opts.keyName) || opts.name || "";
379
+ },
373
380
  compareExcluded() {
374
381
  // 按类型排除
375
382
  let types = this.getCompareExcludeTypes() || [];
376
383
  if (types.indexOf(this.field.type) !== -1) return true;
377
- // 按字段名排除(系统审计字段 + 业务配置 compareExcludeFields)
384
+ // 按字段名排除(系统字段 + 主表黑白名单 + 所在明细表的黑白名单)
378
385
  let fieldKeyName =
379
386
  (this.field.options.keyNameEnabled && this.field.options.keyName) ||
380
387
  this.field.options.name;
381
- return !!this.isCompareExcludeField(fieldKeyName);
388
+ return !!this.isCompareExcludeField(fieldKeyName, this.compareSubName);
382
389
  },
383
390
  isCompareOn() {
384
391
  return !this.designer && !this.compareExcluded && !!this.getIsCompare();
@@ -14,8 +14,8 @@
14
14
  }"
15
15
  >
16
16
  <el-tab-pane
17
- v-for="(tab, index) in visibleTabs"
18
- :key="index"
17
+ v-for="tab in visibleTabs"
18
+ :key="tab.options.name"
19
19
  :label="getI18nLabel(tab.options.label)"
20
20
  :disabled="tab.options.disabled"
21
21
  :name="tab.options.name"
@@ -25,7 +25,7 @@
25
25
  <component
26
26
  :is="subWidget.type + '-item'"
27
27
  :widget="subWidget"
28
- :key="swIdx"
28
+ :key="subWidget.id || swIdx"
29
29
  :parent-list="tab.widgetList"
30
30
  :index-of-parent-list="swIdx"
31
31
  :parent-widget="widget"
@@ -42,7 +42,7 @@
42
42
  <component
43
43
  :is="subWidget.type + '-widget'"
44
44
  :field="subWidget"
45
- :key="swIdx"
45
+ :key="subWidget.id || swIdx"
46
46
  :parent-list="tab.widgetList"
47
47
  :index-of-parent-list="swIdx"
48
48
  :parent-widget="widget"
@@ -190,8 +190,9 @@ modules = {
190
190
  getOldFormData: () => e.oldFormData,
191
191
  getCompareRowKey: () => e.compareRowKey,
192
192
  getCompareExcludeTypes: () => e.compareExcludeTypes,
193
- // 字段级排除判定(系统审计字段默认排除 + 业务配置的 compareExcludeFields)
194
- isCompareExcludeField: (name) => e.isCompareExcludeField(name),
193
+ // 字段级排除判定。subFormName 为明细表属性名(keyName),主表字段传空。
194
+ isCompareExcludeField: (name, subFormName) =>
195
+ e.isCompareExcludeField(name, subFormName),
195
196
  };
196
197
  },
197
198
  data() {
@@ -261,8 +262,19 @@ modules = {
261
262
  "vabUpload2",
262
263
  "singleUpload",
263
264
  ],
264
- // 按「字段名(keyName)」不参与对比,可被 enableCompare(options.excludeFields)/formConfig 覆盖追加
265
+ // 主表黑名单:按「字段名(keyName)」不参与对比。只作用于主表字段,不下渗明细
265
266
  compareExcludeFields: [],
267
+ // 明细黑名单:{ 子表keyName: [字段名] },只作用于该明细表
268
+ compareSubExcludeFields: {},
269
+ // 主表白名单——设置后主表只对比这些字段,主表黑名单失效。跨表(场景B)由 compareMapping 自动推导
270
+ compareIncludeFields: null,
271
+ // 明细白名单:{ 子表keyName: [字段名] },设置后该明细表只对比这些字段,该表黑名单失效
272
+ compareSubIncludeFields: {},
273
+ // 跨表(场景B):{ 子表keyName: 行主键字段 },按子表覆盖全局 compareRowKey
274
+ compareSubRowKeys: {},
275
+ // sanitize 阶段由 subSourceFields 回填出来的字段(如 f_source_item_id)。
276
+ // 这些字段原单行里没有,参与对比会被误判为"变更",故自动排除。
277
+ compareAutoExcludeFields: [],
266
278
  showCompareDelList: false,
267
279
  compareDelListOption: {},
268
280
  };
@@ -2149,8 +2161,13 @@ modules = {
2149
2161
  // ===================== 表单对比(变更前/变更后)=====================
2150
2162
  /**
2151
2163
  * 开启对比。可只传主表旧值,也可传"主表+任意数量子表数组"的整单旧值。
2152
- * @param {Object} oldData 旧整单数据(结构与 getFormData 一致)
2153
- * @param {Object} [options] { compareRowKey, excludeTypes }
2164
+ * @param {Object} oldData 旧整单数据(结构与 getFormData 一致;跨表时为归一化后的数据)
2165
+ * @param {Object} [options]
2166
+ * compareRowKey 全局子表行主键(默认 uuid)
2167
+ * subRowKeys { 子表keyName: 行主键字段 } 按子表覆盖(跨表场景由 mapping 自动推导)
2168
+ * excludeTypes 不参与对比的字段类型
2169
+ * excludeFields 不参与对比的字段名(黑名单,同表场景)
2170
+ * includeFields 只参与对比的字段名(白名单,跨表场景由 mapping 自动推导;一旦设置,黑名单失效)
2154
2171
  */
2155
2172
  enableCompare: function (oldData, options) {
2156
2173
  options = options || {};
@@ -2160,16 +2177,35 @@ modules = {
2160
2177
  (this.formConfig && this.formConfig.compareRowKey) ||
2161
2178
  this.compareRowKey;
2162
2179
  this.compareRowKey = rowKey;
2180
+ // 按子表行主键(跨表场景每张明细的业务键不同)
2181
+ this.compareSubRowKeys = options.subRowKeys || {};
2163
2182
  if (Array.isArray(options.excludeTypes)) {
2164
2183
  this.compareExcludeTypes = options.excludeTypes;
2165
2184
  }
2166
- // 字段级排除:优先 options.excludeFields,其次 formConfig.compareExcludeFields
2185
+ // 主表黑名单:优先 options.excludeFields,其次 formConfig.compareExcludeFields
2167
2186
  let excludeFields =
2168
2187
  options.excludeFields ||
2169
2188
  (this.formConfig && this.formConfig.compareExcludeFields);
2170
2189
  if (Array.isArray(excludeFields)) {
2171
2190
  this.compareExcludeFields = excludeFields;
2172
2191
  }
2192
+ // 明细黑名单:{ 子表keyName: [字段名] },只作用于该明细表
2193
+ this.compareSubExcludeFields =
2194
+ options.subExcludeFields ||
2195
+ (this.formConfig && this.formConfig.compareSubExcludeFields) ||
2196
+ {};
2197
+ // 主表白名单:设置后只比白名单内字段,黑名单失效
2198
+ let includeFields =
2199
+ options.includeFields ||
2200
+ (this.formConfig && this.formConfig.compareIncludeFields);
2201
+ this.compareIncludeFields = Array.isArray(includeFields)
2202
+ ? includeFields
2203
+ : null;
2204
+ // 明细白名单:{ 子表keyName: [字段名] },只作用于该明细表
2205
+ this.compareSubIncludeFields =
2206
+ options.subIncludeFields ||
2207
+ (this.formConfig && this.formConfig.compareSubIncludeFields) ||
2208
+ {};
2173
2209
  this.oldFormData = deepClone(oldData);
2174
2210
  this.isCompare = true;
2175
2211
  // 对旧数据中已带的子表数组,逐个做行快照(单包/混用场景)
@@ -2179,6 +2215,13 @@ modules = {
2179
2215
  }
2180
2216
  });
2181
2217
  },
2218
+ /** 取某子表的行匹配主键:按子表配置优先,否则用全局 compareRowKey */
2219
+ getCompareRowKeyOf: function (subFormName) {
2220
+ return (
2221
+ (this.compareSubRowKeys && this.compareSubRowKeys[subFormName]) ||
2222
+ this.compareRowKey
2223
+ );
2224
+ },
2182
2225
  /**
2183
2226
  * 增量设置某子表的旧数据(多包场景,各子表旧数据回包后分别调用)。
2184
2227
  * 可被任意次调用,重复设置以最后一次为准(幂等)。
@@ -2194,7 +2237,7 @@ modules = {
2194
2237
  * 当前子表尚未加载时仅保留旧数据,待其加载后可再次调用本方法补做。
2195
2238
  */
2196
2239
  applyCompareSubSnapshot: function (subFormName) {
2197
- let rowKey = this.compareRowKey;
2240
+ let rowKey = this.getCompareRowKeyOf(subFormName);
2198
2241
  let oldRows = this.oldFormData[subFormName] || [];
2199
2242
  let curRows = this.formDataModel[subFormName];
2200
2243
  if (!Array.isArray(curRows)) return;
@@ -2210,7 +2253,6 @@ modules = {
2210
2253
  },
2211
2254
  /** 关闭对比,复位状态并移除各行 hData */
2212
2255
  disableCompare: function () {
2213
- let rowKey = this.compareRowKey;
2214
2256
  Object.keys(this.oldFormData || {}).forEach((key) => {
2215
2257
  let curRows = this.formDataModel[key];
2216
2258
  if (Array.isArray(curRows)) {
@@ -2223,6 +2265,11 @@ modules = {
2223
2265
  });
2224
2266
  this.isCompare = false;
2225
2267
  this.oldFormData = {};
2268
+ this.compareIncludeFields = null;
2269
+ this.compareSubIncludeFields = {};
2270
+ this.compareSubExcludeFields = {};
2271
+ this.compareSubRowKeys = {};
2272
+ this.compareAutoExcludeFields = [];
2226
2273
  this.showCompareDelList = false;
2227
2274
  this.compareDelListOption = {};
2228
2275
  },
@@ -2291,14 +2338,50 @@ modules = {
2291
2338
  "object_foreign_id",
2292
2339
  ];
2293
2340
  },
2294
- /** 字段是否排除对比:系统默认排除字段 + 业务配置的 compareExcludeFields。 */
2295
- isCompareExcludeField: function (name) {
2341
+ /**
2342
+ * 字段是否排除对比。主表与明细的名单**互不干涉**:
2343
+ * 主表字段(subFormName 为空) 只看 includeFields / excludeFields
2344
+ * 明细字段 只看 subIncludeFields[表] / subExcludeFields[表]
2345
+ * 主表名单不会下渗到明细——两者字段空间不同,同名字段语义也可能不同。
2346
+ * 单表内:配了白名单则只比白名单内字段,该表黑名单失效;否则走系统字段 + 黑名单。
2347
+ * @param {String} name 字段名(keyName)
2348
+ * @param {String} [subFormName] 所在明细表属性名;主表字段不传
2349
+ */
2350
+ isCompareExcludeField: function (name, subFormName) {
2296
2351
  if (!name) return false;
2352
+ // 附件不参与对比:新单附件按白名单精简过(name/url/...),原单附件是完整对象,
2353
+ // 直接比会必然不等。keyNamePrefixMap 里 vabUpload/baseAttachment 前缀固定为 attachments_。
2354
+ if (name.indexOf("attachments_") === 0) return true;
2355
+ // subSourceFields 回填出来的来源引用字段:原单行没有该字段,比较会误判为“变更”
2356
+ if ((this.compareAutoExcludeFields || []).indexOf(name) !== -1) return true;
2357
+ // 取本表(主表 or 该明细表)的名单
2358
+ let include = subFormName
2359
+ ? (this.compareSubIncludeFields || {})[subFormName]
2360
+ : this.compareIncludeFields;
2361
+ let exclude = subFormName
2362
+ ? (this.compareSubExcludeFields || {})[subFormName]
2363
+ : this.compareExcludeFields;
2364
+ // 白名单模式:不在名单内 = 排除
2365
+ if (Array.isArray(include) && include.length) {
2366
+ return include.indexOf(name) === -1;
2367
+ }
2368
+ // 黑名单模式
2297
2369
  if (this.compareSysExcludeFields().indexOf(name) !== -1) return true;
2298
- return (this.compareExcludeFields || []).indexOf(name) !== -1;
2370
+ return Array.isArray(exclude) && exclude.indexOf(name) !== -1;
2371
+ },
2372
+ /**
2373
+ * 该子表是否参与对比。跨表场景下,未在 compareMapping.subs 里映射的子表
2374
+ * 不应参与对比(否则整表行会被判成「新增」而满屏高亮)。
2375
+ * 判据:旧数据里有没有这张子表的数组。
2376
+ */
2377
+ isCompareSubTable: function (subFormName) {
2378
+ return (
2379
+ !!this.oldFormData &&
2380
+ Array.isArray(this.oldFormData[subFormName])
2381
+ );
2299
2382
  },
2300
2383
  /** 行级差异:比较当前行与原单行的标量字段(跳过内部字段、排除字段与附件列) */
2301
- isCompareRowValuesChanged: function (oldRow, row, skipKeys) {
2384
+ isCompareRowValuesChanged: function (oldRow, row, skipKeys, subFormName) {
2302
2385
  let skip = skipKeys || {};
2303
2386
  let keys = {};
2304
2387
  Object.keys(oldRow || {}).forEach((k) => (keys[k] = 1));
@@ -2308,7 +2391,7 @@ modules = {
2308
2391
  k === "hData" ||
2309
2392
  k.charAt(0) === "_" ||
2310
2393
  skip[k] ||
2311
- this.isCompareExcludeField(k)
2394
+ this.isCompareExcludeField(k, subFormName)
2312
2395
  ) {
2313
2396
  return false;
2314
2397
  }
@@ -2321,8 +2404,10 @@ modules = {
2321
2404
  */
2322
2405
  getCompareRowClass: function (subFormName, row) {
2323
2406
  if (!this.isCompare || !row) return "";
2324
- let rowKey = this.compareRowKey;
2325
- let oldRows = (this.oldFormData && this.oldFormData[subFormName]) || [];
2407
+ // 未映射/不参与对比的子表:不高亮(否则整表会被判成新增而满屏绿)
2408
+ if (!this.isCompareSubTable(subFormName)) return "";
2409
+ let rowKey = this.getCompareRowKeyOf(subFormName);
2410
+ let oldRows = this.oldFormData[subFormName] || [];
2326
2411
  let old = oldRows.find((o) => this.matchCompareRowKey(o, row, rowKey));
2327
2412
  if (!old) return "xform-compare-row-new";
2328
2413
  // 附件列不参与对比:从子表实例收集需跳过的附件 prop(系统字段由 isCompareExcludeField 统一处理)
@@ -2331,14 +2416,16 @@ modules = {
2331
2416
  if (inst && typeof inst.getAttachmentType === "function") {
2332
2417
  (inst.getAttachmentType() || []).forEach((p) => (skip[p] = 1));
2333
2418
  }
2334
- return this.isCompareRowValuesChanged(old, row, skip)
2419
+ return this.isCompareRowValuesChanged(old, row, skip, subFormName)
2335
2420
  ? "xform-compare-row-changed"
2336
2421
  : "";
2337
2422
  },
2338
2423
  /** 返回某子表被删除的行(旧有、当前按主键找不到)。实时计算。 */
2339
2424
  getDelData: function (subFormName) {
2340
- let rowKey = this.compareRowKey;
2341
- let oldRows = (this.oldFormData && this.oldFormData[subFormName]) || [];
2425
+ // 未映射/不参与对比的子表:没有"删除行"概念
2426
+ if (!this.isCompareSubTable(subFormName)) return [];
2427
+ let rowKey = this.getCompareRowKeyOf(subFormName);
2428
+ let oldRows = this.oldFormData[subFormName] || [];
2342
2429
  let curRows = this.getSubTableCurrentRows(subFormName);
2343
2430
  return oldRows.filter(
2344
2431
  (o) => !curRows.some((c) => this.matchCompareRowKey(o, c, rowKey))
@@ -2547,16 +2634,29 @@ modules = {
2547
2634
  });
2548
2635
  },
2549
2636
  /**
2550
- * 通用:按「原单模板」自动多包获取原单整单数据(主表 + 所有 data-table 子表),
2551
- * 组装成与 getFormData 一致的对象返回。默认复用当前表单模板(变更单通常与原单
2552
- * 结构一致);跨模板可用 options 覆盖。
2553
- * @param {String|Number} sourceId 原单 id
2554
- * @param {Object} [options] { reportTemplate, formCode, formVersion, entity, masterScriptCode, isLoading }
2637
+ * 通用:按「原单模板」自动多包获取原单整单数据(主表 + 表头附件 + 所有 data-table 子表),
2638
+ * 组装成与 getFormData 一致的对象返回。默认复用当前表单模板;跨模板用 sourceFormCode。
2639
+ *
2640
+ * 取数顺序:**主表先查 用主表返回的真实 id 查明细/附件**。
2641
+ * 因为明细外键 head_table_id 关联的是主表 id,而入参 sourceId 可能是「编码」
2642
+ * (搜索框绑定字段可能存编码),并行发包会导致明细查空。
2643
+ *
2644
+ * @param {String|Number} sourceId 原单主键值(id 或编码)
2645
+ * @param {Object} [options]
2646
+ * sourceFormCode 原单 formCode(跨模板必填)
2647
+ * sourceKeyField 主表包用哪个字段查,默认 'id'
2648
+ * sourceIdField 从主表响应取真实主键的字段名,默认 'id'
2649
+ * sourceIdIsKey true=明确传的就是明细外键主键 → 走并行快路径
2650
+ * subParams { 子表keyName: 对象 | (ctx)=>对象 } 明细额外查询参数(ctx.masterData)
2651
+ * commonParams 所有明细包都带的公共参数
2652
+ * reportTemplate/formCode/formVersion/entity/masterScriptCode/isLoading
2555
2653
  * @returns {Promise<Object>} 原单整单数据
2556
2654
  */
2557
2655
  loadOldData: function (sourceId, options) {
2558
2656
  options = options || {};
2559
2657
  let isLoading = options.isLoading !== false;
2658
+ let sourceKeyField = options.sourceKeyField || "id";
2659
+ let sourceIdField = options.sourceIdField || "id";
2560
2660
  // 解析「原单模板」:优先按 sourceFormCode 拉原单模板;否则复用当前模板
2561
2661
  let resolveTpl = options.sourceFormCode
2562
2662
  ? this.fetchTemplateByFormCode(options.sourceFormCode)
@@ -2574,94 +2674,224 @@ modules = {
2574
2674
  let masterScript =
2575
2675
  options.masterScriptCode || tplFormConfig.formScriptCode || "getOne";
2576
2676
  let result = {};
2577
- let tasks = [];
2578
- // 主表(含 sub-form/table2 内嵌数组)
2579
- tasks.push(
2580
- this.formHttp({
2581
- vue: this,
2582
- scriptCode: masterScript,
2583
- data: {
2584
- formCode: formCode,
2585
- formVersion: formVersion,
2586
- taBm: entity,
2587
- data: { id: sourceId },
2588
- },
2589
- isLoading: isLoading,
2590
- }).then((res) => {
2591
- Object.assign(result, (res && res.objx) || {});
2592
- })
2593
- );
2594
- // 表头附件:每个附件字段按 taBm=keyName 独立发 getList 包(getOne 不返回表头附件)
2595
- this.getHeaderAttachmentFields(tpl.widgetList).forEach((att) => {
2596
- tasks.push(
2597
- this.formHttp({
2598
- vue: this,
2599
- scriptCode: att.scriptCode,
2600
- data: {
2601
- formCode: formCode,
2602
- formVersion: formVersion,
2603
- taBm: att.name,
2604
- data: { id: sourceId },
2605
- },
2606
- isLoading: isLoading,
2607
- }).then((res) => {
2608
- let o = res && res.objx;
2609
- result[att.name] = (o && o.records) || o || [];
2610
- })
2611
- );
2677
+
2678
+ // ---- 1) 主表包(含 sub-form/table2 内嵌数组)----
2679
+ let masterData = {};
2680
+ masterData[sourceKeyField] = sourceId;
2681
+ let masterTask = this.formHttp({
2682
+ vue: this,
2683
+ scriptCode: masterScript,
2684
+ data: {
2685
+ formCode: formCode,
2686
+ formVersion: formVersion,
2687
+ taBm: entity,
2688
+ data: masterData,
2689
+ },
2690
+ isLoading: isLoading,
2691
+ }).then((res) => {
2692
+ let objx = (res && res.objx) || {};
2693
+ Object.assign(result, objx);
2694
+ return objx;
2612
2695
  });
2613
- // 各 data-table 子表独立发包(按原单模板枚举),附件/三级表列随包上送
2614
- this.getCompareSubTablesFrom(tpl.widgetList).forEach((sub) => {
2615
- let subData = { id: sourceId };
2616
- if (sub.attachmentType && sub.attachmentType.length) {
2617
- subData.attachmentType = sub.attachmentType;
2618
- }
2619
- if (sub.thirdtabm && sub.thirdtabm.length) {
2620
- subData.thirdtabm = sub.thirdtabm;
2696
+
2697
+ // ---- 2) 用主表返回的真实主键发明细/附件包 ----
2698
+ let loadDetails = (master) => {
2699
+ // sourceIdIsKey:明确传的就是明细外键主键 直接用入参(并行快路径已在下方处理)
2700
+ let masterId = options.sourceIdIsKey
2701
+ ? sourceId
2702
+ : master && master[sourceIdField];
2703
+ if (masterId === null || masterId === undefined || masterId === "") {
2704
+ // 主表没查到或取不到主键,明细无从查起
2705
+ return Promise.resolve(result);
2621
2706
  }
2622
- tasks.push(
2623
- this.formHttp({
2624
- vue: this,
2625
- scriptCode: sub.scriptCode,
2626
- data: {
2627
- formCode: formCode,
2628
- formVersion: formVersion,
2629
- taBm: sub.name,
2630
- data: subData,
2631
- },
2632
- isLoading: isLoading,
2633
- }).then((res) => {
2634
- let o = res && res.objx;
2635
- result[sub.name] = (o && o.records) || o || [];
2636
- })
2637
- );
2707
+ let ctx = { masterData: master || {}, masterId: masterId };
2708
+ let tasks = [];
2709
+
2710
+ // 表头附件:每个附件字段按 taBm=keyName 独立发 getList 包(getOne 不返回表头附件)
2711
+ this.getHeaderAttachmentFields(tpl.widgetList).forEach((att) => {
2712
+ tasks.push(
2713
+ this.formHttp({
2714
+ vue: this,
2715
+ scriptCode: att.scriptCode,
2716
+ data: {
2717
+ formCode: formCode,
2718
+ formVersion: formVersion,
2719
+ taBm: att.name,
2720
+ data: { id: masterId },
2721
+ },
2722
+ isLoading: isLoading,
2723
+ }).then((res) => {
2724
+ let o = res && res.objx;
2725
+ result[att.name] = (o && o.records) || o || [];
2726
+ })
2727
+ );
2728
+ });
2729
+
2730
+ // 各 data-table 子表独立发包(按原单模板枚举),附件/三级表列 + 额外参数随包上送
2731
+ this.getCompareSubTablesFrom(tpl.widgetList).forEach((sub) => {
2732
+ let subData = { id: masterId };
2733
+ if (sub.attachmentType && sub.attachmentType.length) {
2734
+ subData.attachmentType = sub.attachmentType;
2735
+ }
2736
+ if (sub.thirdtabm && sub.thirdtabm.length) {
2737
+ subData.thirdtabm = sub.thirdtabm;
2738
+ }
2739
+ // 额外查询参数:公共 + 按子表配置(对象或函数,函数可读主表数据)
2740
+ Object.assign(subData, options.commonParams || {});
2741
+ let sp = (options.subParams || {})[sub.name];
2742
+ if (typeof sp === "function") sp = sp(ctx);
2743
+ Object.assign(subData, sp || {});
2744
+
2745
+ tasks.push(
2746
+ this.formHttp({
2747
+ vue: this,
2748
+ scriptCode: sub.scriptCode,
2749
+ data: {
2750
+ formCode: formCode,
2751
+ formVersion: formVersion,
2752
+ taBm: sub.name,
2753
+ data: subData,
2754
+ },
2755
+ isLoading: isLoading,
2756
+ }).then((res) => {
2757
+ let o = res && res.objx;
2758
+ result[sub.name] = (o && o.records) || o || [];
2759
+ })
2760
+ );
2761
+ });
2762
+ return Promise.all(tasks).then(() => result);
2763
+ };
2764
+
2765
+ // sourceIdIsKey:入参即明细外键主键 → 主表与明细并行(快路径)
2766
+ if (options.sourceIdIsKey) {
2767
+ return Promise.all([masterTask, loadDetails(null)]).then(() => result);
2768
+ }
2769
+ // 默认:主表先查 → 拿真实主键 → 再查明细(兼容编码/ID)
2770
+ return masterTask.then(loadDetails);
2771
+ });
2772
+ },
2773
+ /**
2774
+ * 跨单据对比(场景B):把「源单键空间」的数据归一化成「当前表单键空间」。
2775
+ * 归一化后,现有 diff / hData / 行级高亮 / 删除行逻辑无需改动(它们只认当前表单键名)。
2776
+ *
2777
+ * compareMapping = {
2778
+ * master: [{ from:'源字段', to:'目标字段' }, ...],
2779
+ * subs: [{ from:'源子表keyName', to:'目标子表keyName',
2780
+ * rowKey: { from:'源业务键', to:'目标业务键' },
2781
+ * fields: [{ from:'源列', to:'目标列' }, ...] }, ...]
2782
+ * }
2783
+ * 行匹配技巧:把源行的业务键值写到「目标业务键字段」上,再把该子表 compareRowKey 设为
2784
+ * 目标业务键字段 —— matchCompareRowKey 即可直接匹配,无需改动。
2785
+ *
2786
+ * @returns {Object} { data: 归一化后的旧数据, includeFields: 主表白名单,
2787
+ * subIncludeFields: {子表:白名单}, rowKeyMap: {子表:rowKey} }
2788
+ */
2789
+ normalizeBySourceMapping: function (srcData, mapping) {
2790
+ srcData = srcData || {};
2791
+ mapping = mapping || {};
2792
+ let data = {};
2793
+ let includeFields = [];
2794
+ let subIncludeFields = {};
2795
+ let rowKeyMap = {};
2796
+
2797
+ // 主表字段映射
2798
+ (mapping.master || []).forEach((m) => {
2799
+ if (!m || !m.to) return;
2800
+ data[m.to] = srcData[m.from];
2801
+ includeFields.push(m.to);
2802
+ });
2803
+
2804
+ // 明细映射
2805
+ (mapping.subs || []).forEach((s) => {
2806
+ if (!s || !s.from || !s.to) return;
2807
+ let srcRows = srcData[s.from];
2808
+ if (!Array.isArray(srcRows)) srcRows = [];
2809
+ let rk = s.rowKey || {};
2810
+ let rkFrom = rk.from || "id";
2811
+ let rkTo = rk.to || rkFrom;
2812
+ rowKeyMap[s.to] = rkTo;
2813
+ // 该明细表的白名单只含自己映射到的列,不与主表/其他明细混在一起
2814
+ subIncludeFields[s.to] = (s.fields || [])
2815
+ .filter((f) => f && f.to)
2816
+ .map((f) => f.to);
2817
+ data[s.to] = srcRows.map((row) => {
2818
+ let out = {};
2819
+ // 行匹配键:源业务键值 → 目标业务键字段
2820
+ out[rkTo] = row ? row[rkFrom] : null;
2821
+ (s.fields || []).forEach((f) => {
2822
+ if (f && f.to) out[f.to] = row ? row[f.from] : null;
2823
+ });
2824
+ return out;
2638
2825
  });
2639
- return Promise.all(tasks).then(() => result);
2640
2826
  });
2827
+
2828
+ return {
2829
+ data: data,
2830
+ includeFields: includeFields,
2831
+ subIncludeFields: subIncludeFields,
2832
+ rowKeyMap: rowKeyMap,
2833
+ };
2641
2834
  },
2642
2835
  /**
2643
2836
  * 通用:自动获取原单并开启对比(模式 B「对比」按钮一行接入)。
2644
- * @returns {Promise<Object>} 原单数据
2837
+ * 跨表场景传 compareMapping / transformOldData,内部自动归一化。
2838
+ * @returns {Promise<Object>} 归一化后的旧数据(对比基准)
2645
2839
  */
2646
2840
  enableCompareBySource: function (sourceId, options) {
2647
2841
  options = options || {};
2648
- return this.loadOldData(sourceId, options).then((oldData) => {
2842
+ return this.loadOldData(sourceId, options).then((srcData) => {
2843
+ let oldData = this.applyCompareSourceTransform(srcData, options);
2649
2844
  this.enableCompare(oldData, options);
2650
2845
  return oldData;
2651
2846
  });
2652
2847
  },
2848
+ /**
2849
+ * 统一的「源数据 → 当前表单键空间」转换入口:先 compareMapping 归一化,再 transformOldData 钩子。
2850
+ * 未配 mapping 与 钩子时原样返回(同表场景,行为与之前一致)。
2851
+ * 归一化时把白名单与按子表行主键写入 options,供 enableCompare 消费。
2852
+ */
2853
+ applyCompareSourceTransform: function (srcData, options) {
2854
+ options = options || {};
2855
+ let data = srcData;
2856
+ if (options.compareMapping) {
2857
+ let r = this.normalizeBySourceMapping(srcData, options.compareMapping);
2858
+ data = r.data;
2859
+ // 主表白名单:由 mapping.master 的 to 自动推导(业务未显式指定时)
2860
+ if (!options.includeFields) options.includeFields = r.includeFields;
2861
+ // 明细白名单:由 mapping.subs[].fields 的 to 按表自动推导(与业务显式配置合并)
2862
+ options.subIncludeFields = Object.assign(
2863
+ {},
2864
+ r.subIncludeFields,
2865
+ options.subIncludeFields
2866
+ );
2867
+ // 按子表行主键:由 rowKey.to 自动推导(与业务显式配置合并)
2868
+ options.subRowKeys = Object.assign({}, r.rowKeyMap, options.subRowKeys);
2869
+ }
2870
+ if (typeof options.transformOldData === "function") {
2871
+ data = options.transformOldData(data, srcData) || data;
2872
+ }
2873
+ return data;
2874
+ },
2653
2875
  /**
2654
2876
  * 通用:新建变更单时,自动获取原单 → 清洗 → 填充新单 → 开启对比(模式 A 一行接入)。
2655
- * @param {String|Number} sourceId 原单 id
2656
- * @param {Object} [options] loadOldData 的选项 + { sanitize } 传给 sanitizeChangeData
2657
- * @returns {Promise<Object>} 原单数据(对比基准)
2877
+ * @param {String|Number} sourceId 原单主键值(id 或编码)
2878
+ * @param {Object} [options] loadOldData 的选项 + enableCompare 的选项,另加:
2879
+ * sanitize 传给 sanitizeChangeData 的清洗选项
2880
+ * compare 默认 true 自动开启对比;传 false 则只用原单数据填充新单、不开启对比
2881
+ * (可用返回的 oldData 稍后手动 enableCompare(oldData, options))
2882
+ * @returns {Promise<Object>} 归一化后的原单数据(对比基准)
2658
2883
  */
2659
2884
  initChangeFromSource: function (sourceId, options) {
2660
2885
  options = options || {};
2661
- return this.loadOldData(sourceId, options).then((oldData) => {
2886
+ return this.loadOldData(sourceId, options).then((srcData) => {
2887
+ // 跨表:先归一化到当前表单键空间,否则字段名对不上、数据填不进变更单
2888
+ let oldData = this.applyCompareSourceTransform(srcData, options);
2662
2889
  let newData = this.sanitizeChangeData(oldData, options.sanitize || {});
2663
2890
  this.setFormData(newData);
2664
- this.enableCompare(oldData, options);
2891
+ // compare:false → 只用原单数据填充新单,不开启对比(可稍后手动 enableCompare(oldData)
2892
+ if (options.compare !== false) {
2893
+ this.enableCompare(oldData, options);
2894
+ }
2665
2895
  return oldData;
2666
2896
  });
2667
2897
  },
@@ -2795,10 +3025,17 @@ modules = {
2795
3025
  let subSourceFields =
2796
3026
  options.subSourceFields || options.subSourceIdFields || {};
2797
3027
  let header = data;
3028
+ // 回填出来的字段只存在于新单行、原单行没有,必须排除对比,否则整表都会被判成「变更行」。
3029
+ // 函数式配置无法静态解析目标字段,故比对函数执行前后新增的 key 来捕获。
3030
+ let autoExclude = {};
2798
3031
  let applySourceField = (row, cfg) => {
2799
3032
  if (!row || typeof row !== "object" || !cfg) return;
2800
3033
  if (typeof cfg === "function") {
3034
+ let before = Object.keys(row);
2801
3035
  cfg(row, header);
3036
+ Object.keys(row).forEach((k) => {
3037
+ if (before.indexOf(k) === -1) autoExclude[k] = 1;
3038
+ });
2802
3039
  } else if (Array.isArray(cfg)) {
2803
3040
  cfg.forEach((c) => applySourceField(row, c));
2804
3041
  } else if (typeof cfg === "object") {
@@ -2806,9 +3043,11 @@ modules = {
2806
3043
  row[cfg.to] = cfg.fromHeader
2807
3044
  ? header[cfg.fromHeader]
2808
3045
  : row[cfg.from || "id"];
3046
+ autoExclude[cfg.to] = 1;
2809
3047
  }
2810
3048
  } else {
2811
3049
  row[cfg] = row.id; // 字符串简写:id → 该字段
3050
+ autoExclude[cfg] = 1;
2812
3051
  }
2813
3052
  };
2814
3053
  // 所有子表数组
@@ -2840,6 +3079,8 @@ modules = {
2840
3079
  });
2841
3080
  }
2842
3081
  });
3082
+ // 交给 isCompareExcludeField:这些字段不参与行级/字段级差异判定
3083
+ this.compareAutoExcludeFields = Object.keys(autoExclude);
2843
3084
 
2844
3085
  // 「不被原单值覆盖」的字段:保留变更单自身的值,不用原单值。主表与明细表分开指定:
2845
3086
  // defaultFields 主表字段名数组,如 ['f_status', 'f_no']