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.
- package/package.json +1 -1
- package/src/components/VabUpload/view.vue +25 -25
- package/src/components/excelExport/exportColumnMemory.js +118 -0
- package/src/components/excelExport/exportFieldDialog.vue +50 -38
- package/src/components/excelExport/index.js +30 -13
- package/src/components/excelExport/mixins.js +390 -64
- package/src/components/excelImport/mixins.js +898 -850
- package/src/components/mobile/wf/addTaskUserdialog.vue +100 -100
- package/src/components/mobile/wf/content.vue +5 -5
- package/src/components/mobile/wf/deleteTaskUserDialog.vue +73 -73
- package/src/components/mobile/wf/selectUserDialog.vue +8 -12
- package/src/components/mobile/wf/setCandidateDialog.vue +69 -69
- package/src/components/mobile/wf/urgingDialog.vue +75 -75
- package/src/components/table/index.js +14 -1
- package/src/components/table/vxeFilter/mixin.js +28 -18
- 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
- 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
- package/src/components/xform/form-designer/form-widget/container-widget/detail-h5-widget.vue +243 -243
- package/src/components/xform/form-designer/form-widget/container-widget/h5-card-pane-widget.vue +228 -228
- package/src/components/xform/form-designer/form-widget/container-widget/h5-card-widget.vue +165 -165
- package/src/components/xform/form-designer/form-widget/dialog/importDialogMixin.js +61 -2
- package/src/components/xform/form-designer/form-widget/field-widget/select-export-item-button-widget.vue +3 -0
- package/src/components/xform/form-designer/form-widget/field-widget/singleUpload-widget.vue +145 -145
- package/src/components/xform/form-render/container-item/data-table-item.vue +350 -347
- package/src/components/xform/form-render/container-item/data-table-mixin.js +240 -37
- package/src/components/xform/form-render/container-item/detail-h5-item.vue +1 -1
- package/src/components/xform/form-render/container-item/list-h5-item.vue +1 -1
- package/src/components/xform/styles/h5.scss +483 -459
- package/src/components/xform/utils/dynamicSchemaUtil.js +385 -361
- package/src/layout/components/TagsView/index.vue +325 -323
- package/src/store/modules/tagsView.js +241 -217
- package/src/utils/importLimit.js +72 -0
- package/src/utils/pdfUtil.js +6 -1
- 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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
879
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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:
|
|
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
|
|
1050
|
-
|
|
1051
|
-
|
|
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"] =
|
|
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
|
-
|
|
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
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
that.
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
|
1452
|
+
let key = viewKey(vueTarget.$route);
|
|
1128
1453
|
let visitedViews = vueTarget.$store.state.tagsView.visitedViews;
|
|
1129
|
-
if (visitedViews)
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
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
|
};
|