Skip to content

Commit e68735c

Browse files
committed
refactor(core)!: 将 sheetjs 更换为 hucre
- BREAKING CHANGE: Exporter.export 不再接受 lang、ext、appName 和 appVersion 参数; - BREAKING CHANGE Exporter 删除了对导出 .numbers 的支持; - Exporter 添加了对导出 .md 的支持;
1 parent b88ee6c commit e68735c

7 files changed

Lines changed: 143 additions & 44 deletions

File tree

packages/components/src/table/loader.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useSearchParams } from '@solidjs/router';
88
import { createResource, createSignal, type JSX, mergeProps, Show, splitProps } from 'solid-js';
99
import IconExcel from '~icons/icon-park-twotone/excel';
1010
import IconCSV from '~icons/material-symbols/csv';
11+
import IconMarkdown from '~icons/material-symbols/markdown';
1112
import IconODS from '~icons/material-symbols/ods';
1213
import IconRefresh from '~icons/material-symbols/refresh';
1314
import IconReset from '~icons/material-symbols/restart-alt';
@@ -26,6 +27,8 @@ import * as BasicTable from './basic.mod';
2627
import { fromSearch, type Params, saveSearch } from './search';
2728
import styles from './style.module.css';
2829

30+
type ExportType = (typeof Exporter.exts)[number];
31+
2932
export interface Ref<T extends object> extends BasicTable.RootRef {
3033
/**
3134
* 表格当前页的数据
@@ -196,7 +199,7 @@ export function Root<T extends object, Q extends Query = Query>(props: Props<T,
196199
});
197200
}
198201

199-
const exports = async (ext: Parameters<Exporter<T, Q>['export']>[1]) => {
202+
const exports = async (ext: ExportType) => {
200203
const e = new Exporter<T, Q>(props.columns);
201204
const qq = await queries.object();
202205
if (!qq) {
@@ -210,7 +213,7 @@ export function Root<T extends object, Q extends Query = Query>(props: Props<T,
210213
await e.fetch(load, q);
211214
const filename = await Dialog.prompt(l.t('_c.table.downloadFilename'), props.filename);
212215
if (filename) {
213-
e.export(filename, ext, l.locale.language);
216+
await e.export(`${filename}${ext}`);
214217
}
215218
};
216219

@@ -280,6 +283,13 @@ export function Root<T extends object, Q extends Query = Query>(props: Props<T,
280283
value: '.ods',
281284
label: <Label.Root icon={<IconODS />}>{l.t('_c.table.exportTo', { type: 'ODS' })}</Label.Root>,
282285
},
286+
{
287+
type: 'item',
288+
value: '.md',
289+
label: (
290+
<Label.Root icon={<IconMarkdown />}>{l.t('_c.table.exportTo', { type: 'Markdown' })}</Label.Root>
291+
),
292+
},
283293
{ type: 'divider' },
284294
{
285295
type: 'item',

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@
4141
"dependencies": {
4242
"bowser": "catalog:",
4343
"cbor2": "catalog:",
44+
"hucre": "catalog:",
4445
"intl-messageformat": "catalog:",
45-
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
4646
"yaml": "catalog:",
4747
"zod": "catalog:"
4848
},
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-FileCopyrightText: 2026 caixw
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
/**
6+
* 为内容创建下载功能
7+
*
8+
* @param content - 要下载的内容;
9+
* @param filename - 下载时的文件名;
10+
* @param mimeType - 内容的 MIME 类型;
11+
*/
12+
export function createDownloadLink(content: string | Blob | ArrayBuffer, filename: string, mimeType?: string): void {
13+
let blob: Blob;
14+
15+
if (content instanceof Blob) {
16+
blob = content;
17+
} else if (content instanceof ArrayBuffer) {
18+
blob = new Blob([content], { type: mimeType || 'application/octet-stream' });
19+
} else {
20+
blob = new Blob([content], { type: mimeType || 'text/plain;charset=utf-8' });
21+
}
22+
23+
const url = URL.createObjectURL(blob);
24+
25+
const link = document.createElement('a');
26+
link.href = url;
27+
link.download = filename;
28+
29+
document.body.appendChild(link);
30+
link.click();
31+
document.body.removeChild(link);
32+
URL.revokeObjectURL(url);
33+
}

packages/core/src/api/export/export.ts

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
//
33
// SPDX-License-Identifier: MIT
44

5-
import xlsx from 'xlsx';
5+
import type { CellValue, ColumnDef, WriteOptions } from 'hucre';
6+
import { toMarkdown, writeCsv, writeOds, writeXlsx } from 'hucre';
67

78
import type { CellRenderFunc, CellType, Column } from './column';
89
import { presetCellRenderFunc } from './column';
10+
import { createDownloadLink } from './download';
911
import type { Page, Query } from './query';
1012

1113
/**
@@ -26,11 +28,25 @@ export class Exporter<T extends object, Q extends Query> {
2628
/**
2729
* 支持可导出的文件扩展名
2830
*/
29-
static exts = ['.csv', '.xlsx', '.numbers', '.ods'];
31+
static exts = ['.csv', '.xlsx', '.md', '.ods'] as const;
3032

31-
readonly #sheet: xlsx.WorkSheet;
33+
/**
34+
* 表格的列定义
35+
*
36+
* 这里的 content 主要是改为必选项
37+
*/
3238
readonly #columns: Array<Omit<Column<T>, 'content'> & { content: CellRenderFunc<T> }>;
3339

40+
/**
41+
* 表格的表头
42+
*/
43+
readonly #header: Array<string>;
44+
45+
/**
46+
* 表格的数据
47+
*/
48+
readonly #rows: Array<Array<CellValue>> = [];
49+
3450
/**
3551
* 构造函数
3652
*
@@ -44,30 +60,30 @@ export class Exporter<T extends object, Q extends Query> {
4460
};
4561
});
4662

47-
const row: Array<string> = [];
63+
const header: Array<string> = [];
4864
for (const c of cols) {
4965
if (!c.isUnexported) {
50-
row.push(c.label ?? c.id.toString());
66+
header.push(c.label ?? c.id.toString());
5167
}
5268
}
53-
this.#sheet = xlsx.utils.aoa_to_sheet([row]);
69+
this.#header = header;
5470
}
5571

5672
/**
5773
* 添加一至多行数据
5874
*/
5975
#append(...rows: Array<T>): void {
6076
for (const row of rows) {
61-
const data: Array<CellType> = [];
77+
const line: Array<CellType> = [];
6278
for (const c of this.#columns) {
6379
if (c.isUnexported) {
6480
continue;
6581
}
6682
const val = (row[c.id as keyof T] ?? undefined) as Parameters<CellRenderFunc<T>>[1];
67-
data.push(c.content(c.id, val, row));
83+
line.push(c.content(c.id, val, row));
6884
}
6985

70-
xlsx.utils.sheet_add_aoa(this.#sheet, [data], { origin: -1 });
86+
this.#rows.push(line.map(v => (v === undefined ? null : v)));
7187
}
7288
}
7389

@@ -86,35 +102,72 @@ export class Exporter<T extends object, Q extends Query> {
86102
}
87103
}
88104

105+
#buildHeader(): Array<ColumnDef> {
106+
return this.#header.map(h => ({ header: h }));
107+
}
108+
89109
/**
90110
* 导出数据
91111
*
92112
* 将 {@link Exporter#fetch} 下载的数据导出给用户。
93113
*
94-
* @param filename - 文件名,如果是 excel,也作为工作表的名称;
95-
* @param lang - 语言;
96-
* @param ext - 后缀名,根据此值生成不同类型的文件;
97-
* @param appName - 部分格式的元数据中会标注的应用名称;
98-
* @param appVersion - 部分格式的元数据中会标注的应用版本;
114+
* @param filename - 文件名,如果是 excel 和 ods,文件名部分也作为工作表的名称;
99115
*
100116
* NOTE: 这将通过浏览器创建一个自动下载的功能。
101117
*/
102-
export(
103-
filename: string,
104-
ext: (typeof Exporter.exts)[number],
105-
lang?: string,
106-
appName?: string,
107-
appVersion?: string,
108-
): void {
109-
const book = xlsx.utils.book_new(this.#sheet, filename);
110-
const d = new Date();
111-
book.Props = {
112-
ModifiedDate: d,
113-
CreatedDate: d,
114-
Language: lang,
115-
Application: appName,
116-
AppVersion: appVersion,
118+
async export(filename: `${string}${(typeof Exporter.exts)[number]}`): Promise<void> {
119+
const index = filename.lastIndexOf('.');
120+
const ext = filename.slice(index);
121+
const basename = filename.slice(0, index);
122+
123+
switch (ext) {
124+
case '.csv': {
125+
const content = writeCsv(this.#rows, { headers: this.#header });
126+
createDownloadLink(content, filename, 'text/csv');
127+
break;
128+
}
129+
case '.xlsx': {
130+
const content = await writeXlsx(this.buildWriteOptions(basename));
131+
createDownloadLink(
132+
content.buffer as ArrayBuffer,
133+
filename,
134+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
135+
);
136+
break;
137+
}
138+
case '.md': {
139+
const sheet = {
140+
name: basename,
141+
columns: this.#buildHeader(),
142+
rows: this.#rows,
143+
};
144+
const content = toMarkdown(sheet);
145+
createDownloadLink(content, filename, 'text/markdown');
146+
break;
147+
}
148+
case '.ods': {
149+
const content = await writeOds(this.buildWriteOptions(basename));
150+
createDownloadLink(content.buffer as ArrayBuffer, filename, 'application/vnd.oasis.opendocument.spreadsheet');
151+
break;
152+
}
153+
}
154+
}
155+
156+
buildWriteOptions(basename: string): WriteOptions {
157+
const now = new Date();
158+
return {
159+
sheets: [
160+
{
161+
name: basename,
162+
columns: this.#buildHeader(),
163+
rows: this.#rows,
164+
},
165+
],
166+
properties: {
167+
title: basename,
168+
created: now,
169+
modified: now,
170+
},
117171
};
118-
xlsx.writeFile(book, filename + ext);
119172
}
120173
}

packages/core/src/api/export/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
export type { CellType, Column } from './column';
66
export { isCellType, presetCellRenderFunc } from './column';
7+
export { createDownloadLink } from './download';
78
export { Exporter } from './export';
89
export type { Page, Query } from './query';
910
export { query2Search } from './query';

pnpm-lock.yaml

Lines changed: 12 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ catalog:
2727
colorjs.io: ^0.6.1
2828
echarts: ^6.0.0
2929
fast-deep-equal: ^3.1.3
30+
hucre: ^0.3.0
3031
intl-messageformat: ^11.2.0
3132
jsdom: ^29.0.2
3233
marked: ^18.0.0

0 commit comments

Comments
 (0)