@svadmin/core 0.32.0 → 0.32.2
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 +3 -3
- package/src/data-transfer.svelte.ts +4 -59
- package/src/export-format.ts +35 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/core",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.2",
|
|
4
4
|
"author": "zuohuadong",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
},
|
|
10
10
|
"main": "src/index.ts",
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@happy-dom/global-registrator": "^20.
|
|
12
|
+
"@happy-dom/global-registrator": "^20.11.0",
|
|
13
13
|
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
|
14
14
|
"@testing-library/dom": "^10.4.1",
|
|
15
15
|
"@testing-library/svelte": "^5.4.2",
|
|
16
16
|
"@testing-library/user-event": "^14.6.1",
|
|
17
17
|
"@types/bun": "^1.3.14",
|
|
18
18
|
"@types/jsdom": "^28.0.3",
|
|
19
|
-
"happy-dom": "^20.
|
|
19
|
+
"happy-dom": "^20.11.0",
|
|
20
20
|
"jsdom": "^29.1.1",
|
|
21
21
|
"vitest": "^4.1.8"
|
|
22
22
|
},
|
|
@@ -3,6 +3,7 @@ import { useParsed } from './useParsed.svelte';
|
|
|
3
3
|
import type { Sort, Filter, BaseRecord } from './types';
|
|
4
4
|
import { downloadData } from './export-format';
|
|
5
5
|
import type { ExportFormat } from './export-format';
|
|
6
|
+
import { parseCSV } from './helpers-pure';
|
|
6
7
|
export { downloadData, toCsv, toJson, toXlsx, escapeCsvField } from './export-format';
|
|
7
8
|
export type { ExportFormat } from './export-format';
|
|
8
9
|
|
|
@@ -69,7 +70,9 @@ export function useExport<TData extends BaseRecord = BaseRecord>(options: UseExp
|
|
|
69
70
|
|
|
70
71
|
return allRecords;
|
|
71
72
|
} catch (error) {
|
|
72
|
-
|
|
73
|
+
const exportError = error instanceof Error ? error : new Error(String(error));
|
|
74
|
+
if (!options.onError) throw exportError;
|
|
75
|
+
options.onError(exportError);
|
|
73
76
|
return [];
|
|
74
77
|
} finally {
|
|
75
78
|
isLoading = false;
|
|
@@ -211,61 +214,3 @@ export function useImport<TData = Record<string, unknown>>(options: UseImportOpt
|
|
|
211
214
|
get mutationResult() { return mutationResult; },
|
|
212
215
|
};
|
|
213
216
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// ─── 解析工具 ────────────────────────────────────────────────────
|
|
217
|
-
|
|
218
|
-
/** Parse full CSV text respecting quoted multi-line fields */
|
|
219
|
-
function parseCSV(text: string): string[][] {
|
|
220
|
-
const rows: string[][] = [];
|
|
221
|
-
let currentRow: string[] = [];
|
|
222
|
-
let currentVal = '';
|
|
223
|
-
let inQuotes = false;
|
|
224
|
-
let wasQuoted = false;
|
|
225
|
-
|
|
226
|
-
for (let i = 0; i < text.length; i++) {
|
|
227
|
-
const ch = text[i];
|
|
228
|
-
if (inQuotes) {
|
|
229
|
-
if (ch === '"') {
|
|
230
|
-
if (i + 1 < text.length && text[i + 1] === '"') {
|
|
231
|
-
currentVal += '"';
|
|
232
|
-
i++;
|
|
233
|
-
} else {
|
|
234
|
-
inQuotes = false;
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
currentVal += ch;
|
|
238
|
-
}
|
|
239
|
-
} else {
|
|
240
|
-
if (ch === '"') {
|
|
241
|
-
inQuotes = true;
|
|
242
|
-
wasQuoted = true;
|
|
243
|
-
} else if (ch === ',') {
|
|
244
|
-
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
245
|
-
currentVal = '';
|
|
246
|
-
wasQuoted = false;
|
|
247
|
-
} else if (ch === '\n' || ch === '\r') {
|
|
248
|
-
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
249
|
-
if (currentRow.some(v => v !== '')) {
|
|
250
|
-
rows.push(currentRow);
|
|
251
|
-
}
|
|
252
|
-
currentRow = [];
|
|
253
|
-
currentVal = '';
|
|
254
|
-
wasQuoted = false;
|
|
255
|
-
if (ch === '\r' && i + 1 < text.length && text[i + 1] === '\n') {
|
|
256
|
-
i++;
|
|
257
|
-
}
|
|
258
|
-
} else {
|
|
259
|
-
currentVal += ch;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (currentVal || currentRow.length > 0) {
|
|
265
|
-
currentRow.push(wasQuoted ? currentVal : currentVal.trim());
|
|
266
|
-
if (currentRow.some(v => v !== '')) {
|
|
267
|
-
rows.push(currentRow);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return rows;
|
|
271
|
-
}
|
package/src/export-format.ts
CHANGED
|
@@ -5,24 +5,28 @@
|
|
|
5
5
|
export type ExportFormat = 'csv' | 'json' | 'xlsx';
|
|
6
6
|
|
|
7
7
|
/** CSV 转义,含公式注入防护 */
|
|
8
|
-
export function escapeCsvField(
|
|
9
|
-
let
|
|
10
|
-
if (/^[=+@\t\r]/.test(
|
|
11
|
-
return
|
|
12
|
-
? `"${
|
|
13
|
-
:
|
|
8
|
+
export function escapeCsvField(fieldValue: string): string {
|
|
9
|
+
let escapedValue = fieldValue;
|
|
10
|
+
if (/^[=+@\t\r]/.test(escapedValue)) escapedValue = "'" + escapedValue;
|
|
11
|
+
return escapedValue.includes(',') || escapedValue.includes('"') || escapedValue.includes('\n') || escapedValue.includes('\r')
|
|
12
|
+
? `"${escapedValue.replace(/"/g, '""')}"`
|
|
13
|
+
: escapedValue;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/** 将记录数组转为 CSV 字符串 */
|
|
17
17
|
export function toCsv(records: Record<string, unknown>[]): string {
|
|
18
18
|
if (records.length === 0) return '';
|
|
19
|
-
const
|
|
20
|
-
const header =
|
|
19
|
+
const fieldNames = Object.keys(records[0]);
|
|
20
|
+
const header = fieldNames.map(escapeCsvField).join(',');
|
|
21
21
|
const rows = records.map(record =>
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
22
|
+
fieldNames.map(fieldName => {
|
|
23
|
+
const rawFieldValue = record[fieldName];
|
|
24
|
+
const serializedFieldValue = rawFieldValue === null || rawFieldValue === undefined
|
|
25
|
+
? ''
|
|
26
|
+
: typeof rawFieldValue === 'object'
|
|
27
|
+
? JSON.stringify(rawFieldValue)
|
|
28
|
+
: String(rawFieldValue);
|
|
29
|
+
return escapeCsvField(serializedFieldValue);
|
|
26
30
|
}).join(',')
|
|
27
31
|
);
|
|
28
32
|
return [header, ...rows].join('\n');
|
|
@@ -34,8 +38,8 @@ export function toJson(records: Record<string, unknown>[]): string {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/** 转义 XML 特殊字符 */
|
|
37
|
-
export function escapeXml(
|
|
38
|
-
return
|
|
41
|
+
export function escapeXml(xmlValue: string): string {
|
|
42
|
+
return xmlValue.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/**
|
|
@@ -43,17 +47,17 @@ export function escapeXml(s: string): string {
|
|
|
43
47
|
*/
|
|
44
48
|
export function toXlsx(records: Record<string, unknown>[]): string {
|
|
45
49
|
if (records.length === 0) return '';
|
|
46
|
-
const
|
|
50
|
+
const fieldNames = Object.keys(records[0]);
|
|
47
51
|
|
|
48
|
-
const headerCells =
|
|
52
|
+
const headerCells = fieldNames.map(fieldName => `<Cell><Data ss:Type="String">${escapeXml(fieldName)}</Data></Cell>`).join('');
|
|
49
53
|
const dataRows = records.map(record =>
|
|
50
|
-
`<Row>${
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
if (typeof
|
|
54
|
-
if (typeof
|
|
55
|
-
const
|
|
56
|
-
return `<Cell><Data ss:Type="String">${escapeXml(
|
|
54
|
+
`<Row>${fieldNames.map(fieldName => {
|
|
55
|
+
const fieldValue = record[fieldName];
|
|
56
|
+
if (fieldValue === null || fieldValue === undefined) return '<Cell><Data ss:Type="String"></Data></Cell>';
|
|
57
|
+
if (typeof fieldValue === 'number') return `<Cell><Data ss:Type="Number">${fieldValue}</Data></Cell>`;
|
|
58
|
+
if (typeof fieldValue === 'boolean') return `<Cell><Data ss:Type="Boolean">${fieldValue ? 1 : 0}</Data></Cell>`;
|
|
59
|
+
const serializedValue = typeof fieldValue === 'object' ? JSON.stringify(fieldValue) : String(fieldValue);
|
|
60
|
+
return `<Cell><Data ss:Type="String">${escapeXml(serializedValue)}</Data></Cell>`;
|
|
57
61
|
}).join('')}</Row>`
|
|
58
62
|
).join('\n');
|
|
59
63
|
|
|
@@ -84,32 +88,32 @@ export function downloadData(
|
|
|
84
88
|
|
|
85
89
|
let content: string;
|
|
86
90
|
let mimeType: string;
|
|
87
|
-
let
|
|
91
|
+
let extension: string;
|
|
88
92
|
|
|
89
93
|
switch (format) {
|
|
90
94
|
case 'json':
|
|
91
95
|
content = toJson(records);
|
|
92
96
|
mimeType = 'application/json;charset=utf-8;';
|
|
93
|
-
|
|
97
|
+
extension = 'json';
|
|
94
98
|
break;
|
|
95
99
|
case 'xlsx':
|
|
96
100
|
content = toXlsx(records);
|
|
97
101
|
mimeType = 'application/vnd.ms-excel;charset=utf-8;';
|
|
98
|
-
|
|
102
|
+
extension = 'xls';
|
|
99
103
|
break;
|
|
100
104
|
case 'csv':
|
|
101
105
|
default:
|
|
102
106
|
content = toCsv(records);
|
|
103
107
|
mimeType = 'text/csv;charset=utf-8;';
|
|
104
|
-
|
|
108
|
+
extension = 'csv';
|
|
105
109
|
break;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
const blob = new Blob(['\uFEFF' + content], { type: mimeType });
|
|
109
113
|
const url = URL.createObjectURL(blob);
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
const downloadAnchor = document.createElement('a');
|
|
115
|
+
downloadAnchor.href = url;
|
|
116
|
+
downloadAnchor.download = `${resource}_export.${extension}`;
|
|
117
|
+
downloadAnchor.click();
|
|
114
118
|
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
|
115
119
|
}
|