@bsg-export/svelte 1.0.10
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/dist/ExportButton.svelte +93 -0
- package/dist/ExportButton.svelte.d.ts +31 -0
- package/dist/create-exporter.d.ts +57 -0
- package/dist/create-exporter.d.ts.map +1 -0
- package/dist/create-exporter.js +179 -0
- package/dist/create-worker-exporter.d.ts +58 -0
- package/dist/create-worker-exporter.d.ts.map +1 -0
- package/dist/create-worker-exporter.js +220 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/package.json +53 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
ExportButton - 开箱即用的导出按钮组件 (Svelte 4/5)
|
|
3
|
+
|
|
4
|
+
自动管理 WASM 初始化、导出状态和进度显示。
|
|
5
|
+
|
|
6
|
+
@example
|
|
7
|
+
```svelte
|
|
8
|
+
<script>
|
|
9
|
+
import ExportButton from '@bsg-export/svelte/ExportButton.svelte';
|
|
10
|
+
import { ExportFormat } from '@bsg-export/svelte';
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<ExportButton tableId="my-table" filename="报表.xlsx" format={ExportFormat.Xlsx}>
|
|
14
|
+
导出 Excel
|
|
15
|
+
</ExportButton>
|
|
16
|
+
```
|
|
17
|
+
-->
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
import { onDestroy } from 'svelte';
|
|
20
|
+
import type { ExportFormat } from '@bsg-export/types';
|
|
21
|
+
import { createExporter } from './create-exporter';
|
|
22
|
+
|
|
23
|
+
/** 要导出的 HTML 表格元素的 ID */
|
|
24
|
+
export let tableId: string;
|
|
25
|
+
/** 导出文件名 */
|
|
26
|
+
export let filename: string | undefined = undefined;
|
|
27
|
+
/** 导出格式 */
|
|
28
|
+
export let format: ExportFormat | undefined = undefined;
|
|
29
|
+
/** 是否排除隐藏行/列 */
|
|
30
|
+
export let excludeHidden: boolean | undefined = undefined;
|
|
31
|
+
/** 是否添加 UTF-8 BOM(仅 CSV 有效) */
|
|
32
|
+
export let withBom: boolean | undefined = undefined;
|
|
33
|
+
/** 是否禁用按钮 */
|
|
34
|
+
export let disabled: boolean = false;
|
|
35
|
+
/** 初始化中的提示文本 */
|
|
36
|
+
export let initializingText: string = '初始化中...';
|
|
37
|
+
/** 导出中的提示文本(支持 {progress} 占位符) */
|
|
38
|
+
export let loadingText: string = '导出中 {progress}%';
|
|
39
|
+
/** 导出成功回调 */
|
|
40
|
+
export let onExportSuccess: (() => void) | undefined = undefined;
|
|
41
|
+
/** 导出失败回调 */
|
|
42
|
+
export let onExportError: ((error: Error) => void) | undefined = undefined;
|
|
43
|
+
/** 进度变化回调 */
|
|
44
|
+
export let onExportProgress: ((progress: number) => void) | undefined = undefined;
|
|
45
|
+
|
|
46
|
+
const store = createExporter();
|
|
47
|
+
const { initialized, loading, progress, error, exportTable } = store;
|
|
48
|
+
|
|
49
|
+
onDestroy(() => {
|
|
50
|
+
store.destroy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 监听错误并触发回调
|
|
54
|
+
$: if ($error && onExportError) {
|
|
55
|
+
onExportError($error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 监听进度变化并触发回调
|
|
59
|
+
$: if (onExportProgress) {
|
|
60
|
+
onExportProgress($progress);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
$: isDisabled = disabled || !$initialized || $loading;
|
|
64
|
+
|
|
65
|
+
$: buttonText = !$initialized
|
|
66
|
+
? initializingText
|
|
67
|
+
: $loading
|
|
68
|
+
? loadingText.replace('{progress}', Math.round($progress).toString())
|
|
69
|
+
: undefined;
|
|
70
|
+
|
|
71
|
+
/** 处理点击 */
|
|
72
|
+
function handleClick() {
|
|
73
|
+
const success = exportTable({
|
|
74
|
+
tableId,
|
|
75
|
+
filename,
|
|
76
|
+
format,
|
|
77
|
+
excludeHidden,
|
|
78
|
+
withBom,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (success && onExportSuccess) {
|
|
82
|
+
onExportSuccess();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<button disabled={isDisabled} on:click={handleClick} {...$$restProps}>
|
|
88
|
+
{#if buttonText}
|
|
89
|
+
{buttonText}
|
|
90
|
+
{:else}
|
|
91
|
+
<slot>导出</slot>
|
|
92
|
+
{/if}
|
|
93
|
+
</button>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SvelteComponent } from 'svelte';
|
|
2
|
+
import type { ExportFormat } from '@bsg-export/types';
|
|
3
|
+
|
|
4
|
+
/** ExportButton 组件的 Props */
|
|
5
|
+
export interface ExportButtonProps {
|
|
6
|
+
/** 要导出的 HTML 表格元素的 ID */
|
|
7
|
+
tableId: string;
|
|
8
|
+
/** 导出文件名 */
|
|
9
|
+
filename?: string;
|
|
10
|
+
/** 导出格式 */
|
|
11
|
+
format?: ExportFormat;
|
|
12
|
+
/** 是否排除隐藏行/列 */
|
|
13
|
+
excludeHidden?: boolean;
|
|
14
|
+
/** 是否添加 UTF-8 BOM(仅 CSV 有效) */
|
|
15
|
+
withBom?: boolean;
|
|
16
|
+
/** 是否禁用按钮 */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/** 初始化中的提示文本 */
|
|
19
|
+
initializingText?: string;
|
|
20
|
+
/** 导出中的提示文本(支持 {progress} 占位符) */
|
|
21
|
+
loadingText?: string;
|
|
22
|
+
/** 导出成功回调 */
|
|
23
|
+
onExportSuccess?: () => void;
|
|
24
|
+
/** 导出失败回调 */
|
|
25
|
+
onExportError?: (error: Error) => void;
|
|
26
|
+
/** 进度变化回调 */
|
|
27
|
+
onExportProgress?: (progress: number) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 开箱即用的导出按钮组件 */
|
|
31
|
+
export default class ExportButton extends SvelteComponent<ExportButtonProps> {}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createExporter - WASM 导出管理 Store
|
|
3
|
+
*
|
|
4
|
+
* 自动管理 WASM 初始化生命周期,提供类型安全的导出方法和响应式状态。
|
|
5
|
+
* 使用 Svelte store 实现响应式更新,兼容 Svelte 4 和 5。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* import { createExporter } from '@bsg-export/svelte';
|
|
11
|
+
*
|
|
12
|
+
* const { initialized, loading, progress, exportTable } = createExporter();
|
|
13
|
+
* </script>
|
|
14
|
+
*
|
|
15
|
+
* <button on:click={() => exportTable({ tableId: 'my-table', filename: '报表.xlsx' })}
|
|
16
|
+
* disabled={!$initialized || $loading}>
|
|
17
|
+
* {$loading ? `导出中 ${$progress}%` : '导出 Excel'}
|
|
18
|
+
* </button>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { type Readable } from 'svelte/store';
|
|
22
|
+
import type { ExportDataOptions, ExportTableOptions, ExportTablesXlsxOptions, ExportCsvBatchOptions, ExportXlsxBatchOptions, ExportTablesBatchOptions, DataRow } from '@bsg-export/types';
|
|
23
|
+
/** createExporter 返回值 */
|
|
24
|
+
export interface ExporterStore {
|
|
25
|
+
/** WASM 是否已初始化完成 */
|
|
26
|
+
initialized: Readable<boolean>;
|
|
27
|
+
/** 是否正在导出 */
|
|
28
|
+
loading: Readable<boolean>;
|
|
29
|
+
/** 导出进度 (0-100) */
|
|
30
|
+
progress: Readable<number>;
|
|
31
|
+
/** 错误信息 */
|
|
32
|
+
error: Readable<Error | null>;
|
|
33
|
+
/** 导出 HTML 表格 */
|
|
34
|
+
exportTable: (options: ExportTableOptions) => boolean;
|
|
35
|
+
/** 从 JS 数组直接导出 */
|
|
36
|
+
exportData: (data: DataRow[], options?: ExportDataOptions) => boolean;
|
|
37
|
+
/** 多工作表同步导出 */
|
|
38
|
+
exportTablesXlsx: (options: ExportTablesXlsxOptions) => boolean;
|
|
39
|
+
/** 分批异步导出 CSV */
|
|
40
|
+
exportCsvBatch: (options: ExportCsvBatchOptions) => Promise<boolean>;
|
|
41
|
+
/** 分批异步导出 XLSX */
|
|
42
|
+
exportXlsxBatch: (options: ExportXlsxBatchOptions) => Promise<boolean>;
|
|
43
|
+
/** 多工作表分批异步导出 */
|
|
44
|
+
exportTablesBatch: (options: ExportTablesBatchOptions) => Promise<boolean>;
|
|
45
|
+
/** 销毁实例,取消未完成的操作 */
|
|
46
|
+
destroy: () => void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* WASM 导出管理 Store
|
|
50
|
+
*
|
|
51
|
+
* 自动初始化 WASM 模块,提供类型安全的导出方法,
|
|
52
|
+
* 管理 loading / progress / error 状态。
|
|
53
|
+
*
|
|
54
|
+
* 调用 `destroy()` 以在组件卸载时停止状态更新。
|
|
55
|
+
*/
|
|
56
|
+
export declare function createExporter(): ExporterStore;
|
|
57
|
+
//# sourceMappingURL=create-exporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-exporter.d.ts","sourceRoot":"","sources":["../src/create-exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAiB,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,EAExB,OAAO,EACR,MAAM,mBAAmB,CAAC;AAE3B,yBAAyB;AACzB,MAAM,WAAW,aAAa;IAC5B,oBAAoB;IACpB,WAAW,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC/B,aAAa;IACb,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,mBAAmB;IACnB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC3B,WAAW;IACX,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,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;IAC3E,oBAAoB;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AA6BD;;;;;;;GAOG;AACH,wBAAgB,cAAc,IAAI,aAAa,CAgK9C"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createExporter - WASM 导出管理 Store
|
|
3
|
+
*
|
|
4
|
+
* 自动管理 WASM 初始化生命周期,提供类型安全的导出方法和响应式状态。
|
|
5
|
+
* 使用 Svelte store 实现响应式更新,兼容 Svelte 4 和 5。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* import { createExporter } from '@bsg-export/svelte';
|
|
11
|
+
*
|
|
12
|
+
* const { initialized, loading, progress, exportTable } = createExporter();
|
|
13
|
+
* </script>
|
|
14
|
+
*
|
|
15
|
+
* <button on:click={() => exportTable({ tableId: 'my-table', filename: '报表.xlsx' })}
|
|
16
|
+
* disabled={!$initialized || $loading}>
|
|
17
|
+
* {$loading ? `导出中 ${$progress}%` : '导出 Excel'}
|
|
18
|
+
* </button>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { writable, get } from 'svelte/store';
|
|
22
|
+
/** WASM 模块缓存 */
|
|
23
|
+
let wasmModule = null;
|
|
24
|
+
let wasmInitPromise = null;
|
|
25
|
+
/**
|
|
26
|
+
* 初始化 WASM 模块(单例模式)
|
|
27
|
+
*/
|
|
28
|
+
async function initWasm() {
|
|
29
|
+
if (wasmModule)
|
|
30
|
+
return wasmModule;
|
|
31
|
+
if (!wasmInitPromise) {
|
|
32
|
+
wasmInitPromise = import('belobog-stellar-grid')
|
|
33
|
+
.then(async (mod) => {
|
|
34
|
+
await mod.default();
|
|
35
|
+
wasmModule = mod;
|
|
36
|
+
return mod;
|
|
37
|
+
})
|
|
38
|
+
.catch((err) => {
|
|
39
|
+
// 初始化失败时重置 Promise,允许后续调用重试
|
|
40
|
+
wasmInitPromise = null;
|
|
41
|
+
throw err;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return wasmInitPromise;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* WASM 导出管理 Store
|
|
48
|
+
*
|
|
49
|
+
* 自动初始化 WASM 模块,提供类型安全的导出方法,
|
|
50
|
+
* 管理 loading / progress / error 状态。
|
|
51
|
+
*
|
|
52
|
+
* 调用 `destroy()` 以在组件卸载时停止状态更新。
|
|
53
|
+
*/
|
|
54
|
+
export function createExporter() {
|
|
55
|
+
const initialized = writable(false);
|
|
56
|
+
const loading = writable(false);
|
|
57
|
+
const progress = writable(0);
|
|
58
|
+
const error = writable(null);
|
|
59
|
+
let alive = true;
|
|
60
|
+
// 自动初始化 WASM
|
|
61
|
+
initWasm()
|
|
62
|
+
.then(() => {
|
|
63
|
+
if (alive)
|
|
64
|
+
initialized.set(true);
|
|
65
|
+
})
|
|
66
|
+
.catch((err) => {
|
|
67
|
+
if (alive)
|
|
68
|
+
error.set(err instanceof Error ? err : new Error(String(err)));
|
|
69
|
+
});
|
|
70
|
+
/** 创建进度回调(自动更新 progress 状态) */
|
|
71
|
+
const createProgressCallback = () => {
|
|
72
|
+
return (p) => {
|
|
73
|
+
if (alive)
|
|
74
|
+
progress.set(p);
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
/** 包装同步导出操作 */
|
|
78
|
+
const wrapSync = (fn) => {
|
|
79
|
+
if (!get(initialized) || !wasmModule)
|
|
80
|
+
return false;
|
|
81
|
+
loading.set(true);
|
|
82
|
+
progress.set(0);
|
|
83
|
+
error.set(null);
|
|
84
|
+
try {
|
|
85
|
+
fn();
|
|
86
|
+
if (alive)
|
|
87
|
+
progress.set(100);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (alive)
|
|
92
|
+
error.set(err instanceof Error ? err : new Error(String(err)));
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
if (alive)
|
|
97
|
+
loading.set(false);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
/** 包装异步导出操作 */
|
|
101
|
+
const wrapAsync = async (fn) => {
|
|
102
|
+
if (!get(initialized) || !wasmModule)
|
|
103
|
+
return false;
|
|
104
|
+
loading.set(true);
|
|
105
|
+
progress.set(0);
|
|
106
|
+
error.set(null);
|
|
107
|
+
try {
|
|
108
|
+
await fn();
|
|
109
|
+
if (alive)
|
|
110
|
+
progress.set(100);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (alive)
|
|
115
|
+
error.set(err instanceof Error ? err : new Error(String(err)));
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
if (alive)
|
|
120
|
+
loading.set(false);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
/** 导出 HTML 表格 */
|
|
124
|
+
const exportTable = (options) => {
|
|
125
|
+
return wrapSync(() => {
|
|
126
|
+
wasmModule.export_table(options.tableId, options.filename, options.format, options.excludeHidden, createProgressCallback(), options.withBom, options.strictProgressCallback);
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
/** 从 JS 数组直接导出 */
|
|
130
|
+
const exportData = (data, options) => {
|
|
131
|
+
return wrapSync(() => {
|
|
132
|
+
const opts = options
|
|
133
|
+
? { ...options, progressCallback: options.progressCallback ?? createProgressCallback() }
|
|
134
|
+
: { progressCallback: createProgressCallback() };
|
|
135
|
+
wasmModule.export_data(data, opts);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
/** 多工作表同步导出 */
|
|
139
|
+
const exportTablesXlsx = (options) => {
|
|
140
|
+
return wrapSync(() => {
|
|
141
|
+
wasmModule.export_tables_xlsx(options.sheets, options.filename, createProgressCallback(), options.strictProgressCallback);
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
/** 分批异步导出 CSV */
|
|
145
|
+
const exportCsvBatch = async (options) => {
|
|
146
|
+
return await wrapAsync(async () => {
|
|
147
|
+
await wasmModule.export_table_to_csv_batch(options.tableId, options.tbodyId, options.filename, options.batchSize, options.excludeHidden, createProgressCallback(), options.withBom, options.strictProgressCallback);
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
/** 分批异步导出 XLSX */
|
|
151
|
+
const exportXlsxBatch = async (options) => {
|
|
152
|
+
return await wrapAsync(async () => {
|
|
153
|
+
await wasmModule.export_table_to_xlsx_batch(options.tableId, options.tbodyId, options.filename, options.batchSize, options.excludeHidden, createProgressCallback(), options.strictProgressCallback);
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
/** 多工作表分批异步导出 */
|
|
157
|
+
const exportTablesBatch = async (options) => {
|
|
158
|
+
return await wrapAsync(async () => {
|
|
159
|
+
await wasmModule.export_tables_to_xlsx_batch(options.sheets, options.filename, options.batchSize, createProgressCallback(), options.strictProgressCallback);
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
/** 销毁实例 */
|
|
163
|
+
const destroy = () => {
|
|
164
|
+
alive = false;
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
initialized: { subscribe: initialized.subscribe },
|
|
168
|
+
loading: { subscribe: loading.subscribe },
|
|
169
|
+
progress: { subscribe: progress.subscribe },
|
|
170
|
+
error: { subscribe: error.subscribe },
|
|
171
|
+
exportTable,
|
|
172
|
+
exportData,
|
|
173
|
+
exportTablesXlsx,
|
|
174
|
+
exportCsvBatch,
|
|
175
|
+
exportXlsxBatch,
|
|
176
|
+
exportTablesBatch,
|
|
177
|
+
destroy,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createWorkerExporter - Worker 线程导出管理 Store
|
|
3
|
+
*
|
|
4
|
+
* 将 CSV/XLSX 生成移至 Worker 线程,主线程不阻塞。
|
|
5
|
+
* 用户需要传入一个创建 Worker 实例的工厂函数。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* import { createWorkerExporter } from '@bsg-export/svelte';
|
|
11
|
+
*
|
|
12
|
+
* // Vite
|
|
13
|
+
* import ExportWorkerScript from '@bsg-export/worker/worker?worker';
|
|
14
|
+
* const { initialized, loading, progress, exportData, destroy } =
|
|
15
|
+
* createWorkerExporter(() => new ExportWorkerScript());
|
|
16
|
+
*
|
|
17
|
+
* // 组件卸载时调用 destroy()
|
|
18
|
+
* import { onDestroy } from 'svelte';
|
|
19
|
+
* onDestroy(() => destroy());
|
|
20
|
+
* </script>
|
|
21
|
+
*
|
|
22
|
+
* <button disabled={!$initialized || $loading}
|
|
23
|
+
* on:click={() => exportData(data, { columns, filename: '报表.xlsx', format: 1 })}>
|
|
24
|
+
* {$loading ? `导出中 ${Math.round($progress)}%` : '导出'}
|
|
25
|
+
* </button>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import { type Readable } from 'svelte/store';
|
|
29
|
+
import type { ExportDataOptions, DataRow } from '@bsg-export/types';
|
|
30
|
+
/** createWorkerExporter 返回值 */
|
|
31
|
+
export interface WorkerExporterStore {
|
|
32
|
+
/** Worker 中的 WASM 是否已初始化完成 */
|
|
33
|
+
initialized: Readable<boolean>;
|
|
34
|
+
/** 是否正在导出 */
|
|
35
|
+
loading: Readable<boolean>;
|
|
36
|
+
/** 导出进度 (0-100) */
|
|
37
|
+
progress: Readable<number>;
|
|
38
|
+
/** 错误信息 */
|
|
39
|
+
error: Readable<Error | null>;
|
|
40
|
+
/** 在 Worker 中生成文件并触发下载 */
|
|
41
|
+
exportData: (data: DataRow[], options?: Omit<ExportDataOptions, 'progressCallback'>) => Promise<boolean>;
|
|
42
|
+
/** 在 Worker 中生成文件字节(不触发下载) */
|
|
43
|
+
generateBytes: (data: DataRow[], options?: Omit<ExportDataOptions, 'progressCallback'>) => Promise<Uint8Array | null>;
|
|
44
|
+
/** 销毁 Worker 和清理资源 */
|
|
45
|
+
destroy: () => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Worker 线程导出管理 Store
|
|
49
|
+
*
|
|
50
|
+
* 接收一个 Worker 工厂函数,自动管理 Worker 生命周期和 WASM 初始化。
|
|
51
|
+
* 导出计算在 Worker 线程执行,主线程保持响应。
|
|
52
|
+
*
|
|
53
|
+
* 必须在组件卸载时调用 `destroy()` 清理资源。
|
|
54
|
+
*
|
|
55
|
+
* @param createWorker - 创建 Worker 实例的工厂函数
|
|
56
|
+
*/
|
|
57
|
+
export declare function createWorkerExporter(createWorker: () => Worker): WorkerExporterStore;
|
|
58
|
+
//# sourceMappingURL=create-worker-exporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-worker-exporter.d.ts","sourceRoot":"","sources":["../src/create-worker-exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAiB,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,KAAK,EACV,iBAAiB,EAEjB,OAAO,EACR,MAAM,mBAAmB,CAAC;AAE3B,+BAA+B;AAC/B,MAAM,WAAW,mBAAmB;IAClC,8BAA8B;IAC9B,WAAW,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC/B,aAAa;IACb,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,mBAAmB;IACnB,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC3B,WAAW;IACX,KAAK,EAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAC9B,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,sBAAsB;IACtB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAiCD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,MAAM,GAAG,mBAAmB,CA8LpF"}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createWorkerExporter - Worker 线程导出管理 Store
|
|
3
|
+
*
|
|
4
|
+
* 将 CSV/XLSX 生成移至 Worker 线程,主线程不阻塞。
|
|
5
|
+
* 用户需要传入一个创建 Worker 实例的工厂函数。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* import { createWorkerExporter } from '@bsg-export/svelte';
|
|
11
|
+
*
|
|
12
|
+
* // Vite
|
|
13
|
+
* import ExportWorkerScript from '@bsg-export/worker/worker?worker';
|
|
14
|
+
* const { initialized, loading, progress, exportData, destroy } =
|
|
15
|
+
* createWorkerExporter(() => new ExportWorkerScript());
|
|
16
|
+
*
|
|
17
|
+
* // 组件卸载时调用 destroy()
|
|
18
|
+
* import { onDestroy } from 'svelte';
|
|
19
|
+
* onDestroy(() => destroy());
|
|
20
|
+
* </script>
|
|
21
|
+
*
|
|
22
|
+
* <button disabled={!$initialized || $loading}
|
|
23
|
+
* on:click={() => exportData(data, { columns, filename: '报表.xlsx', format: 1 })}>
|
|
24
|
+
* {$loading ? `导出中 ${Math.round($progress)}%` : '导出'}
|
|
25
|
+
* </button>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import { writable, get } from 'svelte/store';
|
|
29
|
+
let requestCounter = 0;
|
|
30
|
+
function generateId() {
|
|
31
|
+
return `req_${++requestCounter}_${Date.now()}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Worker 线程导出管理 Store
|
|
35
|
+
*
|
|
36
|
+
* 接收一个 Worker 工厂函数,自动管理 Worker 生命周期和 WASM 初始化。
|
|
37
|
+
* 导出计算在 Worker 线程执行,主线程保持响应。
|
|
38
|
+
*
|
|
39
|
+
* 必须在组件卸载时调用 `destroy()` 清理资源。
|
|
40
|
+
*
|
|
41
|
+
* @param createWorker - 创建 Worker 实例的工厂函数
|
|
42
|
+
*/
|
|
43
|
+
export function createWorkerExporter(createWorker) {
|
|
44
|
+
const initialized = writable(false);
|
|
45
|
+
const loading = writable(false);
|
|
46
|
+
const progress = writable(0);
|
|
47
|
+
const error = writable(null);
|
|
48
|
+
let worker = null;
|
|
49
|
+
const pending = new Map();
|
|
50
|
+
let alive = true;
|
|
51
|
+
/** 处理 Worker 消息 */
|
|
52
|
+
const handleMessage = (event) => {
|
|
53
|
+
if (!alive)
|
|
54
|
+
return;
|
|
55
|
+
const { type, id, bytes, message, progress: prog } = event.data;
|
|
56
|
+
const req = pending.get(id);
|
|
57
|
+
if (!req)
|
|
58
|
+
return;
|
|
59
|
+
switch (type) {
|
|
60
|
+
case 'ready':
|
|
61
|
+
pending.delete(id);
|
|
62
|
+
req.resolve(new ArrayBuffer(0));
|
|
63
|
+
break;
|
|
64
|
+
case 'result':
|
|
65
|
+
pending.delete(id);
|
|
66
|
+
req.resolve(bytes);
|
|
67
|
+
break;
|
|
68
|
+
case 'error':
|
|
69
|
+
pending.delete(id);
|
|
70
|
+
req.reject(new Error(message ?? '未知错误'));
|
|
71
|
+
break;
|
|
72
|
+
case 'progress':
|
|
73
|
+
if (req.onProgress && prog !== undefined) {
|
|
74
|
+
req.onProgress(prog);
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/** 处理 Worker 错误 */
|
|
80
|
+
const handleError = (event) => {
|
|
81
|
+
if (!alive)
|
|
82
|
+
return;
|
|
83
|
+
const err = new Error(`Worker 错误: ${event.message}`);
|
|
84
|
+
for (const [id, req] of pending) {
|
|
85
|
+
req.reject(err);
|
|
86
|
+
pending.delete(id);
|
|
87
|
+
}
|
|
88
|
+
error.set(err);
|
|
89
|
+
};
|
|
90
|
+
// 立即创建 Worker 并初始化
|
|
91
|
+
worker = createWorker();
|
|
92
|
+
worker.addEventListener('message', handleMessage);
|
|
93
|
+
worker.addEventListener('error', handleError);
|
|
94
|
+
// 发送 init 消息
|
|
95
|
+
const initId = generateId();
|
|
96
|
+
const initPromise = new Promise((resolve, reject) => {
|
|
97
|
+
pending.set(initId, { resolve, reject });
|
|
98
|
+
});
|
|
99
|
+
const request = { type: 'init', id: initId };
|
|
100
|
+
worker.postMessage(request);
|
|
101
|
+
initPromise
|
|
102
|
+
.then(() => {
|
|
103
|
+
if (alive)
|
|
104
|
+
initialized.set(true);
|
|
105
|
+
})
|
|
106
|
+
.catch((err) => {
|
|
107
|
+
if (alive)
|
|
108
|
+
error.set(err instanceof Error ? err : new Error(String(err)));
|
|
109
|
+
});
|
|
110
|
+
/** 向 Worker 发送生成请求 */
|
|
111
|
+
const sendGenerate = (data, options) => {
|
|
112
|
+
if (!worker)
|
|
113
|
+
return Promise.reject(new Error('Worker 未创建'));
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const id = generateId();
|
|
116
|
+
pending.set(id, {
|
|
117
|
+
resolve,
|
|
118
|
+
reject,
|
|
119
|
+
onProgress: (p) => {
|
|
120
|
+
if (alive)
|
|
121
|
+
progress.set(p);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
const req = {
|
|
125
|
+
type: 'generate',
|
|
126
|
+
id,
|
|
127
|
+
data,
|
|
128
|
+
options: (options ?? {}),
|
|
129
|
+
};
|
|
130
|
+
worker.postMessage(req);
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
/** 在主线程触发文件下载 */
|
|
134
|
+
const downloadFile = (bytes, filename, format) => {
|
|
135
|
+
const isXlsx = format === 1;
|
|
136
|
+
const mimeType = isXlsx
|
|
137
|
+
? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
138
|
+
: 'text/csv;charset=utf-8';
|
|
139
|
+
const defaultExt = isXlsx ? 'xlsx' : 'csv';
|
|
140
|
+
let finalFilename = filename ?? `export.${defaultExt}`;
|
|
141
|
+
if (!finalFilename.endsWith(`.${defaultExt}`)) {
|
|
142
|
+
finalFilename = `${finalFilename}.${defaultExt}`;
|
|
143
|
+
}
|
|
144
|
+
const blob = new Blob([bytes.buffer], { type: mimeType });
|
|
145
|
+
const url = URL.createObjectURL(blob);
|
|
146
|
+
const anchor = document.createElement('a');
|
|
147
|
+
anchor.href = url;
|
|
148
|
+
anchor.download = finalFilename;
|
|
149
|
+
anchor.click();
|
|
150
|
+
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
|
151
|
+
};
|
|
152
|
+
/** 在 Worker 中生成文件并触发下载 */
|
|
153
|
+
const exportData = async (data, options) => {
|
|
154
|
+
if (!get(initialized))
|
|
155
|
+
return false;
|
|
156
|
+
loading.set(true);
|
|
157
|
+
progress.set(0);
|
|
158
|
+
error.set(null);
|
|
159
|
+
try {
|
|
160
|
+
const buffer = await sendGenerate(data, options);
|
|
161
|
+
const bytes = new Uint8Array(buffer);
|
|
162
|
+
downloadFile(bytes, options?.filename, options?.format);
|
|
163
|
+
if (alive)
|
|
164
|
+
progress.set(100);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (alive)
|
|
169
|
+
error.set(err instanceof Error ? err : new Error(String(err)));
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
if (alive)
|
|
174
|
+
loading.set(false);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
/** 在 Worker 中生成文件字节(不触发下载) */
|
|
178
|
+
const generateBytes = async (data, options) => {
|
|
179
|
+
if (!get(initialized))
|
|
180
|
+
return null;
|
|
181
|
+
loading.set(true);
|
|
182
|
+
progress.set(0);
|
|
183
|
+
error.set(null);
|
|
184
|
+
try {
|
|
185
|
+
const buffer = await sendGenerate(data, options);
|
|
186
|
+
if (alive)
|
|
187
|
+
progress.set(100);
|
|
188
|
+
return new Uint8Array(buffer);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (alive)
|
|
192
|
+
error.set(err instanceof Error ? err : new Error(String(err)));
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
if (alive)
|
|
197
|
+
loading.set(false);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
/** 销毁 Worker 和清理资源 */
|
|
201
|
+
const destroy = () => {
|
|
202
|
+
alive = false;
|
|
203
|
+
// 拒绝所有待处理请求
|
|
204
|
+
for (const [, req] of pending) {
|
|
205
|
+
req.reject(new Error('组件已卸载'));
|
|
206
|
+
}
|
|
207
|
+
pending.clear();
|
|
208
|
+
worker?.terminate();
|
|
209
|
+
worker = null;
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
initialized: { subscribe: initialized.subscribe },
|
|
213
|
+
loading: { subscribe: loading.subscribe },
|
|
214
|
+
progress: { subscribe: progress.subscribe },
|
|
215
|
+
error: { subscribe: error.subscribe },
|
|
216
|
+
exportData,
|
|
217
|
+
generateBytes,
|
|
218
|
+
destroy,
|
|
219
|
+
};
|
|
220
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bsg-export/svelte - belobog-stellar-grid 的 Svelte 官方封装
|
|
3
|
+
*
|
|
4
|
+
* 提供基于 Svelte store 的导出功能封装,简化在 Svelte 项目中使用表格导出功能。
|
|
5
|
+
* 兼容 Svelte 4 和 Svelte 5。
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
export { createExporter } from './create-exporter';
|
|
10
|
+
export type { ExporterStore } from './create-exporter';
|
|
11
|
+
export { createWorkerExporter } from './create-worker-exporter';
|
|
12
|
+
export type { WorkerExporterStore } from './create-worker-exporter';
|
|
13
|
+
export type { ExportButtonProps } from './ExportButton.svelte';
|
|
14
|
+
export type { Column, MergeCellValue, CellValue, MergeableCellValue, DataRow, ExportDataOptions, SheetConfig, BatchSheetConfig, ProgressCallback, ExportTableOptions, ExportTablesXlsxOptions, ExportCsvBatchOptions, ExportXlsxBatchOptions, ExportTablesBatchOptions, } from '@bsg-export/types';
|
|
15
|
+
export { ExportFormat } from '@bsg-export/types';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAGpE,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAG/D,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
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bsg-export/svelte - belobog-stellar-grid 的 Svelte 官方封装
|
|
3
|
+
*
|
|
4
|
+
* 提供基于 Svelte store 的导出功能封装,简化在 Svelte 项目中使用表格导出功能。
|
|
5
|
+
* 兼容 Svelte 4 和 Svelte 5。
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
// Store 封装
|
|
10
|
+
export { createExporter } from './create-exporter';
|
|
11
|
+
export { createWorkerExporter } from './create-worker-exporter';
|
|
12
|
+
export { ExportFormat } from '@bsg-export/types';
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bsg-export/svelte",
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"description": "belobog-stellar-grid 的 Svelte 官方封装组件",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"svelte": "./dist/index.js",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"svelte": "./dist/index.js",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./ExportButton.svelte": {
|
|
16
|
+
"types": "./dist/ExportButton.svelte.d.ts",
|
|
17
|
+
"svelte": "./dist/ExportButton.svelte",
|
|
18
|
+
"import": "./dist/ExportButton.svelte"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc && cp src/ExportButton.svelte src/ExportButton.svelte.d.ts dist/",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"belobog",
|
|
30
|
+
"stellar-grid",
|
|
31
|
+
"svelte",
|
|
32
|
+
"export",
|
|
33
|
+
"table"
|
|
34
|
+
],
|
|
35
|
+
"author": "Kurisu <makise_kurisuu@outlook.jp>",
|
|
36
|
+
"license": "MIT OR Apache-2.0",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/kurisu994/belobog-stellar-grid",
|
|
40
|
+
"directory": "packages/svelte"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"belobog-stellar-grid": ">=1.0.0",
|
|
44
|
+
"svelte": ">=4.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@bsg-export/types": "file:../types"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"svelte": "^5.34.7",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
}
|
|
53
|
+
}
|