cloud-web-corejs 1.0.283 → 1.0.284

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 (19) hide show
  1. package/package.json +1 -1
  2. package/src/components/excelExport/conditionExportConfig.js +112 -0
  3. package/src/components/excelExport/exportType.js +8 -0
  4. package/src/components/excelExport/index.vue +7 -1
  5. package/src/components/excelExport/mixins.js +410 -259
  6. package/src/components/xform/form-designer/form-widget/dialog/importDialog.vue +190 -185
  7. package/src/components/xform/form-designer/form-widget/dialog/importDialogMixin.js +2641 -1833
  8. package/src/components/xform/form-designer/form-widget/field-widget/import-button-widget.vue +18 -5
  9. package/src/components/xform/form-designer/form-widget/field-widget/select-export-item-button-widget.vue +2 -1
  10. package/src/components/xform/form-designer/form-widget/field-widget/table-export-button-widget.vue +2 -1
  11. package/src/components/xform/form-designer/setting-panel/property-editor/field-import-button/import-button-editor.vue +62 -1
  12. package/src/components/xform/form-designer/setting-panel/property-editor/field-import-button/import2-button-editor.vue +236 -96
  13. package/src/components/xform/form-designer/setting-panel/property-editor/field-table-export-button/empty-number-input.vue +51 -0
  14. package/src/components/xform/form-designer/setting-panel/property-editor/field-table-export-button/select-export-item-button-editor.vue +27 -2
  15. package/src/components/xform/form-designer/setting-panel/property-editor/field-table-export-button/table-export-button-editor.vue +33 -3
  16. package/src/components/xform/form-designer/widget-panel/widgetsConfig.js +31 -0
  17. package/src/components/xform/form-render/container-item/data-table-mixin.js +3 -1
  18. 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" +0 -389
  19. 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" +0 -233
@@ -1,1833 +1,2641 @@
1
- /**version-1.0*/
2
- import { read, utils } from "xlsx";
3
- import axios from "axios";
4
- import SparkMD5 from "spark-md5";
5
-
6
- let tmixins = {};
7
- let XLSX = window.XLSX || { read, utils };
8
- import { getToken } from "../../../../../utils/auth";
9
- import XEUtils from "xe-utils";
10
- import { getErrorMsg } from "@base/utils/index.js";
11
- import {
12
- getImportCellText,
13
- getImportFileSizeLimit,
14
- getImportRowLimit,
15
- isImportFileOversize,
16
- isImportRowsOverLimit,
17
- toFileSizeMb,
18
- } from "../../../../../utils/importLimit";
19
-
20
- let configUtil = {
21
- baseUrl: process.env.VUE_APP_BASE_API,
22
- XLSX,
23
- getToken,
24
- };
25
- // importTimer / loadingObj 原为模块级单例:两个导入弹窗并存时后者覆盖前者引用,
26
- // 前一个 interval(100ms/次)与 loading 永久失控——已改为实例属性 this.importTimer / this.loadingObj
27
- tmixins = {
28
- name: "excelImport",
29
- props: ["param", "parentTarget"],
30
- inject: ["getReportTemplate", "formHttp"],
31
- data() {
32
- return {
33
- excTime: 0,
34
- showImportDialog: false,
35
- showImportDialog2: false,
36
- falseValue: false,
37
- fileRaw: false,
38
- isEnd: false,
39
- currentIndex: 0,
40
- excelTemplate: "",
41
- prefix: "/user",
42
- dataSize: 0,
43
- totalSuccessNum: 0,
44
- totalErrorNum: 0,
45
- percentageNum: 0,
46
- successRows: [],
47
- failRows: [],
48
- oriCols: [],
49
- showImportResult: false,
50
- resultType: "",
51
- option: {},
52
- showContent: false,
53
- resOption: {},
54
- fileCellFields: [],
55
- importLoading: false,
56
- loadingTitle: null,
57
-
58
- percent1: 0,
59
- percent2: 0,
60
- percent3: 0,
61
-
62
- loadingPercent: 0,
63
- handleRowSize: 1,
64
-
65
- headerRows: 3,
66
- };
67
- },
68
- computed: {
69
- importFileLimitSize() {
70
- return Math.min(
71
- this.param?.importOption?.importFileLimitSize || 200,
72
- 200
73
- );
74
- },
75
- },
76
- created() {
77
- this.exc();
78
- },
79
- beforeDestroy() {
80
- this.clearImportTimer();
81
- },
82
- methods: {
83
- initResGird() {
84
- let that = this;
85
- let tableRef = "resGrid";
86
- let columns = [
87
- {
88
- type: "checkbox",
89
- fixed: "left",
90
- width: 48,
91
- resizable: false,
92
- headerClassName: "vxe-hcs",
93
- className: "vxe-hcs",
94
- },
95
- {
96
- field: "impNumber",
97
- title: this.$t2("行号", "components.excelImport.impNumber"),
98
- width: 100,
99
- fixed: "left",
100
- },
101
- {
102
- field: "_resultContent",
103
- title: this.$t2("结果", "components.excelImport.resultContent"),
104
- width: 250,
105
- fixed: "left",
106
- slots: {
107
- default: ({ row, $rowIndex, $table }) => {
108
- return that.handleRes(row._resultContent);
109
- },
110
- },
111
- },
112
- ];
113
- let cCols = this.oriCols.map((oriCol, index) => {
114
- let col = {
115
- field: oriCol.field,
116
- title: oriCol.title,
117
- width: 150,
118
- ...oriCol.otherConfig,
119
- };
120
- if (index === 0) {
121
- col.fixed = "left";
122
- }
123
- return col;
124
- });
125
- columns = columns.concat(cCols);
126
-
127
- let tableOption = {
128
- vue: this,
129
- tableRef: tableRef,
130
- tableNameRequired: false,
131
- columns: columns,
132
- config: {
133
- toolbarConfig: {
134
- custom: false,
135
- },
136
- },
137
- };
138
-
139
- this.$vxeTableUtil.initVxeTable(tableOption).then((opts) => {
140
- this.resOption = opts;
141
- });
142
- },
143
- exc() {
144
- this.option = this.param;
145
- this.showContent = true;
146
- this.showImportDialog = true;
147
- this.showImportDialog2 = false;
148
- },
149
- showSuccessResult() {
150
- this.showImportResult = true;
151
- this.resultType = 1;
152
- this.$nextTick(() => {
153
- this.initResGird();
154
- this.$nextTick(() => {
155
- let tDatas = this.successRows.map((item, index) => {
156
- item.id = index;
157
- return item;
158
- });
159
- this.$refs.resGrid.loadData(tDatas);
160
- });
161
- });
162
- },
163
- showErrorResult() {
164
- this.showImportResult = true;
165
- this.resultType = 0;
166
- this.$nextTick(() => {
167
- this.initResGird();
168
- this.$nextTick(() => {
169
- let tDatas = this.failRows.map((item, index) => {
170
- item.id = index;
171
- return item;
172
- });
173
- this.$refs.resGrid.loadData(tDatas);
174
- });
175
- });
176
- },
177
- fileChange(file) {
178
- if (file) {
179
- this.fileRaw = file.raw;
180
- } else {
181
- this.fileRaw = false;
182
- }
183
- },
184
- dialogClose1() {
185
- this.importLoading = false;
186
- if (!this.showImportDialog2) {
187
- this.$emit("update:visiable", false);
188
- this.showContent = false;
189
- }
190
- },
191
- async dialogPrimary1() {
192
- let that = this;
193
- let option = this.option;
194
-
195
- let fileRaw = this.fileRaw;
196
- if (!fileRaw) {
197
- this.$baseAlert(
198
- this.$t2("请上传附件", "components.excelImport.warmMsg1")
199
- );
200
- return false;
201
- }
202
- let importFileLimitSize = this.importFileLimitSize;
203
- let fileSize = fileRaw.size;
204
- let limitSize = importFileLimitSize * 1024 * 1024;
205
- if (fileSize > limitSize) {
206
- this.$baseAlert(
207
- this.$t1("导入文件不允许超过{fileSize}MB", {
208
- fileSize: importFileLimitSize,
209
- })
210
- );
211
- return false;
212
- }
213
-
214
- if (option.importFrontOnly) {
215
- this.dialogPrimary3();
216
- return false;
217
- }
218
-
219
- /* this.createImportTimer();
220
- this.showImportDialog = false;
221
- if (!option.onConfirm) {
222
- this.showImportDialog2 = true;
223
- } */
224
- // this.$nextTick(() => {
225
- try {
226
- this.readExcel(this.fileRaw, function (resultData) {
227
- that.createImportTimer();
228
- that.showImportDialog = false;
229
- if (!option.onConfirm) {
230
- that.showImportDialog2 = true;
231
- }
232
- that.$nextTick(() => {
233
- if (option.onConfirm) {
234
- option.onConfirm(resultData.data, that.fileRaw);
235
- } else {
236
- that.handleBeforeImport0(that.fileRaw, resultData, () => {
237
- that.syncDataForImport({
238
- multi: false,
239
- data: resultData,
240
- // saveUrl: saveUrl
241
- });
242
- });
243
- }
244
- });
245
- });
246
- } catch (et) {
247
- that.clearImportTimer();
248
- console.error(et);
249
- }
250
- // });
251
- },
252
- handleBeforeImport(file, resultData, callback) {
253
- let importOption = this.option.importOption;
254
- let that = this;
255
- if (importOption.onBeforeImport) {
256
- const toDo = (res) => {
257
- let rows = res?.objx || [];
258
- rows.forEach((row, rowIndex) => {
259
- let item = resultData[rowIndex];
260
- Object.keys(row).forEach((key) => {
261
- item[key] = row[key];
262
- });
263
- });
264
- callback && callback();
265
- };
266
- this.getFieldTarget().handleCustomEvent(
267
- importOption.onBeforeImport,
268
- ["file", "resultData", "done"],
269
- [file, resultData, toDo]
270
- );
271
- } else {
272
- callback && callback();
273
- }
274
- },
275
- handleBeforeImport0(file, resultData, callback) {
276
- if (!this.showImportDialog2) return;
277
- let importOption = this.option.importOption;
278
- if (!importOption.enabledImportPreHandle) {
279
- this.handleBeforeImport(file, resultData, () => {
280
- callback && callback();
281
- });
282
- return;
283
- }
284
-
285
- this.uploadFile(() => {
286
- this.importLoading = false;
287
- this.handleBeforeImport(file, resultData, () => {
288
- callback && callback();
289
- });
290
- });
291
- return;
292
- },
293
- handleBeforeClose(hide) {
294
- this.option.onCancel &&
295
- this.option.onCancel(this.getCallbackData(), this.fileRaw);
296
- let importOption = this.option.importOption;
297
- this.getFieldTarget().handleCustomEvent(
298
- importOption.onSuccessImport,
299
- ["resultData", "file"],
300
- [this.getCallbackData(), this.fileRaw]
301
- );
302
- this.handleTableCallback();
303
- hide();
304
- },
305
- dialogClose2() {
306
- this.showImportDialog2 = false;
307
- this.option.onBeforeCancel &&
308
- this.option.onBeforeCancel(this.getCallbackData(), this.fileRaw);
309
- let importOption = this.option.importOption;
310
- this.getFieldTarget().handleCustomEvent(
311
- importOption.onSuccessImport,
312
- ["resultData", "file"],
313
- [this.getCallbackData(), this.fileRaw]
314
- );
315
- this.handleTableCallback();
316
- },
317
- handleClose2() {
318
- this.clearImportTimer();
319
- this.showImportDialog2 = false;
320
- this.showContent = false;
321
- this.option.onClose &&
322
- this.option.onClose(this.getCallbackData(), this.fileRaw);
323
- this.option.callback &&
324
- this.option.callback(this.getCallbackData(), this.fileRaw);
325
- this.dialogClose1();
326
- },
327
- dialogPrimary2() {
328
- let importOption = this.option.importOption;
329
- this.showImportDialog2 = false;
330
- this.option.onBeforeConfirm &&
331
- this.option.onBeforeConfirm(this.getCallbackData(), this.fileRaw);
332
- this.getFieldTarget().handleCustomEvent(
333
- importOption.onSuccessImport,
334
- ["resultData", "file"],
335
- [this.getCallbackData(), this.fileRaw]
336
- );
337
- this.handleTableCallback();
338
- },
339
- getCallbackData() {
340
- return {
341
- ...this.resultData,
342
- successRows: this.successRows,
343
- failRows: this.failRows,
344
- };
345
- },
346
-
347
- /*dialogClose2(flag) {
348
- let importOption = this.option.importOption;
349
- this.clearImportTimer();
350
- this.showImportDialog2 = false;
351
- this.dialogClose1();
352
- this.handleTableCallback(flag);
353
- this.parentTarget.handleCustomEvent(importOption.onSuccessImport, ['resultData', 'file'], [this.resultData, this.fileRaw])
354
- this.option.callback && this.option.callback(this.resultData, this.fileRaw);
355
- },
356
- dialogPrimary2() {
357
- let importOption = this.option.importOption;
358
- this.clearImportTimer();
359
- this.showImportDialog2 = false;
360
- this.dialogClose1()
361
- this.handleTableCallback();
362
- let fileRaw = this.fileRaw;
363
- this.parentTarget.handleCustomEvent(importOption.onSuccessImport, ['resultData', 'file'], [this.resultData, this.fileRaw])
364
- this.option.callback && this.option.callback(this.resultData, this.fileRaw);
365
- },*/
366
- handleTableCallback(flag) {
367
- if (flag === 0) return;
368
- let importOption = this.option.importOption;
369
- let tableTarget;
370
- if (importOption.tableRef)
371
- tableTarget = this.getFieldTarget().getWidgetRef(importOption.tableRef);
372
- if (tableTarget) {
373
- let isQueryTable = tableTarget.widget.options.isQueryTable;
374
- if (isQueryTable) {
375
- tableTarget.searchEvent();
376
- }
377
- }
378
- },
379
- async readExcel(file, callback) {
380
- let that = this;
381
- let option = this.option;
382
-
383
- // 业务侧的 importFileLimitSize 允许到 200MB,而这里是整文件 readAsBinaryString +
384
- // XLSX.read 同步解析,占用约为文件体积的 5~20 倍 —— 那个量级必然把标签页撑爆,
385
- // 且崩在库内部,连提示都给不出来。这里再收一道解析安全上限,可用
386
- // importOption.maxFileSize 覆盖(0 表示不限制)。
387
- if (isImportFileOversize(file, option)) {
388
- this.$baseAlert(
389
- this.$t1(
390
- "导入文件过大({size}MB),当前最多支持 {limit}MB,请拆分后分批导入",
391
- {
392
- size: toFileSizeMb(file.size),
393
- limit: toFileSizeMb(getImportFileSizeLimit(option)),
394
- }
395
- )
396
- );
397
- // 与上游那道 importFileLimitSize 校验保持一致:只提示、不关弹窗,
398
- // 用户可以直接重选一个小文件。
399
- return false;
400
- }
401
-
402
- if (option.importOption.multipleSheet) {
403
- this.readExcel2(file, callback);
404
- return;
405
- }
406
-
407
- const fileReader = new FileReader();
408
- fileReader.onload = async (ev) => {
409
- let resultData;
410
- try {
411
- const data = ev.target.result;
412
- const workbook = configUtil.XLSX.read(data, {
413
- type: "binary",
414
- });
415
-
416
- let sheet = workbook.Sheets[workbook.SheetNames[0]];
417
- if (sheet["!ref"] === undefined || sheet["!ref"] === null) {
418
- /*that.showImportDialog2 = false;
419
- that.showContent = false;
420
- that.clearImportTimer();*/
421
- that.$baseAlert(
422
- this.$t2(
423
- "请使用正确的导入模版!",
424
- "components.excelImport.warmMsg2"
425
- )
426
- );
427
- that.handleClose2();
428
- return false;
429
- }
430
- let range = configUtil.XLSX.utils.decode_range(sheet["!ref"]);
431
- let dataSize = range.e.r + 1; //行数
432
- that.dataSize = dataSize - 3 >= 0 ? dataSize - 3 : 0;
433
- // 行数闸门:下面逐格取值、建行对象、再整批发包全是同步的,行数失控时主线程会被
434
- // 占死甚至 OOM。文件体积不大但行极多(窄表)时只有这一道能拦。
435
- if (isImportRowsOverLimit(that.dataSize, option)) {
436
- that.clearImportTimer();
437
- that.$baseAlert(
438
- that.$t1(
439
- "导入数据 {size} 行,超过单次上限 {limit} 行,请拆分后分批导入",
440
- {
441
- size: that.dataSize,
442
- limit: getImportRowLimit(option),
443
- }
444
- )
445
- );
446
- that.handleClose2();
447
- return false;
448
- }
449
- let titles = [];
450
- let fields = [];
451
-
452
- const ws = workbook.Sheets[sheet];
453
-
454
- let prefix = option.prefix;
455
- let excelUrl = option.excel;
456
-
457
- let excelRows = [];
458
- let row;
459
- let rowNum;
460
- let colNum;
461
- for (rowNum = range.s.r; rowNum <= range.e.r; rowNum++) {
462
- row = [];
463
- for (colNum = range.s.c; colNum <= range.e.c; colNum++) {
464
- let nextCell =
465
- sheet[
466
- configUtil.XLSX.utils.encode_cell({
467
- r: rowNum,
468
- c: colNum,
469
- })
470
- ];
471
- if (typeof nextCell === "undefined") {
472
- row.push(void 0);
473
- } else {
474
- row.push(getImportCellText(nextCell));
475
- }
476
- }
477
- if (rowNum === 1) {
478
- titles = row;
479
- }
480
- if (rowNum === 2) {
481
- fields = row;
482
- let checkResult = true;
483
- /*await that.checkExcelForImport(titles, fields, excelUrl, prefix).then(
484
- res => {
485
- if (res.type !== "success") {
486
- checkResult = false;
487
- }
488
- });
489
- if (!checkResult) {
490
- that.showContent = false;
491
- that.clearImportTimer();
492
- return false;
493
- }*/
494
- }
495
- if (rowNum > 2) {
496
- let item = {};
497
- for (let i = 0; i < fields.length; i++) {
498
- let field = fields[i];
499
- if (field) {
500
- item[field] = row[i] ?? null;
501
- }
502
- }
503
- excelRows.push(item);
504
- }
505
- }
506
- if (
507
- !titles.length ||
508
- !fields.length ||
509
- titles.length !== fields.length
510
- ) {
511
- // that.showContent = false;
512
- // that.clearImportTimer();
513
- that.$baseAlert(
514
- this.$t2(
515
- "请使用正确的导入模版!",
516
- "components.excelImport.warmMsg2"
517
- )
518
- );
519
- that.handleClose2();
520
- return false;
521
- }
522
-
523
- let columns = [];
524
- let cols = [];
525
- let uniqueFields = [];
526
- for (let i = 0; i < titles.length; i++) {
527
- let title = titles[i];
528
- if (title) {
529
- let field = fields[i];
530
- if (field === null || field === undefined) {
531
- // that.showContent = false;
532
- // that.clearImportTimer();
533
- that.$baseAlert(
534
- this.$t2(
535
- "请使用正确的导入模版!",
536
- "components.excelImport.warmMsg2"
537
- )
538
- );
539
- that.handleClose2();
540
- return false;
541
- }
542
- let t = title.substr(-1);
543
- let u = title.length >= 2 ? title.substr(-2) : title;
544
- let isNeed = false;
545
- if (t === "*") {
546
- isNeed = true;
547
- }
548
- let isUnique = false;
549
- if (u === "**") {
550
- uniqueFields.push(field);
551
- isUnique = true;
552
- }
553
-
554
- let otherConfig = {};
555
- let isFile = false;
556
- if (field.startsWith("attachments_")) {
557
- that.fileCellFields.push(field);
558
- isFile = true;
559
- otherConfig = {
560
- slots: {
561
- default: "attachment",
562
- },
563
- };
564
- }
565
-
566
- let col = {
567
- field: field,
568
- title: title,
569
- width: 100,
570
- isNeed: isNeed,
571
- isUnique: isUnique,
572
- isFile,
573
- otherConfig,
574
- };
575
- cols.push(col);
576
- }
577
- }
578
-
579
- let oriCols = JSON.parse(JSON.stringify(cols)); //列信息
580
- if (uniqueFields.length) {
581
- //分组唯一字段为空时,全部不发包。
582
- for (let i = 0; i < oriCols.length; i++) {
583
- let oriCol = oriCols[i];
584
- if (oriCol.isNeed) {
585
- for (let j = 0; j < excelRows.length; j++) {
586
- let data = excelRows[j];
587
- let value = data[oriCol.field];
588
- if (value === null || value === undefined || value === "") {
589
- that.clearImportTimer();
590
- that.$baseAlert(
591
- this.$t2(
592
- "第" + (j + 4) + "行[" + oriCol.title + "]不能为空;",
593
- "components.excelImport.warmMsg3",
594
- {
595
- lineNo: j + 4,
596
- title: oriCol.title,
597
- }
598
- )
599
- );
600
- that.handleClose2();
601
- return false;
602
- }
603
- }
604
- }
605
- }
606
- }
607
-
608
- this.oriCols = oriCols;
609
-
610
- resultData = {
611
- data: excelRows,
612
- titles: titles,
613
- fields: fields,
614
- uniqueFields: uniqueFields,
615
- oriCols: oriCols,
616
- };
617
- this.resultData = resultData;
618
- callback(resultData);
619
- } catch (et) {
620
- that.showContent = false;
621
- that.clearImportTimer();
622
- console.error(et);
623
- }
624
- };
625
- fileReader.readAsBinaryString(file);
626
- },
627
- async checkExcelForImport(titles, fields, turl, prefix) {
628
- if (!this.showImportDialog2) return;
629
- let that = this;
630
- let data = {
631
- filed_name: fields,
632
- field_note: titles,
633
- url: turl,
634
- };
635
- let param = JSON.stringify(data);
636
- return this.$http({
637
- url: prefix + "/sf_common/compareExcelFields",
638
- method: "post",
639
- headers: {
640
- "Content-Type": "application/json;charset=utf-8",
641
- },
642
- // data: param,
643
- data: data,
644
- });
645
- },
646
- saveImportHandle(data, opt) {
647
- let multi = this.option.multi;
648
- let importOption = this.option.importOption;
649
-
650
- let entity = importOption.importEntity;
651
-
652
- if (!entity) return;
653
-
654
- let reportTemplate = this.getReportTemplate();
655
- let formCode = reportTemplate?.formCode;
656
- let scriptCode = importOption.importScriptCode || "import";
657
- // let config = option?.config;
658
-
659
- // let reqData = this.getReqFormData();
660
- let fileCellFields = this.fileCellFields;
661
- let formData = null;
662
- if (multi) {
663
- formData = data;
664
- } else {
665
- formData = {
666
- [entity]: { ...data },
667
- };
668
- if (fileCellFields.length) {
669
- fileCellFields.forEach((field) => {
670
- let attachments = data[field] || [];
671
- delete formData[entity][field];
672
- formData[field] = attachments;
673
- });
674
- }
675
- }
676
-
677
- let reqData = {
678
- formCode: formCode,
679
- formVersion: reportTemplate.formVersion,
680
- taBm: entity,
681
- data: formData,
682
- };
683
-
684
- this.formHttp({
685
- scriptCode: scriptCode,
686
- data: reqData,
687
- modal: false,
688
- ...opt,
689
- });
690
- },
691
- syncDataForImport(opts) {
692
- let that = this;
693
- let multi = this.option.multi;
694
- let resultData = this.resultData;
695
- let datas = resultData.data;
696
- let titles = resultData.titles;
697
- let fields = resultData.fields;
698
- let uniqueFields = resultData.uniqueFields;
699
- let saveUrl = opts.saveUrl;
700
- let size = datas.length;
701
- let index = 0;
702
- let successNum = 0;
703
- let failNum = 0;
704
- let dialogObj = this.$refs.importDialog.$el;
705
- let successNumObj = dialogObj.querySelector(".successNum");
706
- let failNumObj = dialogObj.querySelector(".failNum");
707
- let percentageObj = dialogObj.querySelector(".percentage");
708
- let doneNumObj = dialogObj.querySelector(".doneNum");
709
- dialogObj.querySelector(".dataSize").innerText = size;
710
-
711
- that.successRows = [];
712
- that.failRows = [];
713
-
714
- let multiSize = this.option.multiSize || 2;
715
-
716
- let groupIndex = 0;
717
- let groupDatas = [];
718
- if (uniqueFields.length) {
719
- let d = XEUtils.groupBy(datas, function (item, itemIndex, items) {
720
- let keyArr = [];
721
- uniqueFields.forEach((field) => {
722
- keyArr.push(item[field]);
723
- });
724
- return keyArr.join("&_&");
725
- });
726
- for (let key in d) {
727
- groupDatas.push(d[key]);
728
- }
729
- datas.forEach((item, itemIndex) => {
730
- item.impSeq = itemIndex;
731
- item.impNumber = itemIndex + 4;
732
- });
733
- }
734
-
735
- let compareV = function (property) {
736
- return function (a, b) {
737
- let value1 = a[property];
738
- let value2 = b[property];
739
- return value1 - value2;
740
- };
741
- };
742
-
743
- let prevHandleData = function (percentageStr, excNum) {
744
- successNumObj.innerText = successNum;
745
- failNumObj.innerText = failNum;
746
- doneNumObj.innerText = successNum + failNum;
747
- that.percentageNum = percentageStr;
748
- let isEnd = false;
749
- if (multi && uniqueFields.length) {
750
- isEnd = groupIndex + 1 >= groupDatas.length;
751
- } else {
752
- isEnd = index + excNum >= size;
753
- }
754
- if (!isEnd) {
755
- index = index + excNum;
756
- groupIndex = groupIndex + 1;
757
- setTimeout(function () {
758
- handleData();
759
- }, 1);
760
- } else {
761
- setTimeout(function () {
762
- that.clearImportTimer();
763
- }, 300);
764
- }
765
- };
766
-
767
- let getDefaultValue = (row) => {
768
- let value = null;
769
- let defaultValue = this.option.defaultValue;
770
- if (defaultValue) {
771
- if (typeof defaultValue === "function") {
772
- value = defaultValue(row) || {};
773
- } else {
774
- value = defaultValue || {};
775
- }
776
- }
777
- return value;
778
- };
779
-
780
- let handleData = () => {
781
- if (!that.isEnd && index < size) {
782
- let param;
783
- let excNum;
784
- if (multi) {
785
- if (!uniqueFields.length) {
786
- if (index + multiSize < size) {
787
- excNum = multiSize;
788
- } else {
789
- excNum = size - index;
790
- }
791
-
792
- let dataList = [];
793
- for (let i = 0; i < excNum; i++) {
794
- let mIndex = index + i;
795
- let mData = datas[mIndex];
796
- mData.impSeq = mIndex;
797
- mData.impNumber = mIndex + 4;
798
-
799
- let errorInfo = that.vailData(mData, opts);
800
- if (errorInfo.length > 0) {
801
- mData._resultType = "error";
802
- mData._resultContent = errorInfo;
803
- failNum++;
804
- that.failRows.push(mData);
805
- } else {
806
- let defaultValue = getDefaultValue(mData);
807
- if (defaultValue) {
808
- Object.assign(mData, defaultValue);
809
- }
810
- dataList.push(mData);
811
- }
812
-
813
- datas[mIndex] = mData;
814
- }
815
- if (dataList.length > 0) {
816
- param = dataList;
817
- }
818
- } else {
819
- excNum = groupDatas[groupIndex].length;
820
- param = groupDatas[groupIndex];
821
- }
822
- } else {
823
- let data = that.resultData.data[index];
824
- data.impSeq = index;
825
- data.impNumber = index + 4;
826
- excNum = 1;
827
- datas[index] = data;
828
-
829
- let errorInfo = that.vailData(data, opts);
830
- if (errorInfo.length > 0) {
831
- data._resultType = "error";
832
- data._resultContent = errorInfo;
833
- failNum++;
834
- that.failRows.push(data);
835
- } else {
836
- param = data;
837
- let defaultValue = getDefaultValue(data);
838
- if (defaultValue) {
839
- Object.assign(param, defaultValue);
840
- }
841
- }
842
- }
843
-
844
- let percentageStr = parseInt(((index + excNum) * 100) / size);
845
- if (param === null || param === undefined || param.length === 0) {
846
- if (that.showContent) {
847
- prevHandleData(percentageStr, excNum);
848
- }
849
- return;
850
- }
851
-
852
- that.saveImportHandle(param, {
853
- failMsg: false,
854
- callback: (resultMsg) => {
855
- if (multi) {
856
- if (resultMsg.type === "success") {
857
- let multiRows = resultMsg.objx;
858
- for (let i = 0; i < multiRows.length; i++) {
859
- let multiRow = multiRows[i];
860
- let data = datas[multiRow.impSeq];
861
- data._resultType = multiRow.impReturnType;
862
- data._resultContent =
863
- multiRow.impReturnType === "success"
864
- ? this.$t2(
865
- "导入成功",
866
- "components.excelImport.importSucess"
867
- )
868
- : multiRow.impReturnMsg;
869
- if (multiRow.impReturnType === "success") {
870
- successNum++;
871
- that.successRows.push(data);
872
- } else {
873
- failNum++;
874
- that.failRows.push(data);
875
- }
876
- }
877
-
878
- that.failRows = that.failRows.sort(compareV("impSeq"));
879
- } else {
880
- // error_win(resultMsg);
881
- // layer.close(opts.dialogObjIndex);
882
-
883
- let items = param.map((item) => {
884
- return {
885
- ...item,
886
- _resultType: "error",
887
- _resultContent: resultMsg.content,
888
- };
889
- });
890
- failNum = failNum + items.length;
891
- that.failRows.push(...items);
892
-
893
- that.failRows = that.failRows.sort(compareV("impSeq"));
894
-
895
- /* that.$errorMsg(resultMsg);
896
- that.showContent = false;
897
- that.clearImportTimer();
898
- return; */
899
- }
900
- } else {
901
- let data = that.resultData.data[index];
902
- data._resultType = resultMsg.type;
903
- data._resultContent =
904
- resultMsg.type === "success"
905
- ? this.$t2(
906
- "导入成功",
907
- "components.excelImport.importSucess"
908
- )
909
- : resultMsg.content;
910
-
911
- if (resultMsg.type === "success") {
912
- successNum++;
913
- that.successRows.push(data);
914
- } else {
915
- failNum++;
916
- that.failRows.push(data);
917
- }
918
- }
919
- if (that.showContent) {
920
- prevHandleData(percentageStr, excNum);
921
- }
922
- },
923
- error: (et) => {
924
- that.isEnd = true;
925
- that.clearImportTimer();
926
- console.error(et);
927
- },
928
- });
929
- }
930
- };
931
- handleData();
932
- },
933
- commonLocalDownload() {
934
- let importOption = this.option.importOption;
935
- let importAttachCode = importOption.importAttachCode;
936
- if (!importAttachCode) return;
937
- this.$http({
938
- url: USER_PREFIX + "/bd_attach_setting/getByCode",
939
- method: "post",
940
- data: {
941
- stringOne: importAttachCode,
942
- },
943
- success: (res) => {
944
- let fileObj = res.objx;
945
- let url = fileObj?.url;
946
- if (fileObj && url) {
947
- this.$commonFileUtil.downloadFile(url,fileObj.name);
948
- }
949
- },
950
- });
951
- },
952
- vailData(data, opts) {
953
- let oriCols = opts.data.oriCols;
954
- let errorInfo = [];
955
- for (let i = 0; i < oriCols.length; i++) {
956
- let oriCol = oriCols[i];
957
- if (oriCol.isNeed) {
958
- let value = data[oriCol.field];
959
- if (value === null || value === undefined || value === "") {
960
- errorInfo.push(
961
- this.$t2(
962
- "[" + oriCol.title + "]不能为空;",
963
- "components.excelImport.warmMsg4",
964
- { title: oriCol.title }
965
- )
966
- );
967
- }
968
- }
969
- }
970
- return errorInfo.join("");
971
- },
972
- handleRes(val) {
973
- /* let content = val;
974
- let text = content;
975
- if (content && content.indexOf("Exception") >= 0) {
976
- let index0 = content.lastIndexOf("*]");
977
- if (index0 >= 0) {
978
- text = content.substr(index0);
979
- if (text) {
980
- let index1 = text.indexOf("Exception:");
981
- if (index1 >= 0) {
982
- text = text.substr(index1 + 10);
983
- }
984
- }
985
- }
986
- } */
987
- let text = getErrorMsg(val);
988
- return text;
989
- },
990
- exportToExcel() {
991
- let str =
992
- this.resultType === 1
993
- ? this.$t2("成功", "components.excelImport.success")
994
- : this.$t2("失败", "components.excelImport.fail");
995
- let fileName =
996
- this.$t2("导入结果导出", "components.excelImport.exportFileName") +
997
- "(" +
998
- str +
999
- ")";
1000
- let $grid = this.$refs.resGrid;
1001
- let columns = $grid.getColumns();
1002
- if (columns && columns.length) {
1003
- let fields = columns
1004
- .filter((item, index) => {
1005
- return index > 0 && item.property;
1006
- })
1007
- .map((item, index) => {
1008
- return item.property;
1009
- });
1010
- this.$refs.resGrid.exportData({
1011
- filename: fileName,
1012
- mode: "all",
1013
- type: "csv",
1014
- columns: fields,
1015
- });
1016
- }
1017
- },
1018
- createImportTimer() {
1019
- this.excTime = 0;
1020
- let beginTime = new Date().getTime();
1021
- this.importTimer = setInterval(() => {
1022
- let nowTime = new Date().getTime();
1023
- let diffTime = (nowTime - beginTime) / 1000;
1024
- this.$set(this, "excTime", this.$baseLodash.round(diffTime, 2));
1025
- }, 100);
1026
- },
1027
- clearImportTimer() {
1028
- clearInterval(this.importTimer);
1029
- this.importTimer = null;
1030
- this.loadingObj && this.loadingObj.close();
1031
- this.loadingObj = null;
1032
- },
1033
- getWidgetTarget() {},
1034
- getFieldTarget() {
1035
- return this.param.target;
1036
- },
1037
- async dialogPrimary3() {
1038
- if (!this.fileRaw) {
1039
- this.$baseAlert(
1040
- this.$t2("请上传附件", "components.excelImport.warmMsg1")
1041
- );
1042
- return false;
1043
- }
1044
- let that = this;
1045
- let option = this.option;
1046
- this.loadingObj = this.$baseLoading({
1047
- target: document.body,
1048
- background: "unset",
1049
- });
1050
-
1051
- let importOption = option.importOption;
1052
- this.readExcel(this.fileRaw, function (resultData) {
1053
- let file = that.fileRaw;
1054
- let flag = false;
1055
- let done = () => {
1056
- if (flag) return;
1057
- flag = true;
1058
- that.showImportDialog = false;
1059
- return that
1060
- .getFieldTarget()
1061
- .handleCustomEvent(
1062
- importOption.onSuccessImport,
1063
- ["resultData", "file"],
1064
- [resultData, file]
1065
- );
1066
- };
1067
- const close = () => {
1068
- that.dialogClose1();
1069
- }
1070
- try {
1071
- if (importOption.onConfirmImportEnabled) {
1072
- if (
1073
- that
1074
- .getFieldTarget()
1075
- .handleCustomEvent(
1076
- importOption.onConfirmImport,
1077
- ["resultData", "file", "done", 'close'],
1078
- [resultData, file, done, close]
1079
- ) !== false
1080
- ) {
1081
- done();
1082
- }
1083
- } else if (importOption.tableRef) {
1084
- let tableRef = that
1085
- .getFieldTarget()
1086
- .getWidgetRef(importOption.tableRef);
1087
- tableRef && tableRef.addTableData(resultData.data);
1088
- done();
1089
- }
1090
- } catch (et) {
1091
- console.error(et);
1092
- }
1093
- setTimeout(function () {
1094
- that.loadingObj && that.loadingObj.close();
1095
- }, 200);
1096
- });
1097
- },
1098
-
1099
- //begin
1100
- fileParse(file, type = "base64", callback) {
1101
- let fileRead = new FileReader();
1102
- if (type === "base64") {
1103
- fileRead.readAsDataURL(file);
1104
- } else if (type === "buffer") {
1105
- fileRead.readAsArrayBuffer(file);
1106
- }
1107
- fileRead.onload = (ev) => {
1108
- callback(ev.target.result);
1109
- };
1110
- },
1111
- getUploadText(percent) {
1112
- return this.$t1("上传导入文件 {percent}%", { percent });
1113
- },
1114
- changeUploadPercent(percent) {
1115
- // return this.$t1('上传导入文件 {percent}%', {percent})
1116
- this.percent1 = percent;
1117
- this.loadingIndex = 1;
1118
- },
1119
- uploadFile(callback) {
1120
- let that = this;
1121
- that.loadingPercent = 0;
1122
- // that.loadingTitle = this.getUploadText(0);
1123
- that.changeUploadPercent(0);
1124
- let file = this.fileRaw;
1125
- let uploadChunkSize = 5;
1126
- this.importLoading = true;
1127
- this.fileUploadCallback = callback;
1128
- this.fileParse(file, "buffer", (buffer) => {
1129
- let spark = new SparkMD5.ArrayBuffer(),
1130
- chunkSize = uploadChunkSize * 1024 * 1024, // Read in chunks of 2MB
1131
- chunks = Math.ceil(file.size / chunkSize),
1132
- hash,
1133
- suffix;
1134
- spark.append(buffer);
1135
- hash = spark.end();
1136
- let md5 = hash;
1137
- let nameSplit = /\.([0-9a-zA-Z]+)$/i.exec(file.name);
1138
-
1139
- suffix = nameSplit && nameSplit.length > 1 ? nameSplit[1] : "";
1140
-
1141
- let chunkParam = {
1142
- fullFile: file,
1143
- chunkSize: chunkSize,
1144
- chunks: chunks,
1145
- chunkIndex: 0,
1146
- md5: hash,
1147
- uuid: "",
1148
- name: file.name,
1149
- };
1150
- file.chunkParam = chunkParam;
1151
-
1152
- this.sendRequest(file);
1153
- });
1154
- },
1155
- async sendRequest(fullFile) {
1156
- if (!this.showImportDialog2) return;
1157
- let that = this;
1158
- let uploadUrl = USER_PREFIX + "/excel_import/uploadFile";
1159
- let chunkParam = fullFile.chunkParam;
1160
- let target = that.$refs.upload;
1161
- let abort;
1162
- let config = {
1163
- headers: {
1164
- "Content-Type": "multipart/form-data",
1165
- },
1166
- cancelToken: new axios.CancelToken(function executor(c) {
1167
- abort = c;
1168
- fullFile.abort = c;
1169
- }),
1170
- onUploadProgress: (progressEvent) => {
1171
- let start = chunkParam.start;
1172
- let uploadedSize =
1173
- start +
1174
- Math.floor(
1175
- (chunkParam.currentSize * progressEvent.loaded) /
1176
- progressEvent.total
1177
- );
1178
- let percent = Math.floor((uploadedSize * 100) / fullFile.size);
1179
- if (percent === 100) {
1180
- percent = 99;
1181
- }
1182
- that.loadingPercent = percent;
1183
-
1184
- // that.$set(that, 'loadingTitle', this.getUploadText(percent))
1185
- that.changeUploadPercent(percent);
1186
- },
1187
- // failMsg: false,
1188
- modal: false,
1189
- callback: (res1) => {
1190
- if (res1.type === "success") {
1191
- let uuid = res1.objx.uuid;
1192
- chunkParam.uuid = uuid;
1193
- if (preFormData.chunk + 1 >= preFormData.chunks) {
1194
- let mergeData = {
1195
- name: chunkParam.name,
1196
- md5: chunkParam.md5,
1197
- uuid: uuid,
1198
- };
1199
- let _param = {
1200
- mergeData: mergeData,
1201
- chunkParam: chunkParam,
1202
- };
1203
- setTimeout(() => {
1204
- this.mergeFile(_param);
1205
- }, 100);
1206
- } else {
1207
- chunkParam.chunkIndex = chunkParam.chunkIndex + 1;
1208
- chunkParam.uuid = uuid;
1209
- setTimeout(() => {
1210
- this.sendRequest(fullFile);
1211
- }, 100);
1212
- }
1213
- } else {
1214
- this.dialogClose2(0);
1215
- }
1216
- return res1;
1217
- },
1218
- error: (err) => {
1219
- console.log(err);
1220
- this.dialogClose2(0);
1221
- },
1222
- }; //添加请求头
1223
-
1224
- let chunkIndex = chunkParam.chunkIndex;
1225
- let chunkSize = chunkParam.chunkSize;
1226
- let start = chunkIndex * chunkSize,
1227
- end =
1228
- start + chunkSize >= fullFile.size
1229
- ? fullFile.size
1230
- : start + chunkParam.chunkSize;
1231
- chunkParam.currentSize = end - start;
1232
- chunkParam.start = start;
1233
- chunkParam.end = end;
1234
-
1235
- let preFormData = {
1236
- file: fullFile.slice(start, end),
1237
- name: fullFile.name,
1238
- chunks: chunkParam.chunks,
1239
- chunk: chunkIndex,
1240
- md5: chunkParam.md5,
1241
- uuid: chunkParam.uuid,
1242
- };
1243
-
1244
- let formData = new FormData();
1245
- Object.keys(preFormData).forEach((key) => {
1246
- formData.append(key, preFormData[key]);
1247
- });
1248
-
1249
- let req = this.$http.post(uploadUrl, formData, config);
1250
- req.abort = () => {
1251
- abort(this.$t2("取消上传", "components.VabUpload.cancelUpload"));
1252
- };
1253
- },
1254
- mergeFile(param) {
1255
- if (!this.showImportDialog2) return;
1256
- let that = this;
1257
- let mergeData = param.mergeData;
1258
- let chunkParam = param.chunkParam;
1259
- let fullFile = chunkParam.fullFile;
1260
- let mergeUrl = USER_PREFIX + "/excel_import/mergeChunk";
1261
- let target = this.$refs.upload;
1262
- this.$http({
1263
- url: mergeUrl,
1264
- method: "post",
1265
- data: mergeData,
1266
- // failMsg: false,
1267
- modal: false,
1268
- cancelToken: new axios.CancelToken(function executor(c) {
1269
- if (fullFile) fullFile.abort = c;
1270
- }),
1271
- callback: (res) => {
1272
- if (res.type === "success") {
1273
- that.loadingPercent = 100;
1274
- // that.loadingTitle = this.getUploadText(100);
1275
- this.changeUploadPercent(100);
1276
- setTimeout(() => {
1277
- this.checkImportData();
1278
- }, 100);
1279
- } else {
1280
- this.dialogClose2(0);
1281
- }
1282
- },
1283
- error: (e) => {
1284
- console.log(err);
1285
- this.dialogClose2(0);
1286
- },
1287
- });
1288
- },
1289
- getCheckText(percent) {
1290
- return this.$t1("校验导入文件中的图片数据格式 {percent}%", { percent });
1291
- },
1292
- changeCheckTextPercent(percent) {
1293
- this.percent2 = percent;
1294
- this.loadingIndex = 2;
1295
- },
1296
- checkImportData() {
1297
- if (!this.showImportDialog2) return;
1298
- let that = this;
1299
- that.loadingPercent = 0;
1300
- // that.loadingTitle = this.getCheckText(0);
1301
- that.changeCheckTextPercent(0);
1302
- let fullFile = this.fileRaw;
1303
- let chunkParam = fullFile.chunkParam;
1304
-
1305
- this.$http({
1306
- url: USER_PREFIX + "/excel_import/checkData",
1307
- method: "post",
1308
- data: {
1309
- name: chunkParam.name,
1310
- md5: chunkParam.md5,
1311
- uuid: chunkParam.uuid,
1312
- },
1313
- // failMsg: false,
1314
- modal: false,
1315
- cancelToken: new axios.CancelToken(function executor(c) {
1316
- if (fullFile) fullFile.abort = c;
1317
- }),
1318
- onUploadProgress: (progressEvent) => {
1319
- let percent = Math.floor(
1320
- (progressEvent.loaded * 100) / progressEvent.total
1321
- );
1322
- if (percent === 100) {
1323
- percent = 99;
1324
- }
1325
- that.loadingPercent = percent;
1326
- // that.$set(that, 'loadingTitle', this.getCheckText(percent))
1327
- that.changeCheckTextPercent(percent);
1328
- },
1329
- callback: (res) => {
1330
- if (res.type === "success") {
1331
- that.loadingPercent = 100;
1332
- // that.loadingTitle = this.getCheckText(100);
1333
- that.changeCheckTextPercent(100);
1334
- this.handleImageData();
1335
- } else {
1336
- this.dialogClose2(0);
1337
- }
1338
- },
1339
- error: (e) => {
1340
- console.log(err);
1341
- this.dialogClose2(0);
1342
- },
1343
- });
1344
- },
1345
- getSaveImageText(percent) {
1346
- return this.$t1("存储导入文件中的图片 {percent}%", { percent });
1347
- },
1348
- changeSaveImageTextPercent(percent) {
1349
- // return this.$t1('存储导入文件中的图片 {percent}%', {percent})
1350
- this.percent3 = percent;
1351
- this.loadingIndex = 3;
1352
- },
1353
- handleImageData() {
1354
- let that = this;
1355
- that.loadingPercent = 0;
1356
- // that.loadingTitle = this.getSaveImageText(0);
1357
- that.changeSaveImageTextPercent(0);
1358
- this.getImportData(0);
1359
- },
1360
- getImportData(index) {
1361
- if (!this.showImportDialog2) return;
1362
- let that = this;
1363
- let rowSize = this.handleRowSize;
1364
- let fullFile = this.fileRaw;
1365
- let chunkParam = fullFile.chunkParam;
1366
- let rowDatas = this.resultData.data;
1367
- let size = rowDatas.length;
1368
- let currentLineNo = index + 1;
1369
- let rowNumber = index + 3;
1370
- let startRow = index + 3;
1371
- let endRow = index + 3 + rowSize - 1;
1372
- this.$http({
1373
- url: USER_PREFIX + "/excel_import/getData",
1374
- method: "post",
1375
- data: {
1376
- name: chunkParam.name,
1377
- md5: chunkParam.md5,
1378
- uuid: chunkParam.uuid,
1379
- startRow: startRow,
1380
- endRow: endRow,
1381
- },
1382
- // failMsg: false,
1383
- modal: false,
1384
- cancelToken: new axios.CancelToken(function executor(c) {
1385
- if (fullFile) fullFile.abort = c;
1386
- }),
1387
- callback: (res) => {
1388
- if (res.type === "success") {
1389
- let rows = res?.objx?.datas || [];
1390
- let fileCellFields = this.fileCellFields;
1391
- rows.forEach((row) => {
1392
- let rowIndex = row.row - 3;
1393
- let item = rowDatas[rowIndex];
1394
- Object.keys(row).forEach((key) => {
1395
- if (fileCellFields.includes(key)) {
1396
- let value = row[key] || [];
1397
- if (!Array.isArray(value)) {
1398
- value = [value];
1399
- }
1400
- item[key] = value;
1401
- }
1402
- });
1403
- });
1404
- let cIndex = index + rowSize - 1;
1405
- let percent = Math.floor(((cIndex + 1) * 100) / size);
1406
- that.loadingPercent = percent;
1407
- // that.loadingTitle = this.getSaveImageText(percent);
1408
- that.changeSaveImageTextPercent(percent);
1409
- if (cIndex + 1 < size) {
1410
- this.getImportData(cIndex + 1);
1411
- } else {
1412
- this.fileUploadCallback();
1413
- }
1414
- } else {
1415
- this.dialogClose2(0);
1416
- }
1417
- },
1418
- error: (e) => {
1419
- this.dialogClose2(0);
1420
- },
1421
- });
1422
- },
1423
- async generateNewExcel() {
1424
- let file = this.fileRaw;
1425
- try {
1426
- // 1. 读取原始文件
1427
- const workbook = XLSX.read(file, { type: "binary" });
1428
- const sheetName = workbook.SheetNames[0];
1429
- const rawData = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]);
1430
-
1431
- // 2. 提取表头和数据
1432
- const headers = rawData.slice(0, this.headerRows);
1433
- const bodyData = rawData.slice(this.headerRows);
1434
-
1435
- let exporLineNos = [];
1436
- if (this.resultType) {
1437
- exporLineNos = this.successRows.map((item) => item.impNumber - 1);
1438
- } else {
1439
- exporLineNos = this.failRows.map((item) => item.impNumber - 1);
1440
- }
1441
- // 3. 应用筛选规则
1442
- let selectRule = (row, index) => {
1443
- return exporLineNos.includes(index);
1444
- };
1445
- const filteredData = bodyData.filter(
1446
- (row, index) => {
1447
- return selectRule(row, index + this.headerRows);
1448
- }
1449
- // this.selectRule(row, index + this.headerRows) // 注意索引转换
1450
- );
1451
-
1452
- // 4. 合并数据
1453
- const newData = [...headers, ...filteredData];
1454
-
1455
- // 5. 生成新文件
1456
- const newWorkbook = XLSX.utils.book_new();
1457
- const newSheet = XLSX.utils.aoa_to_sheet(newData);
1458
- XLSX.utils.book_append_sheet(newWorkbook, newSheet, "Sheet1");
1459
-
1460
- // 6. 下载文件
1461
- const blob = XLSX.write(newWorkbook, {
1462
- bookType: "xlsx",
1463
- type: "array",
1464
- });
1465
-
1466
- const url = URL.createObjectURL(blob);
1467
- const a = document.createElement("a");
1468
- a.href = url;
1469
- let fileName = null;
1470
- if (this.resultType) {
1471
- fileName = `导入结果(成功).xlsx`;
1472
- } else {
1473
- fileName = `导入结果(失败).xlsx`;
1474
- }
1475
- a.download = fileName;
1476
- a.click();
1477
-
1478
- // 7. 更新进度
1479
- this.progress = 100;
1480
- setTimeout(() => (this.progress = 0), 1000);
1481
- } catch (error) {
1482
- console.error("处理文件时出错:", error);
1483
- }
1484
- },
1485
- //end
1486
- getLoadingClass(flag, percent) {
1487
- let classStr = null;
1488
- let loadingIndex = this.loadingIndex;
1489
- if (percent >= 100) {
1490
- classStr = "ok";
1491
- } else if (flag === loadingIndex) {
1492
- classStr = "loading";
1493
- }
1494
- return classStr;
1495
- },
1496
-
1497
- /***sheet1 */
1498
- async readExcel2(file, callback) {
1499
- let that = this;
1500
- let option = this.option;
1501
-
1502
- const fileReader = new FileReader();
1503
- fileReader.onload = async (ev) => {
1504
- let resultData;
1505
- try {
1506
- const data = ev.target.result;
1507
- const workbook = configUtil.XLSX.read(data, {
1508
- type: "binary",
1509
- });
1510
-
1511
- let sheetDatas = [];
1512
- for (let pi = 0; pi < workbook.SheetNames.length; pi++) {
1513
- let sheetName = workbook.SheetNames[pi];
1514
- let sheet = workbook.Sheets[sheetName];
1515
-
1516
- if (sheet["!ref"] === undefined || sheet["!ref"] === null) {
1517
- that.$baseAlert(
1518
- this.$t2(
1519
- "请使用正确的导入模版!",
1520
- "components.excelImport.warmMsg2"
1521
- )
1522
- );
1523
- that.handleClose2();
1524
- return false;
1525
- }
1526
- let range = configUtil.XLSX.utils.decode_range(sheet["!ref"]);
1527
- let dataSize = range.e.r + 1; //行数
1528
- that.dataSize = dataSize - 3 >= 0 ? dataSize - 3 : 0;
1529
- // 行数闸门:下面逐格取值、建行对象、再整批发包全是同步的,行数失控时主线程会被
1530
- // 占死甚至 OOM。文件体积不大但行极多(窄表)时只有这一道能拦。
1531
- if (isImportRowsOverLimit(that.dataSize, option)) {
1532
- that.clearImportTimer();
1533
- that.$baseAlert(
1534
- that.$t1(
1535
- "导入数据 {size} 行,超过单次上限 {limit} 行,请拆分后分批导入",
1536
- {
1537
- size: that.dataSize,
1538
- limit: getImportRowLimit(option),
1539
- }
1540
- )
1541
- );
1542
- that.handleClose2();
1543
- return false;
1544
- }
1545
- let titles = [];
1546
- let fields = [];
1547
-
1548
- const ws = workbook.Sheets[sheet];
1549
-
1550
- let prefix = option.prefix;
1551
- let excelUrl = option.excel;
1552
-
1553
- let excelRows = [];
1554
- let row;
1555
- let rowNum;
1556
- let colNum;
1557
- for (rowNum = range.s.r; rowNum <= range.e.r; rowNum++) {
1558
- row = [];
1559
- for (colNum = range.s.c; colNum <= range.e.c; colNum++) {
1560
- let nextCell =
1561
- sheet[
1562
- configUtil.XLSX.utils.encode_cell({
1563
- r: rowNum,
1564
- c: colNum,
1565
- })
1566
- ];
1567
- if (typeof nextCell === "undefined") {
1568
- row.push(void 0);
1569
- } else {
1570
- row.push(getImportCellText(nextCell));
1571
- }
1572
- }
1573
- if (rowNum === 1) {
1574
- titles = row;
1575
- }
1576
- if (rowNum === 2) {
1577
- fields = row;
1578
- let checkResult = true;
1579
- }
1580
- if (rowNum > 2) {
1581
- let item = {};
1582
- for (let i = 0; i < fields.length; i++) {
1583
- let field = fields[i];
1584
- if (field) {
1585
- item[field] = row[i] ?? null;
1586
- }
1587
- }
1588
- excelRows.push(item);
1589
- }
1590
- }
1591
- if (
1592
- !titles.length ||
1593
- !fields.length ||
1594
- titles.length !== fields.length
1595
- ) {
1596
- that.$baseAlert(
1597
- this.$t2(
1598
- "请使用正确的导入模版!",
1599
- "components.excelImport.warmMsg2"
1600
- )
1601
- );
1602
- // that.handleClose2();
1603
- return false;
1604
- }
1605
-
1606
- let columns = [];
1607
- let cols = [];
1608
- let uniqueFields = [];
1609
- let uniqueKeys = [];
1610
- for (let i = 0; i < titles.length; i++) {
1611
- let title = titles[i];
1612
- if (title) {
1613
- let field = fields[i];
1614
- if (field === null || field === undefined) {
1615
- that.$baseAlert(
1616
- this.$t2(
1617
- "请使用正确的导入模版!",
1618
- "components.excelImport.warmMsg2"
1619
- )
1620
- );
1621
- // that.handleClose2();
1622
- return false;
1623
- }
1624
- let t = title.substr(-1);
1625
- let u = title.length >= 2 ? title.substr(-2) : title;
1626
- let p = title.length >= 3 ? title.substr(-3) : title;
1627
- let isNeed = false;
1628
- if (t === "*") {
1629
- isNeed = true;
1630
- }
1631
- let isUnique = false;
1632
- /* if (u === "**") {
1633
- uniqueFields.push(field);
1634
- isUnique = true;
1635
- } */
1636
- let isSheetUnique = false;
1637
- if (p === "***") {
1638
- uniqueKeys.push(field);
1639
- isSheetUnique = true;
1640
- }
1641
- if (!uniqueKeys.length) {
1642
- // that.clearImportTimer();
1643
- that.$baseAlert(
1644
- `[${sheetName}]` + that.$t1("缺失单据唯一标识***")
1645
- );
1646
- // that.handleClose2();
1647
- return false;
1648
- }
1649
-
1650
- let otherConfig = {};
1651
- let isFile = false;
1652
- if (field.startsWith("attachments_")) {
1653
- that.fileCellFields.push(field);
1654
- isFile = true;
1655
- otherConfig = {
1656
- slots: {
1657
- default: "attachment",
1658
- },
1659
- };
1660
- }
1661
-
1662
- let col = {
1663
- field: field,
1664
- title: title,
1665
- width: 100,
1666
- isNeed: isNeed,
1667
- isUnique: isUnique,
1668
- isFile,
1669
- otherConfig,
1670
- isSheetUnique,
1671
- };
1672
- cols.push(col);
1673
- }
1674
- }
1675
-
1676
- let oriCols = JSON.parse(JSON.stringify(cols)); //列信息
1677
- /* if (uniqueKeys.length) {
1678
- //分组唯一字段为空时,全部不发包。
1679
- for (let i = 0; i < oriCols.length; i++) {
1680
- let oriCol = oriCols[i];
1681
- if (uniqueKeys.includes(oriCol.field)) {
1682
- for (let j = 0; j < excelRows.length; j++) {
1683
- let data = excelRows[j];
1684
- let value = data[oriCol.field];
1685
- if (value === null || value === undefined || value === "") {
1686
- that.errorHandle(`[${sheetName}]`+
1687
- that.$t2(
1688
- "第" + (j + 4) + "行[" + oriCol.title + "]不能为空;",
1689
- "components.excelImport.warmMsg3",
1690
- {
1691
- lineNo: j + 4,
1692
- title: oriCol.title,
1693
- }
1694
- ));
1695
- return false;
1696
- }
1697
- }
1698
- }
1699
- }
1700
- } */
1701
- // this.oriCols = oriCols;
1702
-
1703
- let resultData1 = {
1704
- sheetName: sheetName,
1705
- data: excelRows,
1706
- titles: titles,
1707
- fields: fields,
1708
- uniqueFields: uniqueFields,
1709
- oriCols: oriCols,
1710
- uniqueKeys,
1711
- };
1712
- sheetDatas.push(resultData1);
1713
- }
1714
-
1715
- //校验必填
1716
- if (!that.checkAllSheetData(sheetDatas)) {
1717
- return false;
1718
- }
1719
-
1720
- //合并多sheet数据
1721
- let newDatas = [];
1722
- resultData = that.$baseLodash.cloneDeep(sheetDatas[0]);
1723
- let uniqueKey = resultData.uniqueKeys[0];
1724
- let data2 = [];
1725
- for (let i = 0; i < resultData.data.length; i++) {
1726
- let item = resultData.data[i];
1727
- let sheetName = resultData[sheetName];
1728
- let uniqueValue = item[uniqueKey];
1729
-
1730
- if (!that.isNull(uniqueValue)) {
1731
- let newItem = { ...item };
1732
- let allItems = [];
1733
- for (let j = 1; j < sheetDatas.length; j++) {
1734
- let sheetData = sheetDatas[j];
1735
- let fDatas = [];
1736
- for (let k = 0; k < sheetData.data.length; k++) {
1737
- let sheetItem = sheetData.data[k];
1738
- if (that.isNull(sheetItem[uniqueKey])) {
1739
- that.handleCellNotNull(k, sheetData);
1740
- return;
1741
- }
1742
- if (sheetItem[uniqueKey] === uniqueValue) {
1743
- fDatas.push(sheetItem);
1744
- }
1745
- }
1746
- allItems.push(fDatas);
1747
- }
1748
- newItem.sheetItems = allItems;
1749
- data2.push(newItem);
1750
- } else {
1751
- that.handleCellNotNull(i, resultData);
1752
- return false;
1753
- }
1754
- }
1755
-
1756
- resultData.sheetDatas = sheetDatas;
1757
- resultData.data = data2;
1758
- this.oriCols = resultData.oriCols;
1759
- this.resultData = resultData;
1760
- callback(resultData);
1761
- } catch (et) {
1762
- that.showContent = false;
1763
- that.clearImportTimer();
1764
- console.error(et);
1765
- }
1766
- };
1767
- fileReader.readAsBinaryString(file);
1768
- },
1769
- isNull(val) {
1770
- if (val === null || val === undefined || val === "" || val === undefined || val === null) {
1771
- return true;
1772
- }
1773
- },
1774
- checkAllSheetData(sheetItems){
1775
- let that = this;
1776
- for(let sheetItem of sheetItems){
1777
- let oriCols = sheetItem.oriCols;
1778
- let uniqueKeys = sheetItem.uniqueKeys;
1779
- let excelRows = sheetItem.data;
1780
- let sheetName = sheetItem.sheetName;
1781
-
1782
- for (let i = 0; i < oriCols.length; i++) {
1783
- let oriCol = oriCols[i];
1784
- if (oriCol.isNeed || uniqueKeys.includes(oriCol.field)) {
1785
- for (let j = 0; j < excelRows.length; j++) {
1786
- let data = excelRows[j];
1787
- let value = data[oriCol.field];
1788
- if (that.isNull(value)) {
1789
- that.errorHandle(`[${sheetName}]`+
1790
- that.$t2(
1791
- "第" + (j + 4) + "行[" + oriCol.title + "]不能为空;",
1792
- "components.excelImport.warmMsg3",
1793
- {
1794
- lineNo: j + 4,
1795
- title: oriCol.title,
1796
- }
1797
- ));
1798
- return false;
1799
- }
1800
- }
1801
- }
1802
- }
1803
-
1804
- }
1805
- return true;
1806
- },
1807
-
1808
-
1809
- handleCellNotNull(i, sheetData) {
1810
- let that = this;
1811
- let uniqueKey = sheetData.uniqueKeys[0];
1812
- let oriCol = sheetData.oriCols.find((item) => item.field === uniqueKey);
1813
- that.errorHandle(
1814
- `[${sheetData.sheetName}]` +
1815
- that.$t2(
1816
- "第" + (i + 4) + "行[" + oriCol.title + "]不能为空;",
1817
- "components.excelImport.warmMsg3",
1818
- {
1819
- lineNo: i + 4,
1820
- title: oriCol.title,
1821
- }
1822
- )
1823
- );
1824
- },
1825
- errorHandle(errorText) {
1826
- this.clearImportTimer();
1827
- this.$baseAlert(errorText);
1828
- // this.handleClose2();
1829
- },
1830
- /***sheet2 */
1831
- },
1832
- };
1833
- export const mixins = tmixins;
1
+ /**version-1.0*/
2
+ import { read, utils } from "xlsx";
3
+ import axios from "axios";
4
+ import SparkMD5 from "spark-md5";
5
+
6
+ let tmixins = {};
7
+ let XLSX = window.XLSX || { read, utils };
8
+ import { getToken } from "../../../../../utils/auth";
9
+ import XEUtils from "xe-utils";
10
+ import { getErrorMsg } from "@base/utils/index.js";
11
+ import {
12
+ getImportCellText,
13
+ getImportFileSizeLimit,
14
+ getImportRowLimit,
15
+ isImportFileOversize,
16
+ isImportRowsOverLimit,
17
+ toFileSizeMb,
18
+ } from "@base/utils/importLimit";
19
+ import {
20
+ normalizeAttachmentValue,
21
+ toUrlValue,
22
+ } from "@base/components/xform/utils/attachmentValueUtil";
23
+
24
+ let configUtil = {
25
+ baseUrl: process.env.VUE_APP_BASE_API,
26
+ XLSX,
27
+ getToken,
28
+ };
29
+ // 执行壳打在行上的记账字段:只服务进度框与结果表。行对象是原地复用的,
30
+ // 这几个字段不剥掉就会跟着数据进明细表格,最终随表单一起提交给后端。
31
+ const IMPORT_META_FIELDS = [
32
+ "impSeq",
33
+ "impNumber",
34
+ "_resultType",
35
+ "_resultContent",
36
+ ];
37
+ // 后台校验回包里的协议字段,合并补数时不能盖掉原行的序号
38
+ const BACKEND_VALIDATE_SKIP_FIELDS = [
39
+ "impSeq",
40
+ "impNumber",
41
+ "impReturnType",
42
+ "impReturnMsg",
43
+ ];
44
+ // 一格里填多个附件的分隔符,与 attachmentValueUtil 的 URL_SEPARATOR_REG 同口径
45
+ const IMPORT_ATTACH_SEPARATOR_REG = /[,\n\r]+/;
46
+ // 可用的附件地址:带协议(或协议相对)的绝对 URL,或以 / 开头的存储路径
47
+ const IMPORT_ATTACH_URL_REG = /^((https?:)?\/\/|\/)/i;
48
+ const IMPORT_ATTACH_PREFIX = "attachments_";
49
+ // 「值存为URL」列名里的取值字段 token,与附件字段的 urlValueField 同一套取值
50
+ const IMPORT_ATTACH_URL_FIELDS = ["url", "large", "medium", "thumbnail"];
51
+ // importTimer / loadingObj 原为模块级单例:两个导入弹窗并存时后者覆盖前者引用,
52
+ // 前一个 interval(100ms/次)与 loading 永久失控——已改为实例属性 this.importTimer / this.loadingObj
53
+ tmixins = {
54
+ name: "excelImport",
55
+ props: ["param", "parentTarget"],
56
+ inject: ["getReportTemplate", "formHttp"],
57
+ data() {
58
+ return {
59
+ excTime: 0,
60
+ showImportDialog: false,
61
+ showImportDialog2: false,
62
+ falseValue: false,
63
+ fileRaw: false,
64
+ isEnd: false,
65
+ currentIndex: 0,
66
+ excelTemplate: "",
67
+ prefix: "/user",
68
+ dataSize: 0,
69
+ totalSuccessNum: 0,
70
+ totalErrorNum: 0,
71
+ percentageNum: 0,
72
+ successRows: [],
73
+ failRows: [],
74
+ oriCols: [],
75
+ showImportResult: false,
76
+ resultType: "",
77
+ option: {},
78
+ showContent: false,
79
+ resOption: {},
80
+ fileCellFields: [],
81
+ // 「值存为URL」附件列:源列名 → { urlField, target },见 parseUrlAttachmentField
82
+ urlAttachFields: {},
83
+ importLoading: false,
84
+ loadingTitle: null,
85
+ backendValidating: false,
86
+
87
+ percent1: 0,
88
+ percent2: 0,
89
+ percent3: 0,
90
+
91
+ loadingPercent: 0,
92
+ handleRowSize: 1,
93
+
94
+ headerRows: 3,
95
+ };
96
+ },
97
+ computed: {
98
+ /** 进度框左下角的"已处理/总行数",由记账器维护的两个计数派生。 */
99
+ doneNum() {
100
+ return (this.totalSuccessNum || 0) + (this.totalErrorNum || 0);
101
+ },
102
+ importFileLimitSize() {
103
+ return Math.min(
104
+ this.param?.importOption?.importFileLimitSize || 200,
105
+ 200
106
+ );
107
+ },
108
+ },
109
+ created() {
110
+ this.exc();
111
+ },
112
+ beforeDestroy() {
113
+ this.clearImportTimer();
114
+ },
115
+ methods: {
116
+ initResGird() {
117
+ let that = this;
118
+ let tableRef = "resGrid";
119
+ let columns = [
120
+ {
121
+ type: "checkbox",
122
+ fixed: "left",
123
+ width: 48,
124
+ resizable: false,
125
+ headerClassName: "vxe-hcs",
126
+ className: "vxe-hcs",
127
+ },
128
+ {
129
+ field: "impNumber",
130
+ title: this.$t2("行号", "components.excelImport.impNumber"),
131
+ width: 100,
132
+ fixed: "left",
133
+ },
134
+ {
135
+ field: "_resultContent",
136
+ title: this.$t2("结果", "components.excelImport.resultContent"),
137
+ width: 250,
138
+ fixed: "left",
139
+ slots: {
140
+ default: ({ row, $rowIndex, $table }) => {
141
+ return that.handleRes(row._resultContent);
142
+ },
143
+ },
144
+ },
145
+ ];
146
+ let cCols = this.oriCols.map((oriCol, index) => {
147
+ let col = {
148
+ field: oriCol.field,
149
+ title: oriCol.title,
150
+ width: 150,
151
+ ...oriCol.otherConfig,
152
+ };
153
+ if (index === 0) {
154
+ col.fixed = "left";
155
+ }
156
+ return col;
157
+ });
158
+ columns = columns.concat(cCols);
159
+
160
+ let tableOption = {
161
+ vue: this,
162
+ tableRef: tableRef,
163
+ tableNameRequired: false,
164
+ columns: columns,
165
+ config: {
166
+ toolbarConfig: {
167
+ custom: false,
168
+ },
169
+ },
170
+ };
171
+
172
+ this.$vxeTableUtil.initVxeTable(tableOption).then((opts) => {
173
+ this.resOption = opts;
174
+ });
175
+ },
176
+ exc() {
177
+ this.option = this.param;
178
+ this.showContent = true;
179
+ this.showImportDialog = true;
180
+ this.showImportDialog2 = false;
181
+ },
182
+ showSuccessResult() {
183
+ this.showImportResult = true;
184
+ this.resultType = 1;
185
+ this.$nextTick(() => {
186
+ this.initResGird();
187
+ this.$nextTick(() => {
188
+ let tDatas = this.successRows.map((item, index) => {
189
+ item.id = index;
190
+ return item;
191
+ });
192
+ this.$refs.resGrid.loadData(tDatas);
193
+ });
194
+ });
195
+ },
196
+ /**
197
+ * 失败表按 Excel 行序展示:批量与分组通道的回包顺序与行序无关,
198
+ * 排序放在消费点做一次,而不是每标一行排一次。
199
+ * @param {Array} rows 失败行。@returns {Array} 排好序的副本。
200
+ */
201
+ sortRowsByImpSeq(rows) {
202
+ return (rows || [])
203
+ .slice()
204
+ .sort((a, b) => (a.impSeq || 0) - (b.impSeq || 0));
205
+ },
206
+ showErrorResult() {
207
+ this.showImportResult = true;
208
+ this.resultType = 0;
209
+ this.$nextTick(() => {
210
+ this.initResGird();
211
+ this.$nextTick(() => {
212
+ let tDatas = this.sortRowsByImpSeq(this.failRows).map(
213
+ (item, index) => {
214
+ item.id = index;
215
+ return item;
216
+ }
217
+ );
218
+ this.$refs.resGrid.loadData(tDatas);
219
+ });
220
+ });
221
+ },
222
+ fileChange(file) {
223
+ if (file) {
224
+ this.fileRaw = file.raw;
225
+ } else {
226
+ this.fileRaw = false;
227
+ }
228
+ },
229
+ dialogClose1() {
230
+ this.importLoading = false;
231
+ if (!this.showImportDialog2) {
232
+ this.$emit("update:visiable", false);
233
+ this.showContent = false;
234
+ }
235
+ },
236
+ async dialogPrimary1() {
237
+ let that = this;
238
+ let option = this.option;
239
+
240
+ let fileRaw = this.fileRaw;
241
+ if (!fileRaw) {
242
+ this.$baseAlert(
243
+ this.$t2("请上传附件", "components.excelImport.warmMsg1")
244
+ );
245
+ return false;
246
+ }
247
+ let importFileLimitSize = this.importFileLimitSize;
248
+ let fileSize = fileRaw.size;
249
+ let limitSize = importFileLimitSize * 1024 * 1024;
250
+ if (fileSize > limitSize) {
251
+ this.$baseAlert(
252
+ this.$t1("导入文件不允许超过{fileSize}MB", {
253
+ fileSize: importFileLimitSize,
254
+ })
255
+ );
256
+ return false;
257
+ }
258
+
259
+ if (option.importFrontOnly) {
260
+ this.dialogPrimary3();
261
+ return false;
262
+ }
263
+
264
+ /* this.createImportTimer();
265
+ this.showImportDialog = false;
266
+ if (!option.onConfirm) {
267
+ this.showImportDialog2 = true;
268
+ } */
269
+ // this.$nextTick(() => {
270
+ try {
271
+ this.readExcel(this.fileRaw, function (resultData) {
272
+ // strict:解析期整批硬拦,一处不合规就整份不导(选文件框留着,改完可直接重选)
273
+ if (option.importOption.requiredCheckMode === "strict") {
274
+ let requiredError = that.checkRequiredRows(
275
+ resultData?.data,
276
+ resultData?.oriCols
277
+ );
278
+ if (requiredError) {
279
+ that.clearImportTimer();
280
+ that.$baseAlert(requiredError);
281
+ return;
282
+ }
283
+ }
284
+ that.createImportTimer();
285
+ that.showImportDialog = false;
286
+ if (!option.onConfirm) {
287
+ that.showImportDialog2 = true;
288
+ }
289
+ that.$nextTick(() => {
290
+ if (option.onConfirm) {
291
+ option.onConfirm(resultData.data, that.fileRaw);
292
+ } else {
293
+ that.handleBeforeImport0(that.fileRaw, resultData, () => {
294
+ that.syncDataForImport({
295
+ multi: false,
296
+ data: resultData,
297
+ // saveUrl: saveUrl
298
+ });
299
+ });
300
+ }
301
+ });
302
+ });
303
+ } catch (et) {
304
+ that.clearImportTimer();
305
+ console.error(et);
306
+ }
307
+ // });
308
+ },
309
+ handleBeforeImport(file, resultData, callback) {
310
+ let importOption = this.option.importOption;
311
+ let that = this;
312
+ if (importOption.onBeforeImport) {
313
+ const toDo = (res) => {
314
+ let rows = res?.objx || [];
315
+ // 入参 resultData 是 readExcel 的信封 {data,titles,fields,oriCols,...},不是行数组。
316
+ // 早前直接按 resultData[rowIndex] 取行,恒为 undefined —— 脚本只要 done(带 objx)
317
+ // 回填数据就 TypeError,且崩在 new Function 里,现象是"导入点了没反应"。
318
+ let dataRows = Array.isArray(resultData)
319
+ ? resultData
320
+ : resultData?.data || [];
321
+ rows.forEach((row, rowIndex) => {
322
+ let item = dataRows[rowIndex];
323
+ if (!item) return;
324
+ Object.keys(row).forEach((key) => {
325
+ item[key] = row[key];
326
+ });
327
+ });
328
+ callback && callback();
329
+ };
330
+ this.getFieldTarget().handleCustomEvent(
331
+ importOption.onBeforeImport,
332
+ ["file", "resultData", "done"],
333
+ [file, resultData, toDo]
334
+ );
335
+ } else {
336
+ callback && callback();
337
+ }
338
+ },
339
+ handleBeforeImport0(file, resultData, callback) {
340
+ if (!this.showImportDialog2) return;
341
+ let importOption = this.option.importOption;
342
+ if (!importOption.enabledImportPreHandle) {
343
+ this.handleBeforeImport(file, resultData, () => {
344
+ callback && callback();
345
+ });
346
+ return;
347
+ }
348
+
349
+ this.uploadFile(() => {
350
+ this.importLoading = false;
351
+ this.handleBeforeImport(file, resultData, () => {
352
+ callback && callback();
353
+ });
354
+ });
355
+ return;
356
+ },
357
+ handleBeforeClose(hide) {
358
+ if (this.backendValidating) {
359
+ this.isEnd = true;
360
+ this.backendValidating = false;
361
+ hide();
362
+ return;
363
+ }
364
+ this.option.onCancel &&
365
+ this.option.onCancel(this.getCallbackData(), this.fileRaw);
366
+ let importOption = this.option.importOption;
367
+ this.getFieldTarget().handleCustomEvent(
368
+ importOption.onSuccessImport,
369
+ ["resultData", "file"],
370
+ [this.getCallbackData(), this.fileRaw]
371
+ );
372
+ this.handleTableCallback();
373
+ hide();
374
+ },
375
+ dialogClose2() {
376
+ if (this.backendValidating) return;
377
+ this.showImportDialog2 = false;
378
+ this.option.onBeforeCancel &&
379
+ this.option.onBeforeCancel(this.getCallbackData(), this.fileRaw);
380
+ let importOption = this.option.importOption;
381
+ this.getFieldTarget().handleCustomEvent(
382
+ importOption.onSuccessImport,
383
+ ["resultData", "file"],
384
+ [this.getCallbackData(), this.fileRaw]
385
+ );
386
+ this.handleTableCallback();
387
+ },
388
+ handleClose2() {
389
+ this.backendValidating = false;
390
+ this.clearImportTimer();
391
+ this.showImportDialog2 = false;
392
+ this.showContent = false;
393
+ this.option.onClose &&
394
+ this.option.onClose(this.getCallbackData(), this.fileRaw);
395
+ this.option.callback &&
396
+ this.option.callback(this.getCallbackData(), this.fileRaw);
397
+ this.dialogClose1();
398
+ },
399
+ dialogPrimary2() {
400
+ if (this.backendValidating) return;
401
+ let importOption = this.option.importOption;
402
+ this.showImportDialog2 = false;
403
+ this.option.onBeforeConfirm &&
404
+ this.option.onBeforeConfirm(this.getCallbackData(), this.fileRaw);
405
+ this.getFieldTarget().handleCustomEvent(
406
+ importOption.onSuccessImport,
407
+ ["resultData", "file"],
408
+ [this.getCallbackData(), this.fileRaw]
409
+ );
410
+ this.handleTableCallback();
411
+ },
412
+ getCallbackData() {
413
+ return {
414
+ ...this.resultData,
415
+ successRows: this.successRows,
416
+ failRows: this.sortRowsByImpSeq(this.failRows),
417
+ };
418
+ },
419
+
420
+ /*dialogClose2(flag) {
421
+ let importOption = this.option.importOption;
422
+ this.clearImportTimer();
423
+ this.showImportDialog2 = false;
424
+ this.dialogClose1();
425
+ this.handleTableCallback(flag);
426
+ this.parentTarget.handleCustomEvent(importOption.onSuccessImport, ['resultData', 'file'], [this.resultData, this.fileRaw])
427
+ this.option.callback && this.option.callback(this.resultData, this.fileRaw);
428
+ },
429
+ dialogPrimary2() {
430
+ let importOption = this.option.importOption;
431
+ this.clearImportTimer();
432
+ this.showImportDialog2 = false;
433
+ this.dialogClose1()
434
+ this.handleTableCallback();
435
+ let fileRaw = this.fileRaw;
436
+ this.parentTarget.handleCustomEvent(importOption.onSuccessImport, ['resultData', 'file'], [this.resultData, this.fileRaw])
437
+ this.option.callback && this.option.callback(this.resultData, this.fileRaw);
438
+ },*/
439
+ handleTableCallback(flag) {
440
+ if (flag === 0) return;
441
+ let importOption = this.option.importOption;
442
+ let tableTarget;
443
+ if (importOption.tableRef)
444
+ tableTarget = this.getFieldTarget().getWidgetRef(importOption.tableRef);
445
+ if (tableTarget) {
446
+ let isQueryTable = tableTarget.widget.options.isQueryTable;
447
+ if (isQueryTable) {
448
+ tableTarget.searchEvent();
449
+ }
450
+ }
451
+ },
452
+ async readExcel(file, callback) {
453
+ let that = this;
454
+ let option = this.option;
455
+
456
+ // 业务侧的 importFileLimitSize 允许到 200MB,而这里是整文件 readAsBinaryString +
457
+ // XLSX.read 同步解析,占用约为文件体积的 5~20 倍 —— 那个量级必然把标签页撑爆,
458
+ // 且崩在库内部,连提示都给不出来。这里再收一道解析安全上限,可用
459
+ // importOption.maxFileSize 覆盖(0 表示不限制)。
460
+ if (isImportFileOversize(file, option)) {
461
+ this.$baseAlert(
462
+ this.$t1(
463
+ "导入文件过大({size}MB),当前最多支持 {limit}MB,请拆分后分批导入",
464
+ {
465
+ size: toFileSizeMb(file.size),
466
+ limit: toFileSizeMb(getImportFileSizeLimit(option)),
467
+ }
468
+ )
469
+ );
470
+ // 与上游那道 importFileLimitSize 校验保持一致:只提示、不关弹窗,
471
+ // 用户可以直接重选一个小文件。
472
+ return false;
473
+ }
474
+
475
+ if (option.importOption.multipleSheet) {
476
+ this.readExcel2(file, callback);
477
+ return;
478
+ }
479
+
480
+ // 附件列清单按本次解析重建:解析可以在同一个弹框里跑多次(strict 拦下来后
481
+ // 用户重选文件、"确定"又没有 pending 锁),只 push 不清会把上一份的附件列带进来。
482
+ this.fileCellFields = [];
483
+ this.urlAttachFields = {};
484
+
485
+ const fileReader = new FileReader();
486
+ fileReader.onload = async (ev) => {
487
+ let resultData;
488
+ try {
489
+ const data = ev.target.result;
490
+ const workbook = configUtil.XLSX.read(data, {
491
+ type: "binary",
492
+ });
493
+
494
+ let sheet = workbook.Sheets[workbook.SheetNames[0]];
495
+ if (sheet["!ref"] === undefined || sheet["!ref"] === null) {
496
+ /*that.showImportDialog2 = false;
497
+ that.showContent = false;
498
+ that.clearImportTimer();*/
499
+ that.$baseAlert(
500
+ this.$t2(
501
+ "请使用正确的导入模版!",
502
+ "components.excelImport.warmMsg2"
503
+ )
504
+ );
505
+ that.handleClose2();
506
+ return false;
507
+ }
508
+ let range = configUtil.XLSX.utils.decode_range(sheet["!ref"]);
509
+ let dataSize = range.e.r + 1; //行数
510
+ that.dataSize = dataSize - 3 >= 0 ? dataSize - 3 : 0;
511
+ // 行数闸门:下面逐格取值、建行对象、再整批发包全是同步的,行数失控时主线程会被
512
+ // 占死甚至 OOM。文件体积不大但行极多(窄表)时只有这一道能拦。
513
+ if (isImportRowsOverLimit(that.dataSize, option)) {
514
+ that.clearImportTimer();
515
+ that.$baseAlert(
516
+ that.$t1(
517
+ "导入数据 {size} 行,超过单次上限 {limit} 行,请拆分后分批导入",
518
+ {
519
+ size: that.dataSize,
520
+ limit: getImportRowLimit(option),
521
+ }
522
+ )
523
+ );
524
+ that.handleClose2();
525
+ return false;
526
+ }
527
+ let titles = [];
528
+ let fields = [];
529
+
530
+ const ws = workbook.Sheets[sheet];
531
+
532
+ let prefix = option.prefix;
533
+ let excelUrl = option.excel;
534
+
535
+ let excelRows = [];
536
+ let row;
537
+ let rowNum;
538
+ let colNum;
539
+ for (rowNum = range.s.r; rowNum <= range.e.r; rowNum++) {
540
+ row = [];
541
+ for (colNum = range.s.c; colNum <= range.e.c; colNum++) {
542
+ let nextCell =
543
+ sheet[
544
+ configUtil.XLSX.utils.encode_cell({
545
+ r: rowNum,
546
+ c: colNum,
547
+ })
548
+ ];
549
+ if (typeof nextCell === "undefined") {
550
+ row.push(void 0);
551
+ } else {
552
+ row.push(getImportCellText(nextCell));
553
+ }
554
+ }
555
+ if (rowNum === 1) {
556
+ titles = row;
557
+ }
558
+ if (rowNum === 2) {
559
+ fields = row;
560
+ let checkResult = true;
561
+ /*await that.checkExcelForImport(titles, fields, excelUrl, prefix).then(
562
+ res => {
563
+ if (res.type !== "success") {
564
+ checkResult = false;
565
+ }
566
+ });
567
+ if (!checkResult) {
568
+ that.showContent = false;
569
+ that.clearImportTimer();
570
+ return false;
571
+ }*/
572
+ }
573
+ if (rowNum > 2) {
574
+ let item = {};
575
+ for (let i = 0; i < fields.length; i++) {
576
+ let field = fields[i];
577
+ if (field) {
578
+ item[field] = row[i] ?? null;
579
+ }
580
+ }
581
+ excelRows.push(item);
582
+ }
583
+ }
584
+ if (
585
+ !titles.length ||
586
+ !fields.length ||
587
+ titles.length !== fields.length
588
+ ) {
589
+ // that.showContent = false;
590
+ // that.clearImportTimer();
591
+ that.$baseAlert(
592
+ this.$t2(
593
+ "请使用正确的导入模版!",
594
+ "components.excelImport.warmMsg2"
595
+ )
596
+ );
597
+ that.handleClose2();
598
+ return false;
599
+ }
600
+
601
+ let columns = [];
602
+ let cols = [];
603
+ let uniqueFields = [];
604
+ for (let i = 0; i < titles.length; i++) {
605
+ let title = titles[i];
606
+ if (title) {
607
+ let field = fields[i];
608
+ if (field === null || field === undefined) {
609
+ // that.showContent = false;
610
+ // that.clearImportTimer();
611
+ that.$baseAlert(
612
+ this.$t2(
613
+ "请使用正确的导入模版!",
614
+ "components.excelImport.warmMsg2"
615
+ )
616
+ );
617
+ that.handleClose2();
618
+ return false;
619
+ }
620
+ let t = title.substr(-1);
621
+ let u = title.length >= 2 ? title.substr(-2) : title;
622
+ let isNeed = false;
623
+ if (t === "*") {
624
+ isNeed = true;
625
+ }
626
+ let isUnique = false;
627
+ if (u === "**") {
628
+ uniqueFields.push(field);
629
+ isUnique = true;
630
+ }
631
+
632
+ let otherConfig = {};
633
+ let isFile = false;
634
+ let urlTargetField = null;
635
+ if (field.startsWith(IMPORT_ATTACH_PREFIX)) {
636
+ that.fileCellFields.push(field);
637
+ isFile = true;
638
+ otherConfig = {
639
+ slots: {
640
+ default: "attachment",
641
+ },
642
+ };
643
+ let urlAttach = that.parseUrlAttachmentField(field);
644
+ if (urlAttach) {
645
+ that.urlAttachFields[field] = urlAttach;
646
+ urlTargetField = urlAttach.target;
647
+ }
648
+ }
649
+
650
+ let col = {
651
+ field: field,
652
+ title: title,
653
+ width: 100,
654
+ isNeed: isNeed,
655
+ isUnique: isUnique,
656
+ isFile,
657
+ urlTargetField,
658
+ otherConfig,
659
+ };
660
+ cols.push(col);
661
+ }
662
+ }
663
+
664
+ let oriCols = JSON.parse(JSON.stringify(cols)); //列信息
665
+ // 附件列在这里收口成对象数组,后面的校验、发包、渲染都只面对一种形状。
666
+ // 图片三段式在解析之后才跑,会覆盖同一个 key,两者不冲突。
667
+ that.normalizeImportAttachmentCells(excelRows, oriCols);
668
+ // 「值存为URL」列再往前一步转成 URL 字符串挪到目标实体列;三段式回填后
669
+ // 原始列会被重新写回来,那边跑完还要再转一次
670
+ that.applyImportUrlAttachments(excelRows);
671
+ if (uniqueFields.length) {
672
+ // 分组唯一字段存在时,syncDataForImport 走 groupDatas 分支整组发包,
673
+ // 逐行的 vailData 不会执行 —— 必填只能在这里硬拦。
674
+ let requiredError = that.checkRequiredRows(excelRows, oriCols);
675
+ if (requiredError) {
676
+ that.clearImportTimer();
677
+ that.$baseAlert(requiredError);
678
+ that.handleClose2();
679
+ return false;
680
+ }
681
+ }
682
+
683
+ this.oriCols = oriCols;
684
+
685
+ resultData = {
686
+ data: excelRows,
687
+ titles: titles,
688
+ fields: fields,
689
+ uniqueFields: uniqueFields,
690
+ oriCols: oriCols,
691
+ };
692
+ this.resultData = resultData;
693
+ callback(resultData);
694
+ } catch (et) {
695
+ that.showContent = false;
696
+ that.clearImportTimer();
697
+ console.error(et);
698
+ }
699
+ };
700
+ fileReader.readAsBinaryString(file);
701
+ },
702
+ async checkExcelForImport(titles, fields, turl, prefix) {
703
+ if (!this.showImportDialog2) return;
704
+ let that = this;
705
+ let data = {
706
+ filed_name: fields,
707
+ field_note: titles,
708
+ url: turl,
709
+ };
710
+ let param = JSON.stringify(data);
711
+ return this.$http({
712
+ url: prefix + "/sf_common/compareExcelFields",
713
+ method: "post",
714
+ headers: {
715
+ "Content-Type": "application/json;charset=utf-8",
716
+ },
717
+ // data: param,
718
+ data: data,
719
+ });
720
+ },
721
+ /**
722
+ * 导入单元格是否为空。附件列归一后空值是 `[]`,不能只按 null/undefined/"" 判,
723
+ * 否则必填的附件列填不填都能过。
724
+ *
725
+ * @param {*} value 单元格值。
726
+ * @returns {Boolean}
727
+ */
728
+ isEmptyImportCell(value) {
729
+ if (value === null || value === undefined) return true;
730
+ if (typeof value === "string") return !value.trim();
731
+ if (Array.isArray(value)) return !value.length;
732
+ return false;
733
+ },
734
+ /**
735
+ * 导入模板里填的附件地址是否可用:带协议(或协议相对)的绝对 URL,或以 / 开头的
736
+ * 存储路径。parseFileUrl 为兼容后端各种相对路径写得极宽松——"这不是地址"也会被
737
+ * 当成文件名收下并产出一个附件对象,直接拿它当校验等于没校验,所以这里单独收紧。
738
+ *
739
+ * @param {String} value 单个地址。
740
+ * @returns {Boolean}
741
+ */
742
+ isImportAttachmentUrl(value) {
743
+ if (typeof value !== "string") return false;
744
+ return IMPORT_ATTACH_URL_REG.test(value.trim());
745
+ },
746
+ /**
747
+ * 附件列(isFile)的值归一。下游只认「附件对象数组」这一种形状——后端信封
748
+ * buildImportRowEnvelope、结果表的 base-attachment、明细表的附件字段都是照它写的;
749
+ * 而解析期的来源有三种:图片三段式回填的对象数组、单元格里的 URL 文本、空。
750
+ * 不在这里收口,三种形状就会一路带到发包与渲染(裸字符串给 base-attachment
751
+ * 会拼出 undefinedhttp://...,给后端则是它没见过的形状)。
752
+ *
753
+ * 地址不合法的非空单元格原样留着,交给 vailData / checkRequiredRows 按「不是数组」
754
+ * 判成错行——就地清成 [] 等于把填错的地址静默吞掉,那行还照样算导入成功。
755
+ *
756
+ * @param {Array} rows 数据行。@param {Array} oriCols 列信息。
757
+ */
758
+ normalizeImportAttachmentCells(rows, oriCols) {
759
+ let fileCols = (oriCols || []).filter((col) => col.isFile);
760
+ if (!fileCols.length) return;
761
+ (rows || []).forEach((row) => {
762
+ if (!row) return;
763
+ fileCols.forEach((col) => {
764
+ let value = row[col.field];
765
+ if (this.isEmptyImportCell(value)) {
766
+ row[col.field] = [];
767
+ return;
768
+ }
769
+ if (Array.isArray(value)) {
770
+ // 三段式回填的对象数组:全是对象时 normalizeAttachmentValue 原样返回,
771
+ // 后端带回来的 id/fileSize 等字段不会被重新解析掉
772
+ row[col.field] = normalizeAttachmentValue(value);
773
+ return;
774
+ }
775
+ if (typeof value !== "string") return;
776
+ let parts = value
777
+ .split(IMPORT_ATTACH_SEPARATOR_REG)
778
+ .map((part) => part.trim())
779
+ .filter((part) => !!part);
780
+ if (!parts.length) {
781
+ row[col.field] = [];
782
+ return;
783
+ }
784
+ // 一格里有一个填错就整格判错,免得"导进去了但少了一个附件"这种无声缺失
785
+ if (!parts.every((part) => this.isImportAttachmentUrl(part))) return;
786
+ row[col.field] = normalizeAttachmentValue(parts);
787
+ });
788
+ });
789
+ },
790
+ /**
791
+ * 附件列的值是否没通过归一。归一后附件列一定是数组,还是别的形状就说明
792
+ * 这一格填了东西但一个附件都解析不出来。
793
+ *
794
+ * @param {Object} oriCol 列信息。@param {*} value 单元格值。
795
+ * @returns {Boolean}
796
+ */
797
+ isBadAttachmentCell(oriCol, value) {
798
+ if (!oriCol.isFile) return false;
799
+ if (this.isEmptyImportCell(value)) return false;
800
+ // 「值存为URL」列转换后是单个 URL 字符串而不是数组,得按地址判
801
+ if (oriCol.urlTargetField) return !this.isImportAttachmentUrl(value);
802
+ return !Array.isArray(value);
803
+ },
804
+ /**
805
+ * 解析「值存为URL」附件列的列名:`attachments_<取值字段>_<目标字段>`,
806
+ * 例如 `attachments_large_image_url` = 取大图地址、写进实体列 `image_url`。
807
+ *
808
+ * 判别不会和经典的 `attachments_<uuid>` 撞车:createUUID 产出的是 32 位纯 hex
809
+ * 且连字符已剥掉,前缀之后一个下划线都没有,所以「还有下划线」就是新格式。
810
+ * 只切第一刀,目标字段名自己带下划线(`image_url`)也不受影响。
811
+ *
812
+ * @param {String} field 模板里的列名。
813
+ * @returns {Object|null} { urlField, target };不是新格式返回 null。
814
+ */
815
+ parseUrlAttachmentField(field) {
816
+ let rest = field.substring(IMPORT_ATTACH_PREFIX.length);
817
+ let i = rest.indexOf("_");
818
+ if (i <= 0) return null;
819
+ let urlField = rest.substring(0, i);
820
+ let target = rest.substring(i + 1);
821
+ if (!target) return null;
822
+ if (!IMPORT_ATTACH_URL_FIELDS.includes(urlField)) return null;
823
+ return { urlField, target };
824
+ },
825
+ /**
826
+ * 把「值存为URL」附件列的附件对象数组转成 URL 字符串,写到它的目标实体列上,
827
+ * 并删掉原始列——目标列是实体自己的普通列,不能走 attachments_ 独立附件通道。
828
+ *
829
+ * 要跑两次:解析期归一之后(模板里填 URL 文本的情形),以及图片三段式回填之后
830
+ * (三段式按 fileCellFields 白名单把原始列重新写了回来)。
831
+ *
832
+ * 没通过归一的值不是数组,原样留着不动,交给校验判成错行。
833
+ *
834
+ * @param {Array} rows 数据行。
835
+ */
836
+ applyImportUrlAttachments(rows) {
837
+ let sources = Object.keys(this.urlAttachFields || {});
838
+ if (!sources.length) return;
839
+ (rows || []).forEach((row) => {
840
+ if (!row) return;
841
+ sources.forEach((source) => {
842
+ if (!(source in row)) return;
843
+ let value = row[source];
844
+ if (!Array.isArray(value)) return;
845
+ let config = this.urlAttachFields[source];
846
+ // 实体列基本都是 varchar 单值,一格里有多个附件只取第一个
847
+ let url = toUrlValue(value, {
848
+ field: config.urlField,
849
+ multi: false,
850
+ });
851
+ delete row[source];
852
+ // 空附件格不覆盖目标列已有的值:模板里同时放了普通列 image_url 和
853
+ // attachments_large_image_url 时,空的那个不该把填好的地址清掉
854
+ if (url === null && config.target in row) return;
855
+ row[config.target] = url;
856
+ });
857
+ });
858
+ },
859
+ /**
860
+ * 取某一列在这一行上的值。「值存为URL」列转换后值已经挪到目标列上,原始列
861
+ * 不复存在;转换失败的值还留在原始列上,回退过去才能把那行报成错行。
862
+ *
863
+ * @param {Object} data 数据行。@param {Object} oriCol 列信息。
864
+ * @returns {*}
865
+ */
866
+ getImportCellValue(data, oriCol) {
867
+ if (oriCol.urlTargetField && data[oriCol.urlTargetField] !== undefined) {
868
+ return data[oriCol.urlTargetField];
869
+ }
870
+ return data[oriCol.field];
871
+ },
872
+ /**
873
+ * 剥掉执行壳打的记账字段,得到一份可以入表、可以提交的干净行副本。
874
+ * 原行仍留着这些字段供结果表展示——两者是两份数据,不共享引用。
875
+ *
876
+ * @param {Object} row 数据行。
877
+ * @returns {Object} 副本。
878
+ */
879
+ stripImportMeta(row) {
880
+ let item = { ...row };
881
+ IMPORT_META_FIELDS.forEach((field) => {
882
+ delete item[field];
883
+ });
884
+ return item;
885
+ },
886
+ /**
887
+ * 把一行数据包成后端要的信封:`{ [entity]: {...行}, attachments_x: [...] }`。
888
+ * 附件列从实体里摘出来提到信封层——附件是独立存储通道,留在实体里会被当成表字段插库。
889
+ *
890
+ * @param {Object} row 数据行。
891
+ * @param {String} entity 导入数据表名(importEntity)。
892
+ * @returns {Object} 信封。
893
+ */
894
+ buildImportRowEnvelope(row, entity) {
895
+ let envelope = {
896
+ [entity]: { ...row },
897
+ };
898
+ this.fileCellFields.forEach((field) => {
899
+ // 「值存为URL」列在解析期已转成实体自己的普通列,提到信封层就存不进去了
900
+ if (this.urlAttachFields && this.urlAttachFields[field]) return;
901
+ let attachments = row[field] || [];
902
+ delete envelope[entity][field];
903
+ envelope[field] = attachments;
904
+ });
905
+ return envelope;
906
+ },
907
+ saveImportHandle(data, opt) {
908
+ let multi = this.option.multi;
909
+ let importOption = this.option.importOption;
910
+
911
+ let entity = importOption.importEntity;
912
+
913
+ if (!entity) {
914
+ // 早前这里直接 return:opt.callback 永不触发,syncDataForImport 的逐行循环
915
+ // 断在原地——用户看到的是进度框永久停在 0%,没有任何提示,也无从自查。
916
+ this.handleImportAbort(
917
+ this.$t1("导入数据表名(importEntity)未配置,无法导入")
918
+ );
919
+ return;
920
+ }
921
+
922
+ let reportTemplate = this.getReportTemplate();
923
+ let formCode = reportTemplate?.formCode;
924
+ let scriptCode = importOption.importScriptCode || "import";
925
+ // let config = option?.config;
926
+
927
+ // let reqData = this.getReqFormData();
928
+ let formData = null;
929
+ if (multi) {
930
+ // 批量报文本来就是裸行数组,没有"实体层"可承载附件——附件只能留在行里当字段。
931
+ // importMultiAttachEnvelope 打开时给每行补上与单行同构的信封,让附件有处可去;
932
+ // 这会改掉发包形状,所以是显式开关而不是自动推断(有没有附件列都按开关走,
933
+ // 免得同一张表单的后端脚本要面对两种形状)。默认关=报文一字不变。
934
+ formData = importOption.importMultiAttachEnvelope
935
+ ? data.map((row) => {
936
+ let item = this.buildImportRowEnvelope(row, entity);
937
+ // 后端按 impSeq 把逐行结果映射回 objx,信封化后顶层要留一份,否则那条路会断
938
+ item.impSeq = row.impSeq;
939
+ item.impNumber = row.impNumber;
940
+ return item;
941
+ })
942
+ : data;
943
+ } else {
944
+ formData = this.buildImportRowEnvelope(data, entity);
945
+ }
946
+
947
+ let reqData = {
948
+ formCode: formCode,
949
+ formVersion: reportTemplate.formVersion,
950
+ taBm: entity,
951
+ data: formData,
952
+ };
953
+
954
+ this.formHttp({
955
+ scriptCode: scriptCode,
956
+ data: reqData,
957
+ modal: false,
958
+ ...opt,
959
+ });
960
+ },
961
+ /**
962
+ * 进度记账:进度框上的计数、百分比与成败行集合,前后端两条通道共用。
963
+ *
964
+ * 早前这些数字是 querySelector 直接写 innerText 的,而模板上同名 span 又绑着
965
+ * totalSuccessNum / totalErrorNum —— 两套并存全靠"响应式那份永远不变"侥幸成立。
966
+ * 这里统一收口成响应式数据:模板照常渲染,逐行结果也能被单测直接断言。
967
+ *
968
+ * @param {Number} size 本次要处理的总行数。
969
+ * @returns {Object} { markSuccess, markError, setPercent, finish }
970
+ */
971
+ createImportProgress(size) {
972
+ let that = this;
973
+ this.dataSize = size;
974
+ this.totalSuccessNum = 0;
975
+ this.totalErrorNum = 0;
976
+ this.percentageNum = 0;
977
+ this.successRows = [];
978
+ this.failRows = [];
979
+ return {
980
+ /** @param {Object} row 数据行。@param {String} message 可选的成功文案。 */
981
+ markSuccess(row, message) {
982
+ row._resultType = "success";
983
+ let successText = that.$t2(
984
+ "导入成功",
985
+ "components.excelImport.importSucess"
986
+ );
987
+ row._resultContent = message || successText;
988
+ that.totalSuccessNum = that.totalSuccessNum + 1;
989
+ that.successRows.push(row);
990
+ },
991
+ /** @param {Object} row 数据行。@param {String} message 错误信息。 */
992
+ markError(row, message) {
993
+ row._resultType = "error";
994
+ row._resultContent = message;
995
+ that.totalErrorNum = that.totalErrorNum + 1;
996
+ // 只追加。行序由消费方(结果表、回调数据)排一次——早前每标一行就整体排一次,
997
+ // 数组本就有序、只多一个元素,每次仍是 O(n),整批失败时是 O(n²),
998
+ // 行数上限 20000,足以把主线程卡住。
999
+ that.failRows.push(row);
1000
+ },
1001
+ /** @param {Number} percent 百分比整数。 */
1002
+ setPercent(percent) {
1003
+ that.percentageNum = percent;
1004
+ },
1005
+ finish() {
1006
+ setTimeout(function () {
1007
+ that.clearImportTimer();
1008
+ }, 300);
1009
+ },
1010
+ };
1011
+ },
1012
+ /**
1013
+ * 逐行执行的壳:进度、让步、成败沉淀统一由这里负责,
1014
+ * "每行做什么"下沉给 handler(row, index, done) 回写结果:
1015
+ * done() / done({success:true}) 表示成功;
1016
+ * done({success:false, message}) 表示失败。
1017
+ *
1018
+ * 与 syncDataForImport 的分工:后端通道有"批量"与"唯一键分组"两种发包形态,
1019
+ * 一步推进的行数不固定,循环仍留在那边;这里只服务"一行一步"的前端通道。
1020
+ *
1021
+ * @param {Object} opts { rows, handler, onFinish, progress, deferFinish }
1022
+ * progress 复用已有记账(后台校验后的第二段不能清掉前台失败行)
1023
+ * deferFinish 第一段只收集通过行时不收尾,等后台回来再 finish
1024
+ */
1025
+ runImportRows(opts) {
1026
+ let that = this;
1027
+ let rows = opts.rows || [];
1028
+ let handler = opts.handler;
1029
+ let size = rows.length;
1030
+ let progress = opts.progress || this.createImportProgress(size);
1031
+ this.importProgress = progress;
1032
+ let index = 0;
1033
+ let finish = () => {
1034
+ if (!opts.deferFinish) {
1035
+ progress.setPercent(100);
1036
+ progress.finish();
1037
+ }
1038
+ opts.onFinish && opts.onFinish(that.getCallbackData());
1039
+ };
1040
+ let step = () => {
1041
+ // 弹框已关或已被中止时停在原地,不再往下推
1042
+ if (that.isEnd || !that.showContent || index >= size) {
1043
+ finish();
1044
+ return;
1045
+ }
1046
+ let row = rows[index];
1047
+ // 第二段(后台补数后入表)行上已有 Excel 序号,不能按子集下标重写
1048
+ if (row.impSeq == null) {
1049
+ row.impSeq = index;
1050
+ row.impNumber = index + 4;
1051
+ }
1052
+ let settled = false;
1053
+ let done = (result) => {
1054
+ // handler 重复调 done 不能把进度推两格
1055
+ if (settled) return;
1056
+ settled = true;
1057
+ // done() 不传参直接算成功;只有显式 success:false 才记失败
1058
+ // done({ defer: true }) 只推进、不记账——前台校验通过、等后台结果时用
1059
+ let failed = !!(result && result.success === false);
1060
+ let deferred = !!(result && result.defer);
1061
+ if (failed) {
1062
+ progress.markError(row, result.message);
1063
+ } else if (!deferred) {
1064
+ progress.markSuccess(row, result && result.message);
1065
+ }
1066
+ index = index + 1;
1067
+ progress.setPercent(size ? parseInt((index * 100) / size) : 100);
1068
+ if (index >= size) {
1069
+ finish();
1070
+ return;
1071
+ }
1072
+ // 让步:整趟同步循环不给主线程喘息,正是大数据量卡死的根因
1073
+ setTimeout(step, 1);
1074
+ };
1075
+ try {
1076
+ handler(row, index, done);
1077
+ } catch (et) {
1078
+ console.error(et);
1079
+ done({ success: false, message: String(et?.message || et) });
1080
+ }
1081
+ };
1082
+ if (!size) {
1083
+ finish();
1084
+ return;
1085
+ }
1086
+ step();
1087
+ },
1088
+ syncDataForImport(opts) {
1089
+ let that = this;
1090
+ let multi = this.option.multi;
1091
+ let resultData = this.resultData;
1092
+ let datas = resultData.data;
1093
+ let titles = resultData.titles;
1094
+ let fields = resultData.fields;
1095
+ let uniqueFields = resultData.uniqueFields;
1096
+ let saveUrl = opts.saveUrl;
1097
+ let size = datas.length;
1098
+ let index = 0;
1099
+ // 计数、百分比、成败行集合统一交给进度记账,与前端通道共用同一套
1100
+ let progress = this.createImportProgress(size);
1101
+
1102
+ let multiSize = this.option.multiSize || 2;
1103
+
1104
+ let groupIndex = 0;
1105
+ let groupDatas = [];
1106
+ if (uniqueFields.length) {
1107
+ let d = XEUtils.groupBy(datas, function (item, itemIndex, items) {
1108
+ let keyArr = [];
1109
+ uniqueFields.forEach((field) => {
1110
+ keyArr.push(item[field]);
1111
+ });
1112
+ return keyArr.join("&_&");
1113
+ });
1114
+ for (let key in d) {
1115
+ groupDatas.push(d[key]);
1116
+ }
1117
+ datas.forEach((item, itemIndex) => {
1118
+ item.impSeq = itemIndex;
1119
+ item.impNumber = itemIndex + 4;
1120
+ });
1121
+ }
1122
+
1123
+ let prevHandleData = function (percentageStr, excNum) {
1124
+ progress.setPercent(percentageStr);
1125
+ let isEnd = false;
1126
+ if (multi && uniqueFields.length) {
1127
+ isEnd = groupIndex + 1 >= groupDatas.length;
1128
+ } else {
1129
+ isEnd = index + excNum >= size;
1130
+ }
1131
+ if (!isEnd) {
1132
+ index = index + excNum;
1133
+ groupIndex = groupIndex + 1;
1134
+ setTimeout(function () {
1135
+ handleData();
1136
+ }, 1);
1137
+ } else {
1138
+ progress.finish();
1139
+ }
1140
+ };
1141
+
1142
+ let getDefaultValue = (row) => {
1143
+ let value = null;
1144
+ let defaultValue = this.option.defaultValue;
1145
+ if (defaultValue) {
1146
+ if (typeof defaultValue === "function") {
1147
+ value = defaultValue(row) || {};
1148
+ } else {
1149
+ value = defaultValue || {};
1150
+ }
1151
+ }
1152
+ return value;
1153
+ };
1154
+
1155
+ let handleData = () => {
1156
+ if (!that.isEnd && index < size) {
1157
+ let param;
1158
+ let excNum;
1159
+ if (multi) {
1160
+ if (!uniqueFields.length) {
1161
+ if (index + multiSize < size) {
1162
+ excNum = multiSize;
1163
+ } else {
1164
+ excNum = size - index;
1165
+ }
1166
+
1167
+ let dataList = [];
1168
+ for (let i = 0; i < excNum; i++) {
1169
+ let mIndex = index + i;
1170
+ let mData = datas[mIndex];
1171
+ mData.impSeq = mIndex;
1172
+ mData.impNumber = mIndex + 4;
1173
+
1174
+ let errorInfo = that.validateImportRow(mData, mIndex, opts);
1175
+ if (errorInfo) {
1176
+ progress.markError(mData, errorInfo);
1177
+ } else {
1178
+ let defaultValue = getDefaultValue(mData);
1179
+ if (defaultValue) {
1180
+ Object.assign(mData, defaultValue);
1181
+ }
1182
+ dataList.push(mData);
1183
+ }
1184
+
1185
+ datas[mIndex] = mData;
1186
+ }
1187
+ if (dataList.length > 0) {
1188
+ param = dataList;
1189
+ }
1190
+ } else {
1191
+ excNum = groupDatas[groupIndex].length;
1192
+ param = groupDatas[groupIndex];
1193
+ // 分组整组发包,逐行校验按既有口径由解析期那道兜底;
1194
+ // 但补默认值没有别的入口,这里不补 importDefaultValue 就是静默失效。
1195
+ param.forEach((groupRow) => {
1196
+ let defaultValue = getDefaultValue(groupRow);
1197
+ if (defaultValue) {
1198
+ Object.assign(groupRow, defaultValue);
1199
+ }
1200
+ });
1201
+ }
1202
+ } else {
1203
+ let data = that.resultData.data[index];
1204
+ data.impSeq = index;
1205
+ data.impNumber = index + 4;
1206
+ excNum = 1;
1207
+ datas[index] = data;
1208
+
1209
+ let errorInfo = that.validateImportRow(data, index, opts);
1210
+ if (errorInfo) {
1211
+ progress.markError(data, errorInfo);
1212
+ } else {
1213
+ param = data;
1214
+ let defaultValue = getDefaultValue(data);
1215
+ if (defaultValue) {
1216
+ Object.assign(param, defaultValue);
1217
+ }
1218
+ }
1219
+ }
1220
+
1221
+ let percentageStr = parseInt(((index + excNum) * 100) / size);
1222
+ if (param === null || param === undefined || param.length === 0) {
1223
+ if (that.showContent) {
1224
+ prevHandleData(percentageStr, excNum);
1225
+ }
1226
+ return;
1227
+ }
1228
+
1229
+ that.saveImportHandle(param, {
1230
+ failMsg: false,
1231
+ callback: (resultMsg) => {
1232
+ if (multi) {
1233
+ if (resultMsg.type === "success") {
1234
+ let multiRows = resultMsg.objx;
1235
+ for (let i = 0; i < multiRows.length; i++) {
1236
+ let multiRow = multiRows[i];
1237
+ let data = datas[multiRow.impSeq];
1238
+ if (multiRow.impReturnType === "success") {
1239
+ progress.markSuccess(data);
1240
+ } else {
1241
+ progress.markError(data, multiRow.impReturnMsg);
1242
+ }
1243
+ }
1244
+ } else {
1245
+ // error_win(resultMsg);
1246
+ // layer.close(opts.dialogObjIndex);
1247
+
1248
+ // 整批发包失败:失败行落的是副本,不动原始行(原实现口径)
1249
+ param.forEach((item) => {
1250
+ progress.markError({ ...item }, resultMsg.content);
1251
+ });
1252
+
1253
+ /* that.$errorMsg(resultMsg);
1254
+ that.showContent = false;
1255
+ that.clearImportTimer();
1256
+ return; */
1257
+ }
1258
+ } else {
1259
+ let data = that.resultData.data[index];
1260
+ if (resultMsg.type === "success") {
1261
+ progress.markSuccess(data);
1262
+ } else {
1263
+ progress.markError(data, resultMsg.content);
1264
+ }
1265
+ }
1266
+ if (that.showContent) {
1267
+ prevHandleData(percentageStr, excNum);
1268
+ }
1269
+ },
1270
+ error: (et) => {
1271
+ that.isEnd = true;
1272
+ that.clearImportTimer();
1273
+ console.error(et);
1274
+ },
1275
+ });
1276
+ }
1277
+ };
1278
+ handleData();
1279
+ },
1280
+ commonLocalDownload() {
1281
+ let importOption = this.option.importOption;
1282
+ let importAttachCode = importOption.importAttachCode;
1283
+ if (!importAttachCode) return;
1284
+ this.$http({
1285
+ url: USER_PREFIX + "/bd_attach_setting/getByCode",
1286
+ method: "post",
1287
+ data: {
1288
+ stringOne: importAttachCode,
1289
+ },
1290
+ success: (res) => {
1291
+ let fileObj = res.objx;
1292
+ let url = fileObj?.url;
1293
+ if (fileObj && url) {
1294
+ this.$commonFileUtil.downloadFile(url,fileObj.name);
1295
+ }
1296
+ },
1297
+ });
1298
+ },
1299
+ /**
1300
+ * 整批必填预检:返回第一处缺失的提示文案,全部通过返回 null。
1301
+ * 与 vailData 的分工——vailData 是逐行软校验(该行记失败、其余继续),
1302
+ * 这一遍面向整批、用于解析期硬中断(前端通道与多行分组的后端通道都没有逐行通道)。
1303
+ * @param {Array} rows 数据行。@param {Array} oriCols 解析出的列信息。@returns {String|null}
1304
+ */
1305
+ checkRequiredRows(rows, oriCols) {
1306
+ let cols = oriCols || [];
1307
+ let datas = rows || [];
1308
+ for (let i = 0; i < cols.length; i++) {
1309
+ let oriCol = cols[i];
1310
+ if (!oriCol.isNeed && !oriCol.isFile) continue;
1311
+ for (let j = 0; j < datas.length; j++) {
1312
+ let value = this.getImportCellValue(datas[j], oriCol);
1313
+ if (oriCol.isNeed && this.isEmptyImportCell(value)) {
1314
+ return this.$t2(
1315
+ "第" + (j + 4) + "行[" + oriCol.title + "]不能为空;",
1316
+ "components.excelImport.warmMsg3",
1317
+ {
1318
+ lineNo: j + 4,
1319
+ title: oriCol.title,
1320
+ }
1321
+ );
1322
+ }
1323
+ // 分组分支与 strict 都不走逐行 vailData,附件地址填错只能在这里整批拦
1324
+ if (this.isBadAttachmentCell(oriCol, value)) {
1325
+ return this.$t1("第{lineNo}行[{title}]不是有效的附件地址;", {
1326
+ lineNo: j + 4,
1327
+ title: oriCol.title,
1328
+ });
1329
+ }
1330
+ }
1331
+ }
1332
+ return null;
1333
+ },
1334
+ /**
1335
+ * 行级校验:必填 + 业务脚本,前后端两条通道共用(校验分层里的 L2 + L3)。
1336
+ *
1337
+ * `requiredCheckMode`:
1338
+ * ""(默认)/ "row" —— 逐行软校验,错行记失败、其余继续;
1339
+ * "strict" —— 逐行照常校验,另由调用方在解析期整批硬拦;
1340
+ * "off" —— 不做必填校验,只信业务脚本与后端。
1341
+ * `onValidateRow` 脚本返回 true/空 视为通过,返回字符串即错误文案。
1342
+ *
1343
+ * @param {Object} row 数据行。@param {Number} index 行下标。@param {Object} opts 可选,带 data 信封。
1344
+ * @returns {String|null} 错误文案;null 表示通过。
1345
+ */
1346
+ validateImportRow(row, index, opts) {
1347
+ let importOption = this.option.importOption || {};
1348
+ if (importOption.requiredCheckMode !== "off") {
1349
+ let errorInfo = this.vailData(row, {
1350
+ data: opts?.data || this.resultData,
1351
+ });
1352
+ if (errorInfo && errorInfo.length > 0) return errorInfo;
1353
+ }
1354
+ if (!importOption.onValidateRow) return null;
1355
+ let result = this.getFieldTarget().handleCustomEvent(
1356
+ importOption.onValidateRow,
1357
+ ["row", "index"],
1358
+ [row, index]
1359
+ );
1360
+ let passed =
1361
+ result === true
1362
+ || result === null
1363
+ || result === undefined
1364
+ || result === "";
1365
+ if (passed) return null;
1366
+ if (result === false) return this.$t1("数据校验未通过");
1367
+ return String(result);
1368
+ },
1369
+ /** 导入无法继续时的统一收口:停循环、停计时器、给一次明确提示、关掉进度框。 */
1370
+ handleImportAbort(message) {
1371
+ this.backendValidating = false;
1372
+ this.isEnd = true;
1373
+ this.clearImportTimer();
1374
+ this.$baseAlert(message);
1375
+ this.handleClose2();
1376
+ },
1377
+ vailData(data, opts) {
1378
+ let oriCols = opts.data.oriCols;
1379
+ let errorInfo = [];
1380
+ for (let i = 0; i < oriCols.length; i++) {
1381
+ let oriCol = oriCols[i];
1382
+ let value = this.getImportCellValue(data, oriCol);
1383
+ if (oriCol.isNeed && this.isEmptyImportCell(value)) {
1384
+ errorInfo.push(
1385
+ this.$t2(
1386
+ "[" + oriCol.title + "]不能为空;",
1387
+ "components.excelImport.warmMsg4",
1388
+ { title: oriCol.title }
1389
+ )
1390
+ );
1391
+ continue;
1392
+ }
1393
+ if (this.isBadAttachmentCell(oriCol, value)) {
1394
+ errorInfo.push(
1395
+ this.$t1("[{title}]不是有效的附件地址;", { title: oriCol.title })
1396
+ );
1397
+ }
1398
+ }
1399
+ return errorInfo.join("");
1400
+ },
1401
+ handleRes(val) {
1402
+ /* let content = val;
1403
+ let text = content;
1404
+ if (content && content.indexOf("Exception") >= 0) {
1405
+ let index0 = content.lastIndexOf("*]");
1406
+ if (index0 >= 0) {
1407
+ text = content.substr(index0);
1408
+ if (text) {
1409
+ let index1 = text.indexOf("Exception:");
1410
+ if (index1 >= 0) {
1411
+ text = text.substr(index1 + 10);
1412
+ }
1413
+ }
1414
+ }
1415
+ } */
1416
+ let text = getErrorMsg(val);
1417
+ return text;
1418
+ },
1419
+ exportToExcel() {
1420
+ let str =
1421
+ this.resultType === 1
1422
+ ? this.$t2("成功", "components.excelImport.success")
1423
+ : this.$t2("失败", "components.excelImport.fail");
1424
+ let fileName =
1425
+ this.$t2("导入结果导出", "components.excelImport.exportFileName") +
1426
+ "(" +
1427
+ str +
1428
+ ")";
1429
+ let $grid = this.$refs.resGrid;
1430
+ let columns = $grid.getColumns();
1431
+ if (columns && columns.length) {
1432
+ let fields = columns
1433
+ .filter((item, index) => {
1434
+ return index > 0 && item.property;
1435
+ })
1436
+ .map((item, index) => {
1437
+ return item.property;
1438
+ });
1439
+ this.$refs.resGrid.exportData({
1440
+ filename: fileName,
1441
+ mode: "all",
1442
+ type: "csv",
1443
+ columns: fields,
1444
+ });
1445
+ }
1446
+ },
1447
+ createImportTimer() {
1448
+ this.excTime = 0;
1449
+ let beginTime = new Date().getTime();
1450
+ this.importTimer = setInterval(() => {
1451
+ let nowTime = new Date().getTime();
1452
+ let diffTime = (nowTime - beginTime) / 1000;
1453
+ this.$set(this, "excTime", this.$baseLodash.round(diffTime, 2));
1454
+ }, 100);
1455
+ },
1456
+ clearImportTimer() {
1457
+ clearInterval(this.importTimer);
1458
+ this.importTimer = null;
1459
+ this.loadingObj && this.loadingObj.close();
1460
+ this.loadingObj = null;
1461
+ },
1462
+ getWidgetTarget() {},
1463
+ getFieldTarget() {
1464
+ return this.param.target;
1465
+ },
1466
+ async dialogPrimary3() {
1467
+ if (!this.fileRaw) {
1468
+ this.$baseAlert(
1469
+ this.$t2("请上传附件", "components.excelImport.warmMsg1")
1470
+ );
1471
+ return false;
1472
+ }
1473
+ let that = this;
1474
+ let option = this.option;
1475
+ this.loadingObj = this.$baseLoading({
1476
+ target: document.body,
1477
+ background: "unset",
1478
+ });
1479
+
1480
+ let importOption = option.importOption;
1481
+ this.readExcel(this.fileRaw, function (resultData) {
1482
+ let file = that.fileRaw;
1483
+ // 整批硬拦。两种情形要拦:
1484
+ // 1) 老链路(不开进度框)—— 它没有逐行通道,不拦就等于零校验:前端通道不过
1485
+ // syncDataForImport 的逐行 vailData,解析期那道又只在模板带 ** 时才跑,
1486
+ // 空值会一路灌进明细表格,直到点保存才被表单行级必填拦下;
1487
+ // 2) requiredCheckMode = strict —— 显式要求"一处不合规就整份不导"。
1488
+ let isStrictCheck = importOption.requiredCheckMode === "strict";
1489
+ let hasRowChannel = !!importOption.showProgressDialog;
1490
+ let needBatchCheck =
1491
+ importOption.requiredCheckMode !== "off"
1492
+ && (!hasRowChannel || isStrictCheck);
1493
+ if (needBatchCheck) {
1494
+ let requiredError = that.checkRequiredRows(
1495
+ resultData?.data,
1496
+ resultData?.oriCols
1497
+ );
1498
+ if (requiredError) {
1499
+ that.clearImportTimer();
1500
+ that.$baseAlert(requiredError);
1501
+ return;
1502
+ }
1503
+ }
1504
+ // 进度框形态:逐行校验 → 成败进结果表 → 通过的行一次性入表格。
1505
+ // 默认关闭,关闭时下面仍是原来的"全屏 loading + 整批交付"老链路。
1506
+ if (importOption.showProgressDialog) {
1507
+ // 确认回调在进度框形态下是前置钩子:解析完立刻跑,通过后再 startFrontImport。
1508
+ // done() = 继续导入;close() = 中止。不返回 false 则自动继续(console.log 也能进进度框)。
1509
+ if (importOption.onConfirmImportEnabled) {
1510
+ let started = false;
1511
+ const done = () => {
1512
+ if (started) return;
1513
+ started = true;
1514
+ that.startFrontImport(resultData, file);
1515
+ };
1516
+ const close = () => {
1517
+ if (started) return;
1518
+ started = true;
1519
+ that.loadingObj && that.loadingObj.close();
1520
+ that.loadingObj = null;
1521
+ that.clearImportTimer();
1522
+ };
1523
+ try {
1524
+ if (
1525
+ that
1526
+ .getFieldTarget()
1527
+ .handleCustomEvent(
1528
+ importOption.onConfirmImport,
1529
+ ["resultData", "file", "done", "close"],
1530
+ [resultData, file, done, close]
1531
+ ) !== false
1532
+ ) {
1533
+ done();
1534
+ }
1535
+ } catch (et) {
1536
+ console.error(et);
1537
+ close();
1538
+ }
1539
+ return;
1540
+ }
1541
+ that.startFrontImport(resultData, file);
1542
+ return;
1543
+ }
1544
+ let flag = false;
1545
+ let done = () => {
1546
+ if (flag) return;
1547
+ flag = true;
1548
+ that.showImportDialog = false;
1549
+ return that
1550
+ .getFieldTarget()
1551
+ .handleCustomEvent(
1552
+ importOption.onSuccessImport,
1553
+ ["resultData", "file"],
1554
+ [resultData, file]
1555
+ );
1556
+ };
1557
+ // close done 分工:done 关框并触发 onSuccessImport,close 只关框。
1558
+ // 属性面板一直声明着 close 参数,运行时却没传(xform 那份已修),此处同步。
1559
+ const close = () => {
1560
+ that.dialogClose1();
1561
+ };
1562
+ try {
1563
+ if (importOption.onConfirmImportEnabled) {
1564
+ if (
1565
+ that
1566
+ .getFieldTarget()
1567
+ .handleCustomEvent(
1568
+ importOption.onConfirmImport,
1569
+ ["resultData", "file", "done", "close"],
1570
+ [resultData, file, done, close]
1571
+ ) !== false
1572
+ ) {
1573
+ done();
1574
+ }
1575
+ } else if (importOption.tableRef) {
1576
+ let tableRef = that
1577
+ .getFieldTarget()
1578
+ .getWidgetRef(importOption.tableRef);
1579
+ if (tableRef) {
1580
+ tableRef.addTableData(
1581
+ resultData.data,
1582
+ importOption.dedupField || undefined
1583
+ );
1584
+ }
1585
+ done();
1586
+ }
1587
+ } catch (et) {
1588
+ console.error(et);
1589
+ }
1590
+ setTimeout(function () {
1591
+ that.loadingObj && that.loadingObj.close();
1592
+ }, 200);
1593
+ });
1594
+ },
1595
+
1596
+ /**
1597
+ * 前端通道的进度框形态(importOption.showProgressDialog)。
1598
+ *
1599
+ * 与老链路的差别只在"结果怎么呈现":必填由整批硬拦改成逐行软校验——
1600
+ * 缺必填的行进失败表,其余照常入表格,与后端通道的体感一致。
1601
+ * @param {Object} resultData readExcel 的解析信封。@param {File} file 原始文件。
1602
+ */
1603
+ startFrontImport(resultData, file) {
1604
+ let that = this;
1605
+ let importOption = this.option.importOption;
1606
+ // 进度框接手,选文件框与全屏遮罩让位
1607
+ this.loadingObj && this.loadingObj.close();
1608
+ this.loadingObj = null;
1609
+ this.showImportDialog = false;
1610
+ this.showImportDialog2 = true;
1611
+ this.createImportTimer();
1612
+
1613
+ // 内嵌图片三段式(分片上传 checkData → getData)由 getImportData 把后端解析出的
1614
+ // 附件写回行上的 attachments_ 列。它每一段开头都判 showImportDialog2,所以只能接在
1615
+ // 进度框打开之后、逐行开跑之前;跑完把 importLoading 归位,进度框切回百分比视图。
1616
+ if (importOption.enabledImportPreHandle) {
1617
+ this.uploadFile(() => {
1618
+ that.importLoading = false;
1619
+ that.runFrontImportRows(resultData);
1620
+ });
1621
+ return;
1622
+ }
1623
+ this.runFrontImportRows(resultData);
1624
+ },
1625
+
1626
+ /**
1627
+ * 前端通道的逐行执行。
1628
+ * 默认:校验 逐行处理 → 通过的行一次性入表。
1629
+ * 开了后台校验:先收集前台通过行(失败立刻记账),整批一次 formHttp,
1630
+ * 按 impSeq 合并补数后再走入表 / 逐行脚本(不再重复前台校验)。
1631
+ * @param {Object} resultData readExcel 的解析信封。
1632
+ */
1633
+ runFrontImportRows(resultData) {
1634
+ let that = this;
1635
+ let importOption = this.option.importOption || {};
1636
+ if (importOption.backendValidateEnabled) {
1637
+ if (!importOption.importScriptCode) {
1638
+ this.handleImportAbort(
1639
+ this.$t1("后台校验脚本编码(importScriptCode)未配置,无法校验")
1640
+ );
1641
+ return;
1642
+ }
1643
+ let passedRows = [];
1644
+ this.runImportRows({
1645
+ rows: resultData?.data || [],
1646
+ deferFinish: true,
1647
+ handler: (row, index, done) => {
1648
+ let errorInfo = that.validateImportRow(row, index, {
1649
+ data: resultData,
1650
+ });
1651
+ if (errorInfo) {
1652
+ done({ success: false, message: errorInfo });
1653
+ return;
1654
+ }
1655
+ passedRows.push(row);
1656
+ done({ defer: true });
1657
+ },
1658
+ onFinish: () => {
1659
+ that.requestBackendValidate(resultData, passedRows);
1660
+ },
1661
+ });
1662
+ return;
1663
+ }
1664
+ this.applyFrontImportRows(resultData, resultData?.data || [], {});
1665
+ },
1666
+
1667
+ /**
1668
+ * 入表 / 逐行脚本。skipValidate 时不再跑前台校验(后台那步已经滤过)。
1669
+ * @param {Object} resultData readExcel 的解析信封。
1670
+ * @param {Array} rows 待处理行。
1671
+ * @param {Object} opts { skipValidate, progress }
1672
+ */
1673
+ applyFrontImportRows(resultData, rows, opts) {
1674
+ let that = this;
1675
+ let importOption = this.option.importOption || {};
1676
+ let isScriptMode = importOption.importMode === "script";
1677
+ let skipValidate = !!(opts && opts.skipValidate);
1678
+ let passedRows = [];
1679
+ this.runImportRows({
1680
+ rows: rows || [],
1681
+ progress: opts && opts.progress,
1682
+ handler: (row, index, done) => {
1683
+ // 后台补数后只跑通过的子集,循环下标会从 0 重计;脚本要用 Excel 行序
1684
+ let rowIndex = row.impSeq != null ? row.impSeq : index;
1685
+ if (!skipValidate) {
1686
+ let errorInfo = that.validateImportRow(row, rowIndex, {
1687
+ data: resultData,
1688
+ });
1689
+ if (errorInfo) {
1690
+ done({ success: false, message: errorInfo });
1691
+ return;
1692
+ }
1693
+ }
1694
+ if (isScriptMode) {
1695
+ // 逐行脚本模式:脚本只写"这一行怎么处理",循环/让步/进度归壳管。
1696
+ // 没配脚本时按通过处理,避免整批卡在第一行。
1697
+ if (!importOption.onImportRow) {
1698
+ done({ success: true });
1699
+ return;
1700
+ }
1701
+ that
1702
+ .getFieldTarget()
1703
+ .handleCustomEvent(
1704
+ importOption.onImportRow,
1705
+ ["row", "index", "done"],
1706
+ [row, rowIndex, done]
1707
+ );
1708
+ return;
1709
+ }
1710
+ passedRows.push(row);
1711
+ done({ success: true });
1712
+ },
1713
+ onFinish: () => {
1714
+ if (isScriptMode) return;
1715
+ if (!importOption.tableRef || !passedRows.length) return;
1716
+ let tableRef = that
1717
+ .getFieldTarget()
1718
+ .getWidgetRef(importOption.tableRef);
1719
+ // 逐行 addTableData 会逐行 setValue + handleWbs,几百行就卡;这里一次性追加。
1720
+ // 入表的必须是剥掉记账字段的副本:addTableData 是 Object.assign 全量拷贝,
1721
+ // 不剥就把 impSeq/_resultType 一起写进表单数据,跟着保存提交出去。
1722
+ if (tableRef) {
1723
+ tableRef.addTableData(
1724
+ passedRows.map((row) => that.stripImportMeta(row)),
1725
+ importOption.dedupField || undefined
1726
+ );
1727
+ }
1728
+ },
1729
+ });
1730
+ },
1731
+
1732
+ /** 当前表单的 formCode / formVersion;formImport 通道没有 getReportTemplate,改从配置取。 */
1733
+ getImportFormMeta() {
1734
+ let importOption = this.option.importOption || {};
1735
+ let reportTemplate =
1736
+ typeof this.getReportTemplate === "function"
1737
+ ? this.getReportTemplate()
1738
+ : null;
1739
+ return {
1740
+ formCode: reportTemplate?.formCode || importOption.formCode,
1741
+ formVersion: reportTemplate?.formVersion,
1742
+ };
1743
+ },
1744
+
1745
+ /** onImportParams 整批一次,非对象则当 {}。 */
1746
+ getBackendValidateParams(resultData) {
1747
+ let importOption = this.option.importOption || {};
1748
+ if (!importOption.onImportParams) return {};
1749
+ let result = this.getFieldTarget().handleCustomEvent(
1750
+ importOption.onImportParams,
1751
+ ["resultData", "file"],
1752
+ [resultData, this.fileRaw]
1753
+ );
1754
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
1755
+ return {};
1756
+ }
1757
+ return result;
1758
+ },
1759
+
1760
+ /** 送给后台的行:剥掉进度记账字段,保留 impSeq / impNumber 供回包对位。 */
1761
+ buildBackendValidatePayloadRows(rows) {
1762
+ return (rows || []).map((row) => {
1763
+ let item = this.stripImportMeta(row);
1764
+ item.impSeq = row.impSeq;
1765
+ item.impNumber = row.impNumber;
1766
+ return item;
1767
+ });
1768
+ },
1769
+
1770
+ mergeBackendValidateRow(row, item) {
1771
+ if (!item) return;
1772
+ Object.keys(item).forEach((key) => {
1773
+ if (BACKEND_VALIDATE_SKIP_FIELDS.indexOf(key) >= 0) return;
1774
+ row[key] = item[key];
1775
+ });
1776
+ },
1777
+
1778
+ /**
1779
+ * impSeq 把后台逐行结果合并进原行。
1780
+ * 没返回的行、impReturnType=error 的行记失败,不入后续入表。
1781
+ */
1782
+ applyBackendValidateResult(passedRows, objx) {
1783
+ let progress = this.importProgress;
1784
+ let list = Array.isArray(objx) ? objx : [];
1785
+ let map = {};
1786
+ list.forEach((item) => {
1787
+ if (item && item.impSeq != null) map[item.impSeq] = item;
1788
+ });
1789
+ let enriched = [];
1790
+ (passedRows || []).forEach((row) => {
1791
+ let item = map[row.impSeq];
1792
+ if (!item) {
1793
+ progress.markError(row, this.$t1("后台未返回该行结果"));
1794
+ return;
1795
+ }
1796
+ if (item.impReturnType === "error") {
1797
+ progress.markError(
1798
+ row,
1799
+ item.impReturnMsg || this.$t1("数据校验未通过")
1800
+ );
1801
+ return;
1802
+ }
1803
+ this.mergeBackendValidateRow(row, item);
1804
+ enriched.push(row);
1805
+ });
1806
+ return enriched;
1807
+ },
1808
+
1809
+ /**
1810
+ * 前台通过的行整批一次送给后台校验 + 补数。
1811
+ * 不要求 importEntity、不套附件信封;data 就是行数组,params 来自 onImportParams。
1812
+ */
1813
+ requestBackendValidate(resultData, passedRows) {
1814
+ let that = this;
1815
+ let finishProgress = () => {
1816
+ that.backendValidating = false;
1817
+ if (that.importProgress) that.importProgress.finish();
1818
+ };
1819
+ if (!passedRows || !passedRows.length) {
1820
+ finishProgress();
1821
+ return;
1822
+ }
1823
+ let importOption = this.option.importOption || {};
1824
+ let scriptCode = importOption.importScriptCode;
1825
+ if (!scriptCode) {
1826
+ this.handleImportAbort(
1827
+ this.$t1("后台校验脚本编码(importScriptCode)未配置,无法校验")
1828
+ );
1829
+ return;
1830
+ }
1831
+ let formMeta = this.getImportFormMeta();
1832
+ let params;
1833
+ try {
1834
+ params = this.getBackendValidateParams(resultData);
1835
+ } catch (et) {
1836
+ console.error(et);
1837
+ this.handleImportAbort(this.$t1("请求额外参数脚本异常"));
1838
+ return;
1839
+ }
1840
+ this.backendValidating = true;
1841
+ this.formHttp({
1842
+ scriptCode: scriptCode,
1843
+ data: {
1844
+ formCode: formMeta.formCode,
1845
+ formVersion: formMeta.formVersion,
1846
+ data: this.buildBackendValidatePayloadRows(passedRows),
1847
+ params: params,
1848
+ },
1849
+ modal: false,
1850
+ failMsg: false,
1851
+ callback: (resultMsg) => {
1852
+ that.backendValidating = false;
1853
+ if (that.isEnd || !that.showContent) return;
1854
+ if (!resultMsg || resultMsg.type !== "success") {
1855
+ that.handleImportAbort(
1856
+ (resultMsg && resultMsg.content) || that.$t1("后台校验失败")
1857
+ );
1858
+ return;
1859
+ }
1860
+ let enriched = that.applyBackendValidateResult(
1861
+ passedRows,
1862
+ resultMsg.objx
1863
+ );
1864
+ that.applyFrontImportRows(resultData, enriched, {
1865
+ skipValidate: true,
1866
+ progress: that.importProgress,
1867
+ });
1868
+ },
1869
+ error: (et) => {
1870
+ that.backendValidating = false;
1871
+ console.error(et);
1872
+ that.handleImportAbort(that.$t1("后台校验失败"));
1873
+ },
1874
+ });
1875
+ },
1876
+
1877
+ //begin
1878
+ fileParse(file, type = "base64", callback) {
1879
+ let fileRead = new FileReader();
1880
+ if (type === "base64") {
1881
+ fileRead.readAsDataURL(file);
1882
+ } else if (type === "buffer") {
1883
+ fileRead.readAsArrayBuffer(file);
1884
+ }
1885
+ fileRead.onload = (ev) => {
1886
+ callback(ev.target.result);
1887
+ };
1888
+ },
1889
+ getUploadText(percent) {
1890
+ return this.$t1("上传导入文件 {percent}%", { percent });
1891
+ },
1892
+ changeUploadPercent(percent) {
1893
+ // return this.$t1('上传导入文件 {percent}%', {percent})
1894
+ this.percent1 = percent;
1895
+ this.loadingIndex = 1;
1896
+ },
1897
+ uploadFile(callback) {
1898
+ let that = this;
1899
+ that.loadingPercent = 0;
1900
+ // that.loadingTitle = this.getUploadText(0);
1901
+ that.changeUploadPercent(0);
1902
+ let file = this.fileRaw;
1903
+ let uploadChunkSize = 5;
1904
+ this.importLoading = true;
1905
+ this.fileUploadCallback = callback;
1906
+ this.fileParse(file, "buffer", (buffer) => {
1907
+ let spark = new SparkMD5.ArrayBuffer(),
1908
+ chunkSize = uploadChunkSize * 1024 * 1024, // Read in chunks of 2MB
1909
+ chunks = Math.ceil(file.size / chunkSize),
1910
+ hash,
1911
+ suffix;
1912
+ spark.append(buffer);
1913
+ hash = spark.end();
1914
+ let md5 = hash;
1915
+ let nameSplit = /\.([0-9a-zA-Z]+)$/i.exec(file.name);
1916
+
1917
+ suffix = nameSplit && nameSplit.length > 1 ? nameSplit[1] : "";
1918
+
1919
+ let chunkParam = {
1920
+ fullFile: file,
1921
+ chunkSize: chunkSize,
1922
+ chunks: chunks,
1923
+ chunkIndex: 0,
1924
+ md5: hash,
1925
+ uuid: "",
1926
+ name: file.name,
1927
+ };
1928
+ file.chunkParam = chunkParam;
1929
+
1930
+ this.sendRequest(file);
1931
+ });
1932
+ },
1933
+ async sendRequest(fullFile) {
1934
+ if (!this.showImportDialog2) return;
1935
+ let that = this;
1936
+ let uploadUrl = USER_PREFIX + "/excel_import/uploadFile";
1937
+ let chunkParam = fullFile.chunkParam;
1938
+ let target = that.$refs.upload;
1939
+ let abort;
1940
+ let config = {
1941
+ headers: {
1942
+ "Content-Type": "multipart/form-data",
1943
+ },
1944
+ cancelToken: new axios.CancelToken(function executor(c) {
1945
+ abort = c;
1946
+ fullFile.abort = c;
1947
+ }),
1948
+ onUploadProgress: (progressEvent) => {
1949
+ let start = chunkParam.start;
1950
+ let uploadedSize =
1951
+ start +
1952
+ Math.floor(
1953
+ (chunkParam.currentSize * progressEvent.loaded) /
1954
+ progressEvent.total
1955
+ );
1956
+ let percent = Math.floor((uploadedSize * 100) / fullFile.size);
1957
+ if (percent === 100) {
1958
+ percent = 99;
1959
+ }
1960
+ that.loadingPercent = percent;
1961
+
1962
+ // that.$set(that, 'loadingTitle', this.getUploadText(percent))
1963
+ that.changeUploadPercent(percent);
1964
+ },
1965
+ // failMsg: false,
1966
+ modal: false,
1967
+ callback: (res1) => {
1968
+ if (res1.type === "success") {
1969
+ let uuid = res1.objx.uuid;
1970
+ chunkParam.uuid = uuid;
1971
+ if (preFormData.chunk + 1 >= preFormData.chunks) {
1972
+ let mergeData = {
1973
+ name: chunkParam.name,
1974
+ md5: chunkParam.md5,
1975
+ uuid: uuid,
1976
+ };
1977
+ let _param = {
1978
+ mergeData: mergeData,
1979
+ chunkParam: chunkParam,
1980
+ };
1981
+ setTimeout(() => {
1982
+ this.mergeFile(_param);
1983
+ }, 100);
1984
+ } else {
1985
+ chunkParam.chunkIndex = chunkParam.chunkIndex + 1;
1986
+ chunkParam.uuid = uuid;
1987
+ setTimeout(() => {
1988
+ this.sendRequest(fullFile);
1989
+ }, 100);
1990
+ }
1991
+ } else {
1992
+ this.dialogClose2(0);
1993
+ }
1994
+ return res1;
1995
+ },
1996
+ error: (err) => {
1997
+ console.log(err);
1998
+ this.dialogClose2(0);
1999
+ },
2000
+ }; //添加请求头
2001
+
2002
+ let chunkIndex = chunkParam.chunkIndex;
2003
+ let chunkSize = chunkParam.chunkSize;
2004
+ let start = chunkIndex * chunkSize,
2005
+ end =
2006
+ start + chunkSize >= fullFile.size
2007
+ ? fullFile.size
2008
+ : start + chunkParam.chunkSize;
2009
+ chunkParam.currentSize = end - start;
2010
+ chunkParam.start = start;
2011
+ chunkParam.end = end;
2012
+
2013
+ let preFormData = {
2014
+ file: fullFile.slice(start, end),
2015
+ name: fullFile.name,
2016
+ chunks: chunkParam.chunks,
2017
+ chunk: chunkIndex,
2018
+ md5: chunkParam.md5,
2019
+ uuid: chunkParam.uuid,
2020
+ };
2021
+
2022
+ let formData = new FormData();
2023
+ Object.keys(preFormData).forEach((key) => {
2024
+ formData.append(key, preFormData[key]);
2025
+ });
2026
+
2027
+ let req = this.$http.post(uploadUrl, formData, config);
2028
+ req.abort = () => {
2029
+ abort(this.$t2("取消上传", "components.VabUpload.cancelUpload"));
2030
+ };
2031
+ },
2032
+ mergeFile(param) {
2033
+ if (!this.showImportDialog2) return;
2034
+ let that = this;
2035
+ let mergeData = param.mergeData;
2036
+ let chunkParam = param.chunkParam;
2037
+ let fullFile = chunkParam.fullFile;
2038
+ let mergeUrl = USER_PREFIX + "/excel_import/mergeChunk";
2039
+ let target = this.$refs.upload;
2040
+ this.$http({
2041
+ url: mergeUrl,
2042
+ method: "post",
2043
+ data: mergeData,
2044
+ // failMsg: false,
2045
+ modal: false,
2046
+ cancelToken: new axios.CancelToken(function executor(c) {
2047
+ if (fullFile) fullFile.abort = c;
2048
+ }),
2049
+ callback: (res) => {
2050
+ if (res.type === "success") {
2051
+ that.loadingPercent = 100;
2052
+ // that.loadingTitle = this.getUploadText(100);
2053
+ this.changeUploadPercent(100);
2054
+ setTimeout(() => {
2055
+ this.checkImportData();
2056
+ }, 100);
2057
+ } else {
2058
+ this.dialogClose2(0);
2059
+ }
2060
+ },
2061
+ error: (e) => {
2062
+ console.log(err);
2063
+ this.dialogClose2(0);
2064
+ },
2065
+ });
2066
+ },
2067
+ getCheckText(percent) {
2068
+ return this.$t1("校验导入文件中的图片数据格式 {percent}%", { percent });
2069
+ },
2070
+ changeCheckTextPercent(percent) {
2071
+ this.percent2 = percent;
2072
+ this.loadingIndex = 2;
2073
+ },
2074
+ checkImportData() {
2075
+ if (!this.showImportDialog2) return;
2076
+ let that = this;
2077
+ that.loadingPercent = 0;
2078
+ // that.loadingTitle = this.getCheckText(0);
2079
+ that.changeCheckTextPercent(0);
2080
+ let fullFile = this.fileRaw;
2081
+ let chunkParam = fullFile.chunkParam;
2082
+
2083
+ this.$http({
2084
+ url: USER_PREFIX + "/excel_import/checkData",
2085
+ method: "post",
2086
+ data: {
2087
+ name: chunkParam.name,
2088
+ md5: chunkParam.md5,
2089
+ uuid: chunkParam.uuid,
2090
+ },
2091
+ // failMsg: false,
2092
+ modal: false,
2093
+ cancelToken: new axios.CancelToken(function executor(c) {
2094
+ if (fullFile) fullFile.abort = c;
2095
+ }),
2096
+ onUploadProgress: (progressEvent) => {
2097
+ let percent = Math.floor(
2098
+ (progressEvent.loaded * 100) / progressEvent.total
2099
+ );
2100
+ if (percent === 100) {
2101
+ percent = 99;
2102
+ }
2103
+ that.loadingPercent = percent;
2104
+ // that.$set(that, 'loadingTitle', this.getCheckText(percent))
2105
+ that.changeCheckTextPercent(percent);
2106
+ },
2107
+ callback: (res) => {
2108
+ if (res.type === "success") {
2109
+ that.loadingPercent = 100;
2110
+ // that.loadingTitle = this.getCheckText(100);
2111
+ that.changeCheckTextPercent(100);
2112
+ this.handleImageData();
2113
+ } else {
2114
+ this.dialogClose2(0);
2115
+ }
2116
+ },
2117
+ error: (e) => {
2118
+ console.log(err);
2119
+ this.dialogClose2(0);
2120
+ },
2121
+ });
2122
+ },
2123
+ getSaveImageText(percent) {
2124
+ return this.$t1("存储导入文件中的图片 {percent}%", { percent });
2125
+ },
2126
+ changeSaveImageTextPercent(percent) {
2127
+ // return this.$t1('存储导入文件中的图片 {percent}%', {percent})
2128
+ this.percent3 = percent;
2129
+ this.loadingIndex = 3;
2130
+ },
2131
+ handleImageData() {
2132
+ let that = this;
2133
+ that.loadingPercent = 0;
2134
+ // that.loadingTitle = this.getSaveImageText(0);
2135
+ that.changeSaveImageTextPercent(0);
2136
+ this.getImportData(0);
2137
+ },
2138
+ getImportData(index) {
2139
+ if (!this.showImportDialog2) return;
2140
+ let that = this;
2141
+ let rowSize = this.handleRowSize;
2142
+ let fullFile = this.fileRaw;
2143
+ let chunkParam = fullFile.chunkParam;
2144
+ let rowDatas = this.resultData.data;
2145
+ let size = rowDatas.length;
2146
+ let currentLineNo = index + 1;
2147
+ let rowNumber = index + 3;
2148
+ let startRow = index + 3;
2149
+ let endRow = index + 3 + rowSize - 1;
2150
+ this.$http({
2151
+ url: USER_PREFIX + "/excel_import/getData",
2152
+ method: "post",
2153
+ data: {
2154
+ name: chunkParam.name,
2155
+ md5: chunkParam.md5,
2156
+ uuid: chunkParam.uuid,
2157
+ startRow: startRow,
2158
+ endRow: endRow,
2159
+ },
2160
+ // failMsg: false,
2161
+ modal: false,
2162
+ cancelToken: new axios.CancelToken(function executor(c) {
2163
+ if (fullFile) fullFile.abort = c;
2164
+ }),
2165
+ callback: (res) => {
2166
+ if (res.type === "success") {
2167
+ let rows = res?.objx?.datas || [];
2168
+ let fileCellFields = this.fileCellFields;
2169
+ rows.forEach((row) => {
2170
+ let rowIndex = row.row - 3;
2171
+ let item = rowDatas[rowIndex];
2172
+ Object.keys(row).forEach((key) => {
2173
+ if (fileCellFields.includes(key)) {
2174
+ let value = row[key] || [];
2175
+ if (!Array.isArray(value)) {
2176
+ value = [value];
2177
+ }
2178
+ item[key] = value;
2179
+ }
2180
+ });
2181
+ });
2182
+ let cIndex = index + rowSize - 1;
2183
+ let percent = Math.floor(((cIndex + 1) * 100) / size);
2184
+ that.loadingPercent = percent;
2185
+ // that.loadingTitle = this.getSaveImageText(percent);
2186
+ that.changeSaveImageTextPercent(percent);
2187
+ if (cIndex + 1 < size) {
2188
+ this.getImportData(cIndex + 1);
2189
+ } else {
2190
+ // 三段式按 fileCellFields 白名单把原始附件列写了回来,「值存为URL」
2191
+ // 列要在这里再转一次,否则目标实体列拿到的是附件对象数组
2192
+ this.applyImportUrlAttachments(rowDatas);
2193
+ this.fileUploadCallback();
2194
+ }
2195
+ } else {
2196
+ this.dialogClose2(0);
2197
+ }
2198
+ },
2199
+ error: (e) => {
2200
+ this.dialogClose2(0);
2201
+ },
2202
+ });
2203
+ },
2204
+ async generateNewExcel() {
2205
+ let file = this.fileRaw;
2206
+ try {
2207
+ // 1. 读取原始文件
2208
+ const workbook = XLSX.read(file, { type: "binary" });
2209
+ const sheetName = workbook.SheetNames[0];
2210
+ const rawData = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]);
2211
+
2212
+ // 2. 提取表头和数据
2213
+ const headers = rawData.slice(0, this.headerRows);
2214
+ const bodyData = rawData.slice(this.headerRows);
2215
+
2216
+ let exporLineNos = [];
2217
+ if (this.resultType) {
2218
+ exporLineNos = this.successRows.map((item) => item.impNumber - 1);
2219
+ } else {
2220
+ exporLineNos = this.failRows.map((item) => item.impNumber - 1);
2221
+ }
2222
+ // 3. 应用筛选规则
2223
+ let selectRule = (row, index) => {
2224
+ return exporLineNos.includes(index);
2225
+ };
2226
+ const filteredData = bodyData.filter(
2227
+ (row, index) => {
2228
+ return selectRule(row, index + this.headerRows);
2229
+ }
2230
+ // this.selectRule(row, index + this.headerRows) // 注意索引转换
2231
+ );
2232
+
2233
+ // 4. 合并数据
2234
+ const newData = [...headers, ...filteredData];
2235
+
2236
+ // 5. 生成新文件
2237
+ const newWorkbook = XLSX.utils.book_new();
2238
+ const newSheet = XLSX.utils.aoa_to_sheet(newData);
2239
+ XLSX.utils.book_append_sheet(newWorkbook, newSheet, "Sheet1");
2240
+
2241
+ // 6. 下载文件
2242
+ const blob = XLSX.write(newWorkbook, {
2243
+ bookType: "xlsx",
2244
+ type: "array",
2245
+ });
2246
+
2247
+ const url = URL.createObjectURL(blob);
2248
+ const a = document.createElement("a");
2249
+ a.href = url;
2250
+ let fileName = null;
2251
+ if (this.resultType) {
2252
+ fileName = `导入结果(成功).xlsx`;
2253
+ } else {
2254
+ fileName = `导入结果(失败).xlsx`;
2255
+ }
2256
+ a.download = fileName;
2257
+ a.click();
2258
+
2259
+ // 7. 更新进度
2260
+ this.progress = 100;
2261
+ setTimeout(() => (this.progress = 0), 1000);
2262
+ } catch (error) {
2263
+ console.error("处理文件时出错:", error);
2264
+ }
2265
+ },
2266
+ //end
2267
+ getLoadingClass(flag, percent) {
2268
+ let classStr = null;
2269
+ let loadingIndex = this.loadingIndex;
2270
+ if (percent >= 100) {
2271
+ classStr = "ok";
2272
+ } else if (flag === loadingIndex) {
2273
+ classStr = "loading";
2274
+ }
2275
+ return classStr;
2276
+ },
2277
+
2278
+ /***sheet1 */
2279
+ async readExcel2(file, callback) {
2280
+ let that = this;
2281
+ let option = this.option;
2282
+
2283
+ // 附件列清单按本次解析重建:解析可以在同一个弹框里跑多次(strict 拦下来后
2284
+ // 用户重选文件、"确定"又没有 pending 锁),只 push 不清会把上一份的附件列带进来。
2285
+ this.fileCellFields = [];
2286
+ this.urlAttachFields = {};
2287
+
2288
+ const fileReader = new FileReader();
2289
+ fileReader.onload = async (ev) => {
2290
+ let resultData;
2291
+ try {
2292
+ const data = ev.target.result;
2293
+ const workbook = configUtil.XLSX.read(data, {
2294
+ type: "binary",
2295
+ });
2296
+
2297
+ let sheetDatas = [];
2298
+ for (let pi = 0; pi < workbook.SheetNames.length; pi++) {
2299
+ let sheetName = workbook.SheetNames[pi];
2300
+ let sheet = workbook.Sheets[sheetName];
2301
+
2302
+ if (sheet["!ref"] === undefined || sheet["!ref"] === null) {
2303
+ that.$baseAlert(
2304
+ this.$t2(
2305
+ "请使用正确的导入模版!",
2306
+ "components.excelImport.warmMsg2"
2307
+ )
2308
+ );
2309
+ that.handleClose2();
2310
+ return false;
2311
+ }
2312
+ let range = configUtil.XLSX.utils.decode_range(sheet["!ref"]);
2313
+ let dataSize = range.e.r + 1; //行数
2314
+ that.dataSize = dataSize - 3 >= 0 ? dataSize - 3 : 0;
2315
+ // 行数闸门:下面逐格取值、建行对象、再整批发包全是同步的,行数失控时主线程会被
2316
+ // 占死甚至 OOM。文件体积不大但行极多(窄表)时只有这一道能拦。
2317
+ if (isImportRowsOverLimit(that.dataSize, option)) {
2318
+ that.clearImportTimer();
2319
+ that.$baseAlert(
2320
+ that.$t1(
2321
+ "导入数据 {size} 行,超过单次上限 {limit} 行,请拆分后分批导入",
2322
+ {
2323
+ size: that.dataSize,
2324
+ limit: getImportRowLimit(option),
2325
+ }
2326
+ )
2327
+ );
2328
+ that.handleClose2();
2329
+ return false;
2330
+ }
2331
+ let titles = [];
2332
+ let fields = [];
2333
+
2334
+ const ws = workbook.Sheets[sheet];
2335
+
2336
+ let prefix = option.prefix;
2337
+ let excelUrl = option.excel;
2338
+
2339
+ let excelRows = [];
2340
+ let row;
2341
+ let rowNum;
2342
+ let colNum;
2343
+ for (rowNum = range.s.r; rowNum <= range.e.r; rowNum++) {
2344
+ row = [];
2345
+ for (colNum = range.s.c; colNum <= range.e.c; colNum++) {
2346
+ let nextCell =
2347
+ sheet[
2348
+ configUtil.XLSX.utils.encode_cell({
2349
+ r: rowNum,
2350
+ c: colNum,
2351
+ })
2352
+ ];
2353
+ if (typeof nextCell === "undefined") {
2354
+ row.push(void 0);
2355
+ } else {
2356
+ row.push(getImportCellText(nextCell));
2357
+ }
2358
+ }
2359
+ if (rowNum === 1) {
2360
+ titles = row;
2361
+ }
2362
+ if (rowNum === 2) {
2363
+ fields = row;
2364
+ let checkResult = true;
2365
+ }
2366
+ if (rowNum > 2) {
2367
+ let item = {};
2368
+ for (let i = 0; i < fields.length; i++) {
2369
+ let field = fields[i];
2370
+ if (field) {
2371
+ item[field] = row[i] ?? null;
2372
+ }
2373
+ }
2374
+ excelRows.push(item);
2375
+ }
2376
+ }
2377
+ if (
2378
+ !titles.length ||
2379
+ !fields.length ||
2380
+ titles.length !== fields.length
2381
+ ) {
2382
+ that.$baseAlert(
2383
+ this.$t2(
2384
+ "请使用正确的导入模版!",
2385
+ "components.excelImport.warmMsg2"
2386
+ )
2387
+ );
2388
+ // that.handleClose2();
2389
+ return false;
2390
+ }
2391
+
2392
+ let columns = [];
2393
+ let cols = [];
2394
+ let uniqueFields = [];
2395
+ let uniqueKeys = [];
2396
+ for (let i = 0; i < titles.length; i++) {
2397
+ let title = titles[i];
2398
+ if (title) {
2399
+ let field = fields[i];
2400
+ if (field === null || field === undefined) {
2401
+ that.$baseAlert(
2402
+ this.$t2(
2403
+ "请使用正确的导入模版!",
2404
+ "components.excelImport.warmMsg2"
2405
+ )
2406
+ );
2407
+ // that.handleClose2();
2408
+ return false;
2409
+ }
2410
+ let t = title.substr(-1);
2411
+ let u = title.length >= 2 ? title.substr(-2) : title;
2412
+ let p = title.length >= 3 ? title.substr(-3) : title;
2413
+ let isNeed = false;
2414
+ if (t === "*") {
2415
+ isNeed = true;
2416
+ }
2417
+ let isUnique = false;
2418
+ /* if (u === "**") {
2419
+ uniqueFields.push(field);
2420
+ isUnique = true;
2421
+ } */
2422
+ let isSheetUnique = false;
2423
+ if (p === "***") {
2424
+ uniqueKeys.push(field);
2425
+ isSheetUnique = true;
2426
+ }
2427
+ let otherConfig = {};
2428
+ let isFile = false;
2429
+ let urlTargetField = null;
2430
+ if (field.startsWith(IMPORT_ATTACH_PREFIX)) {
2431
+ that.fileCellFields.push(field);
2432
+ isFile = true;
2433
+ otherConfig = {
2434
+ slots: {
2435
+ default: "attachment",
2436
+ },
2437
+ };
2438
+ let urlAttach = that.parseUrlAttachmentField(field);
2439
+ if (urlAttach) {
2440
+ that.urlAttachFields[field] = urlAttach;
2441
+ urlTargetField = urlAttach.target;
2442
+ }
2443
+ }
2444
+
2445
+ let col = {
2446
+ field: field,
2447
+ title: title,
2448
+ width: 100,
2449
+ isNeed: isNeed,
2450
+ isUnique: isUnique,
2451
+ isFile,
2452
+ urlTargetField,
2453
+ otherConfig,
2454
+ isSheetUnique,
2455
+ };
2456
+ cols.push(col);
2457
+ }
2458
+ }
2459
+
2460
+ // 单据唯一标识是整张 sheet 的属性,必须等该 sheet 所有列解析完再判。
2461
+ // 早前这段写在列循环内部:第一列还没轮到带 *** 的那列,uniqueKeys 必然是空,
2462
+ // 于是"唯一标识不在第一列"的模板一律误报缺失。
2463
+ if (!uniqueKeys.length) {
2464
+ that.$baseAlert(
2465
+ `[${sheetName}]` + that.$t1("缺失单据唯一标识***")
2466
+ );
2467
+ return false;
2468
+ }
2469
+
2470
+ let oriCols = JSON.parse(JSON.stringify(cols)); //列信息
2471
+ // 附件列收口成对象数组;多 sheet 下 fileCellFields 是全表累积的,
2472
+ // 所以按本 sheet 自己的 oriCols 判 isFile,不能用 fileCellFields。
2473
+ that.normalizeImportAttachmentCells(excelRows, oriCols);
2474
+ that.applyImportUrlAttachments(excelRows);
2475
+ /* if (uniqueKeys.length) {
2476
+ //分组唯一字段为空时,全部不发包。
2477
+ for (let i = 0; i < oriCols.length; i++) {
2478
+ let oriCol = oriCols[i];
2479
+ if (uniqueKeys.includes(oriCol.field)) {
2480
+ for (let j = 0; j < excelRows.length; j++) {
2481
+ let data = excelRows[j];
2482
+ let value = data[oriCol.field];
2483
+ if (value === null || value === undefined || value === "") {
2484
+ that.errorHandle(`[${sheetName}]`+
2485
+ that.$t2(
2486
+ "第" + (j + 4) + "行[" + oriCol.title + "]不能为空;",
2487
+ "components.excelImport.warmMsg3",
2488
+ {
2489
+ lineNo: j + 4,
2490
+ title: oriCol.title,
2491
+ }
2492
+ ));
2493
+ return false;
2494
+ }
2495
+ }
2496
+ }
2497
+ }
2498
+ } */
2499
+ // this.oriCols = oriCols;
2500
+
2501
+ let resultData1 = {
2502
+ sheetName: sheetName,
2503
+ data: excelRows,
2504
+ titles: titles,
2505
+ fields: fields,
2506
+ uniqueFields: uniqueFields,
2507
+ oriCols: oriCols,
2508
+ uniqueKeys,
2509
+ };
2510
+ sheetDatas.push(resultData1);
2511
+ }
2512
+
2513
+ //校验必填
2514
+ if (!that.checkAllSheetData(sheetDatas)) {
2515
+ return false;
2516
+ }
2517
+
2518
+ //合并多sheet数据
2519
+ let newDatas = [];
2520
+ resultData = that.$baseLodash.cloneDeep(sheetDatas[0]);
2521
+ let uniqueKey = resultData.uniqueKeys[0];
2522
+ let data2 = [];
2523
+ for (let i = 0; i < resultData.data.length; i++) {
2524
+ let item = resultData.data[i];
2525
+ // 这里原有一行 `let sheetName = resultData[sheetName];`:初始化式里引用了正在
2526
+ // 声明的同名变量,命中 TDZ 抛 ReferenceError,被外层 try/catch 吞成
2527
+ // showContent=false —— 表现为多 sheet 导入弹框直接消失、只有 console 有错。
2528
+ // 该变量全程没人读,直接删除。
2529
+ let uniqueValue = item[uniqueKey];
2530
+
2531
+ if (!that.isNull(uniqueValue)) {
2532
+ let newItem = { ...item };
2533
+ let allItems = [];
2534
+ for (let j = 1; j < sheetDatas.length; j++) {
2535
+ let sheetData = sheetDatas[j];
2536
+ let fDatas = [];
2537
+ for (let k = 0; k < sheetData.data.length; k++) {
2538
+ let sheetItem = sheetData.data[k];
2539
+ if (that.isNull(sheetItem[uniqueKey])) {
2540
+ that.handleCellNotNull(k, sheetData);
2541
+ return;
2542
+ }
2543
+ if (sheetItem[uniqueKey] === uniqueValue) {
2544
+ fDatas.push(sheetItem);
2545
+ }
2546
+ }
2547
+ allItems.push(fDatas);
2548
+ }
2549
+ newItem.sheetItems = allItems;
2550
+ data2.push(newItem);
2551
+ } else {
2552
+ that.handleCellNotNull(i, resultData);
2553
+ return false;
2554
+ }
2555
+ }
2556
+
2557
+ resultData.sheetDatas = sheetDatas;
2558
+ resultData.data = data2;
2559
+ this.oriCols = resultData.oriCols;
2560
+ this.resultData = resultData;
2561
+ callback(resultData);
2562
+ } catch (et) {
2563
+ that.showContent = false;
2564
+ that.clearImportTimer();
2565
+ console.error(et);
2566
+ }
2567
+ };
2568
+ fileReader.readAsBinaryString(file);
2569
+ },
2570
+ isNull(val) {
2571
+ return val === null || val === undefined || val === "";
2572
+ },
2573
+ checkAllSheetData(sheetItems){
2574
+ let that = this;
2575
+ for(let sheetItem of sheetItems){
2576
+ let oriCols = sheetItem.oriCols;
2577
+ let uniqueKeys = sheetItem.uniqueKeys;
2578
+ let excelRows = sheetItem.data;
2579
+ let sheetName = sheetItem.sheetName;
2580
+
2581
+ for (let i = 0; i < oriCols.length; i++) {
2582
+ let oriCol = oriCols[i];
2583
+ let isNeed = oriCol.isNeed || uniqueKeys.includes(oriCol.field);
2584
+ if (!isNeed && !oriCol.isFile) continue;
2585
+ for (let j = 0; j < excelRows.length; j++) {
2586
+ let data = excelRows[j];
2587
+ let value = that.getImportCellValue(data, oriCol);
2588
+ // 归一后附件列的空值是 [],isNull 判不出来,必填的附件列会漏过
2589
+ if (isNeed && that.isEmptyImportCell(value)) {
2590
+ that.errorHandle(`[${sheetName}]`+
2591
+ that.$t2(
2592
+ "第" + (j + 4) + "行[" + oriCol.title + "]不能为空;",
2593
+ "components.excelImport.warmMsg3",
2594
+ {
2595
+ lineNo: j + 4,
2596
+ title: oriCol.title,
2597
+ }
2598
+ ));
2599
+ return false;
2600
+ }
2601
+ if (that.isBadAttachmentCell(oriCol, value)) {
2602
+ let message = that.$t1("第{lineNo}行[{title}]不是有效的附件地址;", {
2603
+ lineNo: j + 4,
2604
+ title: oriCol.title,
2605
+ });
2606
+ that.errorHandle(`[${sheetName}]${message}`);
2607
+ return false;
2608
+ }
2609
+ }
2610
+ }
2611
+
2612
+ }
2613
+ return true;
2614
+ },
2615
+
2616
+
2617
+ handleCellNotNull(i, sheetData) {
2618
+ let that = this;
2619
+ let uniqueKey = sheetData.uniqueKeys[0];
2620
+ let oriCol = sheetData.oriCols.find((item) => item.field === uniqueKey);
2621
+ that.errorHandle(
2622
+ `[${sheetData.sheetName}]` +
2623
+ that.$t2(
2624
+ "第" + (i + 4) + "行[" + oriCol.title + "]不能为空;",
2625
+ "components.excelImport.warmMsg3",
2626
+ {
2627
+ lineNo: i + 4,
2628
+ title: oriCol.title,
2629
+ }
2630
+ )
2631
+ );
2632
+ },
2633
+ errorHandle(errorText) {
2634
+ this.clearImportTimer();
2635
+ this.$baseAlert(errorText);
2636
+ // this.handleClose2();
2637
+ },
2638
+ /***sheet2 */
2639
+ },
2640
+ };
2641
+ export const mixins = tmixins;