dex-simple-report-excel 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/example/demo.html +34 -0
- package/example/test.js +40 -0
- package/package.json +35 -0
- package/src/index.d.ts +230 -0
- package/src/index.js +1051 -0
- package/test_output.xlsx +0 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
import xlsx from 'xlsx-js-style';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 格式转换,将数据按字段顺序转换为 AOA (Array of Arrays) 格式
|
|
5
|
+
* @param {Array<string>} filterVal - 字段映射顺序
|
|
6
|
+
* @param {Array<Object>} jsonData - 原始 JSON 数据
|
|
7
|
+
* @returns {Array<Array>}
|
|
8
|
+
*/
|
|
9
|
+
export function formatJson(filterVal, jsonData) {
|
|
10
|
+
return jsonData.map(v => filterVal.map(j => v[j]));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 内部警告提示(模拟浏览器端提示,若在非浏览器环境则仅 console.warn)
|
|
15
|
+
*/
|
|
16
|
+
let notification = null;
|
|
17
|
+
export const warning = (message, title = '警告信息') => {
|
|
18
|
+
if (typeof window !== 'undefined' && window.Notification && window.Notification.warning) {
|
|
19
|
+
!notification && (notification = window.Notification.warning({
|
|
20
|
+
title,
|
|
21
|
+
dangerouslyUseHTMLString: true,
|
|
22
|
+
message: `<strong>信息:</strong> ${message}`,
|
|
23
|
+
duration: 2000,
|
|
24
|
+
onClose: () => notification = null
|
|
25
|
+
}));
|
|
26
|
+
} else {
|
|
27
|
+
console.warn(`${title}: ${message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 高级版 Excel 导出工具类:支持多级列头与动态行列合并
|
|
33
|
+
* 提供同步/异步两种导出模式,具备分块计算与性能监控
|
|
34
|
+
*/
|
|
35
|
+
export class ExcelExporter {
|
|
36
|
+
/**
|
|
37
|
+
* 导出 Excel 文件(同步)
|
|
38
|
+
*/
|
|
39
|
+
static exportExcel(options = {}) {
|
|
40
|
+
try {
|
|
41
|
+
const {
|
|
42
|
+
sheets = [],
|
|
43
|
+
fileName = '导出数据.xlsx',
|
|
44
|
+
defaultStyles = {},
|
|
45
|
+
showBorder = true,
|
|
46
|
+
styleOptions = {}
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
if (!sheets || sheets.length === 0) {
|
|
50
|
+
console.warn('未提供工作表数据');
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.time('ExcelExporter:Total');
|
|
55
|
+
const workbook = xlsx.utils.book_new();
|
|
56
|
+
|
|
57
|
+
const defaultCellStyle = this.getDefaultCellStyle(defaultStyles.cellStyle, showBorder, styleOptions);
|
|
58
|
+
const defaultTitleCellStyle = this.getDefaultTitleCellStyle(defaultStyles.titleCellStyle, showBorder, styleOptions);
|
|
59
|
+
const defaultHeadCellStyle = this.getDefaultHeadCellStyle(defaultStyles.headCellStyle, showBorder, styleOptions);
|
|
60
|
+
|
|
61
|
+
sheets.forEach(sheet => {
|
|
62
|
+
this.addSheetToWorkbook(
|
|
63
|
+
workbook,
|
|
64
|
+
sheet.data || [],
|
|
65
|
+
sheet.columns || [],
|
|
66
|
+
defaultCellStyle,
|
|
67
|
+
defaultTitleCellStyle,
|
|
68
|
+
defaultHeadCellStyle,
|
|
69
|
+
sheet.sheetName || 'Sheet1',
|
|
70
|
+
sheet.title || '',
|
|
71
|
+
sheet.customOptions || {},
|
|
72
|
+
sheet.mergeOptions || {},
|
|
73
|
+
showBorder
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
xlsx.writeFile(workbook, fileName, { type: 'binary' });
|
|
78
|
+
console.timeEnd('ExcelExporter:Total');
|
|
79
|
+
return true;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('导出Excel时发生错误:', error);
|
|
82
|
+
warning('导出Excel时发生错误,请稍后再试。');
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 导出 Excel 文件(异步)
|
|
89
|
+
*/
|
|
90
|
+
static exportExcelAsync(options = {}) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
try {
|
|
93
|
+
const {
|
|
94
|
+
sheets = [],
|
|
95
|
+
fileName = '导出数据.xlsx',
|
|
96
|
+
defaultStyles = {},
|
|
97
|
+
showBorder = true,
|
|
98
|
+
styleOptions = {}
|
|
99
|
+
} = options;
|
|
100
|
+
|
|
101
|
+
// 更新警告提示信息,使用项目名称替代硬编码字符串
|
|
102
|
+
if (!sheets || sheets.length === 0) {
|
|
103
|
+
console.warn('未提供工作表数据');
|
|
104
|
+
resolve(false);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
console.time('ExcelExporter:TotalAsync');
|
|
110
|
+
const workbook = xlsx.utils.book_new();
|
|
111
|
+
|
|
112
|
+
const defaultCellStyle = this.getDefaultCellStyle(defaultStyles.cellStyle, showBorder, styleOptions);
|
|
113
|
+
const defaultTitleCellStyle = this.getDefaultTitleCellStyle(defaultStyles.titleCellStyle, showBorder, styleOptions);
|
|
114
|
+
const defaultHeadCellStyle = this.getDefaultHeadCellStyle(defaultStyles.headCellStyle, showBorder, styleOptions);
|
|
115
|
+
|
|
116
|
+
const buildNext = (index) => {
|
|
117
|
+
if (index >= sheets.length) {
|
|
118
|
+
xlsx.writeFile(workbook, fileName, { type: 'binary' });
|
|
119
|
+
console.timeEnd('ExcelExporter:TotalAsync');
|
|
120
|
+
resolve(true);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const sheet = sheets[index];
|
|
124
|
+
this.addSheetToWorkbookAsync(
|
|
125
|
+
workbook,
|
|
126
|
+
sheet.data || [],
|
|
127
|
+
sheet.columns || [],
|
|
128
|
+
defaultCellStyle,
|
|
129
|
+
defaultTitleCellStyle,
|
|
130
|
+
defaultHeadCellStyle,
|
|
131
|
+
sheet.sheetName || 'Sheet1',
|
|
132
|
+
sheet.title || '',
|
|
133
|
+
sheet.customOptions || {},
|
|
134
|
+
sheet.mergeOptions || {},
|
|
135
|
+
showBorder
|
|
136
|
+
).then(() => {
|
|
137
|
+
setTimeout(() => buildNext(index + 1), 0);
|
|
138
|
+
}).catch(err => {
|
|
139
|
+
console.error('异步添加工作表时发生错误:', err);
|
|
140
|
+
warning('导出Excel时发生错误,请稍后再试。');
|
|
141
|
+
resolve(false);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
setTimeout(() => buildNext(0), 0);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error('导出Excel(异步)时发生错误:', error);
|
|
147
|
+
warning('导出Excel时发生错误,请稍后再试。');
|
|
148
|
+
resolve(false);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static addSheetToWorkbook(workbook, data, columns, cellStyle, titleCellStyle, headCellStyle, sheetName, title, customOptions, mergeOptions, showBorder) {
|
|
154
|
+
try {
|
|
155
|
+
const options = {
|
|
156
|
+
data,
|
|
157
|
+
columns,
|
|
158
|
+
cellStyle,
|
|
159
|
+
titleCellStyle,
|
|
160
|
+
headCellStyle,
|
|
161
|
+
title,
|
|
162
|
+
merges: [],
|
|
163
|
+
cols: [...(Array.isArray(columns) ? this.estimateCols(columns) : [])],
|
|
164
|
+
rows: [...data.map(() => ({ hpt: 30 }))],
|
|
165
|
+
...customOptions,
|
|
166
|
+
mergeOptions: mergeOptions || {},
|
|
167
|
+
indexColumn: (mergeOptions && mergeOptions.serialNumberColumn)
|
|
168
|
+
? {
|
|
169
|
+
enabled: true,
|
|
170
|
+
header: mergeOptions.serialNumberHeader || '序号',
|
|
171
|
+
startAt: typeof mergeOptions.serialNumberStartAt === 'number' ? mergeOptions.serialNumberStartAt : 1
|
|
172
|
+
}
|
|
173
|
+
: (customOptions && customOptions.indexColumn) || {},
|
|
174
|
+
applyBorders: typeof (customOptions && customOptions.applyBorders) === 'boolean' ? customOptions.applyBorders : (showBorder !== false)
|
|
175
|
+
}
|
|
176
|
+
const worksheet = this.createSheet(options);
|
|
177
|
+
xlsx.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('添加工作表时发生错误:', error);
|
|
180
|
+
throw new Error('添加工作表时发生错误');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static addSheetToWorkbookAsync(workbook, data, columns, cellStyle, titleCellStyle, headCellStyle, sheetName, title, customOptions, mergeOptions, showBorder) {
|
|
185
|
+
const options = {
|
|
186
|
+
data,
|
|
187
|
+
columns,
|
|
188
|
+
cellStyle,
|
|
189
|
+
titleCellStyle,
|
|
190
|
+
headCellStyle,
|
|
191
|
+
title,
|
|
192
|
+
merges: [],
|
|
193
|
+
cols: [...(Array.isArray(columns) ? this.estimateCols(columns) : [])],
|
|
194
|
+
rows: [...data.map(() => ({ hpt: 30 }))],
|
|
195
|
+
...customOptions,
|
|
196
|
+
mergeOptions: mergeOptions || {},
|
|
197
|
+
indexColumn: (mergeOptions && mergeOptions.serialNumberColumn)
|
|
198
|
+
? {
|
|
199
|
+
enabled: true,
|
|
200
|
+
header: mergeOptions.serialNumberHeader || '序号',
|
|
201
|
+
startAt: typeof mergeOptions.serialNumberStartAt === 'number' ? mergeOptions.serialNumberStartAt : 1
|
|
202
|
+
}
|
|
203
|
+
: (customOptions && customOptions.indexColumn) || {},
|
|
204
|
+
applyBorders: typeof (customOptions && customOptions.applyBorders) === 'boolean' ? customOptions.applyBorders : (showBorder !== false)
|
|
205
|
+
}
|
|
206
|
+
return this.createSheetAsync(options).then(worksheet => {
|
|
207
|
+
xlsx.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
static createSheet(options) {
|
|
212
|
+
console.time('ExcelExporter:CreateSheet');
|
|
213
|
+
try {
|
|
214
|
+
const { data, columns, cellStyle, titleCellStyle, headCellStyle, title, cols, mergeOptions = {}, rowHeights = {}, indexColumn = {} } = options;
|
|
215
|
+
const enableIndex = !!indexColumn.enabled;
|
|
216
|
+
const indexHeader = indexColumn.header || '序号';
|
|
217
|
+
const indexStartAt = typeof indexColumn.startAt === 'number' ? indexColumn.startAt : 1;
|
|
218
|
+
const columnsAug = enableIndex ? [{ header: indexHeader, prop: '__index__', mergeable: false }, ...columns] : columns;
|
|
219
|
+
|
|
220
|
+
const { headerRows, leafNodes, headerMerges } = this.buildHeaderStructure(columnsAug);
|
|
221
|
+
|
|
222
|
+
const leafKeys = leafNodes.map(n => n.prop || n.header || n.key || '');
|
|
223
|
+
const filterVal = options.filterVal || [...leafKeys];
|
|
224
|
+
|
|
225
|
+
const xlsxData = formatJson(filterVal, data);
|
|
226
|
+
|
|
227
|
+
const aoa = [];
|
|
228
|
+
if (title) aoa.push([title]);
|
|
229
|
+
if (headerRows.length > 0) headerRows.forEach(r => aoa.push(r));
|
|
230
|
+
aoa.push(...xlsxData);
|
|
231
|
+
|
|
232
|
+
const worksheet = xlsx.utils.aoa_to_sheet(aoa);
|
|
233
|
+
|
|
234
|
+
if (cols) {
|
|
235
|
+
worksheet['!cols'] = enableIndex ? [{ wch: 6 }, ...cols] : cols;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const merges = [];
|
|
239
|
+
const titleOffset = title ? 1 : 0;
|
|
240
|
+
const headerRowCount = headerRows.length;
|
|
241
|
+
const totalRows = aoa.length;
|
|
242
|
+
const rowsFull = Array.from({ length: totalRows }, () => ({}));
|
|
243
|
+
const explicitRows = new Set();
|
|
244
|
+
const dataDefaultHpt = typeof rowHeights.dataDefault === 'number' ? rowHeights.dataDefault : 30;
|
|
245
|
+
|
|
246
|
+
for (let r = titleOffset + headerRowCount; r < totalRows; r++) {
|
|
247
|
+
rowsFull[r] = { hpt: dataDefaultHpt };
|
|
248
|
+
}
|
|
249
|
+
if (title && typeof rowHeights.title === 'number') {
|
|
250
|
+
rowsFull[0] = { ...(rowsFull[0] || {}), hpt: rowHeights.title };
|
|
251
|
+
explicitRows.add(0);
|
|
252
|
+
}
|
|
253
|
+
if (typeof rowHeights.header === 'number') {
|
|
254
|
+
for (let r = titleOffset; r < titleOffset + headerRowCount; r++) {
|
|
255
|
+
rowsFull[r] = { ...(rowsFull[r] || {}), hpt: rowHeights.header };
|
|
256
|
+
explicitRows.add(r);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (rowHeights.dataByIndex && typeof rowHeights.dataByIndex === 'object') {
|
|
260
|
+
Object.keys(rowHeights.dataByIndex).forEach(k => {
|
|
261
|
+
const idx = Number(k);
|
|
262
|
+
const h = rowHeights.dataByIndex[idx];
|
|
263
|
+
if (!Number.isNaN(idx) && typeof h === 'number') {
|
|
264
|
+
const row = titleOffset + headerRowCount + idx;
|
|
265
|
+
if (row >= titleOffset + headerRowCount && row < totalRows) {
|
|
266
|
+
rowsFull[row] = { ...(rowsFull[row] || {}), hpt: h };
|
|
267
|
+
explicitRows.add(row);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (title) {
|
|
274
|
+
const totalCols = Math.max(leafNodes.length, (aoa[titleOffset] || []).length);
|
|
275
|
+
merges.push({ s: { r: 0, c: 0 }, e: { r: 0, c: Math.max(0, totalCols - 1) } });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
headerMerges.forEach(m => {
|
|
279
|
+
merges.push({ s: { r: m.s.r + titleOffset, c: m.s.c }, e: { r: m.e.r + titleOffset, c: m.e.c } });
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const rowMerges = this.computeRowMerges(
|
|
283
|
+
data,
|
|
284
|
+
leafNodes,
|
|
285
|
+
titleOffset + headerRowCount,
|
|
286
|
+
mergeOptions
|
|
287
|
+
);
|
|
288
|
+
merges.push(...rowMerges);
|
|
289
|
+
|
|
290
|
+
if (enableIndex && mergeOptions && mergeOptions.mergeKey) {
|
|
291
|
+
const segments = this.computeKeySegments(data, mergeOptions);
|
|
292
|
+
let seq = indexStartAt;
|
|
293
|
+
segments.forEach(seg => {
|
|
294
|
+
const startR = titleOffset + headerRowCount + seg.start;
|
|
295
|
+
const endR = titleOffset + headerRowCount + seg.end;
|
|
296
|
+
const refTop = xlsx.utils.encode_cell({ r: startR, c: 0 });
|
|
297
|
+
if (!worksheet[refTop]) worksheet[refTop] = { v: '', t: 's' };
|
|
298
|
+
worksheet[refTop].v = seq;
|
|
299
|
+
worksheet[refTop].t = 'n';
|
|
300
|
+
if (endR > startR) {
|
|
301
|
+
merges.push({ s: { r: startR, c: 0 }, e: { r: endR, c: 0 } });
|
|
302
|
+
}
|
|
303
|
+
seq += 1;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
{
|
|
308
|
+
const dataStart = titleOffset + headerRowCount;
|
|
309
|
+
const rangeLocal = xlsx.utils.decode_range(worksheet['!ref']);
|
|
310
|
+
const imgCols = [];
|
|
311
|
+
for (let c = 0; c < leafNodes.length; c++) {
|
|
312
|
+
if (leafNodes[c] && leafNodes[c].cellType === 'image') imgCols.push(c);
|
|
313
|
+
}
|
|
314
|
+
if (imgCols.length) {
|
|
315
|
+
if (!worksheet['!cols']) worksheet['!cols'] = Array.from({ length: rangeLocal.e.c + 1 }, () => ({ wch: 15 }));
|
|
316
|
+
imgCols.forEach(c => {
|
|
317
|
+
if (!worksheet['!cols'][c]) worksheet['!cols'][c] = {};
|
|
318
|
+
worksheet['!cols'][c].wch = Math.max(worksheet['!cols'][c].wch || 15, 20);
|
|
319
|
+
});
|
|
320
|
+
for (let r = dataStart; r <= rangeLocal.e.r; r++) {
|
|
321
|
+
imgCols.forEach(c => {
|
|
322
|
+
const ref = xlsx.utils.encode_cell({ r, c });
|
|
323
|
+
const cell = worksheet[ref];
|
|
324
|
+
const val = cell ? cell.v : '';
|
|
325
|
+
if (this.isValidImageUrl(String(val || ''))) {
|
|
326
|
+
const text = '查看图片';
|
|
327
|
+
worksheet[ref] = { v: text, t: 's', l: { Target: String(val), Tooltip: '打开图片链接' } };
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (merges.length) worksheet['!merges'] = merges;
|
|
335
|
+
this.adjustRowHeightsForMerges(
|
|
336
|
+
rowsFull,
|
|
337
|
+
merges,
|
|
338
|
+
titleOffset + headerRowCount,
|
|
339
|
+
explicitRows,
|
|
340
|
+
{ mode: 'multiply', baseDataHpt: dataDefaultHpt }
|
|
341
|
+
);
|
|
342
|
+
worksheet['!rows'] = rowsFull;
|
|
343
|
+
|
|
344
|
+
const range = xlsx.utils.decode_range(worksheet['!ref']);
|
|
345
|
+
for (let R = range.s.r; R <= range.e.r; ++R) {
|
|
346
|
+
for (let C = range.s.c; C <= range.e.c; ++C) {
|
|
347
|
+
const cellRef = xlsx.utils.encode_cell({ r: R, c: C });
|
|
348
|
+
if (!worksheet[cellRef]) worksheet[cellRef] = { v: '', t: 's' };
|
|
349
|
+
if (title && R === 0) {
|
|
350
|
+
worksheet[cellRef].s = titleCellStyle || headCellStyle;
|
|
351
|
+
} else if (R >= titleOffset && R < titleOffset + headerRowCount) {
|
|
352
|
+
worksheet[cellRef].s = headCellStyle;
|
|
353
|
+
} else {
|
|
354
|
+
worksheet[cellRef].s = cellStyle;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (options.applyBorders) {
|
|
359
|
+
if (merges.length) this.ensureBorderForMergedCells(worksheet, merges);
|
|
360
|
+
this.ensureBorderForHeaderArea(worksheet, merges, titleOffset, headerRowCount);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
console.timeEnd('ExcelExporter:CreateSheet');
|
|
364
|
+
return worksheet;
|
|
365
|
+
} catch (error) {
|
|
366
|
+
console.error('创建工作表时发生错误:', error);
|
|
367
|
+
throw new Error('创建工作表时发生错误');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
static createSheetAsync(options) {
|
|
372
|
+
console.time('ExcelExporter:CreateSheetAsync');
|
|
373
|
+
return new Promise((resolve, reject) => {
|
|
374
|
+
try {
|
|
375
|
+
const { data, columns, cellStyle, titleCellStyle, headCellStyle, title, cols, mergeOptions = {}, rowHeights = {}, indexColumn = {} } = options;
|
|
376
|
+
const enableIndex = !!indexColumn.enabled;
|
|
377
|
+
const indexHeader = indexColumn.header || '序号';
|
|
378
|
+
const indexStartAt = typeof indexColumn.startAt === 'number' ? indexColumn.startAt : 1;
|
|
379
|
+
const columnsAug = enableIndex ? [{ header: indexHeader, prop: '__index__', mergeable: false }, ...columns] : columns;
|
|
380
|
+
|
|
381
|
+
const { headerRows, leafNodes, headerMerges } = this.buildHeaderStructure(columnsAug);
|
|
382
|
+
const leafKeys = leafNodes.map(n => n.prop || n.header || n.key || '');
|
|
383
|
+
const filterVal = options.filterVal || [...leafKeys];
|
|
384
|
+
const xlsxData = formatJson(filterVal, data);
|
|
385
|
+
|
|
386
|
+
const aoa = [];
|
|
387
|
+
if (title) aoa.push([title]);
|
|
388
|
+
if (headerRows.length > 0) headerRows.forEach(r => aoa.push(r));
|
|
389
|
+
aoa.push(...xlsxData);
|
|
390
|
+
|
|
391
|
+
const worksheet = xlsx.utils.aoa_to_sheet(aoa);
|
|
392
|
+
if (cols) worksheet['!cols'] = enableIndex ? [{ wch: 6 }, ...cols] : cols;
|
|
393
|
+
|
|
394
|
+
const merges = [];
|
|
395
|
+
const titleOffset = title ? 1 : 0;
|
|
396
|
+
const headerRowCount = headerRows.length;
|
|
397
|
+
|
|
398
|
+
if (title) {
|
|
399
|
+
const totalCols = Math.max(leafNodes.length, (aoa[titleOffset] || []).length);
|
|
400
|
+
merges.push({ s: { r: 0, c: 0 }, e: { r: 0, c: Math.max(0, totalCols - 1) } });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
headerMerges.forEach(m => {
|
|
404
|
+
merges.push({ s: { r: m.s.r + titleOffset, c: m.s.c }, e: { r: m.e.r + titleOffset, c: m.e.c } });
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const totalRows = aoa.length;
|
|
408
|
+
const rowsFull = Array.from({ length: totalRows }, () => ({}));
|
|
409
|
+
const explicitRows = new Set();
|
|
410
|
+
const dataDefaultHpt = typeof rowHeights.dataDefault === 'number' ? rowHeights.dataDefault : 30;
|
|
411
|
+
for (let r = titleOffset + headerRowCount; r < totalRows; r++) {
|
|
412
|
+
rowsFull[r] = { hpt: dataDefaultHpt };
|
|
413
|
+
}
|
|
414
|
+
if (title && typeof rowHeights.title === 'number') {
|
|
415
|
+
rowsFull[0] = { ...(rowsFull[0] || {}), hpt: rowHeights.title };
|
|
416
|
+
explicitRows.add(0);
|
|
417
|
+
}
|
|
418
|
+
if (typeof rowHeights.header === 'number') {
|
|
419
|
+
for (let r = titleOffset; r < titleOffset + headerRowCount; r++) {
|
|
420
|
+
rowsFull[r] = { ...(rowsFull[r] || {}), hpt: rowHeights.header };
|
|
421
|
+
explicitRows.add(r);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (rowHeights.dataByIndex && typeof rowHeights.dataByIndex === 'object') {
|
|
425
|
+
Object.keys(rowHeights.dataByIndex).forEach(k => {
|
|
426
|
+
const idx = Number(k);
|
|
427
|
+
const h = rowHeights.dataByIndex[idx];
|
|
428
|
+
if (!Number.isNaN(idx) && typeof h === 'number') {
|
|
429
|
+
const row = titleOffset + headerRowCount + idx;
|
|
430
|
+
if (row >= titleOffset + headerRowCount && row < totalRows) {
|
|
431
|
+
rowsFull[row] = { ...(rowsFull[row] || {}), hpt: h };
|
|
432
|
+
explicitRows.add(row);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
this.computeRowMergesAsync(
|
|
439
|
+
data,
|
|
440
|
+
leafNodes,
|
|
441
|
+
titleOffset + headerRowCount,
|
|
442
|
+
mergeOptions
|
|
443
|
+
).then(rowMerges => {
|
|
444
|
+
merges.push(...rowMerges);
|
|
445
|
+
if (enableIndex && mergeOptions && mergeOptions.mergeKey) {
|
|
446
|
+
const segments = this.computeKeySegments(data, mergeOptions);
|
|
447
|
+
let seq = indexStartAt;
|
|
448
|
+
segments.forEach(seg => {
|
|
449
|
+
const startR = titleOffset + headerRowCount + seg.start;
|
|
450
|
+
const endR = titleOffset + headerRowCount + seg.end;
|
|
451
|
+
const refTop = xlsx.utils.encode_cell({ r: startR, c: 0 });
|
|
452
|
+
if (!worksheet[refTop]) worksheet[refTop] = { v: '', t: 's' };
|
|
453
|
+
worksheet[refTop].v = seq;
|
|
454
|
+
worksheet[refTop].t = 'n';
|
|
455
|
+
if (endR > startR) merges.push({ s: { r: startR, c: 0 }, e: { r: endR, c: 0 } });
|
|
456
|
+
seq += 1;
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
{
|
|
460
|
+
const dataStart = titleOffset + headerRowCount;
|
|
461
|
+
const range = xlsx.utils.decode_range(worksheet['!ref']);
|
|
462
|
+
const imgCols = [];
|
|
463
|
+
for (let c = 0; c < leafNodes.length; c++) {
|
|
464
|
+
if (leafNodes[c] && leafNodes[c].cellType === 'image') imgCols.push(c);
|
|
465
|
+
}
|
|
466
|
+
if (imgCols.length) {
|
|
467
|
+
if (!worksheet['!cols']) worksheet['!cols'] = Array.from({ length: range.e.c + 1 }, () => ({ wch: 15 }));
|
|
468
|
+
imgCols.forEach(c => {
|
|
469
|
+
if (!worksheet['!cols'][c]) worksheet['!cols'][c] = {};
|
|
470
|
+
worksheet['!cols'][c].wch = Math.max(worksheet['!cols'][c].wch || 15, 20);
|
|
471
|
+
});
|
|
472
|
+
for (let r = dataStart; r <= range.e.r; r++) {
|
|
473
|
+
imgCols.forEach(c => {
|
|
474
|
+
const ref = xlsx.utils.encode_cell({ r, c });
|
|
475
|
+
const cell = worksheet[ref];
|
|
476
|
+
const val = cell ? cell.v : '';
|
|
477
|
+
if (this.isValidImageUrl(String(val || ''))) {
|
|
478
|
+
const text = '查看图片';
|
|
479
|
+
worksheet[ref] = { v: text, t: 's', l: { Target: String(val), Tooltip: '打开图片链接' } };
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (merges.length) worksheet['!merges'] = merges;
|
|
486
|
+
this.adjustRowHeightsForMerges(
|
|
487
|
+
rowsFull,
|
|
488
|
+
merges,
|
|
489
|
+
titleOffset + headerRowCount,
|
|
490
|
+
explicitRows,
|
|
491
|
+
{ mode: 'multiply', baseDataHpt: dataDefaultHpt }
|
|
492
|
+
);
|
|
493
|
+
worksheet['!rows'] = rowsFull;
|
|
494
|
+
|
|
495
|
+
const range = xlsx.utils.decode_range(worksheet['!ref']);
|
|
496
|
+
for (let R = range.s.r; R <= range.e.r; ++R) {
|
|
497
|
+
for (let C = range.s.c; C <= range.e.c; ++C) {
|
|
498
|
+
const cellRef = xlsx.utils.encode_cell({ r: R, c: C });
|
|
499
|
+
if (!worksheet[cellRef]) worksheet[cellRef] = { v: '', t: 's' };
|
|
500
|
+
if (title && R === 0) {
|
|
501
|
+
worksheet[cellRef].s = titleCellStyle || headCellStyle;
|
|
502
|
+
} else if (R >= titleOffset && R < titleOffset + headerRowCount) {
|
|
503
|
+
worksheet[cellRef].s = headCellStyle;
|
|
504
|
+
} else {
|
|
505
|
+
worksheet[cellRef].s = cellStyle;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (options.applyBorders) {
|
|
510
|
+
if (merges.length) this.ensureBorderForMergedCells(worksheet, merges);
|
|
511
|
+
this.ensureBorderForHeaderArea(worksheet, merges, titleOffset, headerRowCount);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
console.timeEnd('ExcelExporter:CreateSheetAsync');
|
|
515
|
+
resolve(worksheet);
|
|
516
|
+
}).catch(err => {
|
|
517
|
+
reject(err);
|
|
518
|
+
});
|
|
519
|
+
} catch (error) {
|
|
520
|
+
reject(error);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
static buildHeaderStructure(columns = []) {
|
|
526
|
+
console.time('ExcelExporter:BuildHeader');
|
|
527
|
+
try {
|
|
528
|
+
const isHierarchical = Array.isArray(columns) && columns.some(c => c.children && c.children.length);
|
|
529
|
+
if (!isHierarchical) {
|
|
530
|
+
const visible = columns.filter(c => !c.hidden);
|
|
531
|
+
const simpleHeaders = visible.map(c => c.label || c.header || (c.prop || ''));
|
|
532
|
+
const leafNodes = visible.map(c => ({ header: c.label || c.header || (c.prop || ''), prop: c.prop, mergeable: c.mergeable === true, hidden: !!c.hidden, cellType: c.cellType || 'text' }));
|
|
533
|
+
console.timeEnd('ExcelExporter:BuildHeader');
|
|
534
|
+
return { headerRows: simpleHeaders.length ? [simpleHeaders] : [], leafNodes, headerMerges: [] };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const depth = this.getDepth(columns);
|
|
538
|
+
const leafNodes = [];
|
|
539
|
+
const headerRows = Array.from({ length: depth }, () => []);
|
|
540
|
+
const headerMerges = [];
|
|
541
|
+
|
|
542
|
+
const traverse = (nodes, level, startCol) => {
|
|
543
|
+
let colIndex = startCol;
|
|
544
|
+
nodes.forEach(node => {
|
|
545
|
+
const span = this.countLeaf(node);
|
|
546
|
+
const headerText = node.header || node.label || '';
|
|
547
|
+
for (let i = 0; i < span; i++) {
|
|
548
|
+
headerRows[level][colIndex + i] = headerRows[level][colIndex + i] || '';
|
|
549
|
+
}
|
|
550
|
+
headerRows[level][colIndex] = headerText;
|
|
551
|
+
if (node.children && node.children.length) {
|
|
552
|
+
headerMerges.push({ s: { r: level, c: colIndex }, e: { r: level, c: colIndex + span - 1 } });
|
|
553
|
+
node.children.forEach(child => {
|
|
554
|
+
const childSpan = this.countLeaf(child);
|
|
555
|
+
traverse([child], level + 1, colIndex);
|
|
556
|
+
colIndex += childSpan;
|
|
557
|
+
});
|
|
558
|
+
} else {
|
|
559
|
+
if (!node.hidden) {
|
|
560
|
+
const rowSpan = depth - level - 1;
|
|
561
|
+
if (rowSpan > 0) {
|
|
562
|
+
headerMerges.push({ s: { r: level, c: colIndex }, e: { r: level + rowSpan, c: colIndex } });
|
|
563
|
+
}
|
|
564
|
+
leafNodes.push({ header: headerText, prop: node.prop, mergeable: node.mergeable === true, hidden: !!node.hidden, cellType: node.cellType || 'text' });
|
|
565
|
+
colIndex += 1;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
return colIndex - startCol;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const totalLeaf = columns.reduce((sum, n) => sum + this.countLeaf(n), 0);
|
|
573
|
+
for (let r = 0; r < depth; r++) headerRows[r] = Array.from({ length: totalLeaf }, () => '');
|
|
574
|
+
traverse(columns, 0, 0);
|
|
575
|
+
|
|
576
|
+
console.timeEnd('ExcelExporter:BuildHeader');
|
|
577
|
+
return { headerRows, leafNodes, headerMerges };
|
|
578
|
+
} catch (error) {
|
|
579
|
+
console.error('构建表头结构时发生错误:', error);
|
|
580
|
+
throw new Error('构建表头结构时发生错误');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
static getDepth(node) {
|
|
585
|
+
if (Array.isArray(node)) return Math.max(0, ...node.map(n => this.getDepth(n)));
|
|
586
|
+
if (!node || !node.children || node.children.length === 0) return 1;
|
|
587
|
+
return 1 + Math.max(...node.children.map(n => this.getDepth(n)));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
static countLeaf(node) {
|
|
591
|
+
if (!node) return 0;
|
|
592
|
+
if (!node.children || node.children.length === 0) return node.hidden ? 0 : 1;
|
|
593
|
+
return node.children.reduce((sum, c) => sum + this.countLeaf(c), 0);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
static estimateCols(columns) {
|
|
597
|
+
const isHierarchical = Array.isArray(columns) && columns.some(c => c.children && c.children.length);
|
|
598
|
+
const leafCount = isHierarchical ? columns.reduce((sum, n) => sum + this.countLeaf(n), 0) : columns.length;
|
|
599
|
+
return Array.from({ length: leafCount }, () => ({ wch: 20 }));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
static computeRowMerges(data, leafNodes, startRow, mergeOptions = {}) {
|
|
603
|
+
console.time('ExcelExporter:RowMerge');
|
|
604
|
+
const {
|
|
605
|
+
rowMerge = true,
|
|
606
|
+
excludeColumns = [],
|
|
607
|
+
mergeStrategy = 'auto',
|
|
608
|
+
regex,
|
|
609
|
+
customComparator,
|
|
610
|
+
mergeKey,
|
|
611
|
+
mergeNull = false,
|
|
612
|
+
childrenStrict = false,
|
|
613
|
+
mergeChildrenByDefault = false
|
|
614
|
+
} = mergeOptions;
|
|
615
|
+
const merges = [];
|
|
616
|
+
if (!rowMerge || !Array.isArray(data) || data.length === 0 || !mergeKey) {
|
|
617
|
+
console.timeEnd('ExcelExporter:RowMerge');
|
|
618
|
+
return merges;
|
|
619
|
+
}
|
|
620
|
+
const excludeSet = new Set(excludeColumns || []);
|
|
621
|
+
const colIndexByProp = new Map();
|
|
622
|
+
leafNodes.forEach((n, idx) => {
|
|
623
|
+
const p = n.prop || n.header || n.key || '';
|
|
624
|
+
colIndexByProp.set(p, idx);
|
|
625
|
+
});
|
|
626
|
+
const keyColIndex = colIndexByProp.get(mergeKey);
|
|
627
|
+
const getVal = (row, prop, fallback) => {
|
|
628
|
+
if (!row) return undefined;
|
|
629
|
+
if (prop in row) return row[prop];
|
|
630
|
+
return row[fallback];
|
|
631
|
+
}
|
|
632
|
+
const shouldMerge = (a, b, key) => {
|
|
633
|
+
if (excludeSet.has(key)) return false;
|
|
634
|
+
if (!mergeNull && (a === undefined || a === null || a === '')) return false;
|
|
635
|
+
if (!mergeNull && (b === undefined || b === null || b === '')) return false;
|
|
636
|
+
if (mergeStrategy === 'custom' && typeof customComparator === 'function') return !!customComparator(a, b, undefined, undefined, key);
|
|
637
|
+
if (mergeStrategy === 'regex') {
|
|
638
|
+
const reg = typeof regex === 'string' ? new RegExp(regex) : regex;
|
|
639
|
+
return reg ? (reg.test(String(a)) && reg.test(String(b)) && String(a) === String(b)) : String(a) === String(b);
|
|
640
|
+
}
|
|
641
|
+
return String(a) === String(b);
|
|
642
|
+
}
|
|
643
|
+
let r = 0;
|
|
644
|
+
while (r < data.length) {
|
|
645
|
+
const start = r;
|
|
646
|
+
const baseKey = data[r] ? data[r][mergeKey] : undefined;
|
|
647
|
+
let end = r;
|
|
648
|
+
while (end + 1 < data.length) {
|
|
649
|
+
const nextKey = data[end + 1] ? data[end + 1][mergeKey] : undefined;
|
|
650
|
+
if (shouldMerge(baseKey, nextKey, mergeKey)) {
|
|
651
|
+
end += 1;
|
|
652
|
+
} else {
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (end > start) {
|
|
657
|
+
if (keyColIndex !== undefined) {
|
|
658
|
+
merges.push({ s: { r: startRow + start, c: keyColIndex }, e: { r: startRow + end, c: keyColIndex } });
|
|
659
|
+
}
|
|
660
|
+
for (let c = 0; c < leafNodes.length; c++) {
|
|
661
|
+
if (c === keyColIndex) continue;
|
|
662
|
+
const node = leafNodes[c];
|
|
663
|
+
if (!(mergeChildrenByDefault || node.mergeable)) continue;
|
|
664
|
+
const keyName = node.prop || node.header || '';
|
|
665
|
+
const baseAll = getVal(data[start], node.prop || node.header, node.prop || node.header);
|
|
666
|
+
let allEqual = baseAll !== undefined;
|
|
667
|
+
if (allEqual) {
|
|
668
|
+
for (let k = start + 1; k <= end; k++) {
|
|
669
|
+
const val = getVal(data[k], node.prop || node.header, node.prop || node.header);
|
|
670
|
+
if (!shouldMerge(baseAll, val, keyName)) { allEqual = false; break }
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (allEqual) {
|
|
674
|
+
merges.push({ s: { r: startRow + start, c }, e: { r: startRow + end, c } });
|
|
675
|
+
} else if (!childrenStrict) {
|
|
676
|
+
let i = start;
|
|
677
|
+
while (i <= end) {
|
|
678
|
+
const base = getVal(data[i], node.prop || node.header, node.prop || node.header);
|
|
679
|
+
let j = i;
|
|
680
|
+
while (j + 1 <= end) {
|
|
681
|
+
const next = getVal(data[j + 1], node.prop || node.header, node.prop || node.header);
|
|
682
|
+
if (shouldMerge(base, next, keyName)) {
|
|
683
|
+
j += 1;
|
|
684
|
+
} else {
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (j > i) merges.push({ s: { r: startRow + i, c }, e: { r: startRow + j, c } });
|
|
689
|
+
i = j + 1;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
r = end + 1;
|
|
695
|
+
}
|
|
696
|
+
console.timeEnd('ExcelExporter:RowMerge');
|
|
697
|
+
return merges;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
static computeRowMergesAsync(data, leafNodes, startRow, mergeOptions = {}) {
|
|
701
|
+
console.time('ExcelExporter:RowMergeAsync');
|
|
702
|
+
return new Promise((resolve) => {
|
|
703
|
+
const {
|
|
704
|
+
rowMerge = true,
|
|
705
|
+
excludeColumns = [],
|
|
706
|
+
mergeStrategy = 'auto',
|
|
707
|
+
regex,
|
|
708
|
+
customComparator,
|
|
709
|
+
mergeKey,
|
|
710
|
+
mergeNull = false,
|
|
711
|
+
childrenStrict = false,
|
|
712
|
+
chunkSize = 1000,
|
|
713
|
+
mergeChildrenByDefault = false
|
|
714
|
+
} = mergeOptions;
|
|
715
|
+
const merges = [];
|
|
716
|
+
if (!rowMerge || !Array.isArray(data) || data.length === 0 || !mergeKey) {
|
|
717
|
+
console.timeEnd('ExcelExporter:RowMergeAsync');
|
|
718
|
+
resolve(merges);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const excludeSet = new Set(excludeColumns || []);
|
|
722
|
+
const colIndexByProp = new Map();
|
|
723
|
+
leafNodes.forEach((n, idx) => {
|
|
724
|
+
const p = n.prop || n.header || n.key || '';
|
|
725
|
+
colIndexByProp.set(p, idx);
|
|
726
|
+
});
|
|
727
|
+
const keyColIndex = colIndexByProp.get(mergeKey);
|
|
728
|
+
if (keyColIndex === undefined) {
|
|
729
|
+
console.timeEnd('ExcelExporter:RowMergeAsync');
|
|
730
|
+
resolve(merges);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const getVal = (row, prop, fallback) => {
|
|
734
|
+
if (!row) return undefined;
|
|
735
|
+
if (prop in row) return row[prop];
|
|
736
|
+
return row[fallback];
|
|
737
|
+
}
|
|
738
|
+
const shouldMerge = (a, b, key) => {
|
|
739
|
+
if (excludeSet.has(key)) return false;
|
|
740
|
+
if (!mergeNull && (a === undefined || a === null || a === '')) return false;
|
|
741
|
+
if (!mergeNull && (b === undefined || b === null || b === '')) return false;
|
|
742
|
+
if (mergeStrategy === 'custom' && typeof customComparator === 'function') return !!customComparator(a, b, undefined, undefined, key);
|
|
743
|
+
if (mergeStrategy === 'regex') {
|
|
744
|
+
const reg = typeof regex === 'string' ? new RegExp(regex) : regex;
|
|
745
|
+
return reg ? (reg.test(String(a)) && reg.test(String(b)) && String(a) === String(b)) : String(a) === String(b);
|
|
746
|
+
}
|
|
747
|
+
return String(a) === String(b);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let r = 0;
|
|
751
|
+
const processChunk = () => {
|
|
752
|
+
let iterations = 0;
|
|
753
|
+
while (r < data.length && iterations < chunkSize) {
|
|
754
|
+
const start = r;
|
|
755
|
+
const baseKey = data[r] ? data[r][mergeKey] : undefined;
|
|
756
|
+
let end = r;
|
|
757
|
+
while (end + 1 < data.length) {
|
|
758
|
+
const nextKey = data[end + 1] ? data[end + 1][mergeKey] : undefined;
|
|
759
|
+
if (shouldMerge(baseKey, nextKey, mergeKey)) {
|
|
760
|
+
end += 1;
|
|
761
|
+
} else {
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (end > start) {
|
|
766
|
+
if (keyColIndex !== undefined) {
|
|
767
|
+
merges.push({ s: { r: startRow + start, c: keyColIndex }, e: { r: startRow + end, c: keyColIndex } });
|
|
768
|
+
}
|
|
769
|
+
for (let c = 0; c < leafNodes.length; c++) {
|
|
770
|
+
if (c === keyColIndex) continue;
|
|
771
|
+
const node = leafNodes[c];
|
|
772
|
+
if (!(mergeChildrenByDefault || node.mergeable)) continue;
|
|
773
|
+
const keyName = node.prop || node.header || '';
|
|
774
|
+
const baseAll = getVal(data[start], node.prop || node.header, node.prop || node.header);
|
|
775
|
+
let allEqual = baseAll !== undefined;
|
|
776
|
+
if (allEqual) {
|
|
777
|
+
for (let k = start + 1; k <= end; k++) {
|
|
778
|
+
const val = getVal(data[k], node.prop || node.header, node.prop || node.header);
|
|
779
|
+
if (!shouldMerge(baseAll, val, keyName)) { allEqual = false; break }
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (allEqual) {
|
|
783
|
+
merges.push({ s: { r: startRow + start, c }, e: { r: startRow + end, c } });
|
|
784
|
+
} else if (!childrenStrict) {
|
|
785
|
+
let i = start;
|
|
786
|
+
while (i <= end) {
|
|
787
|
+
const base = getVal(data[i], node.prop || node.header, node.prop || node.header);
|
|
788
|
+
let j = i;
|
|
789
|
+
while (j + 1 <= end) {
|
|
790
|
+
const next = getVal(data[j + 1], node.prop || node.header, node.prop || node.header);
|
|
791
|
+
if (shouldMerge(base, next, keyName)) {
|
|
792
|
+
j += 1;
|
|
793
|
+
} else {
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (j > i) merges.push({ s: { r: startRow + i, c }, e: { r: startRow + j, c } });
|
|
798
|
+
i = j + 1;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
r = end + 1;
|
|
804
|
+
iterations += (end - start + 1);
|
|
805
|
+
}
|
|
806
|
+
if (r < data.length) {
|
|
807
|
+
setTimeout(processChunk, 0);
|
|
808
|
+
} else {
|
|
809
|
+
console.timeEnd('ExcelExporter:RowMergeAsync');
|
|
810
|
+
resolve(merges);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
setTimeout(processChunk, 0);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
static computeKeySegments(data, mergeOptions = {}) {
|
|
818
|
+
const { mergeKey, mergeNull = false, mergeStrategy = 'auto', regex, customComparator } = mergeOptions;
|
|
819
|
+
const segments = [];
|
|
820
|
+
if (!Array.isArray(data) || data.length === 0 || !mergeKey) return segments;
|
|
821
|
+
const shouldMerge = (a, b) => {
|
|
822
|
+
if (!mergeNull && (a === undefined || a === null || a === '')) return false;
|
|
823
|
+
if (!mergeNull && (b === undefined || b === null || b === '')) return false;
|
|
824
|
+
if (mergeStrategy === 'custom' && typeof customComparator === 'function') return !!customComparator(a, b, undefined, undefined, mergeKey);
|
|
825
|
+
if (mergeStrategy === 'regex') {
|
|
826
|
+
const reg = typeof regex === 'string' ? new RegExp(regex) : regex;
|
|
827
|
+
return reg ? (reg.test(String(a)) && reg.test(String(b)) && String(a) === String(b)) : String(a) === String(b);
|
|
828
|
+
}
|
|
829
|
+
return String(a) === String(b);
|
|
830
|
+
}
|
|
831
|
+
let r = 0;
|
|
832
|
+
while (r < data.length) {
|
|
833
|
+
const start = r;
|
|
834
|
+
const baseKey = data[r] ? data[r][mergeKey] : undefined;
|
|
835
|
+
let end = r;
|
|
836
|
+
while (end + 1 < data.length) {
|
|
837
|
+
const nextKey = data[end + 1] ? data[end + 1][mergeKey] : undefined;
|
|
838
|
+
if (shouldMerge(baseKey, nextKey)) {
|
|
839
|
+
end += 1;
|
|
840
|
+
} else {
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
segments.push({ start, end });
|
|
845
|
+
r = end + 1;
|
|
846
|
+
}
|
|
847
|
+
return segments;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
static getDefaultCellStyle(customStyle = {}, showBorder = true, styleOptions = {}) {
|
|
851
|
+
const bodyOpts = styleOptions.body || {};
|
|
852
|
+
const borderStyle = showBorder ? {
|
|
853
|
+
top: { style: 'thin', color: { rgb: '000000' } },
|
|
854
|
+
bottom: { style: 'thin', color: { rgb: '000000' } },
|
|
855
|
+
left: { style: 'thin', color: { rgb: '000000' } },
|
|
856
|
+
right: { style: 'thin', color: { rgb: '000000' } },
|
|
857
|
+
...(customStyle.border || {})
|
|
858
|
+
} : {
|
|
859
|
+
top: { style: 'none' },
|
|
860
|
+
bottom: { style: 'none' },
|
|
861
|
+
left: { style: 'none' },
|
|
862
|
+
right: { style: 'none' },
|
|
863
|
+
...(customStyle.border || {})
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
font: {
|
|
867
|
+
name: bodyOpts.fontName || '宋体',
|
|
868
|
+
sz: bodyOpts.fontSize || 11,
|
|
869
|
+
bold: typeof bodyOpts.bold === 'boolean' ? bodyOpts.bold : false,
|
|
870
|
+
underline: false,
|
|
871
|
+
...(bodyOpts.colorRgb ? { color: { rgb: bodyOpts.colorRgb } } : {}),
|
|
872
|
+
...(customStyle.font || {})
|
|
873
|
+
},
|
|
874
|
+
alignment: {
|
|
875
|
+
horizontal: bodyOpts.horizontal || 'center',
|
|
876
|
+
vertical: bodyOpts.vertical || 'center',
|
|
877
|
+
wrapText: typeof bodyOpts.wrapText === 'boolean' ? bodyOpts.wrapText : true,
|
|
878
|
+
...(customStyle.alignment || {})
|
|
879
|
+
},
|
|
880
|
+
border: borderStyle,
|
|
881
|
+
...customStyle
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
static getDefaultHeadCellStyle(customStyle = {}, showBorder = true, styleOptions = {}) {
|
|
886
|
+
const headerOpts = styleOptions.header || {};
|
|
887
|
+
const borderStyle = showBorder ? {
|
|
888
|
+
top: { style: 'none', color: { rgb: '000000' } },
|
|
889
|
+
bottom: { style: 'thin', color: { rgb: '000000' } },
|
|
890
|
+
left: { style: 'none', color: { rgb: '000000' } },
|
|
891
|
+
right: { style: 'none', color: { rgb: '000000' } },
|
|
892
|
+
...(customStyle.border || {})
|
|
893
|
+
} : {
|
|
894
|
+
top: { style: 'none' },
|
|
895
|
+
bottom: { style: 'none' },
|
|
896
|
+
left: { style: 'none' },
|
|
897
|
+
right: { style: 'none' },
|
|
898
|
+
...(customStyle.border || {})
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
alignment: {
|
|
902
|
+
horizontal: headerOpts.horizontal || 'center',
|
|
903
|
+
vertical: headerOpts.vertical || 'center',
|
|
904
|
+
wrapText: typeof headerOpts.wrapText === 'boolean' ? headerOpts.wrapText : true,
|
|
905
|
+
...(customStyle.alignment || {})
|
|
906
|
+
},
|
|
907
|
+
font: {
|
|
908
|
+
name: headerOpts.fontName || '宋体',
|
|
909
|
+
sz: headerOpts.fontSize || 16,
|
|
910
|
+
bold: typeof headerOpts.bold === 'boolean' ? headerOpts.bold : false,
|
|
911
|
+
underline: false,
|
|
912
|
+
...(headerOpts.colorRgb ? { color: { rgb: headerOpts.colorRgb } } : {}),
|
|
913
|
+
...(customStyle.font || {})
|
|
914
|
+
},
|
|
915
|
+
border: borderStyle,
|
|
916
|
+
...customStyle
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
static getDefaultTitleCellStyle(customStyle = {}, showBorder = true, styleOptions = {}) {
|
|
921
|
+
const titleOpts = styleOptions.title || {};
|
|
922
|
+
const borderStyle = showBorder ? {
|
|
923
|
+
top: { style: 'none', color: { rgb: '000000' } },
|
|
924
|
+
bottom: { style: 'thin', color: { rgb: '000000' } },
|
|
925
|
+
left: { style: 'none', color: { rgb: '000000' } },
|
|
926
|
+
right: { style: 'none', color: { rgb: '000000' } },
|
|
927
|
+
...(customStyle.border || {})
|
|
928
|
+
} : {
|
|
929
|
+
top: { style: 'none' },
|
|
930
|
+
bottom: { style: 'none' },
|
|
931
|
+
left: { style: 'none' },
|
|
932
|
+
right: { style: 'none' },
|
|
933
|
+
...(customStyle.border || {})
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
alignment: {
|
|
937
|
+
horizontal: titleOpts.horizontal || 'center',
|
|
938
|
+
vertical: titleOpts.vertical || 'center',
|
|
939
|
+
wrapText: typeof titleOpts.wrapText === 'boolean' ? titleOpts.wrapText : true,
|
|
940
|
+
...(customStyle.alignment || {})
|
|
941
|
+
},
|
|
942
|
+
font: {
|
|
943
|
+
name: titleOpts.fontName || '宋体',
|
|
944
|
+
sz: titleOpts.fontSize || 20,
|
|
945
|
+
bold: typeof titleOpts.bold === 'boolean' ? titleOpts.bold : true,
|
|
946
|
+
underline: false,
|
|
947
|
+
...(titleOpts.colorRgb ? { color: { rgb: titleOpts.colorRgb } } : {}),
|
|
948
|
+
...(customStyle.font || {})
|
|
949
|
+
},
|
|
950
|
+
border: borderStyle,
|
|
951
|
+
...customStyle
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
static ensureBorderForMergedCells(worksheet, merges) {
|
|
956
|
+
const thinSide = (obj, side) => ({ ...(obj || {}), [side]: { style: 'thin', color: { rgb: '000000' } } });
|
|
957
|
+
merges.forEach(m => {
|
|
958
|
+
for (let r = m.s.r; r <= m.e.r; r++) {
|
|
959
|
+
for (let c = m.s.c; c <= m.e.c; c++) {
|
|
960
|
+
const ref = xlsx.utils.encode_cell({ r, c });
|
|
961
|
+
const cell = worksheet[ref] || { v: '', t: 's' };
|
|
962
|
+
const style = cell.s || {};
|
|
963
|
+
let border = { ...(style.border || {}) };
|
|
964
|
+
if (r === m.s.r) border = thinSide(border, 'top');
|
|
965
|
+
if (r === m.e.r) border = thinSide(border, 'bottom');
|
|
966
|
+
if (c === m.s.c) border = thinSide(border, 'left');
|
|
967
|
+
if (c === m.e.c) border = thinSide(border, 'right');
|
|
968
|
+
cell.s = { ...style, border };
|
|
969
|
+
worksheet[ref] = cell;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
static ensureBorderForHeaderArea(worksheet, merges, titleOffset, headerRowCount) {
|
|
976
|
+
const range = xlsx.utils.decode_range(worksheet['!ref']);
|
|
977
|
+
const isInMerge = (r, c) => {
|
|
978
|
+
for (let i = 0; i < merges.length; i++) {
|
|
979
|
+
const m = merges[i];
|
|
980
|
+
if (r >= m.s.r && r <= m.e.r && c >= m.s.c && c <= m.e.c) return true;
|
|
981
|
+
}
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
for (let r = 0; r < titleOffset + headerRowCount; r++) {
|
|
985
|
+
for (let c = range.s.c; c <= range.e.c; c++) {
|
|
986
|
+
if (isInMerge(r, c)) continue;
|
|
987
|
+
const ref = xlsx.utils.encode_cell({ r, c });
|
|
988
|
+
const cell = worksheet[ref] || { v: '', t: 's' };
|
|
989
|
+
const style = cell.s || {};
|
|
990
|
+
const border = {
|
|
991
|
+
top: { style: 'thin', color: { rgb: '000000' } },
|
|
992
|
+
bottom: { style: 'thin', color: { rgb: '000000' } },
|
|
993
|
+
left: { style: 'thin', color: { rgb: '000000' } },
|
|
994
|
+
right: { style: 'thin', color: { rgb: '000000' } }
|
|
995
|
+
}
|
|
996
|
+
cell.s = { ...style, border: { ...(style.border || {}), ...border } };
|
|
997
|
+
worksheet[ref] = cell;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
static adjustRowHeightsForMerges(rowsFull, merges, dataStartRow, explicitRows, opts = {}) {
|
|
1003
|
+
if (!Array.isArray(rowsFull) || !Array.isArray(merges)) return;
|
|
1004
|
+
const mode = opts.mode || 'multiply';
|
|
1005
|
+
const baseDataHpt = typeof opts.baseDataHpt === 'number' ? opts.baseDataHpt : 30;
|
|
1006
|
+
if (mode === 'multiply') {
|
|
1007
|
+
merges.forEach(m => {
|
|
1008
|
+
const isDataMerge = m.s.r >= dataStartRow && m.e.r >= dataStartRow && m.e.r >= m.s.r;
|
|
1009
|
+
if (!isDataMerge) return;
|
|
1010
|
+
for (let r = m.s.r; r <= m.e.r; r++) {
|
|
1011
|
+
if (!rowsFull[r]) rowsFull[r] = {};
|
|
1012
|
+
rowsFull[r].hpt = baseDataHpt;
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const affected = new Set();
|
|
1018
|
+
merges.forEach(m => {
|
|
1019
|
+
for (let r = m.s.r; r <= m.e.r; r++) {
|
|
1020
|
+
if (r >= dataStartRow) affected.add(r);
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
affected.forEach(r => {
|
|
1024
|
+
if (!rowsFull[r]) rowsFull[r] = {};
|
|
1025
|
+
const locked = explicitRows && explicitRows instanceof Set && explicitRows.has(r);
|
|
1026
|
+
if (!locked && rowsFull[r].hpt) delete rowsFull[r].hpt;
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
static isValidImageUrl(url) {
|
|
1031
|
+
if (typeof url !== 'string' || !/^https?:\/\//i.test(url)) return false;
|
|
1032
|
+
const lower = url.split('?')[0].toLowerCase();
|
|
1033
|
+
return ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'].some(ext => lower.endsWith(ext));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* 便捷导出(同步)
|
|
1039
|
+
*/
|
|
1040
|
+
export function exportExcel(options) {
|
|
1041
|
+
return ExcelExporter.exportExcel(options);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* 便捷导出(异步)
|
|
1046
|
+
*/
|
|
1047
|
+
export function exportExcelAsync(options) {
|
|
1048
|
+
return ExcelExporter.exportExcelAsync(options);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export default ExcelExporter;
|