cloud-web-corejs 1.0.280 → 1.0.282

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 (23) hide show
  1. package/package.json +1 -1
  2. package/src/components/VabUpload/image-viewer.vue +1 -1
  3. package/src/components/VabUpload/mixins.js +48 -15
  4. package/src/components/VabUpload/previewGroup.js +128 -0
  5. package/src/components/VabUpload/view.vue +1 -1
  6. package/src/components/excelExport/exportColumnMemory.js +118 -0
  7. package/src/components/excelExport/exportFieldDialog.vue +63 -40
  8. package/src/components/excelExport/exportItemConfigUtil.js +30 -0
  9. package/src/components/excelExport/index.js +30 -13
  10. package/src/components/excelExport/mixins.js +406 -68
  11. package/src/components/table/index.js +14 -1
  12. package/src/components/xform/form-designer/form-widget/field-widget/select-export-item-button-widget.vue +9 -0
  13. package/src/components/xform/form-designer/setting-panel/property-editor/container-data-table/exportItemColumns-dialog.vue +7 -1
  14. package/src/components/xform/form-designer/setting-panel/property-editor/field-table-export-button/select-export-item-button-editor.vue +51 -0
  15. package/src/components/xform/form-designer/widget-panel/widgetsConfig.js +4 -0
  16. package/src/components/xform/form-render/container-item/data-table-item.vue +7 -4
  17. package/src/components/xform/form-render/container-item/data-table-mixin.js +369 -118
  18. package/src/components/xform/utils/dynamicSchemaUtil.js +24 -0
  19. package/src/components/xform/utils/textMaskUtil.js +6 -3
  20. package/src/layout/components/TagsView/index.vue +4 -2
  21. package/src/store/modules/tagsView.js +27 -3
  22. package/src/utils/pdfUtil.js +6 -1
  23. package/src/utils/request.js +17 -0
@@ -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
  }
@@ -1761,6 +1860,11 @@ modules = {
1761
1860
  t.type || t.widget?.type,
1762
1861
  t.widget?.options || t.options
1763
1862
  ),
1863
+ exportBlank: isBlankExportColumn(
1864
+ t.formatS,
1865
+ t.type || t.widget?.type,
1866
+ t.exportType
1867
+ ),
1764
1868
  },
1765
1869
  visible: this.getColumnVisible(t),
1766
1870
  slots: {},
@@ -1848,6 +1952,12 @@ modules = {
1848
1952
  "textarea",
1849
1953
  "a-link",
1850
1954
  "a-text",
1955
+ // 脚本输入框:展示值就是 fieldModel,不做任何转换,走纯函数完全等价。
1956
+ // 不走这里会落到 getCellValue 的 vNode 挂载兜底,而它内部是**异步组件**
1957
+ // code-editor —— 同步挂载后立刻爬 DOM 根本读不到内容(异步组件那一刻还没
1958
+ // 解析完),所以这类列此前导出为空;顺带每个单元格挂一个代码编辑器,是所有
1959
+ // 类型里最贵的一种。
1960
+ "script-input",
1851
1961
  ];
1852
1962
  if (widget && types.includes(widget.type)) {
1853
1963
  const func = (obj) => {
@@ -1866,9 +1976,14 @@ modules = {
1866
1976
  if (fieldWidget && !fieldWidget.options.hidden) {
1867
1977
  let widgetType = fieldWidget.type;
1868
1978
  if (
1869
- ["input", "number", "textarea", "a-link", "a-text"].includes(
1870
- widgetType
1871
- )
1979
+ [
1980
+ "input",
1981
+ "number",
1982
+ "textarea",
1983
+ "a-link",
1984
+ "a-text",
1985
+ "script-input",
1986
+ ].includes(widgetType)
1872
1987
  ) {
1873
1988
  return value;
1874
1989
  }
@@ -2442,76 +2557,7 @@ modules = {
2442
2557
  };
2443
2558
  }
2444
2559
 
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
- }
2560
+ let exportItemConfig = this.buildExportItemConfig();
2515
2561
  let rowConfig = {};
2516
2562
  if (this.widget.options.tableRowHeight) {
2517
2563
  rowConfig.height = this.widget.options.tableRowHeight;
@@ -2594,6 +2640,9 @@ modules = {
2594
2640
  let customParam = {
2595
2641
  config: {
2596
2642
  modal: false,
2643
+ // 导出取消时要能掐断在途取数请求。令牌由 excelExport 生成后随导出参数
2644
+ // 带过来,这里塞进 config,formHttp 的 ...opts 会透传给 axios。
2645
+ cancelToken: options?.cancelToken,
2597
2646
  },
2598
2647
  export: true,
2599
2648
  exportParam: options,
@@ -2710,8 +2759,8 @@ modules = {
2710
2759
  : [];
2711
2760
  // that.handleNullValue(rows);
2712
2761
  if (customParam?.export) {
2713
- //导出
2714
- this.initExportFieldSchemaData(rows);
2762
+ //导出:只补行主键+标记,不建行级 schema(大数据量会 OOM)
2763
+ this.initExportRows(rows);
2715
2764
  } else {
2716
2765
  // VXE 与表单必须共用同一批标准化行,否则 _X_ROW_KEY 不一致,
2717
2766
  // 行虽能进入编辑态,但 editWidget 无法取得对应的行级 schema。
@@ -2724,7 +2773,7 @@ modules = {
2724
2773
  }
2725
2774
 
2726
2775
  resolve(res);
2727
- if (res.type === "success") {
2776
+ if (that.shouldEmitQueryLoaded(res, customParam)) {
2728
2777
  if (that.widget.options.isTreeTable) {
2729
2778
  if (rows.length > 0) {
2730
2779
  let fullAllDataRowMap =
@@ -2790,18 +2839,7 @@ modules = {
2790
2839
  }
2791
2840
  }
2792
2841
  }
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
- });
2842
+ that.emitTableDataLoaded(rows, tableOption);
2805
2843
  }
2806
2844
  };
2807
2845
  this.loadDefaultQueryList(reqData, done, customParam);
@@ -3209,6 +3247,53 @@ modules = {
3209
3247
  let scriptCode = this.widget.options.formScriptCode || defaultScriptCode;
3210
3248
  return scriptCode;
3211
3249
  },
3250
+ /**
3251
+ * 本次取数要不要跑「屏幕上那张表格」的成功副作用(树表展开、宿主回调、
3252
+ * tableOption.callback 回写表单模型、formScriptCallback)。
3253
+ *
3254
+ * 导出取数复用的是普通查询这条 proxyConfig.ajax.query,此前只在「建不建行级
3255
+ * schema」那一处分了叉,成功副作用整段照跑,而它们对导出**零意义**:数据在
3256
+ * `resolve(res)` 时就已经交给 excelExport 了,回调还都排在 $nextTick +
3257
+ * setTimeout 之后,改什么也影响不到导出内容。跑了反而有害:
3258
+ * 1. `formScriptCallback`(用户配的「数据加载完成」脚本)会按导出页数被连着触发
3259
+ * N 次,`rows` 是屏幕上根本看不见的数据;脚本里发的请求还会跟导出取数抢连接;
3260
+ * 2. `tableOption.callback` 是 `formModel[fieldKeyName] = rows`,会把表单模型覆盖
3261
+ * 成导出的最后一页 —— 导出之后 `getValue()` 返回的不再是当前页,且页面上看不
3262
+ * 出任何异常,直到下次查询才被冲掉;
3263
+ * 3. 树表分支会对没进表格的导出行 `setTreeExpand`、去 `fullAllDataRowMap` 里查它们,
3264
+ * 中途还反复开关 `$grid.treeConfig.lazy`,并发几页时互相打架。
3265
+ *
3266
+ * `customParam.export` 由 exportAjax 统一置位,覆盖条件导出与明细的选择/条件导出
3267
+ * 三条会发请求的通道(勾选导出/当前页导出不走这里)。
3268
+ * @param {Object} res 取数响应。
3269
+ * @param {Object} [customParam] 调用方附加参数。
3270
+ * @returns {Boolean}
3271
+ */
3272
+ shouldEmitQueryLoaded(res, customParam) {
3273
+ if (res?.type !== "success") return false;
3274
+ return !customParam?.export;
3275
+ },
3276
+ /**
3277
+ * 派发「表格数据加载完成」的三个回调:宿主注入的 callback、tableOption.callback
3278
+ * (回写表单模型),以及用户配的 formScriptCallback。
3279
+ * 是否该派发由 shouldEmitQueryLoaded 判定,这里只负责发。
3280
+ * @param {Array} rows 本次加载的行。
3281
+ * @param {Object} tableOption 表格配置(callback 挂在它上面)。
3282
+ */
3283
+ emitTableDataLoaded(rows, tableOption) {
3284
+ this.$nextTick(() => {
3285
+ setTimeout(() => {
3286
+ let dataTableConfig = this.dataTableConfig || {};
3287
+ dataTableConfig.callback && dataTableConfig.callback(rows);
3288
+ tableOption && tableOption.callback && tableOption.callback(rows);
3289
+ this.handleCustomEvent(
3290
+ this.widget.options.formScriptCallback,
3291
+ ["rows"],
3292
+ [rows]
3293
+ );
3294
+ }, 0);
3295
+ });
3296
+ },
3212
3297
  /**
3213
3298
  * 执行默认列表查询。
3214
3299
  * @param {Object} reqData 查询请求数据。
@@ -3228,7 +3313,9 @@ modules = {
3228
3313
  if (isItemExport(exportType)) {
3229
3314
  let $grid = this.getGridTable();
3230
3315
  let originOption = $grid.params.originOption;
3231
- let exportItemConfig = originOption.exportItemConfig || {};
3316
+ // 导出按钮上单独维护了明细列时,配置随导出参数下发,优先于表格属性那套
3317
+ let btnConfig = customParam?.exportParam?.exportItemConfig;
3318
+ let exportItemConfig = btnConfig || originOption.exportItemConfig || {};
3232
3319
  if (exportItemConfig.scriptCode) {
3233
3320
  scriptCode = exportItemConfig.scriptCode;
3234
3321
  }
@@ -3278,6 +3365,16 @@ modules = {
3278
3365
  };
3279
3366
  requestData = extendDeeply(requestData, accessParam);
3280
3367
 
3368
+ // 导出时间快照按 xform 协议放在**信封层**(与 formCode/taBm 同级),不能留在
3369
+ // data 里 —— 那一层是业务查询条件的地盘,后端按字段名当条件解析。它由
3370
+ // excelExport 的分页调度器塞进取数参数(mixins.js loopToDo 的 aParam),一路
3371
+ // 并进 requestData,到这里统一提上来。只有导出通道会带它。
3372
+ let nowDate;
3373
+ if (customParam?.export && requestData.nowDate !== undefined) {
3374
+ nowDate = requestData.nowDate;
3375
+ delete requestData.nowDate;
3376
+ }
3377
+
3281
3378
  return this.formHttp({
3282
3379
  // url: prefix + "/form_ins/getPage",
3283
3380
  scriptCode: scriptCode,
@@ -3285,6 +3382,7 @@ modules = {
3285
3382
  formCode: formCode,
3286
3383
  formVersion: reportTemplate.formVersion,
3287
3384
  taBm: this.fieldKeyName,
3385
+ ...(nowDate !== undefined ? { nowDate } : {}),
3288
3386
  data: requestData,
3289
3387
  },
3290
3388
  isLoading: false,
@@ -3339,30 +3437,136 @@ modules = {
3339
3437
  let tableRef = this.widget.id;
3340
3438
  return tableRef;
3341
3439
  },
3342
- /** 导出前按脱敏规则剔除列,实现见 textMaskUtil.markMaskedExportColumns。 */
3343
- markMaskedColumnsForExport() {
3440
+ /**
3441
+ * 导出前按脱敏规则剔除列,实现见 textMaskUtil.markMaskedExportColumns。
3442
+ * @param {Array} [exportItemColumns] 本次明细导出实际使用的列;不传则取表格属性上配的那套。
3443
+ */
3444
+ markMaskedColumnsForExport(exportItemColumns) {
3344
3445
  markMaskedExportColumns(
3345
3446
  this.getGridTable(),
3346
3447
  buildTextMaskContext(
3347
3448
  this.getFormRef(),
3348
3449
  this.$store.getters.companyCode
3349
- )
3450
+ ),
3451
+ exportItemColumns
3350
3452
  );
3351
3453
  },
3454
+ /**
3455
+ * 明细导出列配置 → vxe 列:formatS/render 转成渲染插槽,exportType 落到 params。
3456
+ * @param {Array} rawColumns 设计器维护的明细导出列。
3457
+ * @returns {Array} vxe 列定义。
3458
+ */
3459
+ buildExportItemColumns(rawColumns) {
3460
+ let formatKeys = [
3461
+ "d1",
3462
+ "d2",
3463
+ "d3",
3464
+ "d4",
3465
+ "d5",
3466
+ "n1",
3467
+ "n2",
3468
+ "n3",
3469
+ "n4",
3470
+ "n5",
3471
+ "n6",
3472
+ "n7",
3473
+ ];
3474
+ return rawColumns.map((item) => {
3475
+ let columnSlots = null;
3476
+ let columnParams = {};
3477
+ if (item.formatS === "render") {
3478
+ let r = item.render ? new Function("params", "h", item.render) : null;
3479
+ columnSlots = {
3480
+ default: (params, h) => {
3481
+ return r ? r.call(this, params, h) : "";
3482
+ },
3483
+ };
3484
+ } else if (formatKeys.includes(item.formatS)) {
3485
+ columnSlots = {
3486
+ default: (params, h) => {
3487
+ let cellValue = params.row[params.column.field];
3488
+ return this.getFormatterValue(
3489
+ cellValue,
3490
+ item.formatS,
3491
+ item.utcTransformEnabled
3492
+ );
3493
+ },
3494
+ };
3495
+ }
3496
+ if (item.exportType) {
3497
+ columnParams.exportType = item.exportType;
3498
+ }
3499
+ return {
3500
+ id: item.id,
3501
+ title: item.title,
3502
+ field: item.field,
3503
+ params: columnParams,
3504
+ slots: columnSlots,
3505
+ };
3506
+ });
3507
+ },
3508
+ /**
3509
+ * 组装一份明细导出配置。导出列、脚本编码、自定义导出参数三项都可以由导出按钮单独维护,
3510
+ * 各自独立生效:按钮留空的那一项回落到表格属性,三项都留空就是表格属性上原来那套。
3511
+ * @param {Object} [overrides] 按钮级配置。
3512
+ * @param {Array} [overrides.columns] 明细导出列。
3513
+ * @param {String} [overrides.scriptCode] 明细导出脚本编码。
3514
+ * @param {String} [overrides.paramScript] 明细导出查询参数脚本。
3515
+ * @returns {Object|null} 两处都没维护列时返回 null。
3516
+ */
3517
+ buildExportItemConfig(overrides) {
3518
+ let options = this.widget.options;
3519
+ let o = overrides || {};
3520
+ let hasColumns = o.columns && o.columns.length;
3521
+ let columns = hasColumns ? o.columns : options.exportItemColumns;
3522
+ if (!columns || !columns.length) return null;
3523
+ let scriptCode = o.scriptCode || options.exportItemScriptCode;
3524
+ let paramScript = o.paramScript || options.exportItemParam;
3525
+ return {
3526
+ scriptCode: scriptCode || options.formScriptCode,
3527
+ columns: this.buildExportItemColumns(columns),
3528
+ // 取数参数脚本与表格上其它脚本一样走 handleCustomEvent(它负责注入 dataId /
3529
+ // formCode 并统一弹前端脚本异常)。原先写的 handleCustomParam 全库没有定义,
3530
+ // 配了「明细导出查询参数」的表格一点导出就抛 TypeError。
3531
+ param: () => {
3532
+ if (paramScript) {
3533
+ return this.handleCustomEvent(paramScript);
3534
+ }
3535
+ },
3536
+ };
3537
+ },
3352
3538
  /**
3353
3539
  * 使用统一 Excel 导出器导出当前表格。
3354
3540
  * @param {Object} option 导出参数;prefix 为空时自动使用当前服务前缀。
3541
+ * option.exportItemColumns / exportItemScriptCode / exportItemParam 为按钮上维护的
3542
+ * 明细导出列、脚本编码与取数参数脚本,三项各自独立:传了哪项就以哪项为准(同一张表格
3543
+ * 可以挂多个明细导出按钮、各导各自的列、各走各自的脚本),组装成 exportItemConfig 随
3544
+ * 导出参数下发,下游(字段选择弹框 / 分页取数 / 脱敏剔除)一律优先读它,读不到才回落
3545
+ * 表格属性那套。
3546
+ * option.exportButtonName 为导出按钮标识,原样透传给字段选择弹框,用于按按钮分组存放
3547
+ * 勾选记忆(见 excelExport/exportColumnMemory.js)。
3355
3548
  */
3356
3549
  exportData(option) {
3357
- this.markMaskedColumnsForExport();
3550
+ let exportOption = { ...option };
3551
+ let itemColumns = exportOption.exportItemColumns;
3552
+ let itemScriptCode = exportOption.exportItemScriptCode;
3553
+ let itemParam = exportOption.exportItemParam;
3554
+ delete exportOption.exportItemColumns;
3555
+ delete exportOption.exportItemScriptCode;
3556
+ delete exportOption.exportItemParam;
3557
+ let hasOverride = itemColumns?.length || itemScriptCode || itemParam;
3558
+ if (isItemExport(exportOption.type) && hasOverride) {
3559
+ exportOption.exportItemConfig = this.buildExportItemConfig({
3560
+ columns: itemColumns,
3561
+ scriptCode: itemScriptCode,
3562
+ paramScript: itemParam,
3563
+ });
3564
+ }
3565
+ this.markMaskedColumnsForExport(exportOption.exportItemConfig?.columns);
3358
3566
  let tableRef = this.getTableRef();
3359
3567
  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 });
3568
+ exportOption.prefix = exportOption.prefix || "/" + serviceName;
3569
+ this.$excelExport({ targetRef: tableRef, ...exportOption });
3366
3570
  },
3367
3571
  async deleteRow(row, rowIndex) {
3368
3572
  let isTreeTable = this.widget.options.isTreeTable;
@@ -3574,11 +3778,13 @@ modules = {
3574
3778
  const fieldsToValidate = [];
3575
3779
  let tableColumns = this.widget.options.tableColumns;
3576
3780
  checkRows.forEach((row) => {
3577
- let fieldSchema = this.fieldSchemaMap[row._X_ROW_KEY];
3781
+ let fieldSchema = this.getRowFieldSchema(row) || {};
3578
3782
  tableColumns.forEach((column) => {
3579
3783
  if (column.widget) {
3784
+ // 列未就绪时 getRowFieldSchema 会给 null,取不到行级组件就按"不必填"跳过,
3785
+ // 而不是在这里抛异常把整个校验打断
3580
3786
  let widget = fieldSchema[column.widget.id];
3581
- let required = widget.options.required || false;
3787
+ let required = widget?.options?.required || false;
3582
3788
  if (required) {
3583
3789
  let propName = this.getColumnWidgetProp(widget, row);
3584
3790
  fieldsToValidate.push(propName);
@@ -3587,7 +3793,7 @@ modules = {
3587
3793
  if (column.widgetList) {
3588
3794
  column.widgetList.forEach((itemWidget) => {
3589
3795
  let widget = fieldSchema[itemWidget.id];
3590
- let required = widget.options.required || false;
3796
+ let required = widget?.options?.required || false;
3591
3797
  if (required) {
3592
3798
  let propName = this.getColumnWidgetProp(widget, row);
3593
3799
  fieldsToValidate.push(propName);
@@ -3628,9 +3834,9 @@ modules = {
3628
3834
  let sourceWidgetId = isEdit
3629
3835
  ? obj.column?.params?.editWidget?.id
3630
3836
  : 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];
3837
+ let rowSchema = this.getRowFieldSchema(obj.row);
3838
+ if (!sourceWidgetId || !rowSchema) return "false";
3839
+ let fieldWidget = rowSchema[sourceWidgetId];
3634
3840
  if (!fieldWidget) {
3635
3841
  return "false";
3636
3842
  }
@@ -5632,8 +5838,9 @@ modules = {
5632
5838
  /**
5633
5839
  * 为每一行创建独立字段 schema,避免组件状态在不同行之间共享。
5634
5840
  * @param {Boolean} initFlag true 时忽略旧 schema 并全部重新创建。
5841
+ * @param {Number} retryCount 列未就绪时已重试的次数,见 MAX_SCHEMA_INIT_RETRY。
5635
5842
  */
5636
- initFieldSchemaData(initFlag) {
5843
+ initFieldSchemaData(initFlag, retryCount = 0) {
5637
5844
  //初始化fieldSchemaData!!!
5638
5845
  /*if (this.widget.type !== 'sub-form') {
5639
5846
  return
@@ -5648,6 +5855,14 @@ modules = {
5648
5855
  if (!this.getTableFullColumns().length) {
5649
5856
  return false;
5650
5857
  }
5858
+ // 大数据量不再预建:按「行数 × 组件列数」深克隆,万行量级直接把标签页撑爆
5859
+ // (与条件导出那次崩溃同源)。交给 getRowFieldSchema 在渲染/校验时按需建。
5860
+ if (rowLength > this.getLazyRowSchemaThreshold()) {
5861
+ // initFlag 表示"整体重建",此时旧 schema 必须丢弃;否则(addRow /
5862
+ // deleteRow)保留已建的那些,避免把行内脚本写进去的状态一起清掉。
5863
+ if (initFlag) this.fieldSchemaMap = {};
5864
+ return true;
5865
+ }
5651
5866
  let fieldSchemaMap = {};
5652
5867
  let fieldSchemaMap0 = this.fieldSchemaMap;
5653
5868
  for (let i = 0; i < rowLength; i++) {
@@ -5656,10 +5871,12 @@ modules = {
5656
5871
  if (initFlag) {
5657
5872
  fieldSchemaMap2 = this.createRowFieldSchemaMap();
5658
5873
  } else {
5659
- let fieldSchemaMap01 = fieldSchemaMap0[rowId];
5660
- fieldSchemaMap2 = fieldSchemaMap01
5661
- ? this.$baseLodash.cloneDeep(fieldSchemaMap01)
5662
- : this.createRowFieldSchemaMap();
5874
+ // 沿用旧行的 schema 对象本身,不再 cloneDeep:该对象本来就是这一行
5875
+ // 独享的(按 rowId 分桶),克隆只会产出语义完全相同的另一份,白占一份
5876
+ // 内存。而本分支的调用方是 addRow / deleteRow —— 每增删一行都把全表深
5877
+ // 克隆一遍是 O(),千行树表上每次增删都要停顿数秒并制造等量垃圾。
5878
+ fieldSchemaMap2
5879
+ = fieldSchemaMap0[rowId] || this.createRowFieldSchemaMap();
5663
5880
  }
5664
5881
  fieldSchemaMap[rowId] = fieldSchemaMap2;
5665
5882
  }
@@ -5667,9 +5884,19 @@ modules = {
5667
5884
  return true;
5668
5885
  };
5669
5886
  if (!build()) {
5887
+ // 列未就绪时隔一帧重试。必须有上限:动态列请求失败 / 列配置为空时列永远不会
5888
+ // 出现,而 rowIdData 非空,这条 $nextTick 自递归就成了每帧一次的死循环,
5889
+ // 页面永久卡死(setValue 侧另有 pendingSchemaInit 挡了一条入口,但
5890
+ // initValue / addRow / deleteRow / 动态列重载都能直接进到这里)。
5891
+ if (retryCount >= MAX_SCHEMA_INIT_RETRY) {
5892
+ console.warn(
5893
+ "[data-table] 表格列始终未就绪,已停止行级 schema 重建重试"
5894
+ );
5895
+ return;
5896
+ }
5670
5897
  this.$nextTick(() => {
5671
5898
  if (this.rowIdData.length) {
5672
- this.initFieldSchemaData(initFlag);
5899
+ this.initFieldSchemaData(initFlag, retryCount + 1);
5673
5900
  }
5674
5901
  });
5675
5902
  }
@@ -5766,6 +5993,30 @@ modules = {
5766
5993
  return newFieldSchema;
5767
5994
  },
5768
5995
  //end1
5996
+ /**
5997
+ * 导出取数的行初始化:只补行主键并打导出标记,**不建行级 schema**。
5998
+ *
5999
+ * 行级 schema 的意义是让同一列的组件在不同行之间互不干扰(行内脚本改 options、
6000
+ * 编辑态等)。导出行既不进 vxe 表格也不会被编辑,createRowFieldSchemaMap 产出的
6001
+ * 只是列模板的等价深克隆(仅 id 不同)—— 异步选项同步写的是列模板本身
6002
+ * (collectColumnWidgetConfigRequests),行内脚本改的是已挂载的组件实例,都不会让
6003
+ * 导出行的 schema 与列模板产生差异。
6004
+ *
6005
+ * 但代价是按「行数 × 组件列数」做 deepClone 并常驻 fieldSchemaMap,而该 map 在导出
6006
+ * 期间没有任何释放路径(只有删行、或下次正常查询走 initFieldSchemaData 整体重建才清),
6007
+ * 10 万行量级直接把标签页撑爆。取值侧对缺失行级 schema 本就有回退列模板的分支
6008
+ * (列 filterVal、getTableColumnWidget、getRowWidget),故跳过构建不改变导出内容。
6009
+ *
6010
+ * 注意:树表懒加载走的仍是 initExportFieldSchemaData —— 那批行是要进表格的真实行。
6011
+ * @param {Array} rows 导出取回的当页数据。
6012
+ */
6013
+ initExportRows(rows) {
6014
+ if (!rows) return;
6015
+ for (let row of rows) {
6016
+ if (!row._X_ROW_KEY) row._X_ROW_KEY = "row_" + generateId();
6017
+ row._X_EXPORT_ROW = true;
6018
+ }
6019
+ },
5769
6020
  /** @param {Array} rows 导出数据。为无行主键的数据创建临时 schema。 */
5770
6021
  initExportFieldSchemaData(rows) {
5771
6022
  if (!rows) return;