aegon-eep 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/package.json +12 -0
- package/sss.md +262 -0
package/package.json
ADDED
package/sss.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// excelExport.ts
|
|
2
|
+
import ExcelJS from "exceljs";
|
|
3
|
+
import type { CellValue, Worksheet, Row, Cell } from "exceljs";
|
|
4
|
+
|
|
5
|
+
// 核心配置接口
|
|
6
|
+
export interface ExportOptions {
|
|
7
|
+
excelName?: string;
|
|
8
|
+
sheetName?: string;
|
|
9
|
+
dateColumnName?: string;
|
|
10
|
+
groupColumnName?: string;
|
|
11
|
+
columnsToMerge?: string[];
|
|
12
|
+
rightAlignedColumnsLetter?: string[];
|
|
13
|
+
centerAlignedColumnsLetter?: string[];
|
|
14
|
+
leftAlignedColumnsLetter?: string[];
|
|
15
|
+
rightAlignedColumns?: string[];
|
|
16
|
+
centerAlignedColumns?: string[];
|
|
17
|
+
leftAlignedColumns?: string[];
|
|
18
|
+
exportType?: 'single' | 'multi-sheet';
|
|
19
|
+
headerStyle?: any;
|
|
20
|
+
dataRowStyle?: any;
|
|
21
|
+
columnWidthLimit?: number;
|
|
22
|
+
customFileName?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 工具函数:列字母转索引(A→0, B→1...)
|
|
26
|
+
const letterToIndex = (letter: string): number => {
|
|
27
|
+
let index = 0;
|
|
28
|
+
for (const c of letter.toUpperCase().trim()) {
|
|
29
|
+
index = index \* 26 + (c.charCodeAt(0) - 65);
|
|
30
|
+
}
|
|
31
|
+
return index;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 工具函数:判断列是否在指定字母列表中
|
|
35
|
+
const isColumnInLetters = (idx: number, letters: string[] = []): boolean => {
|
|
36
|
+
return letters.length && letters.map(letterToIndex).includes(idx);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// 工具函数:获取单元格值长度
|
|
40
|
+
const getCellLen = (val: CellValue | undefined | null): number => {
|
|
41
|
+
if (!val) return 0;
|
|
42
|
+
const str = typeof val === 'string' ? val :
|
|
43
|
+
val instanceof Date ? val.toISOString() : String(val);
|
|
44
|
+
return str.length;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// 工具函数:判断是否为数值
|
|
48
|
+
const isNum = (val: any): boolean => {
|
|
49
|
+
if (val == null || typeof val !== 'string') return typeof val === 'number';
|
|
50
|
+
if (/[a-zA-Z\-_:]/.test(val)) return false;
|
|
51
|
+
return /^-?\d+(\.\d+)?$/.test(val.replace(/[,$\s]/g, ''));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// 工具函数:判断是否为编码值
|
|
55
|
+
const isCode = (val: any): boolean => {
|
|
56
|
+
return val && typeof val === 'string' &&
|
|
57
|
+
(/^\d{3}-[A-Z]\d-\d{8}-\d{3}$/.test(val) || /^[A-Za-z0-9\-_.:]+$/.test(val));
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// 判断是否居左
|
|
61
|
+
const isLeftAlign = (val: any, idx: number, opt: any): boolean => {
|
|
62
|
+
if (isColumnInLetters(idx, opt.leftAlignedColumnsLetter)) return true;
|
|
63
|
+
if (opt.colName && opt.leftAlignedColumns.includes(opt.colName)) return true;
|
|
64
|
+
if (isCode(val)) return true;
|
|
65
|
+
if (typeof val === 'string' && !isNum(val) && /[\/\-:]/.test(val)) return true;
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// 按日期分组数据
|
|
70
|
+
export const groupByDate = (data: string[][], dateCol: string = ""): Record<string, string[][]> => {
|
|
71
|
+
if (data.length <= 1 || !dateCol) return { "全部数据": data };
|
|
72
|
+
const header = data[0];
|
|
73
|
+
const dateIdx = header.indexOf(dateCol);
|
|
74
|
+
if (dateIdx === -1) return { "全部数据": data };
|
|
75
|
+
|
|
76
|
+
const groups: Record<string, string[][]> = {};
|
|
77
|
+
for (let i = 1; i < data.length; i++) {
|
|
78
|
+
const date = data[i][dateIdx];
|
|
79
|
+
if (!date) continue;
|
|
80
|
+
if (!groups[date]) groups[date] = [header];
|
|
81
|
+
groups[date].push(data[i]);
|
|
82
|
+
}
|
|
83
|
+
return Object.keys(groups).length ? groups : { "全部数据": data };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 配置工作表样式
|
|
87
|
+
const setSheetStyle = (sheet: Worksheet, header: string[], opt: any): void => {
|
|
88
|
+
const {
|
|
89
|
+
rightAlignedColumnsLetter = [], centerAlignedColumnsLetter = [], leftAlignedColumnsLetter = [],
|
|
90
|
+
rightAlignedColumns = [], centerAlignedColumns = [], leftAlignedColumns = [],
|
|
91
|
+
headerStyle = {}, dataRowStyle = {}, columnWidthLimit = 50
|
|
92
|
+
} = opt;
|
|
93
|
+
|
|
94
|
+
// 表头样式
|
|
95
|
+
sheet.getRow(1)?.eachCell((cell: Cell, cellNum: number) => {
|
|
96
|
+
const idx = cellNum - 1;
|
|
97
|
+
let align = 'left';
|
|
98
|
+
if (isColumnInLetters(idx, centerAlignedColumnsLetter)) align = 'center';
|
|
99
|
+
else if (isColumnInLetters(idx, rightAlignedColumnsLetter)) align = 'right';
|
|
100
|
+
|
|
101
|
+
cell.font = { name: "Calibri", size: 11, bold: true, color: { argb: "FF000000" }, ...headerStyle.font };
|
|
102
|
+
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFFFFFFF" }, ...headerStyle.fill };
|
|
103
|
+
cell.alignment = { vertical: "middle", horizontal: align, wrapText: true, ...headerStyle.alignment };
|
|
104
|
+
cell.border = { top: { style: "thin", color: { argb: "FF000000" } }, left: { style: "thin", color: { argb: "FF000000" } }, bottom: { style: "thin", color: { argb: "FF000000" } }, right: { style: "thin", color: { argb: "FF000000" } }, ...headerStyle.border };
|
|
105
|
+
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// 数据行样式
|
|
109
|
+
sheet.eachRow((row: Row, rowIdx: number) => {
|
|
110
|
+
if (rowIdx === 1) return;
|
|
111
|
+
row.eachCell((cell: Cell, cellNum: number) => {
|
|
112
|
+
const idx = cellNum - 1;
|
|
113
|
+
const colName = idx < header.length ? header[idx] : "";
|
|
114
|
+
let align = 'left';
|
|
115
|
+
|
|
116
|
+
if (isColumnInLetters(idx, centerAlignedColumnsLetter)) align = 'center';
|
|
117
|
+
else if (isColumnInLetters(idx, rightAlignedColumnsLetter)) align = 'right';
|
|
118
|
+
else if (colName && centerAlignedColumns.includes(colName)) align = 'center';
|
|
119
|
+
else if ((colName && rightAlignedColumns.includes(colName)) || isNum(cell.value)) align = 'right';
|
|
120
|
+
else if (isLeftAlign(cell.value, idx, { leftAlignedColumnsLetter, leftAlignedColumns, colName })) align = 'left';
|
|
121
|
+
|
|
122
|
+
cell.font = { name: "Calibri", size: 11, color: { argb: "FF000000" }, ...dataRowStyle.font };
|
|
123
|
+
cell.alignment = { vertical: "middle", horizontal: align, wrapText: true, ...dataRowStyle.alignment };
|
|
124
|
+
cell.border = { top: { style: "thin", color: { argb: "FF000000" } }, left: { style: "thin", color: { argb: "FF000000" } }, bottom: { style: "thin", color: { argb: "FF000000" } }, right: { style: "thin", color: { argb: "FF000000" } }, ...dataRowStyle.border };
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// 列宽自适应(×1.3)
|
|
130
|
+
sheet.columns?.forEach(col => {
|
|
131
|
+
let maxLen = 10;
|
|
132
|
+
if (col.values?.length) {
|
|
133
|
+
maxLen = col.values.reduce<number>((w, val) => Math.max(w, getCellLen(val)), 10);
|
|
134
|
+
}
|
|
135
|
+
col.width = Math.min(maxLen \* 1.3, columnWidthLimit);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// 按列分组数据
|
|
140
|
+
const groupByColumn = (data: any[][], groupCol: string = ""): any[] => {
|
|
141
|
+
const groups: any[] = [];
|
|
142
|
+
let currGroup: any = null;
|
|
143
|
+
const groupIdx = groupCol ? data[0]?.indexOf(groupCol) : 0;
|
|
144
|
+
|
|
145
|
+
data.forEach((row, idx) => {
|
|
146
|
+
const val = row[groupIdx || 0];
|
|
147
|
+
if (!currGroup || currGroup.groupValue !== val) {
|
|
148
|
+
if (currGroup) {
|
|
149
|
+
currGroup.endRow = idx - 1;
|
|
150
|
+
groups.push(currGroup);
|
|
151
|
+
}
|
|
152
|
+
currGroup = { groupValue: val, startRow: idx, endRow: idx, rows: [row] };
|
|
153
|
+
} else {
|
|
154
|
+
currGroup.endRow = idx;
|
|
155
|
+
currGroup.rows.push(row);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
if (currGroup) groups.push(currGroup);
|
|
159
|
+
return groups;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// 合并单元格
|
|
163
|
+
const mergeCells = (sheet: Worksheet, data: any[][], mergeCols: string[], headers: string[]): void => {
|
|
164
|
+
if (!data.length || !mergeCols.length) return;
|
|
165
|
+
const groups = groupByColumn(data);
|
|
166
|
+
const merged = new Set<string>();
|
|
167
|
+
|
|
168
|
+
groups.forEach(group => {
|
|
169
|
+
if (group.startRow === group.endRow) return;
|
|
170
|
+
mergeCols.forEach(colName => {
|
|
171
|
+
headers.forEach((header, colIdx) => {
|
|
172
|
+
if (header !== colName) return;
|
|
173
|
+
const startRow = group.startRow + 2;
|
|
174
|
+
const endRow = group.endRow + 2;
|
|
175
|
+
const key = `${startRow}-${endRow}-${colIdx + 1}`;
|
|
176
|
+
if (merged.has(key)) return;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
sheet.mergeCells(startRow, colIdx + 1, endRow, colIdx + 1);
|
|
180
|
+
merged.add(key);
|
|
181
|
+
const cell = sheet.getCell(startRow, colIdx + 1);
|
|
182
|
+
cell.value = group.rows.find(row => row[colIdx]?.trim())?.[colIdx] || "";
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.warn('合并单元格失败:', e);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// 获取日期范围字符串
|
|
193
|
+
export const getDateRange = (data: string[][], dateCol: string = ""): string => {
|
|
194
|
+
if (data.length <= 1 || !dateCol) return "";
|
|
195
|
+
const dateIdx = data[0].indexOf(dateCol);
|
|
196
|
+
if (dateIdx === -1) return "";
|
|
197
|
+
|
|
198
|
+
const dates = Array.from(new Set(data.slice(1).map(row => row[dateIdx]).filter(Boolean))).sort();
|
|
199
|
+
return dates.length === 1 ? dates[0] : dates.length ? `${dates[0]}~${dates[dates.length - 1]}` : "";
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// 核心导出函数
|
|
203
|
+
export const guiExportToExcel = async (data: string[][], options: ExportOptions = {}): Promise<void> => {
|
|
204
|
+
const {
|
|
205
|
+
excelName = "导出数据", sheetName = "数据", dateColumnName = "", groupColumnName = "",
|
|
206
|
+
columnsToMerge = [], rightAlignedColumnsLetter = [], centerAlignedColumnsLetter = [], leftAlignedColumnsLetter = [],
|
|
207
|
+
rightAlignedColumns = [], centerAlignedColumns = [], leftAlignedColumns = [],
|
|
208
|
+
exportType = "single", headerStyle = {}, dataRowStyle = {}, columnWidthLimit = 50, customFileName = ""
|
|
209
|
+
} = options;
|
|
210
|
+
|
|
211
|
+
const workbook = new ExcelJS.Workbook();
|
|
212
|
+
|
|
213
|
+
// 多 sheet 导出(按日期)
|
|
214
|
+
if (exportType === "multi-sheet" && dateColumnName) {
|
|
215
|
+
const dateGroups = groupByDate(data, dateColumnName);
|
|
216
|
+
Object.keys(dateGroups).forEach(date => {
|
|
217
|
+
const dateData = dateGroups[date];
|
|
218
|
+
let sheetNm = date === "全部数据" ? "汇总" : date.replace(/[\/\\\*\?\[\]:]/g, "-").substring(0, 31);
|
|
219
|
+
const sheet = workbook.addWorksheet(sheetNm);
|
|
220
|
+
if (dateData.length) {
|
|
221
|
+
sheet.addRows(dateData);
|
|
222
|
+
setSheetStyle(sheet, dateData[0], {
|
|
223
|
+
rightAlignedColumnsLetter, centerAlignedColumnsLetter, leftAlignedColumnsLetter,
|
|
224
|
+
rightAlignedColumns, centerAlignedColumns, leftAlignedColumns,
|
|
225
|
+
headerStyle, dataRowStyle, columnWidthLimit
|
|
226
|
+
});
|
|
227
|
+
if (columnsToMerge.length && groupColumnName) {
|
|
228
|
+
mergeCells(sheet, dateData.slice(1), columnsToMerge, dateData[0]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// 单 sheet 导出
|
|
234
|
+
else {
|
|
235
|
+
const sheet = workbook.addWorksheet(sheetName);
|
|
236
|
+
sheet.addRows(data);
|
|
237
|
+
setSheetStyle(sheet, data[0], {
|
|
238
|
+
rightAlignedColumnsLetter, centerAlignedColumnsLetter, leftAlignedColumnsLetter,
|
|
239
|
+
rightAlignedColumns, centerAlignedColumns, leftAlignedColumns,
|
|
240
|
+
headerStyle, dataRowStyle, columnWidthLimit
|
|
241
|
+
});
|
|
242
|
+
if (columnsToMerge.length && groupColumnName) {
|
|
243
|
+
mergeCells(sheet, data.slice(1), columnsToMerge, data[0]);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 生成并下载文件
|
|
248
|
+
const buffer = await workbook.xlsx.writeBuffer();
|
|
249
|
+
const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
|
|
250
|
+
const a = document.createElement("a");
|
|
251
|
+
let fileName = customFileName || excelName;
|
|
252
|
+
if (exportType === "multi-sheet" && dateColumnName) {
|
|
253
|
+
const dateRange = getDateRange(data, dateColumnName);
|
|
254
|
+
if (dateRange) fileName = `${excelName}_${dateRange}`;
|
|
255
|
+
}
|
|
256
|
+
a.href = URL.createObjectURL(blob);
|
|
257
|
+
a.download = `${fileName}.xlsx`;
|
|
258
|
+
document.body.appendChild(a);
|
|
259
|
+
a.click();
|
|
260
|
+
document.body.removeChild(a);
|
|
261
|
+
URL.revokeObjectURL(a.href);
|
|
262
|
+
};
|