@bsg-export/react 1.0.6 → 1.0.8
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/README.md +123 -0
- package/dist/ExportButton.js +4 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/use-exporter.d.ts +7 -66
- package/dist/use-exporter.d.ts.map +1 -1
- package/dist/use-exporter.js +23 -13
- package/dist/use-worker-exporter.d.ts +46 -0
- package/dist/use-worker-exporter.d.ts.map +1 -0
- package/dist/use-worker-exporter.js +225 -0
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @bsg-export/react
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@bsg-export/react)
|
|
4
|
+
|
|
5
|
+
> [belobog-stellar-grid](https://github.com/kurisu994/belobog-stellar-grid) 的 React 官方封装
|
|
6
|
+
|
|
7
|
+
## 简介
|
|
8
|
+
|
|
9
|
+
提供 `useExporter` Hook、`useWorkerExporter` Hook 和 `ExportButton` 组件,自动管理 WASM 初始化、导出状态和进度追踪。`useWorkerExporter` 支持将导出计算移至 Worker 线程。
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @bsg-export/react belobog-stellar-grid
|
|
15
|
+
# 或
|
|
16
|
+
pnpm add @bsg-export/react belobog-stellar-grid
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**前置依赖**:`react >= 17.0.0`、`belobog-stellar-grid >= 1.0.0`
|
|
20
|
+
|
|
21
|
+
## 快速开始
|
|
22
|
+
|
|
23
|
+
### useExporter Hook
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { useExporter, ExportFormat } from '@bsg-export/react';
|
|
27
|
+
|
|
28
|
+
function App() {
|
|
29
|
+
const { initialized, loading, progress, error, exportTable, exportData } = useExporter();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
disabled={!initialized || loading}
|
|
34
|
+
onClick={() =>
|
|
35
|
+
exportTable({
|
|
36
|
+
tableId: 'my-table',
|
|
37
|
+
filename: '报表.xlsx',
|
|
38
|
+
format: ExportFormat.Xlsx,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
>
|
|
42
|
+
{loading ? `导出中 ${Math.round(progress)}%` : '导出 Excel'}
|
|
43
|
+
</button>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### ExportButton 组件
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { ExportButton, ExportFormat } from '@bsg-export/react';
|
|
52
|
+
|
|
53
|
+
<ExportButton
|
|
54
|
+
tableId="my-table"
|
|
55
|
+
filename="报表.xlsx"
|
|
56
|
+
format={ExportFormat.Xlsx}
|
|
57
|
+
onExportSuccess={() => console.log('导出成功')}
|
|
58
|
+
onExportError={(err) => console.error('导出失败', err)}
|
|
59
|
+
>
|
|
60
|
+
导出 Excel
|
|
61
|
+
</ExportButton>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
### `useExporter()` 返回值
|
|
67
|
+
|
|
68
|
+
| 属性/方法 | 类型 | 说明 |
|
|
69
|
+
|-----------|------|------|
|
|
70
|
+
| `initialized` | `boolean` | WASM 是否初始化完成 |
|
|
71
|
+
| `loading` | `boolean` | 是否正在导出 |
|
|
72
|
+
| `progress` | `number` | 导出进度 (0-100) |
|
|
73
|
+
| `error` | `Error \| null` | 错误信息 |
|
|
74
|
+
| `exportTable` | `(options) => void` | DOM 表格导出 |
|
|
75
|
+
| `exportData` | `(data, options?) => void` | 纯数据导出 |
|
|
76
|
+
| `exportTablesXlsx` | `(options) => void` | 多 Sheet 导出 |
|
|
77
|
+
| `exportCsvBatch` | `(options) => Promise` | CSV 分批导出 |
|
|
78
|
+
| `exportXlsxBatch` | `(options) => Promise` | XLSX 分批导出 |
|
|
79
|
+
| `exportTablesBatch` | `(options) => Promise` | 多 Sheet 分批导出 |
|
|
80
|
+
|
|
81
|
+
### `useWorkerExporter(createWorker)` 返回值
|
|
82
|
+
|
|
83
|
+
将导出计算移至 Worker 线程,主线程不阻塞。需要传入 Worker 工厂函数。
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { useWorkerExporter } from '@bsg-export/react';
|
|
87
|
+
import ExportWorkerScript from '@bsg-export/worker/worker?worker';
|
|
88
|
+
|
|
89
|
+
const { initialized, loading, progress, exportData } = useWorkerExporter(
|
|
90
|
+
() => new ExportWorkerScript()
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| 属性/方法 | 类型 | 说明 |
|
|
95
|
+
|-----------|------|------|
|
|
96
|
+
| `initialized` | `boolean` | Worker 中 WASM 是否初始化完成 |
|
|
97
|
+
| `loading` | `boolean` | 是否正在导出 |
|
|
98
|
+
| `progress` | `number` | 导出进度 (0-100) |
|
|
99
|
+
| `error` | `Error \| null` | 错误信息 |
|
|
100
|
+
| `exportData` | `(data, opts?) => Promise<boolean>` | Worker 生成并下载 |
|
|
101
|
+
| `generateBytes` | `(data, opts?) => Promise<Uint8Array>` | 仅生成字节 |
|
|
102
|
+
| `terminate` | `() => void` | 销毁 Worker |
|
|
103
|
+
|
|
104
|
+
### `<ExportButton>` Props
|
|
105
|
+
|
|
106
|
+
继承所有 `<button>` HTML 属性,额外支持:
|
|
107
|
+
|
|
108
|
+
| Prop | 类型 | 默认值 | 说明 |
|
|
109
|
+
|------|------|--------|------|
|
|
110
|
+
| `tableId` | `string` | — | 要导出的表格 ID(必填) |
|
|
111
|
+
| `filename` | `string` | — | 导出文件名 |
|
|
112
|
+
| `format` | `ExportFormat` | `Csv` | 导出格式 |
|
|
113
|
+
| `excludeHidden` | `boolean` | `false` | 排除隐藏行/列 |
|
|
114
|
+
| `withBom` | `boolean` | `false` | 添加 UTF-8 BOM |
|
|
115
|
+
| `onExportSuccess` | `() => void` | — | 导出成功回调 |
|
|
116
|
+
| `onExportError` | `(error) => void` | — | 导出失败回调 |
|
|
117
|
+
| `onExportProgress` | `(progress) => void` | — | 进度变化回调 |
|
|
118
|
+
| `initializingText` | `string` | `'初始化中...'` | 初始化中按钮文本 |
|
|
119
|
+
| `loadingText` | `string` | `'导出中 {progress}%'` | 导出中按钮文本 |
|
|
120
|
+
|
|
121
|
+
## 许可证
|
|
122
|
+
|
|
123
|
+
MIT OR Apache-2.0
|
package/dist/ExportButton.js
CHANGED
|
@@ -34,18 +34,18 @@ export function ExportButton({ tableId, filename, format, excludeHidden, withBom
|
|
|
34
34
|
buttonProps.onClick?.(e);
|
|
35
35
|
if (e.defaultPrevented)
|
|
36
36
|
return;
|
|
37
|
-
exportTable({
|
|
37
|
+
const success = exportTable({
|
|
38
38
|
tableId,
|
|
39
39
|
filename,
|
|
40
40
|
format,
|
|
41
41
|
excludeHidden,
|
|
42
42
|
withBom,
|
|
43
43
|
});
|
|
44
|
-
//
|
|
45
|
-
if (
|
|
44
|
+
// 导出成功回调(同步导出完成后检查返回值)
|
|
45
|
+
if (success && onExportSuccess) {
|
|
46
46
|
onExportSuccess();
|
|
47
47
|
}
|
|
48
|
-
}, [tableId, filename, format, excludeHidden, withBom, exportTable, onExportSuccess,
|
|
48
|
+
}, [tableId, filename, format, excludeHidden, withBom, exportTable, onExportSuccess, buttonProps]);
|
|
49
49
|
/** 渲染按钮文本 */
|
|
50
50
|
const renderText = () => {
|
|
51
51
|
if (!initialized)
|
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* @packageDocumentation
|
|
7
7
|
*/
|
|
8
8
|
export { useExporter } from './use-exporter';
|
|
9
|
-
export type { UseExporterReturn,
|
|
9
|
+
export type { UseExporterReturn, } from './use-exporter';
|
|
10
|
+
export { useWorkerExporter } from './use-worker-exporter';
|
|
11
|
+
export type { UseWorkerExporterReturn, } from './use-worker-exporter';
|
|
10
12
|
export { ExportButton } from './ExportButton';
|
|
11
13
|
export type { ExportButtonProps } from './ExportButton';
|
|
12
|
-
export type { Column, MergeCellValue, CellValue, MergeableCellValue, DataRow, ExportDataOptions, SheetConfig, BatchSheetConfig, ProgressCallback, } from '@bsg-export/types';
|
|
14
|
+
export type { Column, MergeCellValue, CellValue, MergeableCellValue, DataRow, ExportDataOptions, SheetConfig, BatchSheetConfig, ProgressCallback, ExportTableOptions, ExportTablesXlsxOptions, ExportCsvBatchOptions, ExportXlsxBatchOptions, ExportTablesBatchOptions, } from '@bsg-export/types';
|
|
13
15
|
export { ExportFormat } from '@bsg-export/types';
|
|
14
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,YAAY,EACV,iBAAiB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,YAAY,EACV,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,YAAY,EACV,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAGxD,YAAY,EACV,MAAM,EACN,cAAc,EACd,SAAS,EACT,kBAAkB,EAClB,OAAO,EACP,iBAAiB,EACjB,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/use-exporter.d.ts
CHANGED
|
@@ -15,66 +15,7 @@
|
|
|
15
15
|
* );
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
-
import type {
|
|
19
|
-
/** export_table 的参数配置 */
|
|
20
|
-
export interface ExportTableOptions {
|
|
21
|
-
/** 要导出的 HTML 表格元素的 ID */
|
|
22
|
-
tableId: string;
|
|
23
|
-
/** 导出文件名 */
|
|
24
|
-
filename?: string;
|
|
25
|
-
/** 导出格式 */
|
|
26
|
-
format?: ExportFormat;
|
|
27
|
-
/** 是否排除隐藏行/列 */
|
|
28
|
-
excludeHidden?: boolean;
|
|
29
|
-
/** 是否添加 UTF-8 BOM(仅 CSV 有效) */
|
|
30
|
-
withBom?: boolean;
|
|
31
|
-
/** 回调失败是否中断导出 */
|
|
32
|
-
strictProgressCallback?: boolean;
|
|
33
|
-
}
|
|
34
|
-
/** 多工作表导出的参数配置 */
|
|
35
|
-
export interface ExportTablesXlsxOptions {
|
|
36
|
-
/** Sheet 配置数组 */
|
|
37
|
-
sheets: SheetConfig[];
|
|
38
|
-
/** 导出文件名 */
|
|
39
|
-
filename?: string;
|
|
40
|
-
}
|
|
41
|
-
/** 分批导出 CSV 的参数配置 */
|
|
42
|
-
export interface ExportCsvBatchOptions {
|
|
43
|
-
/** 要导出的 HTML 表格元素的 ID */
|
|
44
|
-
tableId: string;
|
|
45
|
-
/** 可选的独立 tbody ID */
|
|
46
|
-
tbodyId?: string;
|
|
47
|
-
/** 导出文件名 */
|
|
48
|
-
filename?: string;
|
|
49
|
-
/** 每批处理行数 */
|
|
50
|
-
batchSize?: number;
|
|
51
|
-
/** 是否排除隐藏行/列 */
|
|
52
|
-
excludeHidden?: boolean;
|
|
53
|
-
/** 是否添加 UTF-8 BOM */
|
|
54
|
-
withBom?: boolean;
|
|
55
|
-
}
|
|
56
|
-
/** 分批导出 XLSX 的参数配置 */
|
|
57
|
-
export interface ExportXlsxBatchOptions {
|
|
58
|
-
/** 要导出的 HTML 表格元素的 ID */
|
|
59
|
-
tableId: string;
|
|
60
|
-
/** 可选的独立 tbody ID */
|
|
61
|
-
tbodyId?: string;
|
|
62
|
-
/** 导出文件名 */
|
|
63
|
-
filename?: string;
|
|
64
|
-
/** 每批处理行数 */
|
|
65
|
-
batchSize?: number;
|
|
66
|
-
/** 是否排除隐藏行/列 */
|
|
67
|
-
excludeHidden?: boolean;
|
|
68
|
-
}
|
|
69
|
-
/** 多工作表分批导出的参数配置 */
|
|
70
|
-
export interface ExportTablesBatchOptions {
|
|
71
|
-
/** Sheet 配置数组 */
|
|
72
|
-
sheets: BatchSheetConfig[];
|
|
73
|
-
/** 导出文件名 */
|
|
74
|
-
filename?: string;
|
|
75
|
-
/** 每批处理行数 */
|
|
76
|
-
batchSize?: number;
|
|
77
|
-
}
|
|
18
|
+
import type { ExportDataOptions, ExportTableOptions, ExportTablesXlsxOptions, ExportCsvBatchOptions, ExportXlsxBatchOptions, ExportTablesBatchOptions, DataRow } from '@bsg-export/types';
|
|
78
19
|
/** useExporter Hook 的返回值 */
|
|
79
20
|
export interface UseExporterReturn {
|
|
80
21
|
/** WASM 是否已初始化完成 */
|
|
@@ -86,17 +27,17 @@ export interface UseExporterReturn {
|
|
|
86
27
|
/** 错误信息 */
|
|
87
28
|
error: Error | null;
|
|
88
29
|
/** 导出 HTML 表格 */
|
|
89
|
-
exportTable: (options: ExportTableOptions) =>
|
|
30
|
+
exportTable: (options: ExportTableOptions) => boolean;
|
|
90
31
|
/** 从 JS 数组直接导出 */
|
|
91
|
-
exportData: (data: DataRow[], options?: ExportDataOptions) =>
|
|
32
|
+
exportData: (data: DataRow[], options?: ExportDataOptions) => boolean;
|
|
92
33
|
/** 多工作表同步导出 */
|
|
93
|
-
exportTablesXlsx: (options: ExportTablesXlsxOptions) =>
|
|
34
|
+
exportTablesXlsx: (options: ExportTablesXlsxOptions) => boolean;
|
|
94
35
|
/** 分批异步导出 CSV */
|
|
95
|
-
exportCsvBatch: (options: ExportCsvBatchOptions) => Promise<
|
|
36
|
+
exportCsvBatch: (options: ExportCsvBatchOptions) => Promise<boolean>;
|
|
96
37
|
/** 分批异步导出 XLSX */
|
|
97
|
-
exportXlsxBatch: (options: ExportXlsxBatchOptions) => Promise<
|
|
38
|
+
exportXlsxBatch: (options: ExportXlsxBatchOptions) => Promise<boolean>;
|
|
98
39
|
/** 多工作表分批异步导出 */
|
|
99
|
-
exportTablesBatch: (options: ExportTablesBatchOptions) => Promise<
|
|
40
|
+
exportTablesBatch: (options: ExportTablesBatchOptions) => Promise<boolean>;
|
|
100
41
|
}
|
|
101
42
|
/**
|
|
102
43
|
* WASM 导出管理 Hook
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-exporter.d.ts","sourceRoot":"","sources":["../src/use-exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"use-exporter.d.ts","sourceRoot":"","sources":["../src/use-exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,EAExB,OAAO,EACR,MAAM,mBAAmB,CAAC;AAI3B,4BAA4B;AAC5B,MAAM,WAAW,iBAAiB;IAChC,oBAAoB;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,mBAAmB;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW;IACX,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,iBAAiB;IACjB,WAAW,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACtD,kBAAkB;IAClB,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC;IACtE,eAAe;IACf,gBAAgB,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC;IAChE,iBAAiB;IACjB,cAAc,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE,kBAAkB;IAClB,eAAe,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvE,iBAAiB;IACjB,iBAAiB,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5E;AA6BD;;;;;GAKG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAgL/C"}
|
package/dist/use-exporter.js
CHANGED
|
@@ -26,10 +26,16 @@ async function initWasm() {
|
|
|
26
26
|
if (wasmModule)
|
|
27
27
|
return wasmModule;
|
|
28
28
|
if (!wasmInitPromise) {
|
|
29
|
-
wasmInitPromise = import('belobog-stellar-grid')
|
|
29
|
+
wasmInitPromise = import('belobog-stellar-grid')
|
|
30
|
+
.then(async (mod) => {
|
|
30
31
|
await mod.default();
|
|
31
32
|
wasmModule = mod;
|
|
32
33
|
return mod;
|
|
34
|
+
})
|
|
35
|
+
.catch((err) => {
|
|
36
|
+
// 初始化失败时重置 Promise,允许后续调用重试
|
|
37
|
+
wasmInitPromise = null;
|
|
38
|
+
throw err;
|
|
33
39
|
});
|
|
34
40
|
}
|
|
35
41
|
return wasmInitPromise;
|
|
@@ -71,7 +77,7 @@ export function useExporter() {
|
|
|
71
77
|
/** 包装同步导出操作 */
|
|
72
78
|
const wrapSync = useCallback((fn) => {
|
|
73
79
|
if (!initialized || !wasmModule)
|
|
74
|
-
return;
|
|
80
|
+
return false;
|
|
75
81
|
setLoading(true);
|
|
76
82
|
setProgress(0);
|
|
77
83
|
setError(null);
|
|
@@ -79,10 +85,12 @@ export function useExporter() {
|
|
|
79
85
|
fn();
|
|
80
86
|
if (mountedRef.current)
|
|
81
87
|
setProgress(100);
|
|
88
|
+
return true;
|
|
82
89
|
}
|
|
83
90
|
catch (err) {
|
|
84
91
|
if (mountedRef.current)
|
|
85
92
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
93
|
+
return false;
|
|
86
94
|
}
|
|
87
95
|
finally {
|
|
88
96
|
if (mountedRef.current)
|
|
@@ -92,7 +100,7 @@ export function useExporter() {
|
|
|
92
100
|
/** 包装异步导出操作 */
|
|
93
101
|
const wrapAsync = useCallback(async (fn) => {
|
|
94
102
|
if (!initialized || !wasmModule)
|
|
95
|
-
return;
|
|
103
|
+
return false;
|
|
96
104
|
setLoading(true);
|
|
97
105
|
setProgress(0);
|
|
98
106
|
setError(null);
|
|
@@ -100,10 +108,12 @@ export function useExporter() {
|
|
|
100
108
|
await fn();
|
|
101
109
|
if (mountedRef.current)
|
|
102
110
|
setProgress(100);
|
|
111
|
+
return true;
|
|
103
112
|
}
|
|
104
113
|
catch (err) {
|
|
105
114
|
if (mountedRef.current)
|
|
106
115
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
116
|
+
return false;
|
|
107
117
|
}
|
|
108
118
|
finally {
|
|
109
119
|
if (mountedRef.current)
|
|
@@ -111,12 +121,12 @@ export function useExporter() {
|
|
|
111
121
|
}
|
|
112
122
|
}, [initialized]);
|
|
113
123
|
const exportTable = useCallback((options) => {
|
|
114
|
-
wrapSync(() => {
|
|
124
|
+
return wrapSync(() => {
|
|
115
125
|
wasmModule.export_table(options.tableId, options.filename, options.format, options.excludeHidden, createProgressCallback(), options.withBom, options.strictProgressCallback);
|
|
116
126
|
});
|
|
117
127
|
}, [wrapSync, createProgressCallback]);
|
|
118
128
|
const exportData = useCallback((data, options) => {
|
|
119
|
-
wrapSync(() => {
|
|
129
|
+
return wrapSync(() => {
|
|
120
130
|
const opts = options
|
|
121
131
|
? { ...options, progressCallback: options.progressCallback ?? createProgressCallback() }
|
|
122
132
|
: { progressCallback: createProgressCallback() };
|
|
@@ -124,23 +134,23 @@ export function useExporter() {
|
|
|
124
134
|
});
|
|
125
135
|
}, [wrapSync, createProgressCallback]);
|
|
126
136
|
const exportTablesXlsx = useCallback((options) => {
|
|
127
|
-
wrapSync(() => {
|
|
128
|
-
wasmModule.export_tables_xlsx(options.sheets, options.filename, createProgressCallback());
|
|
137
|
+
return wrapSync(() => {
|
|
138
|
+
wasmModule.export_tables_xlsx(options.sheets, options.filename, createProgressCallback(), options.strictProgressCallback);
|
|
129
139
|
});
|
|
130
140
|
}, [wrapSync, createProgressCallback]);
|
|
131
141
|
const exportCsvBatch = useCallback(async (options) => {
|
|
132
|
-
await wrapAsync(async () => {
|
|
133
|
-
await wasmModule.export_table_to_csv_batch(options.tableId, options.tbodyId, options.filename, options.batchSize, options.excludeHidden, createProgressCallback(), options.withBom);
|
|
142
|
+
return await wrapAsync(async () => {
|
|
143
|
+
await wasmModule.export_table_to_csv_batch(options.tableId, options.tbodyId, options.filename, options.batchSize, options.excludeHidden, createProgressCallback(), options.withBom, options.strictProgressCallback);
|
|
134
144
|
});
|
|
135
145
|
}, [wrapAsync, createProgressCallback]);
|
|
136
146
|
const exportXlsxBatch = useCallback(async (options) => {
|
|
137
|
-
await wrapAsync(async () => {
|
|
138
|
-
await wasmModule.export_table_to_xlsx_batch(options.tableId, options.tbodyId, options.filename, options.batchSize, options.excludeHidden, createProgressCallback());
|
|
147
|
+
return await wrapAsync(async () => {
|
|
148
|
+
await wasmModule.export_table_to_xlsx_batch(options.tableId, options.tbodyId, options.filename, options.batchSize, options.excludeHidden, createProgressCallback(), options.strictProgressCallback);
|
|
139
149
|
});
|
|
140
150
|
}, [wrapAsync, createProgressCallback]);
|
|
141
151
|
const exportTablesBatch = useCallback(async (options) => {
|
|
142
|
-
await wrapAsync(async () => {
|
|
143
|
-
await wasmModule.export_tables_to_xlsx_batch(options.sheets, options.filename, options.batchSize, createProgressCallback());
|
|
152
|
+
return await wrapAsync(async () => {
|
|
153
|
+
await wasmModule.export_tables_to_xlsx_batch(options.sheets, options.filename, options.batchSize, createProgressCallback(), options.strictProgressCallback);
|
|
144
154
|
});
|
|
145
155
|
}, [wrapAsync, createProgressCallback]);
|
|
146
156
|
return {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useWorkerExporter - Worker 线程导出管理 Hook
|
|
3
|
+
*
|
|
4
|
+
* 将 CSV/XLSX 生成移至 Worker 线程,主线程不阻塞。
|
|
5
|
+
* 用户需要传入一个已创建的 Worker 实例。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // Vite
|
|
10
|
+
* import ExportWorkerScript from '@bsg-export/worker/worker?worker';
|
|
11
|
+
* const { initialized, loading, progress, exportData } = useWorkerExporter(() => new ExportWorkerScript());
|
|
12
|
+
*
|
|
13
|
+
* // Webpack 5
|
|
14
|
+
* const { exportData } = useWorkerExporter(
|
|
15
|
+
* () => new Worker(new URL('@bsg-export/worker/worker', import.meta.url), { type: 'module' })
|
|
16
|
+
* );
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import type { ExportDataOptions, DataRow } from '@bsg-export/types';
|
|
20
|
+
/** useWorkerExporter Hook 的返回值 */
|
|
21
|
+
export interface UseWorkerExporterReturn {
|
|
22
|
+
/** Worker 中的 WASM 是否已初始化完成 */
|
|
23
|
+
initialized: boolean;
|
|
24
|
+
/** 是否正在导出 */
|
|
25
|
+
loading: boolean;
|
|
26
|
+
/** 导出进度 (0-100) */
|
|
27
|
+
progress: number;
|
|
28
|
+
/** 错误信息 */
|
|
29
|
+
error: Error | null;
|
|
30
|
+
/** 在 Worker 中生成文件并触发下载 */
|
|
31
|
+
exportData: (data: DataRow[], options?: Omit<ExportDataOptions, 'progressCallback'>) => Promise<boolean>;
|
|
32
|
+
/** 在 Worker 中生成文件字节(不触发下载) */
|
|
33
|
+
generateBytes: (data: DataRow[], options?: Omit<ExportDataOptions, 'progressCallback'>) => Promise<Uint8Array | null>;
|
|
34
|
+
/** 销毁 Worker */
|
|
35
|
+
terminate: () => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Worker 线程导出管理 Hook
|
|
39
|
+
*
|
|
40
|
+
* 接收一个 Worker 工厂函数,自动管理 Worker 生命周期和 WASM 初始化。
|
|
41
|
+
* 导出计算在 Worker 线程执行,主线程保持响应。
|
|
42
|
+
*
|
|
43
|
+
* @param createWorker - 创建 Worker 实例的工厂函数
|
|
44
|
+
*/
|
|
45
|
+
export declare function useWorkerExporter(createWorker: () => Worker): UseWorkerExporterReturn;
|
|
46
|
+
//# sourceMappingURL=use-worker-exporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-worker-exporter.d.ts","sourceRoot":"","sources":["../src/use-worker-exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EACV,iBAAiB,EAEjB,OAAO,EACR,MAAM,mBAAmB,CAAC;AAE3B,kCAAkC;AAClC,MAAM,WAAW,uBAAuB;IACtC,8BAA8B;IAC9B,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,mBAAmB;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW;IACX,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,0BAA0B;IAC1B,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACzG,8BAA8B;IAC9B,aAAa,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACtH,gBAAgB;IAChB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAiCD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,MAAM,GAAG,uBAAuB,CA6MrF"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useWorkerExporter - Worker 线程导出管理 Hook
|
|
3
|
+
*
|
|
4
|
+
* 将 CSV/XLSX 生成移至 Worker 线程,主线程不阻塞。
|
|
5
|
+
* 用户需要传入一个已创建的 Worker 实例。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // Vite
|
|
10
|
+
* import ExportWorkerScript from '@bsg-export/worker/worker?worker';
|
|
11
|
+
* const { initialized, loading, progress, exportData } = useWorkerExporter(() => new ExportWorkerScript());
|
|
12
|
+
*
|
|
13
|
+
* // Webpack 5
|
|
14
|
+
* const { exportData } = useWorkerExporter(
|
|
15
|
+
* () => new Worker(new URL('@bsg-export/worker/worker', import.meta.url), { type: 'module' })
|
|
16
|
+
* );
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
20
|
+
let requestCounter = 0;
|
|
21
|
+
function generateId() {
|
|
22
|
+
return `req_${++requestCounter}_${Date.now()}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Worker 线程导出管理 Hook
|
|
26
|
+
*
|
|
27
|
+
* 接收一个 Worker 工厂函数,自动管理 Worker 生命周期和 WASM 初始化。
|
|
28
|
+
* 导出计算在 Worker 线程执行,主线程保持响应。
|
|
29
|
+
*
|
|
30
|
+
* @param createWorker - 创建 Worker 实例的工厂函数
|
|
31
|
+
*/
|
|
32
|
+
export function useWorkerExporter(createWorker) {
|
|
33
|
+
const [initialized, setInitialized] = useState(false);
|
|
34
|
+
const [loading, setLoading] = useState(false);
|
|
35
|
+
const [progress, setProgress] = useState(0);
|
|
36
|
+
const [error, setError] = useState(null);
|
|
37
|
+
const workerRef = useRef(null);
|
|
38
|
+
const pendingRef = useRef(new Map());
|
|
39
|
+
const mountedRef = useRef(true);
|
|
40
|
+
// 处理 Worker 消息
|
|
41
|
+
const handleMessage = useCallback((event) => {
|
|
42
|
+
if (!mountedRef.current)
|
|
43
|
+
return;
|
|
44
|
+
const { type, id, bytes, message, progress: prog } = event.data;
|
|
45
|
+
const pending = pendingRef.current.get(id);
|
|
46
|
+
if (!pending)
|
|
47
|
+
return;
|
|
48
|
+
switch (type) {
|
|
49
|
+
case 'ready':
|
|
50
|
+
pendingRef.current.delete(id);
|
|
51
|
+
pending.resolve(new ArrayBuffer(0));
|
|
52
|
+
break;
|
|
53
|
+
case 'result':
|
|
54
|
+
pendingRef.current.delete(id);
|
|
55
|
+
pending.resolve(bytes);
|
|
56
|
+
break;
|
|
57
|
+
case 'error':
|
|
58
|
+
pendingRef.current.delete(id);
|
|
59
|
+
pending.reject(new Error(message ?? '未知错误'));
|
|
60
|
+
break;
|
|
61
|
+
case 'progress':
|
|
62
|
+
if (pending.onProgress && prog !== undefined) {
|
|
63
|
+
pending.onProgress(prog);
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}, []);
|
|
68
|
+
// 处理 Worker 错误
|
|
69
|
+
const handleError = useCallback((event) => {
|
|
70
|
+
if (!mountedRef.current)
|
|
71
|
+
return;
|
|
72
|
+
const err = new Error(`Worker 错误: ${event.message}`);
|
|
73
|
+
for (const [id, pending] of pendingRef.current) {
|
|
74
|
+
pending.reject(err);
|
|
75
|
+
pendingRef.current.delete(id);
|
|
76
|
+
}
|
|
77
|
+
setError(err);
|
|
78
|
+
}, []);
|
|
79
|
+
// 初始化 Worker 和 WASM
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
mountedRef.current = true;
|
|
82
|
+
const worker = createWorker();
|
|
83
|
+
workerRef.current = worker;
|
|
84
|
+
worker.addEventListener('message', handleMessage);
|
|
85
|
+
worker.addEventListener('error', handleError);
|
|
86
|
+
// 发送 init 消息
|
|
87
|
+
const id = generateId();
|
|
88
|
+
const initPromise = new Promise((resolve, reject) => {
|
|
89
|
+
pendingRef.current.set(id, { resolve, reject });
|
|
90
|
+
});
|
|
91
|
+
const request = { type: 'init', id };
|
|
92
|
+
worker.postMessage(request);
|
|
93
|
+
initPromise
|
|
94
|
+
.then(() => {
|
|
95
|
+
if (mountedRef.current)
|
|
96
|
+
setInitialized(true);
|
|
97
|
+
})
|
|
98
|
+
.catch((err) => {
|
|
99
|
+
if (mountedRef.current)
|
|
100
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
101
|
+
});
|
|
102
|
+
return () => {
|
|
103
|
+
mountedRef.current = false;
|
|
104
|
+
// 拒绝所有待处理请求
|
|
105
|
+
for (const [, pending] of pendingRef.current) {
|
|
106
|
+
pending.reject(new Error('组件已卸载'));
|
|
107
|
+
}
|
|
108
|
+
pendingRef.current.clear();
|
|
109
|
+
worker.terminate();
|
|
110
|
+
workerRef.current = null;
|
|
111
|
+
};
|
|
112
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
113
|
+
/** 向 Worker 发送生成请求 */
|
|
114
|
+
const sendGenerate = useCallback((data, options) => {
|
|
115
|
+
const worker = workerRef.current;
|
|
116
|
+
if (!worker)
|
|
117
|
+
return Promise.reject(new Error('Worker 未创建'));
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const id = generateId();
|
|
120
|
+
pendingRef.current.set(id, {
|
|
121
|
+
resolve,
|
|
122
|
+
reject,
|
|
123
|
+
onProgress: (p) => {
|
|
124
|
+
if (mountedRef.current)
|
|
125
|
+
setProgress(p);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const request = {
|
|
129
|
+
type: 'generate',
|
|
130
|
+
id,
|
|
131
|
+
data,
|
|
132
|
+
options: (options ?? {}),
|
|
133
|
+
};
|
|
134
|
+
worker.postMessage(request);
|
|
135
|
+
});
|
|
136
|
+
}, []);
|
|
137
|
+
/** 在主线程触发文件下载 */
|
|
138
|
+
const downloadFile = useCallback((bytes, filename, format) => {
|
|
139
|
+
const isXlsx = format === 1;
|
|
140
|
+
const mimeType = isXlsx
|
|
141
|
+
? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
142
|
+
: 'text/csv;charset=utf-8';
|
|
143
|
+
const defaultExt = isXlsx ? 'xlsx' : 'csv';
|
|
144
|
+
let finalFilename = filename ?? `export.${defaultExt}`;
|
|
145
|
+
if (!finalFilename.endsWith(`.${defaultExt}`)) {
|
|
146
|
+
finalFilename = `${finalFilename}.${defaultExt}`;
|
|
147
|
+
}
|
|
148
|
+
const blob = new Blob([bytes.buffer], { type: mimeType });
|
|
149
|
+
const url = URL.createObjectURL(blob);
|
|
150
|
+
const anchor = document.createElement('a');
|
|
151
|
+
anchor.href = url;
|
|
152
|
+
anchor.download = finalFilename;
|
|
153
|
+
anchor.click();
|
|
154
|
+
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
|
155
|
+
}, []);
|
|
156
|
+
/** 在 Worker 中生成文件并触发下载 */
|
|
157
|
+
const exportData = useCallback(async (data, options) => {
|
|
158
|
+
if (!initialized)
|
|
159
|
+
return false;
|
|
160
|
+
setLoading(true);
|
|
161
|
+
setProgress(0);
|
|
162
|
+
setError(null);
|
|
163
|
+
try {
|
|
164
|
+
const buffer = await sendGenerate(data, options);
|
|
165
|
+
const bytes = new Uint8Array(buffer);
|
|
166
|
+
downloadFile(bytes, options?.filename, options?.format);
|
|
167
|
+
if (mountedRef.current)
|
|
168
|
+
setProgress(100);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
if (mountedRef.current)
|
|
173
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
if (mountedRef.current)
|
|
178
|
+
setLoading(false);
|
|
179
|
+
}
|
|
180
|
+
}, [initialized, sendGenerate, downloadFile]);
|
|
181
|
+
/** 在 Worker 中生成文件字节(不触发下载) */
|
|
182
|
+
const generateBytes = useCallback(async (data, options) => {
|
|
183
|
+
if (!initialized)
|
|
184
|
+
return null;
|
|
185
|
+
setLoading(true);
|
|
186
|
+
setProgress(0);
|
|
187
|
+
setError(null);
|
|
188
|
+
try {
|
|
189
|
+
const buffer = await sendGenerate(data, options);
|
|
190
|
+
if (mountedRef.current)
|
|
191
|
+
setProgress(100);
|
|
192
|
+
return new Uint8Array(buffer);
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (mountedRef.current)
|
|
196
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
if (mountedRef.current)
|
|
201
|
+
setLoading(false);
|
|
202
|
+
}
|
|
203
|
+
}, [initialized, sendGenerate]);
|
|
204
|
+
/** 手动销毁 Worker */
|
|
205
|
+
const terminate = useCallback(() => {
|
|
206
|
+
for (const [, pending] of pendingRef.current) {
|
|
207
|
+
pending.reject(new Error('Worker 已被手动销毁'));
|
|
208
|
+
}
|
|
209
|
+
pendingRef.current.clear();
|
|
210
|
+
workerRef.current?.terminate();
|
|
211
|
+
workerRef.current = null;
|
|
212
|
+
if (mountedRef.current) {
|
|
213
|
+
setInitialized(false);
|
|
214
|
+
}
|
|
215
|
+
}, []);
|
|
216
|
+
return {
|
|
217
|
+
initialized,
|
|
218
|
+
loading,
|
|
219
|
+
progress,
|
|
220
|
+
error,
|
|
221
|
+
exportData,
|
|
222
|
+
generateBytes,
|
|
223
|
+
terminate,
|
|
224
|
+
};
|
|
225
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsg-export/react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "belobog-stellar-grid 的 React 官方封装组件",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"@bsg-export/types": "file:../types"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/react": "^19.
|
|
44
|
-
"react": "^19.
|
|
45
|
-
"typescript": "^5.
|
|
43
|
+
"@types/react": "^19.2.14",
|
|
44
|
+
"react": "^19.2.4",
|
|
45
|
+
"typescript": "^5.9.3"
|
|
46
46
|
}
|
|
47
47
|
}
|