cloud-web-corejs 1.0.281 → 1.0.283

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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/src/components/excelExport/exportColumnMemory.js +118 -0
  3. package/src/components/excelExport/exportFieldDialog.vue +63 -40
  4. package/src/components/excelExport/exportItemConfigUtil.js +30 -0
  5. package/src/components/excelExport/index.js +30 -13
  6. package/src/components/excelExport/mixins.js +406 -68
  7. package/src/components/excelImport/index.vue +1 -1
  8. package/src/components/excelImport/mixins.js +56 -8
  9. package/src/components/table/index.js +14 -1
  10. package/src/components/table/vxeFilter/mixin.js +28 -18
  11. package/src/components/xform/docs/2026-07 table/344/270/216excelExport/344/277/256/345/244/215/345/217/212/345/257/274/345/207/272/344/274/230/345/214/226.md" +11 -4
  12. package/src/components/xform/docs/2026-09 /346/235/241/344/273/266/345/257/274/345/207/272/345/244/247/346/225/260/346/215/256/351/207/217/345/264/251/346/272/203/346/216/222/346/237/245.md" +389 -0
  13. package/src/components/xform/docs/2026-09 /347/274/226/350/276/221/350/241/250/350/241/214/345/206/205/344/277/235/345/255/230/346/240/241/351/252/214/350/214/203/345/233/264/344/270/216/345/210/227/345/277/205/345/241/253/350/247/243/346/236/220/344/277/256/345/244/215.md" +233 -0
  14. package/src/components/xform/form-designer/form-widget/dialog/importDialogMixin.js +61 -2
  15. package/src/components/xform/form-designer/form-widget/field-widget/select-export-item-button-widget.vue +9 -0
  16. package/src/components/xform/form-designer/setting-panel/property-editor/container-data-table/exportItemColumns-dialog.vue +7 -1
  17. package/src/components/xform/form-designer/setting-panel/property-editor/field-table-export-button/select-export-item-button-editor.vue +51 -0
  18. package/src/components/xform/form-designer/widget-panel/widgetsConfig.js +4 -0
  19. package/src/components/xform/form-render/container-item/data-table-item.vue +7 -4
  20. package/src/components/xform/form-render/container-item/data-table-mixin.js +465 -128
  21. package/src/components/xform/utils/dynamicSchemaUtil.js +24 -0
  22. package/src/components/xform/utils/tableColumnHelper.js +46 -4
  23. package/src/components/xform/utils/textMaskUtil.js +6 -3
  24. package/src/layout/components/TagsView/index.vue +4 -2
  25. package/src/store/modules/tagsView.js +27 -3
  26. package/src/utils/assetUrl.js +19 -0
  27. package/src/utils/importLimit.js +72 -0
  28. package/src/utils/pdfUtil.js +6 -1
  29. package/src/utils/request.js +17 -0
  30. package/src/utils/vab.js +5 -1
@@ -1,3 +1,4 @@
1
+ import Vue from "vue";
1
2
  import emitter from "../../../../components/xform/utils/emitter";
2
3
  import i18n from "../../../../components/xform/utils/i18n";
3
4
  import refMixin from "../../../../components/xform/form-render/refMixin";
@@ -47,6 +48,7 @@ import {
47
48
  getDynamicSchemaOption,
48
49
  isActionColumnFormat,
49
50
  isNonDataExportColumn,
51
+ isBlankExportColumn,
50
52
  normalizeDynamicColumns as normalizeDynamicColumnList,
51
53
  resolveDynamicSchemaResponse,
52
54
  } from "../../utils/dynamicSchemaUtil";
@@ -71,6 +73,22 @@ import {
71
73
  } from "../../utils/treeTableUtil";
72
74
  import { tableTreeMixins } from "../../../../mixins/tableTree/index.js";
73
75
 
76
+ /**
77
+ * 列一直建不出来时,行级 schema 重建的最大重试次数(每次隔一帧)。
78
+ * 见 initFieldSchemaData:那里的 $nextTick 自递归原本没有上限。
79
+ */
80
+ const MAX_SCHEMA_INIT_RETRY = 50;
81
+
82
+ /**
83
+ * 超过多少行就不再预建全量行级 schema,改由 getRowFieldSchema 按需建。
84
+ * 预建是按「行数 × 组件列数」做 deepClone(createRowFieldSchemaMap),万行量级是
85
+ * GB 级内存 —— 与条件导出那次标签页崩溃是同一个根因,只是这里常驻 fieldSchemaMap。
86
+ * 触发口:表单导入一次灌入大批行、树表全量加载(BOM)。
87
+ * 阈值取 1000:列表分页最大档是 500,普通分页列表永远不会进懒建模式,行为零变化。
88
+ * 可由 widget.options.lazyRowSchemaThreshold 覆盖。
89
+ */
90
+ const LAZY_ROW_SCHEMA_THRESHOLD = 1000;
91
+
74
92
  let modules = {};
75
93
  const baseRefUtil = {
76
94
  emitter,
@@ -299,6 +317,74 @@ modules = {
299
317
  }
300
318
  return widget.options.labelKey || "label";
301
319
  },
320
+ /** @returns {Number} 超过多少行改走行级 schema 懒建。 */
321
+ getLazyRowSchemaThreshold() {
322
+ let value = Number(this.widget?.options?.lazyRowSchemaThreshold);
323
+ if (isFinite(value) && value > 0) return value;
324
+ return LAZY_ROW_SCHEMA_THRESHOLD;
325
+ },
326
+ /**
327
+ * 行级 schema 的统一读取口:预建模式下就是 fieldSchemaMap 里的那份,懒建模式下
328
+ * (行数超阈值,见 LAZY_ROW_SCHEMA_THRESHOLD)按需创建,只有真正渲染/校验到的行
329
+ * 才付克隆代价 —— vxe 虚拟滚动下这远小于全量。
330
+ *
331
+ * 懒建刻意**不用 $set**:本方法会在渲染期被模板调用(#widget / #widgetList /
332
+ * #editWidget 都要读行级 schema),给被 render 依赖的 fieldSchemaMap 新增响应式
333
+ * 属性会当场触发重渲染,形成 "infinite update loop"。改为 Vue.observable 让 schema
334
+ * 对象自身保持响应(行内脚本 setDisabled / setOptionItems 仍能刷新视图),再普通
335
+ * 赋值挂进 map;同一行同一次渲染内多次读取拿到的是同一个对象,不会对不上。
336
+ * @param {Object} row 行数据。
337
+ * @returns {Object|null}
338
+ */
339
+ getRowFieldSchema(row) {
340
+ let rowKey = row && row._X_ROW_KEY;
341
+ if (!rowKey) return null;
342
+ // 导出行有自己的惰性克隆通道(getExportRowWidget),不进 fieldSchemaMap
343
+ if (row._X_EXPORT_ROW) return null;
344
+ let rowSchema = this.fieldSchemaMap[rowKey];
345
+ if (rowSchema) return rowSchema;
346
+ if (!this.getTableFullColumns().length) return null;
347
+ rowSchema = Vue.observable(this.createRowFieldSchemaMap());
348
+ this.fieldSchemaMap[rowKey] = rowSchema;
349
+ return rowSchema;
350
+ },
351
+ /**
352
+ * 导出行的行级组件 schema:按需惰性克隆,缓存挂在行对象上、随行一起丢弃。
353
+ *
354
+ * 导出行不进 fieldSchemaMap(见 initExportRows),但**不能**直接把列模板交出去:
355
+ * 仍会逐单元格挂载的那几类列(widgetRender 自定义组件列、Image2 但本次没开
356
+ * showImageAtTable 的附件列)在挂载时会跑用户脚本,而 setDisabled / setHidden /
357
+ * setOptionItems 这些 API 是直接写 field.options 的 —— 交出模板就等于让脚本改到
358
+ * 列模板本身,而 createRowFieldSchemaMap 又是从该模板克隆各行 schema,污染会一直
359
+ * 留到重新加载表单配置为止。
360
+ *
361
+ * 必须**记忆化**:`#widget` 插槽一个单元格里会多次调用取值(v-if / :field / :key /
362
+ * 字段名),每次返回新对象会让这几处互相对不上。
363
+ *
364
+ * 代价只落在真正会挂载的少数列上,不会退回按「行数 × 列数」克隆。
365
+ * @param {Object} row 导出行。
366
+ * @param {Object} widget 列模板组件 schema。
367
+ * @returns {Object|null}
368
+ */
369
+ getExportRowWidget(row, widget) {
370
+ if (!widget || !widget.id) return widget || null;
371
+ let cache = row.__X_EXPORT_WIDGETS;
372
+ if (!cache) {
373
+ cache = {};
374
+ // 不可枚举:避免混进行数据被序列化或遍历到
375
+ Object.defineProperty(row, "__X_EXPORT_WIDGETS", {
376
+ value: cache,
377
+ enumerable: false,
378
+ configurable: true,
379
+ writable: true,
380
+ });
381
+ }
382
+ // 用原模板 id 做键:克隆体自己的 id 是新生成的,不能拿来查
383
+ if (!cache[widget.id]) {
384
+ cache[widget.id] = this.cloneFieldSchema(widget);
385
+ }
386
+ return cache[widget.id];
387
+ },
302
388
  /**
303
389
  * 获取当前行独立的列组件 schema;缺失时按列模板即时创建。
304
390
  * @param {Object} obj VXE 单元格参数。
@@ -310,11 +396,17 @@ modules = {
310
396
  if (!rowKey || !widget?.id) {
311
397
  return null;
312
398
  }
313
- let rowSchema = this.fieldSchemaMap[rowKey];
314
- if (!rowSchema) {
315
- rowSchema = this.createRowFieldSchemaMap();
316
- this.$set(this.fieldSchemaMap, rowKey, rowSchema);
317
- } else if (!rowSchema[widget.id]) {
399
+ // 导出行不进 fieldSchemaMap(见 initExportRows)。这里必须短路:取值兜底渲染会
400
+ // 走到本方法,缺 schema 时下面那段会按行懒建一整套再 $set 进 fieldSchemaMap,把
401
+ // initExportRows 省下的开销原样加回来,且因为走 $set 还会被深度观测,比原来更贵。
402
+ // 但也不能直接交出列模板——挂载时跑的用户脚本会写进 field.options,
403
+ // 详见 getExportRowWidget。
404
+ if (obj.row._X_EXPORT_ROW) {
405
+ return this.getExportRowWidget(obj.row, widget);
406
+ }
407
+ let rowSchema = this.getRowFieldSchema(obj.row);
408
+ if (!rowSchema) return null;
409
+ if (!rowSchema[widget.id]) {
318
410
  this.$set(rowSchema, widget.id, this.cloneFieldSchema(widget));
319
411
  }
320
412
  return rowSchema[widget.id] || null;
@@ -594,7 +686,14 @@ modules = {
594
686
  let row = rowParam.row;
595
687
  let params = rowParam.column.params;
596
688
  let widgetId = fieldWidget ? fieldWidget.id : params.widget.id;
597
- let widget = this.fieldSchemaMap[row._X_ROW_KEY][widgetId];
689
+ // 导出行不进 fieldSchemaMap,不短路会在 fieldSchemaMap[undefined] 上取值直接崩。
690
+ // 同样走惰性克隆而不是直接交模板,理由见 getExportRowWidget。
691
+ if (row._X_EXPORT_ROW) {
692
+ return this.getExportRowWidget(row, fieldWidget || params.widget);
693
+ }
694
+ // 懒建模式下这一行可能还没建过 schema,直接下标取值会 TypeError
695
+ let rowSchema = this.getRowFieldSchema(row);
696
+ let widget = rowSchema ? rowSchema[widgetId] : null;
598
697
  return widget;
599
698
  },
600
699
  /** @param {String} name 组件名。@param {Function} callback 每行回调。 */
@@ -918,7 +1017,7 @@ modules = {
918
1017
  if (!row || !templateWidget) {
919
1018
  return templateWidget;
920
1019
  }
921
- const schema = this.fieldSchemaMap[row._X_ROW_KEY];
1020
+ const schema = this.getRowFieldSchema(row);
922
1021
  if (schema && schema[templateWidget.id]) {
923
1022
  return schema[templateWidget.id];
924
1023
  }
@@ -952,6 +1051,16 @@ modules = {
952
1051
  return null;
953
1052
  }
954
1053
 
1054
+ return this.runCellsRequiredCheck(rows, rows);
1055
+ },
1056
+ /**
1057
+ * 逐单元格必填校验的核心循环。
1058
+ * @param {Array} targetRows 本次要校验的行。
1059
+ * @param {Array} indexRows 求行下标用的完整数据集(决定校验 prop 里的行下标)。
1060
+ * @returns {String|null} 首个错误提示,全部通过返回 null。
1061
+ */
1062
+ runCellsRequiredCheck(targetRows, indexRows) {
1063
+ const rows = indexRows || [];
955
1064
  const isEditTable = this.widget.options.isEditTable || false;
956
1065
  const tableColumns = this.widget.options.tableColumns || [];
957
1066
  const tableFieldKeyName = this.getFieldKeyName(this.widget);
@@ -961,8 +1070,8 @@ modules = {
961
1070
  this.collectColumnValidateEntries(column, isEditTable, columnEntries);
962
1071
  });
963
1072
 
964
- for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
965
- const row = rows[rowIndex];
1073
+ for (let rowIndex = 0; rowIndex < targetRows.length; rowIndex++) {
1074
+ const row = targetRows[rowIndex];
966
1075
  if (!row) {
967
1076
  continue;
968
1077
  }
@@ -1013,6 +1122,87 @@ modules = {
1013
1122
 
1014
1123
  return null;
1015
1124
  },
1125
+ /**
1126
+ * 行内保存用:只校验当前行的必填,不牵动其他行。
1127
+ * @param {Object} row 当前行。@returns {String|null} 错误提示,通过为 null。
1128
+ */
1129
+ validateEditRowRequired(row) {
1130
+ if (!row || this.widget.options.hidden || this.designState) {
1131
+ return null;
1132
+ }
1133
+ const rows = this.getValidationRows();
1134
+ return this.runCellsRequiredCheck([row], rows.length ? rows : [row]);
1135
+ },
1136
+ /**
1137
+ * 收集当前行所有参与校验的单元格 prop(用于原生规则的行级校验)。
1138
+ * @param {Object} row 当前行。@returns {Array<String>} 已在表单里注册的 prop 列表。
1139
+ */
1140
+ collectRowValidateProps(row) {
1141
+ const formRef = this.getFormRef?.();
1142
+ const renderForm = formRef?.$refs?.renderForm;
1143
+ if (!row || !renderForm) {
1144
+ return [];
1145
+ }
1146
+ const isEditTable = this.widget.options.isEditTable || false;
1147
+ const tableColumns = this.widget.options.tableColumns || [];
1148
+ const tableFieldKeyName = this.getFieldKeyName(this.widget);
1149
+ const rows = this.getValidationRows();
1150
+ const rowIndex = this.getValidationRowIndex(row, rows, 0);
1151
+ const columnEntries = [];
1152
+ this.loodHandleColumns(tableColumns, (column) => {
1153
+ this.collectColumnValidateEntries(column, isEditTable, columnEntries);
1154
+ });
1155
+
1156
+ // 只保留真正注册进 el-form 的 prop:validateField 传入全部未注册的 prop 会打警告
1157
+ const registered = new Set(
1158
+ (renderForm.fields || []).map((item) => item.prop)
1159
+ );
1160
+ const props = [];
1161
+ columnEntries.forEach(({ column, templateWidget }) => {
1162
+ const fieldWidget = this.resolveRowFieldWidget(row, templateWidget);
1163
+ if (shouldSkipCellValidation(fieldWidget)) {
1164
+ return;
1165
+ }
1166
+ const property = getCellFieldKey(fieldWidget, column);
1167
+ const propName = buildTableCellFormProp(
1168
+ tableFieldKeyName,
1169
+ rowIndex,
1170
+ property
1171
+ );
1172
+ if (propName && registered.has(propName) && !props.includes(propName)) {
1173
+ props.push(propName);
1174
+ }
1175
+ });
1176
+ return props;
1177
+ },
1178
+ /**
1179
+ * 行内保存的校验入口:范围收敛到当前行(原生规则 + 必填兜底),
1180
+ * 不校验其他行、也不校验表格外的表单字段。
1181
+ * @param {Object} row 当前行。@returns {Boolean} 通过为 true。
1182
+ */
1183
+ validateEditRow(row) {
1184
+ const formRef = this.getFormRef?.();
1185
+ const renderForm = formRef?.$refs?.renderForm;
1186
+ const props = this.collectRowValidateProps(row);
1187
+ let nativeError = null;
1188
+ if (renderForm && props.length) {
1189
+ renderForm.validateField(props, (errorMsg) => {
1190
+ if (!nativeError && errorMsg) {
1191
+ nativeError = errorMsg;
1192
+ }
1193
+ });
1194
+ }
1195
+ if (nativeError) {
1196
+ this.$message.error(nativeError);
1197
+ return false;
1198
+ }
1199
+ const requiredError = this.validateEditRowRequired(row);
1200
+ if (requiredError) {
1201
+ this.$message.error(requiredError);
1202
+ return false;
1203
+ }
1204
+ return true;
1205
+ },
1016
1206
  /** @param {Boolean} required 是否将整个数据表设为必填。 */
1017
1207
  setRequired(required) {
1018
1208
  this.$set(this.widget.options, "required", !!required);
@@ -1761,6 +1951,11 @@ modules = {
1761
1951
  t.type || t.widget?.type,
1762
1952
  t.widget?.options || t.options
1763
1953
  ),
1954
+ exportBlank: isBlankExportColumn(
1955
+ t.formatS,
1956
+ t.type || t.widget?.type,
1957
+ t.exportType
1958
+ ),
1764
1959
  },
1765
1960
  visible: this.getColumnVisible(t),
1766
1961
  slots: {},
@@ -1848,6 +2043,12 @@ modules = {
1848
2043
  "textarea",
1849
2044
  "a-link",
1850
2045
  "a-text",
2046
+ // 脚本输入框:展示值就是 fieldModel,不做任何转换,走纯函数完全等价。
2047
+ // 不走这里会落到 getCellValue 的 vNode 挂载兜底,而它内部是**异步组件**
2048
+ // code-editor —— 同步挂载后立刻爬 DOM 根本读不到内容(异步组件那一刻还没
2049
+ // 解析完),所以这类列此前导出为空;顺带每个单元格挂一个代码编辑器,是所有
2050
+ // 类型里最贵的一种。
2051
+ "script-input",
1851
2052
  ];
1852
2053
  if (widget && types.includes(widget.type)) {
1853
2054
  const func = (obj) => {
@@ -1866,9 +2067,14 @@ modules = {
1866
2067
  if (fieldWidget && !fieldWidget.options.hidden) {
1867
2068
  let widgetType = fieldWidget.type;
1868
2069
  if (
1869
- ["input", "number", "textarea", "a-link", "a-text"].includes(
1870
- widgetType
1871
- )
2070
+ [
2071
+ "input",
2072
+ "number",
2073
+ "textarea",
2074
+ "a-link",
2075
+ "a-text",
2076
+ "script-input",
2077
+ ].includes(widgetType)
1872
2078
  ) {
1873
2079
  return value;
1874
2080
  }
@@ -2442,76 +2648,7 @@ modules = {
2442
2648
  };
2443
2649
  }
2444
2650
 
2445
- let exportItemConfig = null;
2446
- if (
2447
- this.widget.options.exportItemColumns &&
2448
- this.widget.options.exportItemColumns.length > 0
2449
- ) {
2450
- let formatKeys = [
2451
- "d1",
2452
- "d2",
2453
- "d3",
2454
- "d4",
2455
- "d5",
2456
- "n1",
2457
- "n2",
2458
- "n3",
2459
- "n4",
2460
- "n5",
2461
- "n6",
2462
- "n7",
2463
- ];
2464
- let exportItemColumns = this.widget.options.exportItemColumns.map(
2465
- (item) => {
2466
- let columnSlots = null;
2467
- let columnParams = {};
2468
- if (item.formatS === "render") {
2469
- let r = item.render
2470
- ? new Function("params", "h", item.render)
2471
- : null;
2472
- columnSlots = {
2473
- default: (params, h) => {
2474
- return r ? r.call(this, params, h) : "";
2475
- },
2476
- };
2477
- } else if (formatKeys.includes(item.formatS)) {
2478
- columnSlots = {
2479
- default: (params, h) => {
2480
- let cellValue = params.row[params.column.field];
2481
- return this.getFormatterValue(
2482
- cellValue,
2483
- item.formatS,
2484
- item.utcTransformEnabled
2485
- );
2486
- },
2487
- };
2488
- }
2489
- if (item.exportType) {
2490
- columnParams.exportType = item.exportType;
2491
- }
2492
- return {
2493
- id: item.id,
2494
- title: item.title,
2495
- field: item.field,
2496
- params: columnParams,
2497
- slots: columnSlots,
2498
- };
2499
- }
2500
- );
2501
- exportItemConfig = {
2502
- scriptCode:
2503
- this.widget.options.exportItemScriptCode ||
2504
- this.widget.options.formScriptCode,
2505
- columns: exportItemColumns,
2506
- param: () => {
2507
- if (this.widget.options.exportItemParam) {
2508
- return this.handleCustomParam(
2509
- this.widget.options.exportItemParam
2510
- );
2511
- }
2512
- },
2513
- };
2514
- }
2651
+ let exportItemConfig = this.buildExportItemConfig();
2515
2652
  let rowConfig = {};
2516
2653
  if (this.widget.options.tableRowHeight) {
2517
2654
  rowConfig.height = this.widget.options.tableRowHeight;
@@ -2594,6 +2731,9 @@ modules = {
2594
2731
  let customParam = {
2595
2732
  config: {
2596
2733
  modal: false,
2734
+ // 导出取消时要能掐断在途取数请求。令牌由 excelExport 生成后随导出参数
2735
+ // 带过来,这里塞进 config,formHttp 的 ...opts 会透传给 axios。
2736
+ cancelToken: options?.cancelToken,
2597
2737
  },
2598
2738
  export: true,
2599
2739
  exportParam: options,
@@ -2710,8 +2850,8 @@ modules = {
2710
2850
  : [];
2711
2851
  // that.handleNullValue(rows);
2712
2852
  if (customParam?.export) {
2713
- //导出
2714
- this.initExportFieldSchemaData(rows);
2853
+ //导出:只补行主键+标记,不建行级 schema(大数据量会 OOM)
2854
+ this.initExportRows(rows);
2715
2855
  } else {
2716
2856
  // VXE 与表单必须共用同一批标准化行,否则 _X_ROW_KEY 不一致,
2717
2857
  // 行虽能进入编辑态,但 editWidget 无法取得对应的行级 schema。
@@ -2724,7 +2864,7 @@ modules = {
2724
2864
  }
2725
2865
 
2726
2866
  resolve(res);
2727
- if (res.type === "success") {
2867
+ if (that.shouldEmitQueryLoaded(res, customParam)) {
2728
2868
  if (that.widget.options.isTreeTable) {
2729
2869
  if (rows.length > 0) {
2730
2870
  let fullAllDataRowMap =
@@ -2790,18 +2930,7 @@ modules = {
2790
2930
  }
2791
2931
  }
2792
2932
  }
2793
- that.$nextTick(() => {
2794
- setTimeout(function () {
2795
- dataTableConfig.callback &&
2796
- dataTableConfig.callback(rows);
2797
- tableOption.callback && tableOption.callback(rows);
2798
- that.handleCustomEvent(
2799
- that.widget.options.formScriptCallback,
2800
- ["rows"],
2801
- [rows]
2802
- );
2803
- }, 0);
2804
- });
2933
+ that.emitTableDataLoaded(rows, tableOption);
2805
2934
  }
2806
2935
  };
2807
2936
  this.loadDefaultQueryList(reqData, done, customParam);
@@ -3209,6 +3338,53 @@ modules = {
3209
3338
  let scriptCode = this.widget.options.formScriptCode || defaultScriptCode;
3210
3339
  return scriptCode;
3211
3340
  },
3341
+ /**
3342
+ * 本次取数要不要跑「屏幕上那张表格」的成功副作用(树表展开、宿主回调、
3343
+ * tableOption.callback 回写表单模型、formScriptCallback)。
3344
+ *
3345
+ * 导出取数复用的是普通查询这条 proxyConfig.ajax.query,此前只在「建不建行级
3346
+ * schema」那一处分了叉,成功副作用整段照跑,而它们对导出**零意义**:数据在
3347
+ * `resolve(res)` 时就已经交给 excelExport 了,回调还都排在 $nextTick +
3348
+ * setTimeout 之后,改什么也影响不到导出内容。跑了反而有害:
3349
+ * 1. `formScriptCallback`(用户配的「数据加载完成」脚本)会按导出页数被连着触发
3350
+ * N 次,`rows` 是屏幕上根本看不见的数据;脚本里发的请求还会跟导出取数抢连接;
3351
+ * 2. `tableOption.callback` 是 `formModel[fieldKeyName] = rows`,会把表单模型覆盖
3352
+ * 成导出的最后一页 —— 导出之后 `getValue()` 返回的不再是当前页,且页面上看不
3353
+ * 出任何异常,直到下次查询才被冲掉;
3354
+ * 3. 树表分支会对没进表格的导出行 `setTreeExpand`、去 `fullAllDataRowMap` 里查它们,
3355
+ * 中途还反复开关 `$grid.treeConfig.lazy`,并发几页时互相打架。
3356
+ *
3357
+ * `customParam.export` 由 exportAjax 统一置位,覆盖条件导出与明细的选择/条件导出
3358
+ * 三条会发请求的通道(勾选导出/当前页导出不走这里)。
3359
+ * @param {Object} res 取数响应。
3360
+ * @param {Object} [customParam] 调用方附加参数。
3361
+ * @returns {Boolean}
3362
+ */
3363
+ shouldEmitQueryLoaded(res, customParam) {
3364
+ if (res?.type !== "success") return false;
3365
+ return !customParam?.export;
3366
+ },
3367
+ /**
3368
+ * 派发「表格数据加载完成」的三个回调:宿主注入的 callback、tableOption.callback
3369
+ * (回写表单模型),以及用户配的 formScriptCallback。
3370
+ * 是否该派发由 shouldEmitQueryLoaded 判定,这里只负责发。
3371
+ * @param {Array} rows 本次加载的行。
3372
+ * @param {Object} tableOption 表格配置(callback 挂在它上面)。
3373
+ */
3374
+ emitTableDataLoaded(rows, tableOption) {
3375
+ this.$nextTick(() => {
3376
+ setTimeout(() => {
3377
+ let dataTableConfig = this.dataTableConfig || {};
3378
+ dataTableConfig.callback && dataTableConfig.callback(rows);
3379
+ tableOption && tableOption.callback && tableOption.callback(rows);
3380
+ this.handleCustomEvent(
3381
+ this.widget.options.formScriptCallback,
3382
+ ["rows"],
3383
+ [rows]
3384
+ );
3385
+ }, 0);
3386
+ });
3387
+ },
3212
3388
  /**
3213
3389
  * 执行默认列表查询。
3214
3390
  * @param {Object} reqData 查询请求数据。
@@ -3228,7 +3404,9 @@ modules = {
3228
3404
  if (isItemExport(exportType)) {
3229
3405
  let $grid = this.getGridTable();
3230
3406
  let originOption = $grid.params.originOption;
3231
- let exportItemConfig = originOption.exportItemConfig || {};
3407
+ // 导出按钮上单独维护了明细列时,配置随导出参数下发,优先于表格属性那套
3408
+ let btnConfig = customParam?.exportParam?.exportItemConfig;
3409
+ let exportItemConfig = btnConfig || originOption.exportItemConfig || {};
3232
3410
  if (exportItemConfig.scriptCode) {
3233
3411
  scriptCode = exportItemConfig.scriptCode;
3234
3412
  }
@@ -3278,6 +3456,16 @@ modules = {
3278
3456
  };
3279
3457
  requestData = extendDeeply(requestData, accessParam);
3280
3458
 
3459
+ // 导出时间快照按 xform 协议放在**信封层**(与 formCode/taBm 同级),不能留在
3460
+ // data 里 —— 那一层是业务查询条件的地盘,后端按字段名当条件解析。它由
3461
+ // excelExport 的分页调度器塞进取数参数(mixins.js loopToDo 的 aParam),一路
3462
+ // 并进 requestData,到这里统一提上来。只有导出通道会带它。
3463
+ let nowDate;
3464
+ if (customParam?.export && requestData.nowDate !== undefined) {
3465
+ nowDate = requestData.nowDate;
3466
+ delete requestData.nowDate;
3467
+ }
3468
+
3281
3469
  return this.formHttp({
3282
3470
  // url: prefix + "/form_ins/getPage",
3283
3471
  scriptCode: scriptCode,
@@ -3285,6 +3473,7 @@ modules = {
3285
3473
  formCode: formCode,
3286
3474
  formVersion: reportTemplate.formVersion,
3287
3475
  taBm: this.fieldKeyName,
3476
+ ...(nowDate !== undefined ? { nowDate } : {}),
3288
3477
  data: requestData,
3289
3478
  },
3290
3479
  isLoading: false,
@@ -3339,30 +3528,136 @@ modules = {
3339
3528
  let tableRef = this.widget.id;
3340
3529
  return tableRef;
3341
3530
  },
3342
- /** 导出前按脱敏规则剔除列,实现见 textMaskUtil.markMaskedExportColumns。 */
3343
- markMaskedColumnsForExport() {
3531
+ /**
3532
+ * 导出前按脱敏规则剔除列,实现见 textMaskUtil.markMaskedExportColumns。
3533
+ * @param {Array} [exportItemColumns] 本次明细导出实际使用的列;不传则取表格属性上配的那套。
3534
+ */
3535
+ markMaskedColumnsForExport(exportItemColumns) {
3344
3536
  markMaskedExportColumns(
3345
3537
  this.getGridTable(),
3346
3538
  buildTextMaskContext(
3347
3539
  this.getFormRef(),
3348
3540
  this.$store.getters.companyCode
3349
- )
3541
+ ),
3542
+ exportItemColumns
3350
3543
  );
3351
3544
  },
3545
+ /**
3546
+ * 明细导出列配置 → vxe 列:formatS/render 转成渲染插槽,exportType 落到 params。
3547
+ * @param {Array} rawColumns 设计器维护的明细导出列。
3548
+ * @returns {Array} vxe 列定义。
3549
+ */
3550
+ buildExportItemColumns(rawColumns) {
3551
+ let formatKeys = [
3552
+ "d1",
3553
+ "d2",
3554
+ "d3",
3555
+ "d4",
3556
+ "d5",
3557
+ "n1",
3558
+ "n2",
3559
+ "n3",
3560
+ "n4",
3561
+ "n5",
3562
+ "n6",
3563
+ "n7",
3564
+ ];
3565
+ return rawColumns.map((item) => {
3566
+ let columnSlots = null;
3567
+ let columnParams = {};
3568
+ if (item.formatS === "render") {
3569
+ let r = item.render ? new Function("params", "h", item.render) : null;
3570
+ columnSlots = {
3571
+ default: (params, h) => {
3572
+ return r ? r.call(this, params, h) : "";
3573
+ },
3574
+ };
3575
+ } else if (formatKeys.includes(item.formatS)) {
3576
+ columnSlots = {
3577
+ default: (params, h) => {
3578
+ let cellValue = params.row[params.column.field];
3579
+ return this.getFormatterValue(
3580
+ cellValue,
3581
+ item.formatS,
3582
+ item.utcTransformEnabled
3583
+ );
3584
+ },
3585
+ };
3586
+ }
3587
+ if (item.exportType) {
3588
+ columnParams.exportType = item.exportType;
3589
+ }
3590
+ return {
3591
+ id: item.id,
3592
+ title: item.title,
3593
+ field: item.field,
3594
+ params: columnParams,
3595
+ slots: columnSlots,
3596
+ };
3597
+ });
3598
+ },
3599
+ /**
3600
+ * 组装一份明细导出配置。导出列、脚本编码、自定义导出参数三项都可以由导出按钮单独维护,
3601
+ * 各自独立生效:按钮留空的那一项回落到表格属性,三项都留空就是表格属性上原来那套。
3602
+ * @param {Object} [overrides] 按钮级配置。
3603
+ * @param {Array} [overrides.columns] 明细导出列。
3604
+ * @param {String} [overrides.scriptCode] 明细导出脚本编码。
3605
+ * @param {String} [overrides.paramScript] 明细导出查询参数脚本。
3606
+ * @returns {Object|null} 两处都没维护列时返回 null。
3607
+ */
3608
+ buildExportItemConfig(overrides) {
3609
+ let options = this.widget.options;
3610
+ let o = overrides || {};
3611
+ let hasColumns = o.columns && o.columns.length;
3612
+ let columns = hasColumns ? o.columns : options.exportItemColumns;
3613
+ if (!columns || !columns.length) return null;
3614
+ let scriptCode = o.scriptCode || options.exportItemScriptCode;
3615
+ let paramScript = o.paramScript || options.exportItemParam;
3616
+ return {
3617
+ scriptCode: scriptCode || options.formScriptCode,
3618
+ columns: this.buildExportItemColumns(columns),
3619
+ // 取数参数脚本与表格上其它脚本一样走 handleCustomEvent(它负责注入 dataId /
3620
+ // formCode 并统一弹前端脚本异常)。原先写的 handleCustomParam 全库没有定义,
3621
+ // 配了「明细导出查询参数」的表格一点导出就抛 TypeError。
3622
+ param: () => {
3623
+ if (paramScript) {
3624
+ return this.handleCustomEvent(paramScript);
3625
+ }
3626
+ },
3627
+ };
3628
+ },
3352
3629
  /**
3353
3630
  * 使用统一 Excel 导出器导出当前表格。
3354
3631
  * @param {Object} option 导出参数;prefix 为空时自动使用当前服务前缀。
3632
+ * option.exportItemColumns / exportItemScriptCode / exportItemParam 为按钮上维护的
3633
+ * 明细导出列、脚本编码与取数参数脚本,三项各自独立:传了哪项就以哪项为准(同一张表格
3634
+ * 可以挂多个明细导出按钮、各导各自的列、各走各自的脚本),组装成 exportItemConfig 随
3635
+ * 导出参数下发,下游(字段选择弹框 / 分页取数 / 脱敏剔除)一律优先读它,读不到才回落
3636
+ * 表格属性那套。
3637
+ * option.exportButtonName 为导出按钮标识,原样透传给字段选择弹框,用于按按钮分组存放
3638
+ * 勾选记忆(见 excelExport/exportColumnMemory.js)。
3355
3639
  */
3356
3640
  exportData(option) {
3357
- this.markMaskedColumnsForExport();
3641
+ let exportOption = { ...option };
3642
+ let itemColumns = exportOption.exportItemColumns;
3643
+ let itemScriptCode = exportOption.exportItemScriptCode;
3644
+ let itemParam = exportOption.exportItemParam;
3645
+ delete exportOption.exportItemColumns;
3646
+ delete exportOption.exportItemScriptCode;
3647
+ delete exportOption.exportItemParam;
3648
+ let hasOverride = itemColumns?.length || itemScriptCode || itemParam;
3649
+ if (isItemExport(exportOption.type) && hasOverride) {
3650
+ exportOption.exportItemConfig = this.buildExportItemConfig({
3651
+ columns: itemColumns,
3652
+ scriptCode: itemScriptCode,
3653
+ paramScript: itemParam,
3654
+ });
3655
+ }
3656
+ this.markMaskedColumnsForExport(exportOption.exportItemConfig?.columns);
3358
3657
  let tableRef = this.getTableRef();
3359
3658
  let serviceName = this.getFormRef().reportTemplate.serviceName;
3360
- option.prefix = option.prefix || "/" + serviceName;
3361
- let exportTableTarget = this.$refs.exportTable;
3362
- let getExportTableRef = () => {
3363
- return exportTableTarget;
3364
- };
3365
- this.$excelExport({ targetRef: tableRef, ...option });
3659
+ exportOption.prefix = exportOption.prefix || "/" + serviceName;
3660
+ this.$excelExport({ targetRef: tableRef, ...exportOption });
3366
3661
  },
3367
3662
  async deleteRow(row, rowIndex) {
3368
3663
  let isTreeTable = this.widget.options.isTreeTable;
@@ -3574,11 +3869,13 @@ modules = {
3574
3869
  const fieldsToValidate = [];
3575
3870
  let tableColumns = this.widget.options.tableColumns;
3576
3871
  checkRows.forEach((row) => {
3577
- let fieldSchema = this.fieldSchemaMap[row._X_ROW_KEY];
3872
+ let fieldSchema = this.getRowFieldSchema(row) || {};
3578
3873
  tableColumns.forEach((column) => {
3579
3874
  if (column.widget) {
3875
+ // 列未就绪时 getRowFieldSchema 会给 null,取不到行级组件就按"不必填"跳过,
3876
+ // 而不是在这里抛异常把整个校验打断
3580
3877
  let widget = fieldSchema[column.widget.id];
3581
- let required = widget.options.required || false;
3878
+ let required = widget?.options?.required || false;
3582
3879
  if (required) {
3583
3880
  let propName = this.getColumnWidgetProp(widget, row);
3584
3881
  fieldsToValidate.push(propName);
@@ -3587,7 +3884,7 @@ modules = {
3587
3884
  if (column.widgetList) {
3588
3885
  column.widgetList.forEach((itemWidget) => {
3589
3886
  let widget = fieldSchema[itemWidget.id];
3590
- let required = widget.options.required || false;
3887
+ let required = widget?.options?.required || false;
3591
3888
  if (required) {
3592
3889
  let propName = this.getColumnWidgetProp(widget, row);
3593
3890
  fieldsToValidate.push(propName);
@@ -3628,9 +3925,9 @@ modules = {
3628
3925
  let sourceWidgetId = isEdit
3629
3926
  ? obj.column?.params?.editWidget?.id
3630
3927
  : obj.column?.params?.widget?.id;
3631
- if (!sourceWidgetId || !this.fieldSchemaMap[obj.row._X_ROW_KEY])
3632
- return "false";
3633
- let fieldWidget = this.fieldSchemaMap[obj.row._X_ROW_KEY][sourceWidgetId];
3928
+ let rowSchema = this.getRowFieldSchema(obj.row);
3929
+ if (!sourceWidgetId || !rowSchema) return "false";
3930
+ let fieldWidget = rowSchema[sourceWidgetId];
3634
3931
  if (!fieldWidget) {
3635
3932
  return "false";
3636
3933
  }
@@ -4222,14 +4519,9 @@ modules = {
4222
4519
  let logicalRowKey = obj.row?._X_ROW_KEY || null;
4223
4520
  let originalPhysicalId = null;
4224
4521
  try {
4225
- let valid = await new Promise((resolve, reject) => {
4226
- try {
4227
- formRef.validate(resolve);
4228
- } catch (error) {
4229
- reject(error);
4230
- }
4231
- });
4232
- if (!valid) return false;
4522
+ // 行内保存只提交当前行,校验范围同步收敛到当前行:其他行与表外字段
4523
+ // 的必填不该拦住单行保存(整体保存仍走 formRef.validate 的全量校验)
4524
+ if (!this.validateEditRow(obj.row)) return false;
4233
4525
 
4234
4526
  let requestRow = obj.row;
4235
4527
  if (treeContext) {
@@ -5632,8 +5924,9 @@ modules = {
5632
5924
  /**
5633
5925
  * 为每一行创建独立字段 schema,避免组件状态在不同行之间共享。
5634
5926
  * @param {Boolean} initFlag true 时忽略旧 schema 并全部重新创建。
5927
+ * @param {Number} retryCount 列未就绪时已重试的次数,见 MAX_SCHEMA_INIT_RETRY。
5635
5928
  */
5636
- initFieldSchemaData(initFlag) {
5929
+ initFieldSchemaData(initFlag, retryCount = 0) {
5637
5930
  //初始化fieldSchemaData!!!
5638
5931
  /*if (this.widget.type !== 'sub-form') {
5639
5932
  return
@@ -5648,6 +5941,14 @@ modules = {
5648
5941
  if (!this.getTableFullColumns().length) {
5649
5942
  return false;
5650
5943
  }
5944
+ // 大数据量不再预建:按「行数 × 组件列数」深克隆,万行量级直接把标签页撑爆
5945
+ // (与条件导出那次崩溃同源)。交给 getRowFieldSchema 在渲染/校验时按需建。
5946
+ if (rowLength > this.getLazyRowSchemaThreshold()) {
5947
+ // initFlag 表示"整体重建",此时旧 schema 必须丢弃;否则(addRow /
5948
+ // deleteRow)保留已建的那些,避免把行内脚本写进去的状态一起清掉。
5949
+ if (initFlag) this.fieldSchemaMap = {};
5950
+ return true;
5951
+ }
5651
5952
  let fieldSchemaMap = {};
5652
5953
  let fieldSchemaMap0 = this.fieldSchemaMap;
5653
5954
  for (let i = 0; i < rowLength; i++) {
@@ -5656,10 +5957,12 @@ modules = {
5656
5957
  if (initFlag) {
5657
5958
  fieldSchemaMap2 = this.createRowFieldSchemaMap();
5658
5959
  } else {
5659
- let fieldSchemaMap01 = fieldSchemaMap0[rowId];
5660
- fieldSchemaMap2 = fieldSchemaMap01
5661
- ? this.$baseLodash.cloneDeep(fieldSchemaMap01)
5662
- : this.createRowFieldSchemaMap();
5960
+ // 沿用旧行的 schema 对象本身,不再 cloneDeep:该对象本来就是这一行
5961
+ // 独享的(按 rowId 分桶),克隆只会产出语义完全相同的另一份,白占一份
5962
+ // 内存。而本分支的调用方是 addRow / deleteRow —— 每增删一行都把全表深
5963
+ // 克隆一遍是 O(),千行树表上每次增删都要停顿数秒并制造等量垃圾。
5964
+ fieldSchemaMap2
5965
+ = fieldSchemaMap0[rowId] || this.createRowFieldSchemaMap();
5663
5966
  }
5664
5967
  fieldSchemaMap[rowId] = fieldSchemaMap2;
5665
5968
  }
@@ -5667,9 +5970,19 @@ modules = {
5667
5970
  return true;
5668
5971
  };
5669
5972
  if (!build()) {
5973
+ // 列未就绪时隔一帧重试。必须有上限:动态列请求失败 / 列配置为空时列永远不会
5974
+ // 出现,而 rowIdData 非空,这条 $nextTick 自递归就成了每帧一次的死循环,
5975
+ // 页面永久卡死(setValue 侧另有 pendingSchemaInit 挡了一条入口,但
5976
+ // initValue / addRow / deleteRow / 动态列重载都能直接进到这里)。
5977
+ if (retryCount >= MAX_SCHEMA_INIT_RETRY) {
5978
+ console.warn(
5979
+ "[data-table] 表格列始终未就绪,已停止行级 schema 重建重试"
5980
+ );
5981
+ return;
5982
+ }
5670
5983
  this.$nextTick(() => {
5671
5984
  if (this.rowIdData.length) {
5672
- this.initFieldSchemaData(initFlag);
5985
+ this.initFieldSchemaData(initFlag, retryCount + 1);
5673
5986
  }
5674
5987
  });
5675
5988
  }
@@ -5766,6 +6079,30 @@ modules = {
5766
6079
  return newFieldSchema;
5767
6080
  },
5768
6081
  //end1
6082
+ /**
6083
+ * 导出取数的行初始化:只补行主键并打导出标记,**不建行级 schema**。
6084
+ *
6085
+ * 行级 schema 的意义是让同一列的组件在不同行之间互不干扰(行内脚本改 options、
6086
+ * 编辑态等)。导出行既不进 vxe 表格也不会被编辑,createRowFieldSchemaMap 产出的
6087
+ * 只是列模板的等价深克隆(仅 id 不同)—— 异步选项同步写的是列模板本身
6088
+ * (collectColumnWidgetConfigRequests),行内脚本改的是已挂载的组件实例,都不会让
6089
+ * 导出行的 schema 与列模板产生差异。
6090
+ *
6091
+ * 但代价是按「行数 × 组件列数」做 deepClone 并常驻 fieldSchemaMap,而该 map 在导出
6092
+ * 期间没有任何释放路径(只有删行、或下次正常查询走 initFieldSchemaData 整体重建才清),
6093
+ * 10 万行量级直接把标签页撑爆。取值侧对缺失行级 schema 本就有回退列模板的分支
6094
+ * (列 filterVal、getTableColumnWidget、getRowWidget),故跳过构建不改变导出内容。
6095
+ *
6096
+ * 注意:树表懒加载走的仍是 initExportFieldSchemaData —— 那批行是要进表格的真实行。
6097
+ * @param {Array} rows 导出取回的当页数据。
6098
+ */
6099
+ initExportRows(rows) {
6100
+ if (!rows) return;
6101
+ for (let row of rows) {
6102
+ if (!row._X_ROW_KEY) row._X_ROW_KEY = "row_" + generateId();
6103
+ row._X_EXPORT_ROW = true;
6104
+ }
6105
+ },
5769
6106
  /** @param {Array} rows 导出数据。为无行主键的数据创建临时 schema。 */
5770
6107
  initExportFieldSchemaData(rows) {
5771
6108
  if (!rows) return;