cloud-web-corejs 1.1.0-dev.10 → 1.1.0-dev.13

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/components/VabUpload/view.vue +25 -25
  3. package/src/components/excelExport/exportColumnMemory.js +118 -0
  4. package/src/components/excelExport/exportFieldDialog.vue +50 -38
  5. package/src/components/excelExport/index.js +30 -13
  6. package/src/components/excelExport/mixins.js +390 -64
  7. package/src/components/excelImport/mixins.js +898 -850
  8. package/src/components/mobile/wf/addTaskUserdialog.vue +100 -100
  9. package/src/components/mobile/wf/content.vue +5 -5
  10. package/src/components/mobile/wf/deleteTaskUserDialog.vue +73 -73
  11. package/src/components/mobile/wf/selectUserDialog.vue +8 -12
  12. package/src/components/mobile/wf/setCandidateDialog.vue +69 -69
  13. package/src/components/mobile/wf/urgingDialog.vue +75 -75
  14. package/src/components/table/index.js +14 -1
  15. package/src/components/table/vxeFilter/mixin.js +28 -18
  16. 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" +64 -57
  17. 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
  18. package/src/components/xform/form-designer/form-widget/container-widget/detail-h5-widget.vue +243 -243
  19. package/src/components/xform/form-designer/form-widget/container-widget/h5-card-pane-widget.vue +228 -228
  20. package/src/components/xform/form-designer/form-widget/container-widget/h5-card-widget.vue +165 -165
  21. package/src/components/xform/form-designer/form-widget/dialog/importDialogMixin.js +61 -2
  22. package/src/components/xform/form-designer/form-widget/field-widget/select-export-item-button-widget.vue +3 -0
  23. package/src/components/xform/form-designer/form-widget/field-widget/singleUpload-widget.vue +145 -145
  24. package/src/components/xform/form-render/container-item/data-table-item.vue +350 -347
  25. package/src/components/xform/form-render/container-item/data-table-mixin.js +240 -37
  26. package/src/components/xform/form-render/container-item/detail-h5-item.vue +1 -1
  27. package/src/components/xform/form-render/container-item/list-h5-item.vue +1 -1
  28. package/src/components/xform/styles/h5.scss +483 -459
  29. package/src/components/xform/utils/dynamicSchemaUtil.js +385 -361
  30. package/src/layout/components/TagsView/index.vue +325 -323
  31. package/src/store/modules/tagsView.js +241 -217
  32. package/src/utils/importLimit.js +72 -0
  33. package/src/utils/pdfUtil.js +6 -1
  34. package/src/utils/request.js +17 -0
@@ -1,11 +1,13 @@
1
1
  /**version-1.0*/
2
2
  let tmixins = {};
3
+ import axios from "axios";
3
4
  import { getToken } from "../../utils/auth";
4
5
  import indexUtil from "../../utils/index.js";
5
6
  import exportFieldDialog from "./exportFieldDialog.vue";
6
7
  import { getCellValue } from "@base/components/table/util/index";
7
8
  import { isItemExport, isRowExport, requiresCheckedRows } from "./exportType";
8
9
  import { getExportItemColumns } from "./exportItemConfigUtil";
10
+ import { viewKey } from "@base/utils/repeatOpen";
9
11
 
10
12
  let configUtil = {
11
13
  baseUrl: process.env.VUE_APP_BASE_API,
@@ -13,6 +15,45 @@ let configUtil = {
13
15
  indexUtil,
14
16
  };
15
17
 
18
+ /** 取值转换每处理多少行让出一次主线程,见 getExcelData。 */
19
+ const EXPORT_YIELD_ROWS = 200;
20
+
21
+ /**
22
+ * 让出主线程一个宏任务,给渲染与用户交互留出机会。
23
+ * 必须是宏任务:微任务(Promise.resolve())不会让浏览器插入渲染帧,进度条照样不动。
24
+ * @returns {Promise<void>}
25
+ */
26
+ function yieldToBrowser() {
27
+ return new Promise((resolve) => setTimeout(resolve, 0));
28
+ }
29
+
30
+ /**
31
+ * 可以直接当文件存下来的响应体类型(前缀匹配)。
32
+ */
33
+ const DOWNLOAD_BINARY_TYPES = [
34
+ "application/vnd.openxmlformats",
35
+ "application/vnd.ms-excel",
36
+ "application/octet-stream",
37
+ "application/zip",
38
+ ];
39
+
40
+ /**
41
+ * 这份响应体是不是真的文件流。
42
+ * 用白名单而不是「排除 application/json」:网关拦截、登录态失效这类失败常见的是
43
+ * 200 + text/html,黑名单会把一张 HTML 错误页原样存成打不开的 .xlsx,用户看到的是
44
+ * 一个损坏文件而不是回退后的正常下载。
45
+ * 后端不给 Content-Type 时 blob.type 是空串,按二进制放行 —— 旧的表单 POST 本来也是
46
+ * 这么下的,不能因为这次改造反而下不动。
47
+ * @param {Blob} res 响应体。
48
+ * @returns {Boolean}
49
+ */
50
+ function isDownloadableBlob(res) {
51
+ if (!res) return false;
52
+ let type = (res.type || "").toLowerCase();
53
+ if (!type) return true;
54
+ return DOWNLOAD_BINARY_TYPES.some((item) => type.indexOf(item) === 0);
55
+ }
56
+
16
57
  function getGrid(that, tableRef) {
17
58
  let $grid;
18
59
  if (Array.isArray(that.$refs[tableRef])) {
@@ -93,6 +134,17 @@ function getGrid(that, tableRef) {
93
134
  exportTimer: null,
94
135
  countTimer: null,
95
136
  exportError: false,
137
+ // 用户点「取消」/组件被销毁的终止标记。此前取消只是停掉定时器 + 销毁组件,
138
+ // 已经发出去的那几页请求照样会回调下来继续上送、继续合并,甚至最后弹出一个
139
+ // 半截文件的下载。所有异步续点都要先问 isExportStopped()。
140
+ aborted: false,
141
+ // 本次导出全部请求共用的取消令牌源,取消时一次性掐断在途请求
142
+ cancelSource: null,
143
+ // lockMenu 实际锁上的那一个页签。必须记下来:解锁若再按「当时的
144
+ // $route.path」现查,用户在导出期间切了页签(弹框可最小化,切走完全合法)
145
+ // 解的就是另一把锁 —— 原页签会永久 affix,而 tagsView 的 delOthersViews /
146
+ // delAllViews 都保留 affix,「关闭其他」「全部关闭」也删不掉它,只能刷新页面。
147
+ lockedView: null,
96
148
  };
97
149
  },
98
150
  created() {},
@@ -100,10 +152,46 @@ function getGrid(that, tableRef) {
100
152
  // this.exc();
101
153
  },
102
154
  beforeDestroy() {
155
+ this.aborted = true;
103
156
  this.clearExportTimer();
157
+ this.cancelPendingRequests();
104
158
  this.unlockMenu();
105
159
  },
106
160
  methods: {
161
+ /**
162
+ * 导出是否已终止:用户取消、组件销毁、或失败中止。
163
+ * 每一个跨越 await / .then / setTimeout 的续点都必须先问它,否则在途请求的回调
164
+ * 会在导出早已"取消"之后继续推进流程。
165
+ * @returns {Boolean}
166
+ */
167
+ isExportStopped() {
168
+ if (this.aborted === true) return true;
169
+ if (this.exportError === true) return true;
170
+ return this._isDestroyed === true;
171
+ },
172
+ /**
173
+ * 本次导出共用的取消令牌,首次取用时创建。
174
+ * 每次导出都是一个全新的组件实例(index.js 的 initInstance 会先销毁旧实例),
175
+ * 所以一个 source 覆盖一次导出的全部请求,不存在跨导出串味。
176
+ * @returns {Object} axios CancelToken
177
+ */
178
+ getCancelToken() {
179
+ if (!this.cancelSource) {
180
+ this.cancelSource = axios.CancelToken.source();
181
+ }
182
+ return this.cancelSource.token;
183
+ },
184
+ /**
185
+ * 掐断本次导出全部在途请求。
186
+ * request.js 的错误拦截器已对 axios.isCancel 短路返回,不会弹错误框;
187
+ * 各调用点的 .catch 也都会先问 isExportStopped() 再决定要不要提示。
188
+ */
189
+ cancelPendingRequests() {
190
+ if (this.cancelSource) {
191
+ this.cancelSource.cancel("export-cancelled");
192
+ this.cancelSource = null;
193
+ }
194
+ },
107
195
  exc() {
108
196
  this.option = this.param;
109
197
  let $grid = getGrid(this.option.vue, this.option.targetRef);
@@ -149,6 +237,9 @@ function getGrid(that, tableRef) {
149
237
  },
150
238
  dialogClose2() {
151
239
  if (this.isMinimize) return;
240
+ // 最小化不是取消,只有真正关闭才置终止标记
241
+ this.aborted = true;
242
+ this.cancelPendingRequests();
152
243
  let loadingObj = window.$vueRoot.$baseLoading({
153
244
  target: document.body,
154
245
  background: "unset",
@@ -202,10 +293,13 @@ function getGrid(that, tableRef) {
202
293
  this.abortExport(
203
294
  this.$t1("无法确定导出服务前缀,请在导出配置中指定 prefix")
204
295
  );
205
- this.unlockMenu();
206
296
  return;
207
297
  }
208
298
  this.CURRENT_PREFIX = options.prefix;
299
+ // 取数走 originOption.exportAjax 时请求由 xform 侧构造,前端拿不到那个 config,
300
+ // 只能把令牌顺着导出参数带过去(data-table-mixin 的 exportAjax 会塞进
301
+ // customParam.config,最终由 formHttp 的 ...opts 透传给 axios)
302
+ options.cancelToken = this.getCancelToken();
209
303
 
210
304
  this.title = title;
211
305
  that.tTotalPage = 0;
@@ -213,6 +307,7 @@ function getGrid(that, tableRef) {
213
307
  that
214
308
  .createExcelFile()
215
309
  .then((resultMsg) => {
310
+ if (that.isExportStopped()) return;
216
311
  if (resultMsg.type === "success") {
217
312
  let aObj = resultMsg.objx;
218
313
  that.uuid = aObj.uuid;
@@ -221,6 +316,7 @@ function getGrid(that, tableRef) {
221
316
  setTimeout(function () {
222
317
  /*num = 1;
223
318
  loopToDo(0, num);*/
319
+ if (that.isExportStopped()) return;
224
320
  that.handleLoopToDo();
225
321
  }, 300);
226
322
  } else {
@@ -230,6 +326,7 @@ function getGrid(that, tableRef) {
230
326
  }
231
327
  })
232
328
  .catch((error) => {
329
+ if (that.isExportStopped()) return;
233
330
  console.error(error);
234
331
  that.abortExport(that.$t1("创建导出文件失败,导出已中止"));
235
332
  that.showImportDialog = false;
@@ -250,9 +347,18 @@ function getGrid(that, tableRef) {
250
347
  if (hasExportImage || (hasExportImage2 && showImageAtTable)) {
251
348
  pageSize = Math.min(pageSize, 150);
252
349
  }
350
+ // 单页压力取决于「行数 × 列数」,此前只按有无图片列降档,宽表(100+ 列)一页
351
+ // 的请求体与同步转换耗时是窄表的十几倍,同样的 1000 行/页天差地别。
352
+ let columnNum = this.leafColumns.length;
353
+ if (columnNum > 100) {
354
+ pageSize = Math.min(pageSize, 200);
355
+ } else if (columnNum > 50) {
356
+ pageSize = Math.min(pageSize, 500);
357
+ }
253
358
  return pageSize;
254
359
  },
255
360
  hadleMergeExcel(pPageSize) {
361
+ if (this.isExportStopped()) return;
256
362
  let that = this;
257
363
  let title = this.title;
258
364
  let options = this.option;
@@ -290,6 +396,9 @@ function getGrid(that, tableRef) {
290
396
  that
291
397
  .mergeExcel(startPage, endPage)
292
398
  .then(function (resultMsg3) {
399
+ // 取消后组件与进度弹框都已销毁,下面 $refs["exportTable"].insertAt
400
+ // 会直接抛错
401
+ if (that.isExportStopped()) return;
293
402
  if (resultMsg3.type === "success") {
294
403
  mtExcedSuccessNum++;
295
404
  let fileInfo = resultMsg3.objx;
@@ -311,25 +420,17 @@ function getGrid(that, tableRef) {
311
420
  // that.fileRows.push(dataMap);
312
421
  if (mtExcedNum < fileNum) {
313
422
  loopDo();
314
- } else if (
315
- mtExcedNum >= fileNum &&
316
- mtExcedSuccessNum >= fileNum
317
- ) {
318
- setTimeout(function () {
319
- that.percentageNum = parseInt(100);
320
- }, 100);
321
- that.clearExportTimer();
322
423
  } else if (mtExcedSuccessNum >= fileNum) {
323
- setTimeout(function () {
324
- that.percentageNum = parseInt(100);
325
- }, 100);
326
- that.clearExportTimer();
424
+ // 走到 else 分支时 mtExcedNum >= fileNum 必然成立,
425
+ // 原先拆成的两个 else-if 条件与分支体完全相同,合并为一处
426
+ that.completeExport();
327
427
  }
328
428
  } else {
329
429
  that.abortExport();
330
430
  }
331
431
  })
332
432
  .catch(function (e) {
433
+ if (that.isExportStopped()) return;
333
434
  console.error(e);
334
435
  that.abortExport(that.$t1("文件合并异常,导出已中止"));
335
436
  });
@@ -355,14 +456,28 @@ function getGrid(that, tableRef) {
355
456
  let excedNum = 0;
356
457
  let cSize = this.doneSize;
357
458
 
358
- let handleData = function (resultMsg) {
459
+ let handleData = function (resultMsg, flag) {
460
+ // 取消之后到达的在途响应直接丢弃,不再进入上送/合并流程
461
+ if (that.isExportStopped()) return;
359
462
  let pageNumber = resultMsg.objx.pageNumber || 1;
463
+ // 并发窗口在"该页上送完成"后才释放,而不是"该页拉取完成"就释放。
464
+ // addExcelData 此前完全不受闸门约束:上送慢于拉取时 pending 的 POST 会无限
465
+ // 堆积(每个都攥着一份几 MB 的 JSON body),加上浏览器每域名 6 连接会让上送
466
+ // 排到拉取后面,这种反压对调度器完全不可见 —— 大数据量导出的内存曲线因此
467
+ // 始终只涨不落。
468
+ let released = flag !== 1; // 首页(flag=0)不占窗口,无需释放
469
+ let releaseWindow = function () {
470
+ if (released) return;
471
+ released = true;
472
+ if (waitReqNum > 0) waitReqNum--;
473
+ };
360
474
  that
361
475
  .addExcelData({
362
476
  rows: resultMsg.objx.records,
363
477
  pageNumber: pageNumber,
364
478
  })
365
479
  .then(function (resultMsg2) {
480
+ releaseWindow();
366
481
  let data = resultMsg.objx;
367
482
  let total = data.total;
368
483
 
@@ -481,6 +596,8 @@ function getGrid(that, tableRef) {
481
596
  }
482
597
  })
483
598
  .catch(function (e) {
599
+ releaseWindow();
600
+ if (that.isExportStopped()) return;
484
601
  console.error(e);
485
602
  that.abortExport(that.$t1("导出数据写入失败,导出已中止"));
486
603
  });
@@ -489,6 +606,8 @@ function getGrid(that, tableRef) {
489
606
  let waitReqNum = 0;
490
607
  let promise;
491
608
  let loopToDo = function (flag, pageNumber, retryCount = 0) {
609
+ // 取消后不再发起新页,也不再重试(重试是从 .catch 里递归回来的)
610
+ if (that.isExportStopped()) return;
492
611
  let maxWaitNum = limitThreadNum;
493
612
  let nowDate = options.nowDate;
494
613
  if (flag === 1) {
@@ -508,7 +627,11 @@ function getGrid(that, tableRef) {
508
627
  let url = originOption.exportPath || originOption.path;
509
628
 
510
629
  // let params = originOption.param() || {};
511
- let params = that.queryParam;
630
+ // 每页必须独立参数对象。此前所有页共享 that.queryParam 这一个对象,下面的
631
+ // Object.assign 把 current 写进去后,靠"派发间隔 100ms、微任务已 flush"侥幸
632
+ // 不冲突;请求拦截器一旦变成异步(token 续期等),并发的两页会带上同一个
633
+ // current —— 合并出的文件重复页 + 缺页,且全程不报错。
634
+ let params = that.$baseLodash.cloneDeep(that.queryParam);
512
635
  let aParam = {
513
636
  current: pageNumber,
514
637
  size: pPageSize,
@@ -540,6 +663,7 @@ function getGrid(that, tableRef) {
540
663
  modal: false,
541
664
  queryCreateInfo: originOption.queryCreateInfo,
542
665
  addCreateInfo: originOption.addCreateInfo,
666
+ cancelToken: that.getCancelToken(),
543
667
  });
544
668
  } else {
545
669
  promise = originOption.exportAjax(aParam, options);
@@ -592,9 +716,9 @@ function getGrid(that, tableRef) {
592
716
  clearInterval(that.exportTimer);
593
717
  that.exportTimer = null;
594
718
  }
595
- } else {
596
- if (waitReqNum > 0) waitReqNum--;
597
719
  }
720
+ // 注意:此处不再释放并发窗口。拉取完成只是这一页做完了一半,
721
+ // 窗口由 handleData 在上送(addExcelData)落地后释放,见那里的注释。
598
722
 
599
723
  setTimeout(function () {
600
724
  handleData(resultMsg, flag);
@@ -611,6 +735,8 @@ function getGrid(that, tableRef) {
611
735
  }
612
736
  })
613
737
  .catch(function (e) {
738
+ // 取消导致的 reject 不是失败,也不该重试或弹提示
739
+ if (that.isExportStopped()) return;
614
740
  console.error(e);
615
741
  if (retryCount < 1) {
616
742
  setTimeout(function () {
@@ -641,6 +767,7 @@ function getGrid(that, tableRef) {
641
767
  data: datas,
642
768
  modal: loading || false,
643
769
  isLoading: loading,
770
+ cancelToken: this.getCancelToken(),
644
771
  });
645
772
  },
646
773
  /*getExportTitleJson() {
@@ -742,11 +869,21 @@ function getGrid(that, tableRef) {
742
869
  }
743
870
  return titleArr;
744
871
  },
745
- addExcelData(opts, loading) {
872
+ async addExcelData(opts, loading) {
746
873
  let target = opts.target;
747
874
  let pageNumber = opts.pageNumber || 1;
748
875
  let callback = opts.callback;
749
- let datas = this.getExcelData(opts.rows);
876
+ // 已取消就不要再算、更不要再发。这里是所有上送的唯一入口,卡住它等于卡住
877
+ // 取消之后的全部后续动作。
878
+ if (this.isExportStopped()) {
879
+ return { type: "abort" };
880
+ }
881
+ let datas = await this.getExcelData(opts.rows);
882
+ // 取值过程中导出已被中止(getExcelData 让出主线程时会检查),不再上送这一页。
883
+ // 返回非 success 让调用方走它自己的中止分支,abortExport 幂等不会重复弹窗。
884
+ if (datas === null) {
885
+ return { type: "abort" };
886
+ }
750
887
  let cUUid = this.uuid;
751
888
  let ippaaapp = this.ippaaapp;
752
889
  let url1 =
@@ -766,6 +903,7 @@ function getGrid(that, tableRef) {
766
903
  data: datas,
767
904
  modal: loading || false,
768
905
  isLoading: loading || false,
906
+ cancelToken: this.getCancelToken(),
769
907
  });
770
908
  },
771
909
  mergeExcel(startPage, endPage, loading) {
@@ -780,6 +918,7 @@ function getGrid(that, tableRef) {
780
918
  method: "post",
781
919
  modal: loading || false,
782
920
  isLoading: loading || false,
921
+ cancelToken: this.getCancelToken(),
783
922
  });
784
923
  },
785
924
  createCountTimer() {
@@ -797,13 +936,32 @@ function getGrid(that, tableRef) {
797
936
  this.exportTimer = null;
798
937
  this.countTimer = null;
799
938
  },
939
+ /**
940
+ * 导出全部文件合并成功:进度置满、停掉计时,并**立刻解开页签锁**。
941
+ * 锁只保护取数期间。文件已经生成之后弹框还开着(用户在看列表、点下载)是完全
942
+ * 合法的状态,那时页签必须能正常关闭 —— 解锁此前只挂在 beforeDestroy 上,
943
+ * 用户不点「取消/确定」就永远解不开,正是「导出完成后页签关不掉」的来源。
944
+ */
945
+ completeExport() {
946
+ setTimeout(() => {
947
+ if (this.isExportStopped()) return;
948
+ this.percentageNum = parseInt(100);
949
+ }, 100);
950
+ this.clearExportTimer();
951
+ this.unlockMenu();
952
+ },
800
953
  /**
801
954
  * 中止导出:停止全部定时器并把进度条置为错误态。
802
955
  * @param {String} [msg] 需要额外弹给用户的提示;服务端已 toast 过的失败不传,避免重复提示。
803
956
  */
804
957
  abortExport(msg) {
805
958
  this.clearExportTimer();
959
+ // 必须先置标志再取消:取消会让其余在途请求 reject,它们的 .catch 先问
960
+ // isExportStopped() 才不会重复弹提示
806
961
  this.exportError = true;
962
+ this.cancelPendingRequests();
963
+ // 中止后弹框可能还挂在那儿(错误态进度条),页签没有理由继续锁着
964
+ this.unlockMenu();
807
965
  if (msg) this.$baseAlert(msg);
808
966
  },
809
967
  isPicture: function (suffix) {
@@ -858,7 +1016,7 @@ function getGrid(that, tableRef) {
858
1016
  }
859
1017
  return getCellValue(params);
860
1018
  },
861
- getExcelData(rows) {
1019
+ async getExcelData(rows) {
862
1020
  let that = this;
863
1021
 
864
1022
  let showImageAtTable = this.option.showImageAtTable || false;
@@ -875,8 +1033,19 @@ function getGrid(that, tableRef) {
875
1033
  let contents;
876
1034
  let params;
877
1035
  let resultStr;
878
- // 批量取值期间复用 vNode 渲染的临时实例,结束时统一销毁
879
- rows.forEach(function (row, index) {
1036
+ let index = -1;
1037
+ for (const row of rows) {
1038
+ index++;
1039
+ // 每 EXPORT_YIELD_ROWS 行让出一次主线程。取值本身是同步的双层遍历,一页
1040
+ // 默认 1000 行、渲染兜底列还要逐单元格挂载组件,整页跑完可达数秒;并发几页
1041
+ // 几乎同时到达时主线程被连续占死,进度条不动、「取消」点不动,浏览器还可能
1042
+ // 直接判定页面无响应而杀掉标签页。让出不减少总工作量,只保证 UI 活着。
1043
+ if (index > 0 && index % EXPORT_YIELD_ROWS === 0) {
1044
+ await yieldToBrowser();
1045
+ // 让出期间用户可能已取消/导出已中止,此时继续算完这一页没有意义,
1046
+ // 且合并根本不会发生,返回 null 让调用方跳过上送。
1047
+ if (that.isExportStopped()) return null;
1048
+ }
880
1049
  contents = [];
881
1050
  columns.forEach((column) => {
882
1051
  if (column.title) {
@@ -897,6 +1066,14 @@ function getGrid(that, tableRef) {
897
1066
  : null;
898
1067
  if (exportVal) {
899
1068
  resultStr = exportVal(params);
1069
+ } else if (column.params?.exportBlank && !exportType) {
1070
+ // 列保留、格留空:没配导出类型的附件列没有可写的内容,更不该为此
1071
+ // 逐单元格挂载附件组件去爬文件名。判定见 xform 的 isBlankExportColumn。
1072
+ // 放在 exportVal 之后:调用方显式写了取值函数就以它为准。
1073
+ // 必须再判一次 exportType:exportBlank 是建列时按 t.exportType 算死的,
1074
+ // 而 params.exportType 之后还会被 tableColumnConfig 脚本覆盖,只认
1075
+ // exportBlank 会把运行期才拿到导出类型的图片列错误地导成空。
1076
+ resultStr = null;
900
1077
  } else if (exportType === "Number") {
901
1078
  resultStr = that.getExportCellValue(params);
902
1079
  if (
@@ -911,7 +1088,10 @@ function getGrid(that, tableRef) {
911
1088
  exportType === "Image" ||
912
1089
  (showImageAtTable && exportType === "Image2")
913
1090
  ) {
914
- resultStr = that.getExportCellValue(params);
1091
+ // 取值刻意延到"确实用得上"的那条分支才做:附件的正常形态是数组,
1092
+ // 那条分支会用 row[column.field] 自己拼 [EXPIMG],把取值结果整个覆盖;
1093
+ // 空值分支同样直接给 null。而附件列没有 filterVal,取值会落到
1094
+ // getCellValue 的 vNode 挂载兜底——等于每个单元格白挂一次附件组件。
915
1095
  let attachments = row[column.field];
916
1096
  if (attachments) {
917
1097
  if (Array.isArray(attachments)) {
@@ -931,6 +1111,8 @@ function getGrid(that, tableRef) {
931
1111
  resultStr = null;
932
1112
  }
933
1113
  } else {
1114
+ // 单值形态(字段里直接存 URL)才需要取值来嗅后缀
1115
+ resultStr = that.getExportCellValue(params);
934
1116
  if (
935
1117
  that.isPicture(
936
1118
  that.$commonFileUtil.getFileSuffix(resultStr)
@@ -959,10 +1141,35 @@ function getGrid(that, tableRef) {
959
1141
  }
960
1142
  });
961
1143
  arr.push(contents);
962
- });
1144
+ }
963
1145
  return arr;
964
1146
  },
1147
+ /**
1148
+ * 从响应头解析下载文件名,取不到时用导出标题兜底。
1149
+ * @param {Object} response axios 原始响应。
1150
+ * @param {String} fallbackName 兜底文件名(不含扩展名)。
1151
+ * @returns {String}
1152
+ */
1153
+ resolveDownloadFileName(response, fallbackName) {
1154
+ let headers = response?.headers || {};
1155
+ let disposition = headers["content-disposition"] || headers["Content-Disposition"];
1156
+ if (disposition) {
1157
+ // filename*=UTF-8''xxx 优先,其次 filename="xxx"
1158
+ let starMatch = /filename\*=(?:UTF-8'')?([^;]+)/i.exec(disposition);
1159
+ let plainMatch = /filename="?([^";]+)"?/i.exec(disposition);
1160
+ let raw = (starMatch && starMatch[1]) || (plainMatch && plainMatch[1]);
1161
+ if (raw) {
1162
+ try {
1163
+ return decodeURIComponent(raw.trim());
1164
+ } catch (e) {
1165
+ return raw.trim();
1166
+ }
1167
+ }
1168
+ }
1169
+ return fallbackName + ".xlsx";
1170
+ },
965
1171
  exportAll(row) {
1172
+ let that = this;
966
1173
  let cUUid = encodeURIComponent(row.uuid);
967
1174
  let ippaaapp = this.ippaaapp;
968
1175
  let fileName = row.title;
@@ -1006,7 +1213,63 @@ function getGrid(that, tableRef) {
1006
1213
  document.body.removeChild(form);
1007
1214
  }
1008
1215
 
1009
- downLoadFile2();
1216
+ /**
1217
+ * 优先走 axios blob 下载:token 由 request.js 的请求拦截器注入
1218
+ * X-Token / Authorization 头,不必再把 access_token 塞进表单字段随请求体
1219
+ * 明文提交;失败时也能拿到响应体做判断,而表单 POST 打开新窗口后失败只会给
1220
+ * 用户看一个空白页或一坨错误 JSON。
1221
+ *
1222
+ * 后端若尚未放开该接口的头鉴权(此前只认表单里的 access_token),这里会拿到
1223
+ * JSON 错误体或直接报错 —— 一律回退到原来的表单 POST,保证下载不会因为这次
1224
+ * 改造而失效。同体系的 /export_ins/download 已经是 blob + 头鉴权的用法
1225
+ * (见 components/baseInputExport/mixins.js),大概率是通的,但**需联调确认**。
1226
+ */
1227
+ let fallbackToFormPost = function (reason) {
1228
+ console.warn("[excelExport] blob 下载不可用,回退表单 POST:", reason);
1229
+ downLoadFile2();
1230
+ };
1231
+
1232
+ let apiUrl = `${this.CURRENT_PREFIX}/excel/download?uuid=${cUUid}&ippaaapp=${ippaaapp}&n=${encodeURIComponent(fileName)}`;
1233
+
1234
+ this.$http({
1235
+ url: apiUrl,
1236
+ method: "post",
1237
+ data: {},
1238
+ responseType: "blob",
1239
+ resultType: "other",
1240
+ modal: false,
1241
+ // 失败要静默回退,不能先弹一个错误框
1242
+ errorMsg: false,
1243
+ // 刻意**不带**本次导出的取消令牌。下载不是导出流水线的一环,而是用户单独
1244
+ // 发起的动作:分卷文件大时点完「下载」要等几秒,此时关掉进度弹框(「取消」
1245
+ // 和「确定」都走 dialogClose2 → cancelPendingRequests)会掐断这个请求,而
1246
+ // 下面的 isCancel 分支又刻意不回退表单 POST —— 结果是文件静默地下不下来,
1247
+ // 且没有任何提示。旧的表单 POST 脱离组件生命周期,本就不受关弹框影响。
1248
+ callback: (res, response) => {
1249
+ // 后端把错误当 200 返回时,body 是 JSON / HTML 错误页而不是 xlsx 流
1250
+ if (!isDownloadableBlob(res)) {
1251
+ fallbackToFormPost("响应不是文件流:" + (res && res.type));
1252
+ return;
1253
+ }
1254
+ let blobUrl = window.URL.createObjectURL(res);
1255
+ let a = document.createElement("a");
1256
+ a.href = blobUrl;
1257
+ a.download = that.resolveDownloadFileName(response, fileName);
1258
+ document.body.appendChild(a);
1259
+ a.click();
1260
+ document.body.removeChild(a);
1261
+ // 不释放会一直占着这份文件的内存,大导出下很可观
1262
+ setTimeout(() => window.URL.revokeObjectURL(blobUrl), 0);
1263
+ },
1264
+ }).catch((e) => {
1265
+ // 只挡真正的取消:取消后回退表单 POST 会在取消之后又弹出一次下载。
1266
+ // 本方法已不带取消令牌,这里是防御性保留(组件销毁等外部因素仍可能取消)。
1267
+ // 这里**不能**用 isExportStopped()——它含 exportError,而 exportAll 同时是
1268
+ // 进度弹框里的手动下载按钮:导出中途失败后用户手动下载已生成的分卷,正是最
1269
+ // 需要回退兜底的场景,用它拦会变成点了毫无反应。
1270
+ if (axios.isCancel(e)) return;
1271
+ fallbackToFormPost(e && e.message);
1272
+ });
1010
1273
  },
1011
1274
  startProcess2(datas) {
1012
1275
  this.tableTarget = getGrid(this.option.vue, this.option.targetRef);
@@ -1035,20 +1298,49 @@ function getGrid(that, tableRef) {
1035
1298
  }
1036
1299
  this.CURRENT_PREFIX = options.prefix;
1037
1300
 
1038
- let handleData = function () {
1039
- let total = datas.length;
1040
- that
1301
+ // 勾选/当前页导出此前把全部行塞进**一个**请求上送。`getCheckboxRecords(true)`
1302
+ // 是跨页全量勾选,选得多时请求体轻易超限(后端/网关直接拒),而且整批取值转换
1303
+ // 与 JSON 序列化一次性压在主线程上。改为复用条件导出那套「分页 append + 一次
1304
+ // merge」:按 getExportPageSize() 切片顺序上送。
1305
+ // 注意页数 ≤ 1 时(勾选量不超过一页,即绝大多数场景)请求序列与旧实现完全一致,
1306
+ // 不引入任何行为变化;这里刻意保持**串行**,避免把条件导出那边"上送无并发闸门"
1307
+ // 的老问题原样复制过来。
1308
+ let pageSize = this.getExportPageSize();
1309
+ let totalPages = Math.max(Math.ceil(datas.length / pageSize), 1);
1310
+
1311
+ let appendPage = function (pageNumber) {
1312
+ let start = (pageNumber - 1) * pageSize;
1313
+ return that
1041
1314
  .addExcelData(
1042
1315
  {
1043
- rows: datas,
1044
- pageNumber: 1,
1316
+ rows: datas.slice(start, start + pageSize),
1317
+ pageNumber: pageNumber,
1045
1318
  },
1046
1319
  true
1047
1320
  )
1048
1321
  .then((resultMsg2) => {
1049
- if (resultMsg2.type === "success") {
1050
- setTimeout(() => {
1051
- that.mergeExcel(1, 1, true).then((resultMsg3) => {
1322
+ if (resultMsg2.type !== "success") {
1323
+ // "abort" 是取值过程中组件已销毁/已中止,无需再提示;
1324
+ // 其余失败服务端已 toast 过,abortExport 不带 msg 避免重复弹
1325
+ if (resultMsg2.type !== "abort") that.abortExport();
1326
+ return false;
1327
+ }
1328
+ if (pageNumber >= totalPages) return true;
1329
+ return appendPage(pageNumber + 1);
1330
+ });
1331
+ };
1332
+
1333
+ let handleData = function () {
1334
+ appendPage(1)
1335
+ .then((completed) => {
1336
+ // 缺页合并会静默丢行,任一页失败都必须停在这里
1337
+ if (!completed) return;
1338
+ setTimeout(() => {
1339
+ that
1340
+ .mergeExcel(1, totalPages, true)
1341
+ .then((resultMsg3) => {
1342
+ // 取消后不能再自动弹下载:那会是一个半截文件
1343
+ if (that.isExportStopped()) return;
1052
1344
  if (resultMsg3.type === "success") {
1053
1345
  let fileInfo = resultMsg3.objx;
1054
1346
  let fileName = fileInfo.fileName;
@@ -1056,28 +1348,49 @@ function getGrid(that, tableRef) {
1056
1348
 
1057
1349
  let dataMap = {};
1058
1350
  dataMap["startPage"] = 1;
1059
- dataMap["endPage"] = 1;
1351
+ dataMap["endPage"] = totalPages;
1060
1352
  dataMap["title"] = title;
1061
1353
  dataMap["uuid"] = fileName;
1062
1354
  dataMap["fileSize"] = fileSize;
1063
1355
  that.exportAll(dataMap);
1356
+ } else {
1357
+ that.abortExport();
1064
1358
  }
1359
+ })
1360
+ .catch((e) => {
1361
+ if (that.isExportStopped()) return;
1362
+ console.error(e);
1363
+ that.abortExport(that.$t1("文件合并异常,导出已中止"));
1065
1364
  });
1066
- }, 0);
1067
- }
1365
+ }, 0);
1366
+ })
1367
+ .catch((e) => {
1368
+ if (that.isExportStopped()) return;
1369
+ console.error(e);
1370
+ that.abortExport(that.$t1("导出数据写入失败,导出已中止"));
1068
1371
  });
1069
1372
  };
1070
- that.createExcelFile(true).then((resultMsg) => {
1071
- if (resultMsg.type === "success") {
1072
- let aObj = resultMsg.objx;
1073
- that.uuid = aObj.uuid;
1074
- that.nowDate = aObj.nowDate;
1075
- that.ippaaapp = aObj.ippaaapp;
1076
- setTimeout(() => {
1077
- handleData();
1078
- }, 0);
1079
- }
1080
- });
1373
+ that
1374
+ .createExcelFile(true)
1375
+ .then((resultMsg) => {
1376
+ if (that.isExportStopped()) return;
1377
+ if (resultMsg.type === "success") {
1378
+ let aObj = resultMsg.objx;
1379
+ that.uuid = aObj.uuid;
1380
+ that.nowDate = aObj.nowDate;
1381
+ that.ippaaapp = aObj.ippaaapp;
1382
+ setTimeout(() => {
1383
+ handleData();
1384
+ }, 0);
1385
+ } else {
1386
+ that.abortExport();
1387
+ }
1388
+ })
1389
+ .catch((error) => {
1390
+ if (that.isExportStopped()) return;
1391
+ console.error(error);
1392
+ that.abortExport(that.$t1("创建导出文件失败,导出已中止"));
1393
+ });
1081
1394
  },
1082
1395
  closeExportFieldDialog() {
1083
1396
  this.dialogClose2();
@@ -1122,29 +1435,42 @@ function getGrid(that, tableRef) {
1122
1435
  }
1123
1436
  });
1124
1437
  },
1438
+ /**
1439
+ * 导出期间锁住当前页签(不让关),并记下锁的是哪一个,见 lockedView。
1440
+ * 本就是固定页签(affix)的不接管 —— 那不是我们加的锁,解的时候也不能动它。
1441
+ *
1442
+ * 只写 user_affix,**绝不能再写 meta.affix**:isRepeatable() 见到 affix 就返回
1443
+ * false,可重复打开的页签的 viewKey 会从 fullPath 塌成 path,于是 AppMain 的
1444
+ * router-view key 与 BaseKeepAlive 的缓存名同时错位 —— 切走再切回整页重建。
1445
+ * 「不让批量关闭」的语义由 tagsView 的 isPinned 认 user_affix 兜住。
1446
+ *
1447
+ * 定位页签用 viewKey 而不是 path:可重复打开时同一 path 会有多个页签,
1448
+ * 按 path 找到的可能是另一个实例。
1449
+ */
1125
1450
  lockMenu() {
1126
1451
  let vueTarget = window.$vueRoot;
1127
- let path = vueTarget.$route.path;
1452
+ let key = viewKey(vueTarget.$route);
1128
1453
  let visitedViews = vueTarget.$store.state.tagsView.visitedViews;
1129
- if (visitedViews) {
1130
- let currentItem = visitedViews.find((item) => item.path === path);
1131
- if (currentItem && !currentItem.meta.affix) {
1132
- this.$set(currentItem.meta, "affix", true);
1133
- this.$set(currentItem.meta, "user_affix", true);
1134
- }
1454
+ if (!visitedViews) return;
1455
+ let currentItem = visitedViews.find((item) => viewKey(item) === key);
1456
+ if (currentItem && !currentItem.meta.affix) {
1457
+ this.$set(currentItem.meta, "user_affix", true);
1458
+ this.lockedView = currentItem;
1135
1459
  }
1136
1460
  },
1461
+ /**
1462
+ * 解开自己锁的那一个页签。
1463
+ * **不能**再按当前路由去查:解锁的时机是组件销毁(取消、或下一次导出把上一个
1464
+ * 实例顶掉),那时的当前路由未必还是上锁时那一个。典型漏锁路径:A 页开始条件
1465
+ * 导出 → 切到 B 页 → 在 B 页再点一次导出,initInstance 先销毁旧实例,旧实例按
1466
+ * $route.path 查到的是 B(B 还没锁,什么也没做),于是 A 永久锁死。
1467
+ * 页签可能已被别处删掉,此时改的是一个游离对象,无副作用。
1468
+ */
1137
1469
  unlockMenu() {
1138
- let vueTarget = window.$vueRoot;
1139
- let path = vueTarget.$route.path;
1140
- let visitedViews = vueTarget.$store.state.tagsView.visitedViews;
1141
- if (visitedViews) {
1142
- let currentItem = visitedViews.find((item) => item.path === path);
1143
- if (currentItem && currentItem.meta.user_affix) {
1144
- currentItem.meta.affix = false;
1145
- currentItem.meta.user_affix = false;
1146
- }
1147
- }
1470
+ let view = this.lockedView;
1471
+ this.lockedView = null;
1472
+ if (!view || !view.meta.user_affix) return;
1473
+ view.meta.user_affix = false;
1148
1474
  },
1149
1475
  },
1150
1476
  };