leisure-core 1.0.2 → 1.0.4

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 (94) hide show
  1. package/index.js +223 -0
  2. package/le-area/index.js +7 -0
  3. package/le-area/src/main.vue +112 -0
  4. package/le-button/index.js +7 -0
  5. package/le-button/src/main.vue +47 -0
  6. package/le-button-attach/index.js +6 -0
  7. package/le-button-attach/src/main.vue +171 -0
  8. package/le-button-msg/index.js +7 -0
  9. package/le-button-msg/src/main.vue +65 -0
  10. package/le-button-qr/index.js +7 -0
  11. package/le-button-qr/src/main.vue +96 -0
  12. package/le-date-picker/index.js +7 -0
  13. package/le-date-picker/src/main.vue +67 -0
  14. package/le-date-picker-auto/index.js +6 -0
  15. package/le-date-picker-auto/src/main.vue +131 -0
  16. package/le-dialog-container/index.js +7 -0
  17. package/le-dialog-container/src/main.vue +80 -0
  18. package/le-form/index.js +7 -0
  19. package/le-form/src/main.vue +13 -0
  20. package/le-form-item/index.js +7 -0
  21. package/le-form-item/src/main.vue +13 -0
  22. package/le-home/index.js +7 -0
  23. package/le-home/src/LeHeaderMenu.vue +60 -0
  24. package/le-home/src/main.vue +1307 -0
  25. package/le-home/src/sub.vue +82 -0
  26. package/le-image/index.js +7 -0
  27. package/le-image/src/main.vue +53 -0
  28. package/le-input/index.js +7 -0
  29. package/le-input/src/main.vue +15 -0
  30. package/le-input-advanced/index.js +7 -0
  31. package/le-input-advanced/src/main.vue +71 -0
  32. package/le-input-number/index.js +7 -0
  33. package/le-input-number/src/main.vue +15 -0
  34. package/le-input-validate/index.js +6 -0
  35. package/le-input-validate/src/main.vue +469 -0
  36. package/le-libs/js/drag.js +78 -0
  37. package/le-libs/js/event.js +3 -0
  38. package/le-libs/js/hrequest.js +76 -0
  39. package/le-libs/js/hrequestFile.js +151 -0
  40. package/le-libs/js/info.js +7 -0
  41. package/le-libs/js/tokens.js +13 -0
  42. package/le-libs/mixins/main.js +48 -0
  43. package/le-list/index.js +7 -0
  44. package/le-list/src/main.vue +645 -0
  45. package/le-list-form/index.js +7 -0
  46. package/le-list-form/src/main.vue +474 -0
  47. package/le-login/index.js +7 -0
  48. package/le-login/src/ForgotPassword.vue +613 -0
  49. package/le-login/src/main.vue +982 -0
  50. package/le-media/index.js +7 -0
  51. package/le-media/src/main.vue +298 -0
  52. package/le-media-list/index.js +7 -0
  53. package/le-media-list/src/main.vue +178 -0
  54. package/le-media-upload/index.js +7 -0
  55. package/le-media-upload/src/main.vue +329 -0
  56. package/le-menu/index.js +7 -0
  57. package/le-menu/src/main.vue +248 -0
  58. package/le-report/index.js +6 -0
  59. package/le-report/src/main.vue +1747 -0
  60. package/le-report/src/rmixins.js +30 -0
  61. package/le-role/index.js +7 -0
  62. package/le-role/src/main.vue +183 -0
  63. package/le-role/src/sub.vue +79 -0
  64. package/le-role-user/index.js +7 -0
  65. package/le-role-user/src/main.vue +160 -0
  66. package/le-role-user/src/sub.vue +90 -0
  67. package/le-select/index.js +7 -0
  68. package/le-select/src/main.vue +49 -0
  69. package/le-select-multi/index.js +6 -0
  70. package/le-select-multi/src/main.vue +816 -0
  71. package/le-select-option/index.js +7 -0
  72. package/le-select-option/src/main.vue +69 -0
  73. package/le-select-search/index.js +7 -0
  74. package/le-select-search/src/main.vue +46 -0
  75. package/le-span/index.js +7 -0
  76. package/le-span/src/main.vue +29 -0
  77. package/le-table-edit/index.js +6 -0
  78. package/le-table-edit/src/main.vue +689 -0
  79. package/le-theme/common/vars.css +131 -0
  80. package/le-theme/iconfont/iconfont.css +125 -0
  81. package/le-theme/iconfont/iconfont.ttf +0 -0
  82. package/le-theme/iconfont/iconfont.woff +0 -0
  83. package/le-theme/iconfont/iconfont.woff2 +0 -0
  84. package/le-theme/index.js +189 -0
  85. package/le-theme/pages/index.css +15 -0
  86. package/le-upload/index.js +7 -0
  87. package/le-upload/src/main.vue +134 -0
  88. package/le-upload-table/index.js +6 -0
  89. package/le-upload-table/src/main.vue +648 -0
  90. package/le-url/index.js +7 -0
  91. package/le-url/src/main.vue +27 -0
  92. package/le-video/index.js +7 -0
  93. package/le-video/src/main.vue +42 -0
  94. package/package.json +1 -6
@@ -0,0 +1,1747 @@
1
+ <template>
2
+ <div class="page-container" v-watermark>
3
+ <div class="title-container">
4
+ <span class="title-tag">{{ tag }}</span>
5
+ <div class="title-main">
6
+ <h2>{{ title }}</h2>
7
+ <div v-if="displaySubtitle" class="subtitle">
8
+ {{ displaySubtitle + "&nbsp;&nbsp;&nbsp;" + unit }}
9
+ </div>
10
+ </div>
11
+ </div>
12
+ <el-card shadow="never">
13
+ <el-form :inline="true">
14
+ <div class="query-param-area">
15
+ <slot name="paramSlot"></slot>
16
+ <el-form-item>
17
+ <el-button type="primary" @click="onClickQuery">查询</el-button>
18
+ <el-button type="success" v-if="isUsedExport" @click="handleExport"
19
+ >导出Excel</el-button
20
+ >
21
+ <slot name="topBtns"></slot>
22
+ </el-form-item>
23
+ </div>
24
+ </el-form>
25
+ </el-card>
26
+ <!-- 表格区域 -->
27
+ <vxe-table
28
+ ref="eltablemain"
29
+ :data="displayData"
30
+ :loading="isLoading"
31
+ border
32
+ :row-config="{ keyField: rowId }"
33
+ :column-config="{ resizable: true }"
34
+ :header-row-class-name="headerRowClassName"
35
+ :cell-class-name="cellClassName"
36
+ stripe
37
+ :merge-cells="mergeCells"
38
+ :header-cell-class-name="headerCellClassName"
39
+ show-header
40
+ auto-resize
41
+ :height="elTableMaxHeight"
42
+ :scroll-x="{ gt: 0 }"
43
+ :scroll-y="{ gt: 0 }"
44
+ :show-footer="showSummary && summaryPosition === 'bottom'"
45
+ :footer-method="summaryPosition === 'bottom' ? footerMethod : null"
46
+ >
47
+ <!-- 加载状态插槽 -->
48
+ <template #loading>
49
+ <div v-if="isLoading" class="vxe-loading-mask">
50
+ <div class="vxe-loading-content">
51
+ <i class="el-icon-loading vxe-loading-icon"></i>
52
+ <span class="vxe-loading-text">加载中,请稍候...</span>
53
+ </div>
54
+ </div>
55
+ </template>
56
+
57
+ <!-- 序号列 -->
58
+ <vxe-column
59
+ title="序号"
60
+ width="70"
61
+ fixed="left"
62
+ v-if="showIndex"
63
+ show-overflow-tooltip
64
+ >
65
+ <template #default="{ row, rowIndex }">
66
+ <span v-if="row._isSummary"></span>
67
+ <span v-else>{{
68
+ (searchData.pageNo - 1) * searchData.pageSize + rowIndex
69
+ }}</span>
70
+ </template>
71
+ </vxe-column>
72
+
73
+ <template v-for="(column, index) in complexHeaderColumns">
74
+ <vxe-column
75
+ v-if="!column.children"
76
+ :key="`column-${getUniqueKey(column, index)}`"
77
+ :field="column.prop"
78
+ :title="column.label"
79
+ :width="column.width"
80
+ show-overflow-tooltip
81
+ >
82
+ <template #default="{ row }">
83
+ <component
84
+ :is="renderColumnContent(column)"
85
+ v-bind="getComponentProps(column, row)"
86
+ ></component>
87
+ </template>
88
+ </vxe-column>
89
+
90
+ <!-- 分组列 -->
91
+ <vxe-colgroup
92
+ v-else
93
+ :key="`colgroup-${getUniqueKey(column, index)}`"
94
+ :title="column.label"
95
+ :width="column.width"
96
+ >
97
+ <template v-for="(child, childIndex) in column.children">
98
+ <vxe-colgroup
99
+ v-if="child.children"
100
+ :key="`sub-colgroup-${getUniqueKey(child, childIndex)}`"
101
+ :title="child.label"
102
+ :width="child.width"
103
+ >
104
+ <vxe-column
105
+ v-for="(grandChild, grandChildIndex) in child.children"
106
+ :key="`grand-column-${getUniqueKey(
107
+ grandChild,
108
+ grandChildIndex,
109
+ )}`"
110
+ :field="grandChild.prop"
111
+ :title="grandChild.label"
112
+ :width="grandChild.width"
113
+ show-overflow-tooltip
114
+ >
115
+ <template #default="{ row }">
116
+ <component
117
+ :is="renderColumnContent(grandChild)"
118
+ v-bind="getComponentProps(grandChild, row)"
119
+ ></component>
120
+ </template>
121
+ </vxe-column>
122
+ </vxe-colgroup>
123
+ <vxe-column
124
+ v-else
125
+ :key="`sub-column-${getUniqueKey(child, childIndex)}`"
126
+ :field="child.prop"
127
+ :title="child.label"
128
+ :width="child.width"
129
+ show-overflow-tooltip
130
+ >
131
+ <template #default="{ row }">
132
+ <component
133
+ :is="renderColumnContent(child)"
134
+ v-bind="getComponentProps(child, row)"
135
+ ></component>
136
+ </template>
137
+ </vxe-column>
138
+ </template>
139
+ </vxe-colgroup>
140
+ </template>
141
+
142
+ <!-- 扩展列插槽 -->
143
+ <slot name="extColumn"></slot>
144
+ </vxe-table>
145
+
146
+ <!-- 分页组件 -->
147
+ <div style="text-align: center; margin-top: 30px">
148
+ <el-pagination
149
+ background
150
+ layout="prev, pager, next, jumper,sizes, total"
151
+ :current-page="searchData.pageNo"
152
+ :total="searchData.total"
153
+ :page-size="searchData.pageSize"
154
+ @current-change="current_change"
155
+ @size-change="size_change"
156
+ :page-sizes="[10, 20, 30, 40, 50, 100]"
157
+ >
158
+ </el-pagination>
159
+ </div>
160
+ </div>
161
+ </template>
162
+
163
+ <script>
164
+ import rMixins from "./rmixins.js";
165
+ import ExcelJS from "exceljs";
166
+ import FileSaver from "file-saver";
167
+
168
+ export default {
169
+ name: "le-report",
170
+ mixins: [rMixins],
171
+ props: {
172
+ showIndex: {
173
+ type: Boolean,
174
+ default: true,
175
+ },
176
+ title: {
177
+ type: String,
178
+ default: "",
179
+ },
180
+ unit: {
181
+ type: String,
182
+ default: "单位:万元",
183
+ },
184
+ subtitle: {
185
+ type: String,
186
+ default: "",
187
+ },
188
+ tag: {
189
+ type: String,
190
+ default: "",
191
+ },
192
+ tableData: {
193
+ type: Array,
194
+ default: () => [],
195
+ },
196
+ totalData: {
197
+ type: Number,
198
+ default: 1,
199
+ },
200
+ tableColumns: {
201
+ type: Array,
202
+ default: () => [],
203
+ },
204
+ searchParam: {
205
+ type: Object,
206
+ default: () => ({}),
207
+ },
208
+ rowId: {
209
+ type: String,
210
+ default: "id",
211
+ },
212
+ complexHeaderConfig: {
213
+ type: Array,
214
+ default: () => [],
215
+ },
216
+ // loading: {
217
+ // type: Boolean,
218
+ // default: false,
219
+ // },
220
+ // 是否显示合计行
221
+ showSummary: {
222
+ type: Boolean,
223
+ default: false,
224
+ },
225
+ // 合计行位置配置:'top' 或 'bottom'
226
+ summaryPosition: {
227
+ type: String,
228
+ default: "bottom",
229
+ validator: (value) => ["top", "bottom"].includes(value),
230
+ },
231
+ // 自定义数值字段配置
232
+ numericFields: {
233
+ type: Array,
234
+ default: () => [],
235
+ },
236
+ summaryData: {
237
+ //后台合计数据
238
+ type: Object,
239
+ default: null,
240
+ },
241
+ // 合计行标签字段
242
+ summaryLabelField: {
243
+ type: String,
244
+ default: "area",
245
+ },
246
+ isUsedExport: {
247
+ // 是否启用导出功能
248
+ type: Boolean,
249
+ default: true,
250
+ },
251
+ exportFileName: {
252
+ type: String,
253
+ default: "",
254
+ },
255
+ exportType: {
256
+ type: String,
257
+ default: "xlsx",
258
+ },
259
+ // 新增:仅导出时使用的数值字段(不影响页面显示)
260
+ exportOnlyNumericFields: {
261
+ type: Array,
262
+ default: () => [], // 仅导出Excel时使用
263
+ },
264
+ // 新增:导出时数值字段的Excel格式映射
265
+ exportNumericFormats: {
266
+ type: Object,
267
+ default: () => ({
268
+ currency: "#,##0.00",
269
+ percent: "0.00%",
270
+ number: "#,##0.00",
271
+ integer: "#,##0",
272
+ decimal: "0.00",
273
+ }),
274
+ },
275
+ autoDetectNumericFields: {
276
+ type: Boolean,
277
+ default: true, // 默认自动检测
278
+ },
279
+ autoDetectTypes: {
280
+ type: Array,
281
+ default: () => ["currency", "number", "percent"],
282
+ },
283
+ },
284
+ watch: {
285
+ searchParam: {
286
+ handler(val) {
287
+ this.searchData = Object.assign({}, this.searchData, val);
288
+ },
289
+ deep: true,
290
+ immediate: true,
291
+ },
292
+ totalData: {
293
+ handler(val) {
294
+ this.searchData.total = val;
295
+ },
296
+ },
297
+ complexHeaderConfig: {
298
+ handler(val) {
299
+ if (val && val.length > 0) {
300
+ this.complexHeaderColumns = val;
301
+ }
302
+ },
303
+ deep: true,
304
+ immediate: true,
305
+ },
306
+ tableData: {
307
+ handler() {
308
+ if (this.isLoading) {
309
+ this.isLoading = false; // 关闭
310
+ }
311
+ this.updateDisplayData();
312
+ },
313
+ deep: true,
314
+ },
315
+ showSummary: {
316
+ handler() {
317
+ this.updateDisplayData();
318
+ },
319
+ },
320
+ summaryPosition: {
321
+ handler() {
322
+ this.updateDisplayData();
323
+ },
324
+ },
325
+ },
326
+ data() {
327
+ return {
328
+ searchData: {
329
+ pageNo: 1,
330
+ total: 1,
331
+ pageSize: 20,
332
+ },
333
+ componentMap: {
334
+ url: {
335
+ component: "le-url",
336
+ prop: "value",
337
+ },
338
+ text: {
339
+ component: "le-span",
340
+ prop: "value",
341
+ },
342
+ },
343
+ // 合并单元格配置
344
+ mergeCells: [],
345
+ // 复杂表头配置
346
+ complexHeaderColumns: [],
347
+ displayData: [],
348
+ isLoading: false,
349
+ };
350
+ },
351
+ computed: {
352
+ computedNumericFields() {
353
+ if (this.numericFields && this.numericFields.length > 0) {
354
+ return this.numericFields;
355
+ }
356
+
357
+ const numericFields = [];
358
+ const traverseColumns = (columns) => {
359
+ columns.forEach((column) => {
360
+ if (column.children) {
361
+ traverseColumns(column.children);
362
+ } else {
363
+ if (
364
+ (column.type === "currency" || column.type === "percent") &&
365
+ column.prop
366
+ ) {
367
+ numericFields.push(column.prop);
368
+ }
369
+ }
370
+ });
371
+ };
372
+
373
+ if (this.complexHeaderColumns && this.complexHeaderColumns.length > 0) {
374
+ traverseColumns(this.complexHeaderColumns);
375
+ }
376
+
377
+ return numericFields;
378
+ },
379
+ displaySubtitle() {
380
+ return this.subtitle || this.initDate();
381
+ },
382
+ computedExportNumericFields() {
383
+ // 获取用户明确指定的字段
384
+ const userDefined = this.exportOnlyNumericFields || [];
385
+
386
+ // 获取自动检测的字段
387
+ const autoDetected = this.getAutoDetectedNumericFields();
388
+
389
+ // 合并并去重
390
+ const allFields = [...new Set([...userDefined, ...autoDetected])];
391
+
392
+ return allFields;
393
+ },
394
+ // 列类型映射(用于导出时判断格式)
395
+ columnTypeMap() {
396
+ const map = {};
397
+ const traverse = (columns) => {
398
+ columns.forEach((col) => {
399
+ if (col.children) {
400
+ traverse(col.children);
401
+ } else if (col.prop) {
402
+ // 记录列类型
403
+ map[col.prop] = col.type || "text";
404
+ // 如果是currency或number类型,自动添加到数值字段列表中(用于显示合计行等)
405
+ if (
406
+ col.type === "currency" ||
407
+ col.type === "number" ||
408
+ col.type === "percent"
409
+ ) {
410
+ if (!this.computedExportNumericFields.includes(col.prop)) {
411
+ // 注意:这里不能直接修改prop,需要记录到临时变量或使用计算属性
412
+ // 我们将在isExportNumericField方法中动态判断
413
+ }
414
+ }
415
+ }
416
+ });
417
+ };
418
+
419
+ if (this.complexHeaderColumns && this.complexHeaderColumns.length > 0) {
420
+ traverse(this.complexHeaderColumns);
421
+ }
422
+
423
+ return map;
424
+ },
425
+ },
426
+ mounted() {
427
+ this.isLoading = true;
428
+ this.getList();
429
+ this.updateDisplayData();
430
+ },
431
+
432
+ methods: {
433
+ getUniqueKey(column, index) {
434
+ return `${column.prop || "column"}-${index}-${Date.now()}`;
435
+ },
436
+ // 新增:判断是否为导出数值字段
437
+ isExportNumericField(field) {
438
+ if (this.computedExportNumericFields.includes(field)) {
439
+ return true;
440
+ }
441
+ // 2. 如果字段类型是currency或number,自动视为数值字段
442
+ const columnType = this.columnTypeMap[field];
443
+ const numericTypes = ["currency", "number", "percent"];
444
+
445
+ if (numericTypes.includes(columnType)) {
446
+ return true;
447
+ }
448
+
449
+ return false;
450
+ },
451
+
452
+ // 更新显示数据
453
+ updateDisplayData() {
454
+ if (this.showSummary && this.summaryPosition === "top") {
455
+ // 在顶部添加合计行
456
+ const summaryRow = this.createSummaryRow();
457
+ summaryRow.id = "summary_" + Date.now();
458
+ this.displayData = [summaryRow, ...this.tableData];
459
+ } else {
460
+ // 不显示合计行或合计行在底部
461
+ this.displayData = this.tableData;
462
+ }
463
+ this.changeHeight();
464
+ },
465
+
466
+ createSummaryRow() {
467
+ let summaryRow = {
468
+ _isSummary: true,
469
+ id: "summary",
470
+ seq: "合计",
471
+ };
472
+
473
+ // 如果有后台提供的合计数据,优先使用
474
+ if (this.summaryData && Object.keys(this.summaryData).length > 0) {
475
+ Object.keys(this.summaryData).forEach((key) => {
476
+ summaryRow[key] = this.summaryData[key];
477
+ });
478
+
479
+ if (this.summaryLabelField && !summaryRow[this.summaryLabelField]) {
480
+ summaryRow[this.summaryLabelField] = "合计";
481
+ }
482
+ } else {
483
+ // 前端计算合计
484
+ const leafColumns = this.getLeafColumns();
485
+
486
+ leafColumns.forEach((column) => {
487
+ if (column.prop && this.isNumericField(column.prop)) {
488
+ const sum = this.tableData.reduce((total, row) => {
489
+ const value = parseFloat(row[column.prop]) || 0;
490
+ return total + value;
491
+ }, 0);
492
+ summaryRow[column.prop] = this.isZeroValue(sum) ? "" : sum;
493
+ }
494
+ });
495
+
496
+ if (this.summaryLabelField) {
497
+ summaryRow[this.summaryLabelField] = "合计";
498
+ }
499
+ }
500
+
501
+ this.computedNumericFields.forEach((field) => {
502
+ if (this.isZeroValue(summaryRow[field])) {
503
+ summaryRow[field] = "";
504
+ }
505
+ });
506
+
507
+ return summaryRow;
508
+ },
509
+
510
+ footerMethod({ columns, data }) {
511
+ let summaryColumnIndex = columns.findIndex(
512
+ (column) => column.property === this.summaryLabelField,
513
+ );
514
+
515
+ if (summaryColumnIndex === -1) {
516
+ summaryColumnIndex = 1;
517
+ }
518
+
519
+ const footerData = [
520
+ columns.map((column, columnIndex) => {
521
+ if (columnIndex === summaryColumnIndex) {
522
+ return "合计";
523
+ }
524
+
525
+ const field = column.property;
526
+
527
+ if (this.summaryData && this.computedNumericFields.includes(field)) {
528
+ const value = this.summaryData[field];
529
+ if (this.isZeroValue(value)) {
530
+ return "";
531
+ }
532
+ return this.format("currency", value);
533
+ }
534
+
535
+ if (this.computedNumericFields.includes(field)) {
536
+ const sum = data.reduce((total, row) => {
537
+ const value = parseFloat(row[field]) || 0;
538
+ return total + value;
539
+ }, 0);
540
+ if (this.isZeroValue(sum)) {
541
+ return "";
542
+ }
543
+ return this.format("currency", sum);
544
+ }
545
+
546
+ return "";
547
+ }),
548
+ ];
549
+
550
+ return footerData;
551
+ },
552
+
553
+ // 单元格样式
554
+ cellClassName({ row }) {
555
+ if (row._isSummary) {
556
+ return "summary-row text-center";
557
+ }
558
+ return "text-center";
559
+ },
560
+
561
+ // 表头行样式
562
+ headerRowClassName() {
563
+ return "header-row";
564
+ },
565
+
566
+ // 表头单元格样式
567
+ headerCellClassName() {
568
+ return "text-center header-cell";
569
+ },
570
+
571
+ renderColumnContent(column) {
572
+ return this.componentMap[column.type]?.component || "le-span";
573
+ },
574
+
575
+ getComponentProps(column, row) {
576
+ // 如果是合计行,特殊处理
577
+ if (row._isSummary) {
578
+ const value = row[column.prop];
579
+
580
+ // 如果是序号列
581
+ if (column.type === "seq" || column.prop === "seq") {
582
+ return { value: "合计" };
583
+ }
584
+
585
+ // 如果是合计标签字段
586
+ if (column.prop === this.summaryLabelField) {
587
+ return { value: "合计" };
588
+ }
589
+
590
+ // 对数值字段进行格式化
591
+ if (this.isNumericField(column.prop)) {
592
+ if (this.isZeroValue(value)) {
593
+ return { value: "" };
594
+ }
595
+ const formattedValue = this.format(column.type || "currency", value);
596
+ return { value: formattedValue };
597
+ }
598
+
599
+ // 其他字段正常显示
600
+ return { value: value || "" };
601
+ }
602
+
603
+ // 普通行的处理保持不变
604
+ let value = row[column.prop];
605
+
606
+ // 格式化处理
607
+ if (column.formatter) {
608
+ value = column.formatter(row, column, value);
609
+ }
610
+ if (column.type) {
611
+ value = this.format(column.type, value, column.spec);
612
+ }
613
+
614
+ const ctype = this.componentMap[column.type];
615
+ const prop = ctype?.prop || "value";
616
+ const param = {};
617
+ param[prop] = value || "";
618
+
619
+ if (ctype?.style) {
620
+ param.style = ctype.style;
621
+ }
622
+ return param;
623
+ },
624
+
625
+ // 判断是否为数值字段
626
+ isNumericField(field) {
627
+ return this.computedNumericFields.includes(field);
628
+ },
629
+
630
+ onClickQuery() {
631
+ this.isLoading = true;
632
+ this.searchData.pageNo = 1;
633
+ this.getList();
634
+ },
635
+
636
+ async getList() {
637
+ await this.$emit("getList", this.searchData);
638
+ },
639
+
640
+ size_change(item) {
641
+ this.isLoading = true;
642
+ this.searchData.pageSize = item;
643
+ this.getList();
644
+ },
645
+
646
+ current_change(currentPage) {
647
+ this.isLoading = true;
648
+ this.searchData.pageNo = currentPage;
649
+ this.getList();
650
+ },
651
+
652
+ format(type, value, spec) {
653
+ if (type === undefined || value === undefined) return value;
654
+
655
+ switch (type) {
656
+ case "bool":
657
+ return value === 1 || value === "1" || value === true ? "是" : "否";
658
+ case "date":
659
+ if (!value) return "";
660
+ try {
661
+ return spec ? this.parseTime(value, spec) : this.parseTime(value);
662
+ } catch {
663
+ return value;
664
+ }
665
+ case "currency":
666
+ if (value == null || value === "") return "";
667
+ const num = parseFloat(value);
668
+ if (isNaN(num)) return value;
669
+
670
+ return new Intl.NumberFormat("zh-CN", {
671
+ style: "decimal",
672
+ currency: "CNY",
673
+ minimumFractionDigits: 0,
674
+ maximumFractionDigits: 2,
675
+ }).format(num);
676
+ case "percent":
677
+ if (value == null || value === "") return "";
678
+ const percentNum = parseFloat(value);
679
+ if (isNaN(percentNum)) return value;
680
+ return percentNum.toFixed(2) + "%";
681
+
682
+ default:
683
+ return value;
684
+ }
685
+ },
686
+
687
+ // 设置合并单元格
688
+ setMergeCells(mergeRules) {
689
+ this.mergeCells = mergeRules;
690
+ },
691
+
692
+ // 获取当前合并规则
693
+ getMergeCells() {
694
+ return this.mergeCells;
695
+ },
696
+
697
+ resetClick() {
698
+ this.$emit("resetParams");
699
+ },
700
+
701
+ isZeroValue(value) {
702
+ if (value === null || value === undefined || value === "") {
703
+ return true;
704
+ }
705
+
706
+ const num = parseFloat(value);
707
+ return !isNaN(num) && num === 0;
708
+ },
709
+
710
+ async handleExport() {
711
+ try {
712
+ await this.exportUsingExcelJS();
713
+ } catch (error) {
714
+ console.error("导出失败:", error);
715
+ this.$message.error("导出失败: " + (error.message || "未知错误"));
716
+ }
717
+ },
718
+
719
+ handleExportMore() {
720
+ this.$emit("exportMore");
721
+ },
722
+
723
+ // 使用ExcelJS导出Excel
724
+ async exportUsingExcelJS() {
725
+ try {
726
+ const workbook = new ExcelJS.Workbook();
727
+ const worksheet = workbook.addWorksheet("Sheet1");
728
+
729
+ // 构建数据行
730
+ const dataRows = this.buildExcelJSDataRows();
731
+
732
+ // 添加数据行到工作表
733
+ dataRows.forEach((rowData, rowIndex) => {
734
+ // 提取单元格值(兼容对象数组和普通数组)
735
+ const rowValues = rowData.map((cell) => {
736
+ if (cell && typeof cell === "object" && cell.value !== undefined) {
737
+ return cell.value;
738
+ }
739
+ return cell;
740
+ });
741
+
742
+ const worksheetRow = worksheet.addRow(rowValues);
743
+
744
+ // 设置行高
745
+ this.setExcelJSRowHeight(worksheetRow, rowIndex, dataRows.length);
746
+
747
+ // 设置单元格样式 - 传入行数据
748
+ this.setExcelJSCellStyles(
749
+ worksheet,
750
+ worksheetRow,
751
+ rowIndex,
752
+ dataRows.length,
753
+ rowData, // 传入完整行数据
754
+ );
755
+ });
756
+
757
+ // 设置列宽
758
+ this.setExcelJSColumnWidths(worksheet);
759
+
760
+ // 合并单元格
761
+ this.setExcelJSMerges(worksheet);
762
+
763
+ // 写入到缓冲区
764
+ const buffer = await workbook.xlsx.writeBuffer();
765
+
766
+ // 保存文件
767
+ const blob = new Blob([buffer], {
768
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
769
+ });
770
+
771
+ FileSaver.saveAs(blob, `${this.getExportFileName()}.xlsx`);
772
+ this.$message.success("导出成功");
773
+ } catch (error) {
774
+ console.error("ExcelJS导出失败:", error);
775
+ this.$message.error("导出失败: " + (error.message || "未知错误"));
776
+ }
777
+ },
778
+
779
+ buildExcelJSDataRow(rowData, index, allData) {
780
+ const row = [];
781
+ const leafColumns = this.getLeafColumns();
782
+
783
+ // 计算序号
784
+ if (rowData._isSummary) {
785
+ row.push({ value: "合计", type: "string" });
786
+ } else {
787
+ // ... 序号计算逻辑保持不变 ...
788
+ row.push({ value: actualIndex, type: "number" });
789
+ }
790
+
791
+ // 填充数据列
792
+ leafColumns.forEach((col) => {
793
+ if (col.prop) {
794
+ let value = rowData[col.prop];
795
+ let cellType = "string";
796
+ const isExportNumeric = this.isExportNumericField(col.prop);
797
+ const columnType = this.columnTypeMap[col.prop] || col.type;
798
+ const isCurrencyOrNumber =
799
+ columnType === "currency" || columnType === "number";
800
+ const isPercent = columnType === "percent";
801
+
802
+ // 处理合计行
803
+ if (rowData._isSummary) {
804
+ if (col.prop === this.summaryLabelField) {
805
+ row.push({ value: "合计", type: "string" });
806
+ return;
807
+ }
808
+ }
809
+
810
+ // 智能判断是否为数值字段
811
+ if (isExportNumeric || isCurrencyOrNumber || isPercent) {
812
+ // 使用原始值进行数值解析
813
+ const rawValue = rowData[col.prop]; // 使用原始值,而不是格式化后的值
814
+ const numericValue = this.parseExportValue(rawValue, columnType);
815
+
816
+ if (numericValue !== null) {
817
+ value = numericValue;
818
+
819
+ // 设置单元格类型
820
+ if (isPercent) {
821
+ cellType = "percent";
822
+ } else if (isCurrencyOrNumber || isExportNumeric) {
823
+ cellType = "number";
824
+ }
825
+ } else {
826
+ // 如果无法转换为数字,使用格式化后的值
827
+ if (col.formatter) {
828
+ value = col.formatter(rowData, col, rawValue);
829
+ }
830
+ if (columnType) {
831
+ value = this.format(columnType, rawValue, col.spec);
832
+ }
833
+ cellType = "string";
834
+ }
835
+ } else {
836
+ // 非数值字段,使用页面显示格式
837
+ if (col.formatter) {
838
+ value = col.formatter(rowData, col, value);
839
+ }
840
+ if (columnType) {
841
+ value = this.format(columnType, value, col.spec);
842
+ }
843
+ cellType = "string";
844
+ }
845
+
846
+ row.push({
847
+ value: value !== null && value !== undefined ? value : "",
848
+ type: cellType,
849
+ isExportNumeric: isExportNumeric || isCurrencyOrNumber || isPercent,
850
+ columnType: columnType,
851
+ columnProp: col.prop,
852
+ _isSummary: rowData._isSummary,
853
+ });
854
+ } else {
855
+ row.push({ value: "", type: "string" });
856
+ }
857
+ });
858
+
859
+ return row;
860
+ },
861
+
862
+ // 获取导出数据 - 确保合计行位置正确
863
+ getExportData() {
864
+ // 深拷贝displayData,避免影响原始数据
865
+ const data = JSON.parse(JSON.stringify(this.displayData));
866
+
867
+ // 移除Vue内部属性
868
+ return data.map((item) => {
869
+ const newItem = { ...item };
870
+ delete newItem._XID;
871
+ delete newItem._X_ROW_KEY;
872
+ return newItem;
873
+ });
874
+ },
875
+
876
+ buildExcelJSDataRows() {
877
+ const dataRows = [];
878
+
879
+ // 1. 标题行(第1行)
880
+ const titleRow = new Array(this.getTotalColumnCount()).fill("");
881
+ titleRow[0] = this.title;
882
+ dataRows.push(titleRow);
883
+
884
+ // 2. 副标题行(第2行)
885
+ const subtitleRow = new Array(this.getTotalColumnCount()).fill("");
886
+ let subtitleText = "";
887
+ if (this.displaySubtitle) subtitleText += this.displaySubtitle;
888
+ if (this.displaySubtitle && this.unit) subtitleText += " ";
889
+ if (this.unit) subtitleText += this.unit;
890
+ subtitleRow[0] = subtitleText.trim();
891
+ dataRows.push(subtitleRow);
892
+
893
+ // 3. 表头行(根据complexHeaderColumns动态生成)
894
+ const headerRows = this.buildExcelJSHeaderRows();
895
+ headerRows.forEach((row) => dataRows.push(row));
896
+
897
+ const exportData = this.getExportData();
898
+ exportData.forEach((row, index) => {
899
+ const dataRow = this.buildExcelJSDataRow(row, index, exportData);
900
+ dataRows.push(dataRow);
901
+ });
902
+
903
+ return dataRows;
904
+ },
905
+
906
+ // 构建ExcelJS表头行的优化版本
907
+ buildExcelJSHeaderRows() {
908
+ const leafColumns = this.getLeafColumns();
909
+ const maxDepth = this.getHeaderDepth();
910
+
911
+ // 初始化表头行数组
912
+ const headerRows = Array.from(
913
+ { length: maxDepth },
914
+ () => new Array(leafColumns.length + 1).fill(""), // +1 为序号列
915
+ );
916
+
917
+ // 设置序号列的表头
918
+ headerRows[0][0] = "序号";
919
+ for (let i = 1; i < maxDepth; i++) {
920
+ headerRows[i][0] = ""; // 序号列在表头深度>1时,下方行留空
921
+ }
922
+
923
+ // 填充表头数据
924
+ this.fillExcelJSHeaderData(this.complexHeaderColumns, headerRows, 0, 1);
925
+
926
+ // 确保单层表头列的空白单元格被正确设置
927
+ this.ensureSingleLevelHeaderCells(headerRows);
928
+
929
+ return headerRows;
930
+ },
931
+
932
+ // 确保单层表头列的单元格被正确填充
933
+ ensureSingleLevelHeaderCells(headerRows) {
934
+ const maxDepth = headerRows.length;
935
+ const totalCols = headerRows[0].length;
936
+
937
+ // 遍历每一行和每一列
938
+ for (let row = 0; row < maxDepth; row++) {
939
+ for (let col = 1; col < totalCols; col++) {
940
+ // 从第1列开始(第0列是序号)
941
+ const cellValue = headerRows[row][col];
942
+
943
+ // 如果当前单元格为空,检查上方是否有值需要继承
944
+ if (cellValue === "" && row > 0) {
945
+ // 检查上方单元格是否有值
946
+ const aboveValue = headerRows[row - 1][col];
947
+ if (aboveValue !== "" && headerRows[row - 1][col] !== "") {
948
+ // 如果上方有值,且当前行不是最后一行,设置为空字符串(保持为空)
949
+ headerRows[row][col] = "";
950
+ }
951
+ }
952
+ }
953
+ }
954
+ },
955
+
956
+ // 优化填充表头数据方法
957
+ fillExcelJSHeaderData(columns, headerRows, currentDepth, startCol) {
958
+ let currentCol = startCol;
959
+
960
+ columns.forEach((column) => {
961
+ if (column.children && column.children.length > 0) {
962
+ // 分组列
963
+ const leafCount = this.countLeafColumns(column.children);
964
+
965
+ // 设置分组标题
966
+ headerRows[currentDepth][currentCol] = column.label;
967
+
968
+ // 对于单层表头,需要将标签向下填充到所有行
969
+ if (leafCount === 1) {
970
+ // 如果只有一个子列,这个子列就是单层表头
971
+ // 我们需要确保标签在当前深度设置,下面的行保持为空
972
+ // 留空处理,让合并单元格逻辑来处理
973
+ }
974
+
975
+ // 递归处理子列
976
+ const nextStartCol = this.fillExcelJSHeaderData(
977
+ column.children,
978
+ headerRows,
979
+ currentDepth + 1,
980
+ currentCol,
981
+ );
982
+
983
+ currentCol = nextStartCol;
984
+ } else {
985
+ // 单层表头列(叶子列)
986
+ // 只在当前深度设置标签,下面的行留空(会在合并单元格时处理)
987
+ headerRows[currentDepth][currentCol] = column.label;
988
+
989
+ // 确保下面的行留空(用于垂直合并)
990
+ for (let d = currentDepth + 1; d < headerRows.length; d++) {
991
+ headerRows[d][currentCol] = "";
992
+ }
993
+
994
+ currentCol++;
995
+ }
996
+ });
997
+
998
+ return currentCol;
999
+ },
1000
+
1001
+ // 优化构建数据行方法,正确处理合计行
1002
+ buildExcelJSDataRow(rowData, index, allData) {
1003
+ const row = [];
1004
+ const leafColumns = this.getLeafColumns();
1005
+
1006
+ // 计算序号
1007
+ if (rowData._isSummary) {
1008
+ row.push({ value: "合计", type: "string" });
1009
+ } else {
1010
+ let actualIndex = index + 1;
1011
+
1012
+ // 如果合计行在顶部,需要调整序号
1013
+ if (this.showSummary && this.summaryPosition === "top") {
1014
+ // 找到当前行在非合计行中的位置
1015
+ const dataWithoutSummary = allData.filter((item) => !item._isSummary);
1016
+ const dataIndex = dataWithoutSummary.findIndex(
1017
+ (item) => item.id === rowData.id,
1018
+ );
1019
+ if (dataIndex !== -1) {
1020
+ actualIndex = dataIndex + 1;
1021
+ }
1022
+ } else if (this.showSummary && this.summaryPosition === "bottom") {
1023
+ // 如果合计行在底部,正常计算序号
1024
+ const dataWithoutSummary = allData.filter((item) => !item._isSummary);
1025
+ const dataIndex = dataWithoutSummary.findIndex(
1026
+ (item) => item.id === rowData.id,
1027
+ );
1028
+ if (dataIndex !== -1) {
1029
+ actualIndex = dataIndex + 1;
1030
+ }
1031
+ }
1032
+ row.push({ value: actualIndex, type: "number" });
1033
+ }
1034
+
1035
+ // 填充数据列
1036
+ leafColumns.forEach((col) => {
1037
+ if (col.prop) {
1038
+ let value = rowData[col.prop];
1039
+ let cellType = "string";
1040
+ let isExportNumeric = this.isExportNumericField(col.prop);
1041
+ let columnType = this.columnTypeMap[col.prop];
1042
+
1043
+ // 处理合计行
1044
+ if (rowData._isSummary) {
1045
+ if (col.prop === this.summaryLabelField) {
1046
+ row.push({ value: "合计", type: "string" });
1047
+ return;
1048
+ }
1049
+ }
1050
+
1051
+ // 导出数值字段的特殊处理
1052
+ if (isExportNumeric) {
1053
+ const numericValue = this.parseExportValue(value, columnType);
1054
+
1055
+ if (numericValue !== null) {
1056
+ value = numericValue;
1057
+
1058
+ // 根据列类型设置单元格类型
1059
+ if (columnType === "percent") {
1060
+ cellType = "percent";
1061
+ } else {
1062
+ cellType = "number";
1063
+ }
1064
+ } else {
1065
+ // 如果无法转换为数字,保持原样
1066
+ cellType = "string";
1067
+ }
1068
+ } else {
1069
+ // 非导出数值字段,保持页面显示格式
1070
+ // 直接使用页面显示的格式化值
1071
+ if (col.formatter) {
1072
+ value = col.formatter(rowData, col, value);
1073
+ }
1074
+ if (columnType) {
1075
+ value = this.format(columnType, value, col.spec);
1076
+ }
1077
+ cellType = "string";
1078
+ }
1079
+
1080
+ row.push({
1081
+ value: value !== null && value !== undefined ? value : "",
1082
+ type: cellType,
1083
+ isExportNumeric: isExportNumeric,
1084
+ columnType: columnType,
1085
+ _isSummary: rowData._isSummary,
1086
+ });
1087
+ } else {
1088
+ row.push({ value: "", type: "string" });
1089
+ }
1090
+ });
1091
+
1092
+ return row;
1093
+ },
1094
+
1095
+ // 优化:解析导出数值,支持更多数值类型
1096
+ parseExportValue(value, columnType) {
1097
+ if (value === null || value === undefined || value === "") {
1098
+ return null;
1099
+ }
1100
+
1101
+ // 如果已经是数字类型,直接返回
1102
+ if (typeof value === "number") {
1103
+ // 百分比类型特殊处理
1104
+ if (columnType === "percent") {
1105
+ // 如果数值大于1(如12.34),转换为小数形式(0.1234)
1106
+ return value > 1 ? value / 100 : value;
1107
+ }
1108
+ return value;
1109
+ }
1110
+
1111
+ const strValue = String(value).trim();
1112
+
1113
+ // 空字符串
1114
+ if (strValue === "") {
1115
+ return null;
1116
+ }
1117
+
1118
+ // 特殊处理:如果字符串包含非数字字符(除了负号、小数点、千分位分隔符)
1119
+ const cleanValue = strValue
1120
+ .replace(/[¥$,,\s]/g, "") // 移除货币符号和千分位分隔符
1121
+ .replace(/%/g, ""); // 移除百分比符号
1122
+
1123
+ // 检查是否为合法数字(包括负数和小数)
1124
+ const numericRegex = /^-?\d+(\.\d+)?$/;
1125
+ if (!numericRegex.test(cleanValue)) {
1126
+ return null; // 不是有效的数字格式
1127
+ }
1128
+
1129
+ // 解析数字
1130
+ const parsed = parseFloat(cleanValue);
1131
+ if (isNaN(parsed)) {
1132
+ return null;
1133
+ }
1134
+
1135
+ // 根据列类型进行转换
1136
+ if (columnType === "percent") {
1137
+ // 百分比处理:原始值可能是12.34(表示12.34%),需要转换为0.1234
1138
+ return parsed > 1 ? parsed / 100 : parsed;
1139
+ }
1140
+
1141
+ return parsed;
1142
+ },
1143
+
1144
+ // 设置ExcelJS行高
1145
+ setExcelJSRowHeight(worksheetRow, rowIndex, totalRows) {
1146
+ const headerDepth = this.getHeaderDepth();
1147
+ const dataStartRow = 2 + headerDepth;
1148
+
1149
+ if (rowIndex === 0) {
1150
+ // 标题行高度
1151
+ worksheetRow.height = 38;
1152
+ } else if (rowIndex === 1) {
1153
+ // 副标题行高度
1154
+ worksheetRow.height = 30;
1155
+ } else if (rowIndex >= 2 && rowIndex < dataStartRow) {
1156
+ // 表头行高度
1157
+ worksheetRow.height = 38;
1158
+ } else {
1159
+ // 数据行高度
1160
+ // const dataRowIndex = rowIndex - dataStartRow;
1161
+ // const exportData = this.getExportData();
1162
+ // const rowData = exportData[dataRowIndex];
1163
+ // const isSummaryRow = rowData && rowData._isSummary;
1164
+ // if (isSummaryRow) {
1165
+ // // 合计行稍高一些
1166
+ // worksheetRow.height = 28;
1167
+ // } else {
1168
+ // worksheetRow.height = 25;
1169
+ // }
1170
+ }
1171
+ },
1172
+
1173
+ // 添加一个辅助方法,用于在导出时处理列类型
1174
+ getExportColumnType(columnProp) {
1175
+ const column = this.findColumnByProp(columnProp);
1176
+ return column ? column.type : null;
1177
+ },
1178
+
1179
+ findColumnByProp(prop) {
1180
+ let result = null;
1181
+ const traverse = (columns) => {
1182
+ for (const col of columns) {
1183
+ if (col.prop === prop) {
1184
+ result = col;
1185
+ return true;
1186
+ }
1187
+ if (col.children) {
1188
+ if (traverse(col.children)) {
1189
+ return true;
1190
+ }
1191
+ }
1192
+ }
1193
+ return false;
1194
+ };
1195
+
1196
+ if (this.complexHeaderColumns) {
1197
+ traverse(this.complexHeaderColumns);
1198
+ }
1199
+ return result;
1200
+ },
1201
+
1202
+ // 设置ExcelJS单元格样式
1203
+ setExcelJSCellStyles(
1204
+ worksheet,
1205
+ worksheetRow,
1206
+ rowIndex,
1207
+ totalRows,
1208
+ rowData,
1209
+ ) {
1210
+ const totalCols = this.getTotalColumnCount();
1211
+ const headerDepth = this.getHeaderDepth();
1212
+ const dataStartRow = 2 + headerDepth; // 标题行+副标题行+表头行
1213
+
1214
+ for (let col = 1; col <= totalCols; col++) {
1215
+ const cell = worksheetRow.getCell(col);
1216
+
1217
+ // 安全地获取单元格信息
1218
+ let cellInfo = {};
1219
+ if (rowData && Array.isArray(rowData)) {
1220
+ const rawCellData = rowData[col - 1];
1221
+ if (rawCellData && typeof rawCellData === "object") {
1222
+ // 如果是对象格式的单元格数据
1223
+ cellInfo = {
1224
+ ...rawCellData,
1225
+ // 确保有必要的属性
1226
+ value: rawCellData.value !== undefined ? rawCellData.value : "",
1227
+ type: rawCellData.type || "string",
1228
+ isExportNumeric: rawCellData.isExportNumeric || false,
1229
+ columnType: rawCellData.columnType,
1230
+ _isSummary: rawCellData._isSummary || false,
1231
+ };
1232
+ } else {
1233
+ // 如果是简单值(标题行、副标题行、表头行)
1234
+ cellInfo = {
1235
+ value: rawCellData !== undefined ? rawCellData : "",
1236
+ type: "string",
1237
+ isExportNumeric: false,
1238
+ _isSummary: false,
1239
+ };
1240
+ }
1241
+ } else {
1242
+ // 如果没有传入rowData,使用空对象
1243
+ cellInfo = {
1244
+ value: "",
1245
+ type: "string",
1246
+ isExportNumeric: false,
1247
+ _isSummary: false,
1248
+ };
1249
+ }
1250
+
1251
+ // 设置基本对齐
1252
+ cell.alignment = {
1253
+ vertical: "middle",
1254
+ horizontal: "center",
1255
+ wrapText: true,
1256
+ };
1257
+
1258
+ // 设置边框
1259
+ if (rowIndex >= 2) {
1260
+ cell.border = {
1261
+ top: { style: "thin" },
1262
+ left: { style: "thin" },
1263
+ bottom: { style: "thin" },
1264
+ right: { style: "thin" },
1265
+ };
1266
+ }
1267
+
1268
+ // 设置数值格式(仅对导出数值字段且是数据行)
1269
+ const isDataRow = rowIndex >= dataStartRow;
1270
+ if (isDataRow && cellInfo.isExportNumeric) {
1271
+ if (cellInfo.type === "number") {
1272
+ // 根据列类型获取格式
1273
+ let excelFormat = this.exportNumericFormats.number;
1274
+
1275
+ if (cellInfo.columnType === "currency") {
1276
+ excelFormat = this.exportNumericFormats.currency;
1277
+ } else if (cellInfo.columnType === "percent") {
1278
+ excelFormat = this.exportNumericFormats.percent;
1279
+ }
1280
+
1281
+ cell.numFmt = excelFormat;
1282
+
1283
+ // 确保值是数字类型
1284
+ if (typeof cellInfo.value === "string") {
1285
+ const num = parseFloat(cellInfo.value);
1286
+ cell.value = isNaN(num) ? cellInfo.value : num;
1287
+ } else {
1288
+ cell.value = cellInfo.value;
1289
+ }
1290
+ } else if (cellInfo.type === "percent") {
1291
+ // 百分比类型
1292
+ cell.numFmt = this.exportNumericFormats.percent;
1293
+ cell.value = cellInfo.value;
1294
+ } else {
1295
+ // 其他类型
1296
+ cell.value = cellInfo.value;
1297
+ }
1298
+ } else {
1299
+ // 非数值字段或非数据行,直接设置值
1300
+ cell.value = cellInfo.value;
1301
+ }
1302
+
1303
+ // 设置字体和背景色
1304
+ if (rowIndex === 0) {
1305
+ // 标题行
1306
+ cell.font = { bold: true, size: 16 };
1307
+ cell.alignment.horizontal = "center";
1308
+ } else if (rowIndex === 1) {
1309
+ // 副标题行
1310
+ cell.font = { size: 12, color: { argb: "FF666666" } };
1311
+ } else if (rowIndex >= 2 && rowIndex < dataStartRow) {
1312
+ // 表头行
1313
+ cell.font = { bold: true };
1314
+ cell.fill = {
1315
+ type: "pattern",
1316
+ pattern: "solid",
1317
+ fgColor: { argb: "FFF5F7FA" },
1318
+ };
1319
+ cell.alignment.horizontal = "center";
1320
+ } else {
1321
+ // 数据行
1322
+ cell.alignment.horizontal = "center";
1323
+
1324
+ if (cellInfo._isSummary) {
1325
+ // 合计行样式
1326
+ cell.font = { bold: true, color: { argb: "FF1890FF" } };
1327
+ cell.fill = {
1328
+ type: "pattern",
1329
+ pattern: "solid",
1330
+ fgColor: { argb: "FFF0F9FF" },
1331
+ };
1332
+ }
1333
+ }
1334
+ }
1335
+ },
1336
+
1337
+ // 添加一个方法,自动检测哪些字段应该是数值类型
1338
+ getAutoDetectedNumericFields() {
1339
+ const numericFields = [];
1340
+
1341
+ // 遍历列配置,找出类型为currency或number的字段
1342
+ const traverse = (columns) => {
1343
+ columns.forEach((col) => {
1344
+ if (col.children) {
1345
+ traverse(col.children);
1346
+ } else if (col.prop && col.type) {
1347
+ if (
1348
+ col.type === "currency" ||
1349
+ col.type === "number" ||
1350
+ col.type === "percent"
1351
+ ) {
1352
+ if (!numericFields.includes(col.prop)) {
1353
+ numericFields.push(col.prop);
1354
+ }
1355
+ }
1356
+ }
1357
+ });
1358
+ };
1359
+
1360
+ if (this.complexHeaderColumns && this.complexHeaderColumns.length > 0) {
1361
+ traverse(this.complexHeaderColumns);
1362
+ }
1363
+
1364
+ return numericFields;
1365
+ },
1366
+
1367
+ // 计算最终的导出数值字段
1368
+
1369
+ // 设置ExcelJS列宽
1370
+ setExcelJSColumnWidths(worksheet) {
1371
+ const leafColumns = this.getLeafColumns();
1372
+
1373
+ // 序号列宽度
1374
+ worksheet.getColumn(1).width = 10;
1375
+
1376
+ // 数据列宽度
1377
+ leafColumns.forEach((col, index) => {
1378
+ let width = 15;
1379
+ if (col.width) {
1380
+ width = Math.max(10, Math.min(30, Math.floor(col.width / 7)));
1381
+ }
1382
+ worksheet.getColumn(index + 2).width = width;
1383
+ });
1384
+ },
1385
+
1386
+ setExcelJSMerges(worksheet) {
1387
+ const totalCols = this.getTotalColumnCount();
1388
+
1389
+ // 1. 合并标题行(第1行)
1390
+ if (totalCols > 1) {
1391
+ worksheet.mergeCells(1, 1, 1, totalCols);
1392
+ }
1393
+
1394
+ // 2. 合并副标题行(第2行)
1395
+ if (totalCols > 1) {
1396
+ worksheet.mergeCells(2, 1, 2, totalCols);
1397
+ }
1398
+
1399
+ // 表头起始行(第3行)
1400
+ const headerStartRow = 3;
1401
+ const maxDepth = this.getHeaderDepth();
1402
+
1403
+ // 3. 合并序号列(如果有多层表头)
1404
+ if (maxDepth > 1) {
1405
+ worksheet.mergeCells(
1406
+ headerStartRow,
1407
+ 1,
1408
+ headerStartRow + maxDepth - 1,
1409
+ 1,
1410
+ );
1411
+ }
1412
+
1413
+ // 4. 处理复杂表头合并
1414
+ this.processExcelJSMerges(
1415
+ worksheet,
1416
+ this.complexHeaderColumns,
1417
+ headerStartRow,
1418
+ 2, // 从第2列开始(第1列是序号列)
1419
+ maxDepth,
1420
+ );
1421
+ },
1422
+
1423
+ processExcelJSMerges(
1424
+ worksheet,
1425
+ columns,
1426
+ startRow,
1427
+ startCol,
1428
+ maxDepth,
1429
+ currentDepth = 0,
1430
+ ) {
1431
+ let currentCol = startCol;
1432
+
1433
+ columns.forEach((column) => {
1434
+ if (column.children && column.children.length > 0) {
1435
+ // 分组列 - 计算子列数量
1436
+ const leafCount = this.countLeafColumns(column.children);
1437
+
1438
+ // 水平合并分组标题
1439
+ if (leafCount > 1) {
1440
+ worksheet.mergeCells(
1441
+ startRow + currentDepth,
1442
+ currentCol,
1443
+ startRow + currentDepth,
1444
+ currentCol + leafCount - 1,
1445
+ );
1446
+ }
1447
+
1448
+ // 递归处理子列
1449
+ const nextStartCol = this.processExcelJSMerges(
1450
+ worksheet,
1451
+ column.children,
1452
+ startRow,
1453
+ currentCol,
1454
+ maxDepth,
1455
+ currentDepth + 1,
1456
+ );
1457
+
1458
+ currentCol = nextStartCol;
1459
+ } else {
1460
+ // 单层表头列(没有children)
1461
+ // 如果有多层表头,需要垂直合并到最大深度
1462
+ if (maxDepth > 1 && currentDepth < maxDepth - 1) {
1463
+ // 垂直合并:从当前深度合并到最大深度
1464
+ worksheet.mergeCells(
1465
+ startRow + currentDepth,
1466
+ currentCol,
1467
+ startRow + maxDepth - 1,
1468
+ currentCol,
1469
+ );
1470
+ }
1471
+
1472
+ currentCol++;
1473
+ }
1474
+ });
1475
+
1476
+ return currentCol;
1477
+ },
1478
+
1479
+ // 获取所有叶子列
1480
+ getLeafColumns() {
1481
+ const leafColumns = [];
1482
+
1483
+ const traverse = (columns) => {
1484
+ columns.forEach((col) => {
1485
+ if (col.children && col.children.length > 0) {
1486
+ traverse(col.children);
1487
+ } else if (col.prop) {
1488
+ leafColumns.push(col);
1489
+ }
1490
+ });
1491
+ };
1492
+
1493
+ if (this.complexHeaderColumns && this.complexHeaderColumns.length > 0) {
1494
+ traverse(this.complexHeaderColumns);
1495
+ }
1496
+
1497
+ return leafColumns;
1498
+ },
1499
+
1500
+ // 优化获取表头深度的方法
1501
+ getHeaderDepth(columns = null, currentDepth = 1) {
1502
+ const cols = columns || this.complexHeaderColumns;
1503
+ if (!cols || cols.length === 0) return currentDepth;
1504
+
1505
+ let maxDepth = currentDepth;
1506
+
1507
+ cols.forEach((col) => {
1508
+ if (col.children && col.children.length > 0) {
1509
+ // 递归计算子列的深度
1510
+ const childDepth = this.getHeaderDepth(
1511
+ col.children,
1512
+ currentDepth + 1,
1513
+ );
1514
+ maxDepth = Math.max(maxDepth, childDepth);
1515
+ } else {
1516
+ // 单层表头列,深度就是当前深度
1517
+ maxDepth = Math.max(maxDepth, currentDepth);
1518
+ }
1519
+ });
1520
+
1521
+ return maxDepth;
1522
+ },
1523
+
1524
+ // 计算叶子列数量
1525
+ countLeafColumns(columns) {
1526
+ if (!columns || columns.length === 0) return 0;
1527
+
1528
+ let count = 0;
1529
+ columns.forEach((col) => {
1530
+ if (col.children && col.children.length > 0) {
1531
+ count += this.countLeafColumns(col.children);
1532
+ } else if (col.prop) {
1533
+ count++;
1534
+ }
1535
+ });
1536
+
1537
+ return count;
1538
+ },
1539
+
1540
+ // 获取总列数
1541
+ getTotalColumnCount() {
1542
+ const leafColumns = this.getLeafColumns();
1543
+ return leafColumns.length + 1; // +1 为序号列
1544
+ },
1545
+
1546
+ getExportFileName() {
1547
+ const now = new Date();
1548
+ const year = now.getFullYear();
1549
+ const month = String(now.getMonth() + 1).padStart(2, "0");
1550
+ const day = String(now.getDate()).padStart(2, "0");
1551
+ const hour = String(now.getHours()).padStart(2, "0");
1552
+ const minute = String(now.getMinutes()).padStart(2, "0");
1553
+ const second = String(now.getSeconds()).padStart(2, "0");
1554
+
1555
+ const timestamp = `${year}${month}${day}${hour}${minute}${second}`;
1556
+ const fileName = this.exportFileName || this.title;
1557
+ return `${fileName || "报表"}_${timestamp}`;
1558
+ },
1559
+
1560
+ initDate(timestamp) {
1561
+ let date;
1562
+ if (!timestamp || timestamp === 0 || timestamp == null) {
1563
+ date = new Date();
1564
+ } else {
1565
+ date = new Date(timestamp * 1000);
1566
+ }
1567
+
1568
+ const year = date.getFullYear();
1569
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1570
+ const day = String(date.getDate()).padStart(2, "0");
1571
+
1572
+ let result = "截至" + `${year}年${month}月${day}日`;
1573
+
1574
+ return result;
1575
+ },
1576
+ },
1577
+ };
1578
+ </script>
1579
+
1580
+ <style lang="scss" scoped>
1581
+ /* 样式部分保持不变 */
1582
+ .page-container {
1583
+ width: 100%;
1584
+ }
1585
+
1586
+ h2 {
1587
+ text-align: center;
1588
+ color: #333;
1589
+ }
1590
+
1591
+ .title-container {
1592
+ display: flex;
1593
+ display: -webkit-flex;
1594
+ }
1595
+
1596
+ .subtitle {
1597
+ font-size: 14px;
1598
+ color: #666;
1599
+ margin-top: 8px;
1600
+ line-height: 1.4;
1601
+ font-weight: normal;
1602
+ text-align: center;
1603
+ }
1604
+
1605
+ .title-main {
1606
+ flex: 1;
1607
+ -webkit-flex: 1;
1608
+ h2 {
1609
+ margin-bottom: 4px;
1610
+ }
1611
+ }
1612
+
1613
+ .title-tag {
1614
+ font-size: 12px;
1615
+ color: #666;
1616
+ }
1617
+
1618
+ .text-center {
1619
+ text-align: center;
1620
+ }
1621
+
1622
+ .query-param-area {
1623
+ display: -webkit-flex;
1624
+ display: flex;
1625
+ }
1626
+
1627
+ .vxe-loading-mask {
1628
+ position: absolute;
1629
+ top: 0;
1630
+ left: 0;
1631
+ width: 100%;
1632
+ height: 100%;
1633
+ background-color: rgba(255, 255, 255, 0.75);
1634
+ display: flex;
1635
+ align-items: center;
1636
+ justify-content: center;
1637
+ z-index: 999;
1638
+ backdrop-filter: blur(2px); /* 毛玻璃效果,可选 */
1639
+ }
1640
+
1641
+ .vxe-loading-content {
1642
+ display: flex;
1643
+ flex-direction: column;
1644
+ align-items: center;
1645
+ gap: 14px;
1646
+ padding: 24px 36px;
1647
+ background: #ffffff;
1648
+ border-radius: 12px;
1649
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.01);
1650
+ }
1651
+
1652
+ .vxe-loading-icon {
1653
+ font-size: 40px;
1654
+ color: #409eff;
1655
+ /* el-icon-loading 自带旋转动画,无需额外 keyframes */
1656
+ }
1657
+
1658
+ .vxe-loading-text {
1659
+ font-size: 14px;
1660
+ color: #606266;
1661
+ letter-spacing: 0.5px;
1662
+ }
1663
+
1664
+ @keyframes spin {
1665
+ 0% {
1666
+ transform: rotate(0deg);
1667
+ }
1668
+ 100% {
1669
+ transform: rotate(360deg);
1670
+ }
1671
+ }
1672
+ </style>
1673
+
1674
+ <style>
1675
+ /* vxe-table 全局样式调整 */
1676
+ .header-row {
1677
+ background-color: #f5f7fa !important;
1678
+ }
1679
+
1680
+ .header-cell {
1681
+ text-align: center !important;
1682
+ font-weight: bold !important;
1683
+ background-color: #f5f7fa !important;
1684
+ }
1685
+
1686
+ .vxe-cell {
1687
+ text-align: center !important;
1688
+ }
1689
+
1690
+ .vxe-table--header {
1691
+ background-color: #f5f7fa !important;
1692
+ }
1693
+
1694
+ .vxe-header--column {
1695
+ background-color: #f5f7fa !important;
1696
+ }
1697
+
1698
+ /* 确保 vxe-table 有边框和圆角 */
1699
+ .vxe-table--render-wrapper {
1700
+ border: 1px solid #e8eaec;
1701
+ border-radius: 4px;
1702
+ }
1703
+
1704
+ /* 优化 vxe-table 滚动条样式 */
1705
+ .vxe-table--body-wrapper::-webkit-scrollbar {
1706
+ height: 8px;
1707
+ width: 8px;
1708
+ }
1709
+
1710
+ .vxe-table--body-wrapper::-webkit-scrollbar-track {
1711
+ background: #f1f1f1;
1712
+ border-radius: 4px;
1713
+ }
1714
+
1715
+ .vxe-table--body-wrapper::-webkit-scrollbar-thumb {
1716
+ background: #c1c1c1;
1717
+ border-radius: 4px;
1718
+ }
1719
+
1720
+ .vxe-table--body-wrapper::-webkit-scrollbar-thumb:hover {
1721
+ background: #a8a8a8;
1722
+ }
1723
+
1724
+ /* 修复固定列可能出现的样式问题 */
1725
+ .vxe-table--fixed-left-wrapper,
1726
+ .vxe-table--fixed-right-wrapper {
1727
+ z-index: 4;
1728
+ }
1729
+
1730
+ /* 合计行样式 */
1731
+ .summary-row {
1732
+ background-color: #f0f9ff !important;
1733
+ font-weight: bold !important;
1734
+ color: #1890ff !important;
1735
+ }
1736
+
1737
+ .vxe-table--footer-wrapper {
1738
+ background-color: #f0f9ff !important;
1739
+ }
1740
+
1741
+ .vxe-footer--column {
1742
+ background-color: #f0f9ff !important;
1743
+ font-weight: bold !important;
1744
+ color: #1890ff !important;
1745
+ text-align: center !important;
1746
+ }
1747
+ </style>