@canvas-components/utils 0.2.4 → 0.2.6

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.
@@ -1,3 +1,5 @@
1
+ import * as XLSX from 'xlsx';
2
+
1
3
  /** XLSX 导出阶段;编码阶段无法提供准确的百分比。 */
2
4
  interface XlsxExportProgress {
3
5
  phase: "preparing" | "encoding" | "saving" | "done";
@@ -30,6 +32,10 @@ interface XlsxExportParams<Row> extends XlsxExportOptions {
30
32
  columns: ExportTableColumn<Row>[];
31
33
  sheetName?: string;
32
34
  footerRows?: unknown[][];
35
+ /** 多级表头;省略时使用 columns.title。 */
36
+ headerRows?: string[][];
37
+ /** 基于完整工作表(含表头)的合并范围,分文件时自动裁切并平移。 */
38
+ merges?: XLSX.Range[];
33
39
  }
34
40
  /** 按行数和列数双重限制自动分文件,逐份生成、保存并释放。 */
35
41
  declare function exportTableToXlsx<Row>(params: XlsxExportParams<Row>): Promise<void>;
@@ -52,6 +58,7 @@ declare function mapRowsToExportRecords<Row>(params: {
52
58
  declare function buildCsvContent<Row>(params: {
53
59
  rows: Row[];
54
60
  columns: ExportTableColumn<Row>[];
61
+ footerRows?: unknown[][];
55
62
  }): string;
56
63
  /**
57
64
  * 将二维数据序列化为 CSV 文本。
@@ -63,6 +70,7 @@ declare function serializeCsvRows(rows: readonly (readonly unknown[])[], lineBre
63
70
  declare function buildTxtContent<Row>(params: {
64
71
  rows: Row[];
65
72
  columns: ExportTableColumn<Row>[];
73
+ footerRows?: unknown[][];
66
74
  }): string;
67
75
  /**
68
76
  * 下载 JSON 文件。
@@ -79,6 +87,7 @@ declare function exportTableToCsv<Row>(params: {
79
87
  fileName: string;
80
88
  rows: Row[];
81
89
  columns: ExportTableColumn<Row>[];
90
+ footerRows?: unknown[][];
82
91
  }): Promise<void>;
83
92
  /**
84
93
  * 导出表格 TXT 文件。
@@ -87,6 +96,7 @@ declare function exportTableToTxt<Row>(params: {
87
96
  fileName: string;
88
97
  rows: Row[];
89
98
  columns: ExportTableColumn<Row>[];
99
+ footerRows?: unknown[][];
90
100
  }): Promise<void>;
91
101
 
92
102
  export { type ExportTableColumn, type XlsxExportOptions, type XlsxExportParams, type XlsxExportProgress, buildCsvContent, buildTxtContent, downloadJsonFile, exportTableToCsv, exportTableToTxt, exportTableToXlsx, mapRowsToExportRecords, serializeCsvRows };
@@ -1,3 +1,5 @@
1
+ import * as XLSX from 'xlsx';
2
+
1
3
  /** XLSX 导出阶段;编码阶段无法提供准确的百分比。 */
2
4
  interface XlsxExportProgress {
3
5
  phase: "preparing" | "encoding" | "saving" | "done";
@@ -30,6 +32,10 @@ interface XlsxExportParams<Row> extends XlsxExportOptions {
30
32
  columns: ExportTableColumn<Row>[];
31
33
  sheetName?: string;
32
34
  footerRows?: unknown[][];
35
+ /** 多级表头;省略时使用 columns.title。 */
36
+ headerRows?: string[][];
37
+ /** 基于完整工作表(含表头)的合并范围,分文件时自动裁切并平移。 */
38
+ merges?: XLSX.Range[];
33
39
  }
34
40
  /** 按行数和列数双重限制自动分文件,逐份生成、保存并释放。 */
35
41
  declare function exportTableToXlsx<Row>(params: XlsxExportParams<Row>): Promise<void>;
@@ -52,6 +58,7 @@ declare function mapRowsToExportRecords<Row>(params: {
52
58
  declare function buildCsvContent<Row>(params: {
53
59
  rows: Row[];
54
60
  columns: ExportTableColumn<Row>[];
61
+ footerRows?: unknown[][];
55
62
  }): string;
56
63
  /**
57
64
  * 将二维数据序列化为 CSV 文本。
@@ -63,6 +70,7 @@ declare function serializeCsvRows(rows: readonly (readonly unknown[])[], lineBre
63
70
  declare function buildTxtContent<Row>(params: {
64
71
  rows: Row[];
65
72
  columns: ExportTableColumn<Row>[];
73
+ footerRows?: unknown[][];
66
74
  }): string;
67
75
  /**
68
76
  * 下载 JSON 文件。
@@ -79,6 +87,7 @@ declare function exportTableToCsv<Row>(params: {
79
87
  fileName: string;
80
88
  rows: Row[];
81
89
  columns: ExportTableColumn<Row>[];
90
+ footerRows?: unknown[][];
82
91
  }): Promise<void>;
83
92
  /**
84
93
  * 导出表格 TXT 文件。
@@ -87,6 +96,7 @@ declare function exportTableToTxt<Row>(params: {
87
96
  fileName: string;
88
97
  rows: Row[];
89
98
  columns: ExportTableColumn<Row>[];
99
+ footerRows?: unknown[][];
90
100
  }): Promise<void>;
91
101
 
92
102
  export { type ExportTableColumn, type XlsxExportOptions, type XlsxExportParams, type XlsxExportProgress, buildCsvContent, buildTxtContent, downloadJsonFile, exportTableToCsv, exportTableToTxt, exportTableToXlsx, mapRowsToExportRecords, serializeCsvRows };
@@ -11,7 +11,10 @@ var __export = (target, all) => {
11
11
  };
12
12
 
13
13
  // src/export/xlsx-runtime.ts
14
- function createXlsxWriter(xlsx, header, sheetName, maxRows = 1048576) {
14
+ function createXlsxWriter(xlsx, header, sheetName, maxRows = 1048576, merges = []) {
15
+ const headerRows = Array.isArray(header[0]) ? header : [header];
16
+ if (headerRows.length >= maxRows)
17
+ throw new Error("XLSX \u8868\u5934\u884C\u6570\u8D85\u8FC7\u5DE5\u4F5C\u8868\u5BB9\u91CF");
15
18
  const workbook = xlsx.utils.book_new();
16
19
  let worksheet;
17
20
  let rowCount = 0;
@@ -19,7 +22,7 @@ function createXlsxWriter(xlsx, header, sheetName, maxRows = 1048576) {
19
22
  const baseName = sheetName.replace(/[\\/?*[\]:]/g, "_").replace(/^'+|'+$/g, "") || "Sheet1";
20
23
  const addSheet = () => {
21
24
  const suffix = sheetIndex === 0 ? "" : `_${sheetIndex + 1}`;
22
- worksheet = xlsx.utils.aoa_to_sheet([header], {
25
+ worksheet = xlsx.utils.aoa_to_sheet(headerRows, {
23
26
  dense: true
24
27
  });
25
28
  xlsx.utils.book_append_sheet(
@@ -27,7 +30,8 @@ function createXlsxWriter(xlsx, header, sheetName, maxRows = 1048576) {
27
30
  worksheet,
28
31
  baseName.slice(0, 31 - suffix.length) + suffix
29
32
  );
30
- rowCount = 1;
33
+ if (merges.length && sheetIndex === 0) worksheet["!merges"] = merges;
34
+ rowCount = headerRows.length;
31
35
  sheetIndex += 1;
32
36
  };
33
37
  addSheet();
@@ -103,7 +107,7 @@ let writer;
103
107
  self.onmessage = function(event) {
104
108
  try {
105
109
  const message = event.data;
106
- if (message.type === 'init') writer = createWriter(XLSX, message.header, message.sheetName);
110
+ if (message.type === 'init') writer = createWriter(XLSX, message.header, message.sheetName, undefined, message.merges);
107
111
  else if (message.type === 'append') writer.append(message.rows);
108
112
  else if (message.type === 'finish') {
109
113
  const buffer = writer.finish();
@@ -151,8 +155,22 @@ async function buildXlsxPart(params, rangeStart, rangeEnd) {
151
155
  throw new Error(
152
156
  "\u5355\u4EFD XLSX \u6570\u636E\u8D85\u8FC7\u5B89\u5168\u9650\u5236\uFF0C\u8BF7\u4F7F\u7528 exportTableToXlsx \u81EA\u52A8\u5206\u6587\u4EF6\u5BFC\u51FA"
153
157
  );
154
- const header = columns.map((column) => column.title);
155
- let textSize = header.reduce((size, title) => size + title.length, 0);
158
+ const header = params.headerRows?.length ? params.headerRows : [columns.map((column) => column.title)];
159
+ const headerRowCount = header.length;
160
+ const merges = (params.merges ?? []).flatMap((merge) => {
161
+ if (merge.e.r < headerRowCount) return [merge];
162
+ const start = Math.max(merge.s.r, headerRowCount + rangeStart);
163
+ const end = Math.min(merge.e.r, headerRowCount + rangeEnd - 1);
164
+ return end < start || end === start && merge.s.c === merge.e.c ? [] : [
165
+ {
166
+ s: { r: start - rangeStart, c: merge.s.c },
167
+ e: { r: end - rangeStart, c: merge.e.c }
168
+ }
169
+ ];
170
+ });
171
+ if (header.some((row) => row.length > columns.length))
172
+ throw new Error("XLSX \u8868\u5934\u5355\u5143\u683C\u6570\u4E0D\u80FD\u8D85\u8FC7\u5BFC\u51FA\u5217\u6570");
173
+ let textSize = header.flat().reduce((size, title) => size + title.length, 0);
156
174
  const normalizeExportCell = (value) => {
157
175
  const cell = normalizeCell(value);
158
176
  if (typeof cell === "string") {
@@ -166,7 +184,7 @@ async function buildXlsxPart(params, rangeStart, rangeEnd) {
166
184
  }
167
185
  return cell;
168
186
  };
169
- header.forEach((title) => {
187
+ header.flat().forEach((title) => {
170
188
  if (title.length > 32767) throw new Error("XLSX \u8868\u5934\u8D85\u8FC7 32767 \u5B57\u7B26");
171
189
  });
172
190
  if (textSize > XLSX_MAX_TEXT_CHARS_PER_FILE)
@@ -218,7 +236,7 @@ async function buildXlsxPart(params, rangeStart, rangeEnd) {
218
236
  () => fail(new Error("XLSX Worker \u542F\u52A8\u8D85\u65F6")),
219
237
  1e4
220
238
  );
221
- await request({ type: "init", header, sheetName });
239
+ await request({ type: "init", header, sheetName, merges });
222
240
  } catch {
223
241
  worker?.terminate();
224
242
  worker = void 0;
@@ -228,7 +246,7 @@ async function buildXlsxPart(params, rangeStart, rangeEnd) {
228
246
  }
229
247
  }
230
248
  checkAbort();
231
- const writer = worker ? void 0 : createXlsxWriter(XLSX, header, sheetName);
249
+ const writer = worker ? void 0 : createXlsxWriter(XLSX, header, sheetName, void 0, merges);
232
250
  const totalRows = rangeEnd - rangeStart;
233
251
  const report = (phase, completedRows) => params.onProgress?.({
234
252
  phase,
@@ -359,7 +377,7 @@ function getXlsxRowsPerFile(params) {
359
377
  throw new Error("XLSX \u5206\u6587\u4EF6\u4E0A\u9650\u5FC5\u987B\u4E3A\u6B63\u6574\u6570");
360
378
  const byCells = Math.floor(
361
379
  Math.min(cellLimit, XLSX_MAX_CELLS_PER_FILE) / Math.max(1, params.columns.length)
362
- ) - 1;
380
+ ) - (params.headerRows?.length || 1);
363
381
  if (byCells < 1) throw new Error("XLSX \u5355\u5143\u683C\u9884\u7B97\u4E0D\u8DB3\u4EE5\u5BB9\u7EB3\u8868\u5934\u548C\u4E00\u884C\u6570\u636E");
364
382
  return Math.min(rowLimit, XLSX_MAX_ROWS_PER_FILE, byCells);
365
383
  }
@@ -381,7 +399,8 @@ function buildCsvContent(params) {
381
399
  columns.map((column) => column.title),
382
400
  ...rows.map(
383
401
  (row, rowIndex) => columns.map((column) => column.value(row, rowIndex))
384
- )
402
+ ),
403
+ ...params.footerRows ?? []
385
404
  ]);
386
405
  }
387
406
  function serializeCsvRows(rows, lineBreak = "\n") {
@@ -393,7 +412,13 @@ function buildTxtContent(params) {
393
412
  const body = rows.map(
394
413
  (row, rowIndex) => columns.map((column) => normalizeTextCell(column.value(row, rowIndex))).join(" ")
395
414
  );
396
- return [header, ...body].join("\n");
415
+ return [
416
+ header,
417
+ ...body,
418
+ ...(params.footerRows ?? []).map(
419
+ (row) => row.map(normalizeTextCell).join(" ")
420
+ )
421
+ ].join("\n");
397
422
  }
398
423
  async function downloadJsonFile(params) {
399
424
  const { data, fileName, space = 2 } = params;
@@ -405,8 +430,8 @@ async function downloadJsonFile(params) {
405
430
  });
406
431
  }
407
432
  async function exportTableToCsv(params) {
408
- const { fileName, rows, columns } = params;
409
- const content = await buildCsvContentAsync({ rows, columns });
433
+ const { fileName, rows, columns, footerRows } = params;
434
+ const content = await buildCsvContentAsync({ rows, columns, footerRows });
410
435
  downloadTextFile({
411
436
  fileName: ensureExtension(fileName, "csv"),
412
437
  content,
@@ -414,8 +439,8 @@ async function exportTableToCsv(params) {
414
439
  });
415
440
  }
416
441
  async function exportTableToTxt(params) {
417
- const { fileName, rows, columns } = params;
418
- const content = await buildTxtContentAsync({ rows, columns });
442
+ const { fileName, rows, columns, footerRows } = params;
443
+ const content = await buildTxtContentAsync({ rows, columns, footerRows });
419
444
  downloadTextFile({
420
445
  fileName: ensureExtension(fileName, "txt"),
421
446
  content,
@@ -459,6 +484,9 @@ async function buildCsvContentAsync(params) {
459
484
  }
460
485
  await yieldToMainThread();
461
486
  }
487
+ for (const footer of params.footerRows ?? []) {
488
+ parts.push("\n", footer.map(escapeCsvCell).join(","));
489
+ }
462
490
  return parts.join("");
463
491
  }
464
492
  async function buildTxtContentAsync(params) {
@@ -474,6 +502,9 @@ async function buildTxtContentAsync(params) {
474
502
  }
475
503
  await yieldToMainThread();
476
504
  }
505
+ for (const footer of params.footerRows ?? []) {
506
+ parts.push("\n", footer.map(normalizeTextCell).join(" "));
507
+ }
477
508
  return parts.join("");
478
509
  }
479
510
  function escapeCsvCell(value) {