@ticatec/batch-data-uploader 0.0.1
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/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/BaseTemplate.d.ts +54 -0
- package/dist/BaseTemplate.js +149 -0
- package/dist/DataColumn.d.ts +16 -0
- package/dist/DataColumn.js +1 -0
- package/dist/FileUploadWizard.svelte +136 -0
- package/dist/FileUploadWizard.svelte.d.ts +25 -0
- package/dist/i18n_resources/batch_cn_resource.d.ts +21 -0
- package/dist/i18n_resources/batch_cn_resource.js +21 -0
- package/dist/i18n_resources/batch_en_resource.d.ts +21 -0
- package/dist/i18n_resources/batch_en_resource.js +21 -0
- package/dist/i18n_resources/i18nKeys.d.ts +55 -0
- package/dist/i18n_resources/i18nKeys.js +56 -0
- package/dist/i18n_resources/index.d.ts +3 -0
- package/dist/i18n_resources/index.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +22 -0
- package/package.json +83 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 henryfeng
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Excel Batch Data Upload Component
|
|
2
|
+
|
|
3
|
+
[[中文文档](./README_CN.md)]
|
|
4
|
+
|
|
5
|
+
This component is designed to batch import data from Excel files and handle uploads. It supports upload status management, exporting error rows, multilingual adaptation, data preprocessing, and more. By defining template classes and using a unified UI dialog component, it enables quick adaptation for various types of Excel data upload needs.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
* Parse `.xls` / `.xlsx` files
|
|
10
|
+
* Custom column mapping and formatting
|
|
11
|
+
* Batch upload with configurable batch size
|
|
12
|
+
* Extensible data preprocessing logic (e.g., merging, grouping)
|
|
13
|
+
* Upload status display: Pending, Uploading, Success, Failed
|
|
14
|
+
* Export error rows to Excel
|
|
15
|
+
* Multilingual support (based on `@ticatec/i18n`)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm i @ticatec/batch-data-uploader
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 1. Define a Template Class
|
|
28
|
+
|
|
29
|
+
Extend `BaseTemplate`, provide field definitions and upload logic, and optionally override `consolidateData` to process data.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import BaseTemplate from '$lib/BaseTemplate';
|
|
33
|
+
import type DataColumn from './DataColumn';
|
|
34
|
+
|
|
35
|
+
class MyDataTemplate extends BaseTemplate {
|
|
36
|
+
constructor(uploadFun: UploadFun) {
|
|
37
|
+
const columns: DataColumn[] = [
|
|
38
|
+
{ text: 'Name', field: 'name', pos: 0 },
|
|
39
|
+
{ text: 'Email', field: 'email', pos: 1 },
|
|
40
|
+
{ text: 'Age', field: 'age', pos: 2, parser: val => parseInt(val) },
|
|
41
|
+
];
|
|
42
|
+
super(columns, uploadFun, 50);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Optional: Override to implement merge/group logic
|
|
46
|
+
protected consolidateData(rows: Array<any>) {
|
|
47
|
+
return super.consolidateData(rows);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Use the Upload Dialog Component
|
|
53
|
+
|
|
54
|
+
```svelte
|
|
55
|
+
<script lang="ts">
|
|
56
|
+
import UploadDialog from './UploadDialog.svelte';
|
|
57
|
+
import { MyDataTemplate } from './MyDataTemplate';
|
|
58
|
+
|
|
59
|
+
let showDialog = false;
|
|
60
|
+
|
|
61
|
+
function doUpload(rows: any[]): Promise<void> {
|
|
62
|
+
const dataChunk = rows.map(row => row.data);
|
|
63
|
+
return fetch('/api/upload', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: JSON.stringify(dataChunk),
|
|
66
|
+
}).then(res => {
|
|
67
|
+
if (!res.ok) throw new Error('Upload failed');
|
|
68
|
+
// After upload, write result info to row.error if needed
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const template = new MyDataTemplate(doUpload);
|
|
73
|
+
|
|
74
|
+
const showUploadDialog = () => {
|
|
75
|
+
window.Dialog.showModal(UploadDialog, {
|
|
76
|
+
title: 'Batch Add Employees',
|
|
77
|
+
template,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<button on:click={() => showUploadDialog()}>Import Data</button>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Parameter Reference
|
|
88
|
+
|
|
89
|
+
### `BaseTemplate` Constructor Parameters
|
|
90
|
+
|
|
91
|
+
| Name | Type | Description |
|
|
92
|
+
| ----------- | ------------------------------- | -------------------------------------------- |
|
|
93
|
+
| `columns` | `DataColumn[]` | Defines column position and format |
|
|
94
|
+
| `uploadFun` | `(arr: any[]) => Promise<void>` | Upload function, called in batches |
|
|
95
|
+
| `batchSize` | `number` (default: 50) | Number of rows per upload batch |
|
|
96
|
+
| `rowOffset` | `number` (default: 1) | Row offset for data start (e.g. skip header) |
|
|
97
|
+
|
|
98
|
+
### `DataColumn` Interface
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
interface DataColumn {
|
|
102
|
+
text: string; // Column display text
|
|
103
|
+
field: string; // Data field path (supports nesting)
|
|
104
|
+
pos: number; // Excel column index (starting from 0)
|
|
105
|
+
parser?: (val: any) => any; // Optional parser function for cell values
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Upload Workflow
|
|
112
|
+
|
|
113
|
+
1. User selects an Excel file
|
|
114
|
+
2. Call `BaseTemplate.parseExcelFile(file)` to parse data
|
|
115
|
+
3. Display preview data table with `Pending` status
|
|
116
|
+
4. User clicks upload, system calls `uploadFun` in batches
|
|
117
|
+
5. Mark successful items, retain `error` info for failed items
|
|
118
|
+
6. Failed rows can be exported to Excel
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Exporting Error Data
|
|
123
|
+
|
|
124
|
+
Use `BaseTemplate.exportErrorRowsToExcel(filename: string)` to export rows with errors as an Excel file, including original columns and error messages.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Customization Options
|
|
129
|
+
|
|
130
|
+
* **Custom Column Display**: Define column fields and formatting functions
|
|
131
|
+
* **Custom Status Field**: Built-in `status` column, can be customized per business needs
|
|
132
|
+
* **Data Cleaning & Validation**: Implement in `consolidateData()` method
|
|
133
|
+
* **Multilingual Text**: Use `getI18nText` from `@ticatec/i18n`
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Dependencies
|
|
138
|
+
|
|
139
|
+
* [`xlsx`](https://www.npmjs.com/package/xlsx)
|
|
140
|
+
* [`@ticatec/uniface-element`](https://www.npmjs.com/package/@ticatec/uniface-element)
|
|
141
|
+
* [`@ticatec/i18n`](https://www.npmjs.com/package/@ticatec/i18n)
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT License.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Author
|
|
152
|
+
|
|
153
|
+
Henry Feng
|
|
154
|
+
[huili.f@gmail.com](mailto:huili.f@gmail.com)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type DataColumn from "./DataColumn";
|
|
2
|
+
import type { DataColumn as TableColumn } from "@ticatec/uniface-element/DataTable";
|
|
3
|
+
export type UploadFun = (arr: Array<any>) => Promise<void>;
|
|
4
|
+
export type UpdateProgressStatus = () => void;
|
|
5
|
+
export default abstract class BaseTemplate {
|
|
6
|
+
protected readonly _columns: Array<DataColumn>;
|
|
7
|
+
protected readonly rowOffset: number;
|
|
8
|
+
protected _list: Array<any>;
|
|
9
|
+
protected uploadFun: UploadFun;
|
|
10
|
+
protected batchSize: number;
|
|
11
|
+
protected updateProgressStatus: UpdateProgressStatus | null;
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param columns
|
|
15
|
+
* @param uploadFun
|
|
16
|
+
* @param batchSize
|
|
17
|
+
* @param rowOffset
|
|
18
|
+
* @protected
|
|
19
|
+
*/
|
|
20
|
+
protected constructor(columns: Array<DataColumn>, uploadFun: UploadFun, batchSize?: number, rowOffset?: number);
|
|
21
|
+
/**
|
|
22
|
+
* 状态更新的监听器
|
|
23
|
+
* @param value
|
|
24
|
+
*/
|
|
25
|
+
setProgressStatusListener(value: UpdateProgressStatus): void;
|
|
26
|
+
/**
|
|
27
|
+
* 整理数据,在子类可以通过重载完成数据的二次处理
|
|
28
|
+
* @param rows
|
|
29
|
+
* @protected
|
|
30
|
+
*/
|
|
31
|
+
protected consolidateData(rows: Array<any>): Array<any>;
|
|
32
|
+
/**
|
|
33
|
+
* 解析一个excel文件
|
|
34
|
+
* @param file
|
|
35
|
+
*/
|
|
36
|
+
parseExcelFile(file: File): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* 上传数据
|
|
39
|
+
*/
|
|
40
|
+
upload(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* 获取表格的列定义
|
|
43
|
+
*/
|
|
44
|
+
get columns(): Array<TableColumn>;
|
|
45
|
+
/**
|
|
46
|
+
* 获取数据
|
|
47
|
+
*/
|
|
48
|
+
get list(): Array<any>;
|
|
49
|
+
/**
|
|
50
|
+
* 导出处理异常的数据
|
|
51
|
+
* @param filename
|
|
52
|
+
*/
|
|
53
|
+
exportErrorRowsToExcel(filename: string): void;
|
|
54
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as XLSX from 'xlsx';
|
|
2
|
+
import utils from "./utils";
|
|
3
|
+
import { getI18nText } from "@ticatec/i18n";
|
|
4
|
+
import i18nKeys from "./i18n_resources/i18nKeys";
|
|
5
|
+
const statusColumn = {
|
|
6
|
+
text: "status",
|
|
7
|
+
width: 150,
|
|
8
|
+
resizable: true,
|
|
9
|
+
formatter: row => {
|
|
10
|
+
if (row.status == 'P') {
|
|
11
|
+
return getI18nText(i18nKeys.status.pending);
|
|
12
|
+
}
|
|
13
|
+
else if (row.status == 'U') {
|
|
14
|
+
return getI18nText(i18nKeys.status.uploading);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
if (row.error) {
|
|
18
|
+
return row.errorText;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return getI18nText(i18nKeys.status.successful);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export default class BaseTemplate {
|
|
27
|
+
_columns;
|
|
28
|
+
rowOffset;
|
|
29
|
+
_list = [];
|
|
30
|
+
uploadFun;
|
|
31
|
+
batchSize;
|
|
32
|
+
updateProgressStatus = null;
|
|
33
|
+
/**
|
|
34
|
+
*
|
|
35
|
+
* @param columns
|
|
36
|
+
* @param uploadFun
|
|
37
|
+
* @param batchSize
|
|
38
|
+
* @param rowOffset
|
|
39
|
+
* @protected
|
|
40
|
+
*/
|
|
41
|
+
constructor(columns, uploadFun, batchSize = 50, rowOffset = 1) {
|
|
42
|
+
this._columns = columns;
|
|
43
|
+
this.rowOffset = rowOffset;
|
|
44
|
+
this.uploadFun = uploadFun;
|
|
45
|
+
this.batchSize = batchSize;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 状态更新的监听器
|
|
49
|
+
* @param value
|
|
50
|
+
*/
|
|
51
|
+
setProgressStatusListener(value) {
|
|
52
|
+
this.updateProgressStatus = value;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 整理数据,在子类可以通过重载完成数据的二次处理
|
|
56
|
+
* @param rows
|
|
57
|
+
* @protected
|
|
58
|
+
*/
|
|
59
|
+
consolidateData(rows) {
|
|
60
|
+
return rows;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 解析一个excel文件
|
|
64
|
+
* @param file
|
|
65
|
+
*/
|
|
66
|
+
async parseExcelFile(file) {
|
|
67
|
+
const buffer = await file.arrayBuffer();
|
|
68
|
+
const workbook = XLSX.read(buffer, { type: 'array' });
|
|
69
|
+
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
|
70
|
+
const range = XLSX.utils.decode_range(sheet['!ref'] || ''); // 获取范围
|
|
71
|
+
const rows = [];
|
|
72
|
+
for (let rowIndex = range.s.r + 1 + this.rowOffset; rowIndex <= range.e.r; rowIndex++) {
|
|
73
|
+
const rowObject = {};
|
|
74
|
+
for (const colDef of this._columns) {
|
|
75
|
+
const cellAddress = { r: rowIndex, c: colDef.pos };
|
|
76
|
+
const cellRef = XLSX.utils.encode_cell(cellAddress);
|
|
77
|
+
const cell = sheet[cellRef];
|
|
78
|
+
const rawValue = cell?.v;
|
|
79
|
+
const formattedValue = colDef.parser ? colDef.parser(rawValue) : rawValue;
|
|
80
|
+
utils.setNestedValue(rowObject, colDef.field, formattedValue);
|
|
81
|
+
}
|
|
82
|
+
rows.push({ data: rowObject, status: 'P' });
|
|
83
|
+
}
|
|
84
|
+
this._list = this.consolidateData(rows);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 上传数据
|
|
88
|
+
*/
|
|
89
|
+
async upload() {
|
|
90
|
+
for (let i = 0; i < this.list.length; i += this.batchSize) {
|
|
91
|
+
const chunk = this.list.slice(i, i + this.batchSize);
|
|
92
|
+
chunk.forEach(item => item.status = 'U');
|
|
93
|
+
this.updateProgressStatus?.();
|
|
94
|
+
await this.uploadFun(chunk);
|
|
95
|
+
chunk.forEach(item => item.status = 'D');
|
|
96
|
+
this.updateProgressStatus?.();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 获取表格的列定义
|
|
101
|
+
*/
|
|
102
|
+
get columns() {
|
|
103
|
+
let columns = this._columns.map(col => ({ ...col, field: `data.${col.field}` }));
|
|
104
|
+
return [...columns, statusColumn];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 获取数据
|
|
108
|
+
*/
|
|
109
|
+
get list() {
|
|
110
|
+
return [...this._list];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 导出处理异常的数据
|
|
114
|
+
* @param filename
|
|
115
|
+
*/
|
|
116
|
+
exportErrorRowsToExcel(filename) {
|
|
117
|
+
// 筛选出有错误的行
|
|
118
|
+
const errorRows = this._list.filter(row => row.error != null);
|
|
119
|
+
// 生成 Excel 数据(第一行为标题)
|
|
120
|
+
const header = [...this._columns.map(col => col.text), getI18nText(i18nKeys.errorTitle)];
|
|
121
|
+
const data = errorRows.map(row => {
|
|
122
|
+
const values = this._columns.map(col => {
|
|
123
|
+
return utils.getNestedValue(row.data, col.field);
|
|
124
|
+
});
|
|
125
|
+
return [...values, row.error];
|
|
126
|
+
});
|
|
127
|
+
const worksheetData = [header, ...data];
|
|
128
|
+
const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
|
|
129
|
+
const workbook = XLSX.utils.book_new();
|
|
130
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, getI18nText(i18nKeys.sheetName));
|
|
131
|
+
const wbout = XLSX.write(workbook, {
|
|
132
|
+
bookType: 'xlsx',
|
|
133
|
+
type: 'array'
|
|
134
|
+
});
|
|
135
|
+
// 创建 Blob 并触发下载
|
|
136
|
+
const blob = new Blob([wbout], {
|
|
137
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
138
|
+
});
|
|
139
|
+
const url = URL.createObjectURL(blob);
|
|
140
|
+
const a = document.createElement('a');
|
|
141
|
+
a.href = url;
|
|
142
|
+
a.download = filename;
|
|
143
|
+
a.style.display = 'none';
|
|
144
|
+
document.body.appendChild(a);
|
|
145
|
+
a.click();
|
|
146
|
+
document.body.removeChild(a);
|
|
147
|
+
URL.revokeObjectURL(url);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DataColumn as TableColumn } from "@ticatec/uniface-element/DataTable";
|
|
2
|
+
export type ParserText = (text: string) => any;
|
|
3
|
+
export default interface DataColumn extends TableColumn {
|
|
4
|
+
/**
|
|
5
|
+
* 在excel中的列号
|
|
6
|
+
*/
|
|
7
|
+
pos: number;
|
|
8
|
+
/**
|
|
9
|
+
* 对于的字段名
|
|
10
|
+
*/
|
|
11
|
+
field: string;
|
|
12
|
+
/**
|
|
13
|
+
* 解析函数
|
|
14
|
+
*/
|
|
15
|
+
parser?: ParserText;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Dialog from "@ticatec/uniface-element/Dialog";
|
|
3
|
+
import type {ButtonAction, ButtonActions} from "@ticatec/uniface-element/ActionBar";
|
|
4
|
+
import DataTable, {type IndicatorColumn} from "@ticatec/uniface-element/DataTable";
|
|
5
|
+
import i18n, {getI18nText} from "@ticatec/i18n";
|
|
6
|
+
import Box from "@ticatec/uniface-element/Box"
|
|
7
|
+
import {onMount} from "svelte";
|
|
8
|
+
import type BaseTemplate from "./BaseTemplate";
|
|
9
|
+
import type DataColumn from "@ticatec/uniface-element/DataTable";
|
|
10
|
+
import i18nKeys from "./i18n_resources/i18nKeys";
|
|
11
|
+
|
|
12
|
+
export let title: string;
|
|
13
|
+
|
|
14
|
+
export let width: string = "800px";
|
|
15
|
+
export let height: string = "600px"
|
|
16
|
+
|
|
17
|
+
export let closeHandler: any;
|
|
18
|
+
export let template: BaseTemplate;
|
|
19
|
+
|
|
20
|
+
type ProcessStatus = 'Init' | 'Pending' | 'Uploading' | 'Done'; //初始状态,待上传,上传中,处理完成
|
|
21
|
+
|
|
22
|
+
let status: ProcessStatus = 'Init';
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const btnChoose: ButtonAction = {
|
|
26
|
+
label: getI18nText(i18nKeys.button.open),
|
|
27
|
+
type: 'primary',
|
|
28
|
+
handler: () => {
|
|
29
|
+
uploadField.click();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const btnUpload: ButtonAction = {
|
|
34
|
+
label: getI18nText(i18nKeys.button.upload),
|
|
35
|
+
type: 'primary',
|
|
36
|
+
handler: async ()=> {
|
|
37
|
+
status = 'Uploading';
|
|
38
|
+
try {
|
|
39
|
+
await template.upload();
|
|
40
|
+
} finally {
|
|
41
|
+
status = 'Done';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const btnSave: ButtonAction = {
|
|
47
|
+
label: getI18nText(i18nKeys.button.save),
|
|
48
|
+
type: 'primary',
|
|
49
|
+
handler: async ()=> {
|
|
50
|
+
template.exportErrorRowsToExcel(`error-${filename}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let actions: ButtonActions = [btnChoose];
|
|
55
|
+
let uploadField: any;
|
|
56
|
+
let list: Array<any> = [];
|
|
57
|
+
let filename: string;
|
|
58
|
+
|
|
59
|
+
const parseExcelFile = async (excelFile: File) => {
|
|
60
|
+
filename = excelFile.name;
|
|
61
|
+
window.Indicator.show(getI18nText(i18nKeys.parsing));
|
|
62
|
+
try {
|
|
63
|
+
await template.parseExcelFile(excelFile);
|
|
64
|
+
list = template.list;
|
|
65
|
+
status = list.length > 0 ? 'Pending' : 'Init';
|
|
66
|
+
} catch (ex) {
|
|
67
|
+
window.Toast.show(getI18nText(i18nKeys.parseFailure, {name: excelFile.name}));
|
|
68
|
+
} finally {
|
|
69
|
+
window.Indicator.hide();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
let columns: Array<DataColumn>;
|
|
75
|
+
|
|
76
|
+
onMount(async () => {
|
|
77
|
+
columns = template.columns;
|
|
78
|
+
template.setProgressStatusListener(()=> {
|
|
79
|
+
list = template.list;
|
|
80
|
+
})
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
const indicatorColumn: IndicatorColumn = {
|
|
86
|
+
width: 60,
|
|
87
|
+
selectable: false,
|
|
88
|
+
displayNo: true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
$: {
|
|
93
|
+
switch (status) {
|
|
94
|
+
case 'Init':
|
|
95
|
+
actions = [btnChoose];
|
|
96
|
+
break;
|
|
97
|
+
case 'Pending':
|
|
98
|
+
actions = [btnUpload, btnChoose];
|
|
99
|
+
break;
|
|
100
|
+
case 'Uploading':
|
|
101
|
+
btnUpload.disabled = true;
|
|
102
|
+
btnChoose.disabled = true;
|
|
103
|
+
actions = [...actions];
|
|
104
|
+
break;
|
|
105
|
+
case 'Done':
|
|
106
|
+
btnUpload.disabled = false;
|
|
107
|
+
btnChoose.disabled = false;
|
|
108
|
+
const hasError = list.filter(item => item.error == null).length != list.length;
|
|
109
|
+
actions = hasError ? [btnSave, btnChoose] : [btnChoose];
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
const confirmCloseDialog = async ():Promise<boolean> => {
|
|
116
|
+
if (status == 'Uploading') {
|
|
117
|
+
window.Toast.show(getI18nText(i18nKeys.waitUploading));
|
|
118
|
+
return false;
|
|
119
|
+
} else {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<Dialog {title} {closeHandler} {actions} closeConfirm={confirmCloseDialog}
|
|
127
|
+
content$style="width: {width}; height: {height}; padding: 12px;">
|
|
128
|
+
<Box style="border: 1px solid var(--uniface-editor-border-color, #F8FAFC); width: 100%; height: 100%; cursor: {status == 'Uploading' ? 'progress' : 'default'}" round>
|
|
129
|
+
<DataTable style="width: 100%; height: 100%" {list} {indicatorColumn} {columns}>
|
|
130
|
+
|
|
131
|
+
</DataTable>
|
|
132
|
+
</Box>
|
|
133
|
+
<input type="file" bind:this={uploadField} on:change={(e) => parseExcelFile(e.target.files?.[0])} style="display: none"
|
|
134
|
+
accept=".xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
|
|
135
|
+
|
|
136
|
+
</Dialog>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type BaseTemplate from "./BaseTemplate";
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: Props & {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const FileUploadWizard: $$__sveltets_2_IsomorphicComponent<{
|
|
16
|
+
title: string;
|
|
17
|
+
width?: string;
|
|
18
|
+
height?: string;
|
|
19
|
+
closeHandler: any;
|
|
20
|
+
template: BaseTemplate;
|
|
21
|
+
}, {
|
|
22
|
+
[evt: string]: CustomEvent<any>;
|
|
23
|
+
}, {}, {}, string>;
|
|
24
|
+
type FileUploadWizard = InstanceType<typeof FileUploadWizard>;
|
|
25
|
+
export default FileUploadWizard;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare const langRes: {
|
|
2
|
+
batchUploading: {
|
|
3
|
+
status: {
|
|
4
|
+
pending: string;
|
|
5
|
+
uploading: string;
|
|
6
|
+
successful: string;
|
|
7
|
+
fail: string;
|
|
8
|
+
};
|
|
9
|
+
parsing: string;
|
|
10
|
+
parseFailure: string;
|
|
11
|
+
waitUploading: string;
|
|
12
|
+
button: {
|
|
13
|
+
upload: string;
|
|
14
|
+
save: string;
|
|
15
|
+
open: string;
|
|
16
|
+
};
|
|
17
|
+
errorTitle: string;
|
|
18
|
+
sheetName: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export default langRes;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const langRes = {
|
|
2
|
+
batchUploading: {
|
|
3
|
+
status: {
|
|
4
|
+
pending: '待上传',
|
|
5
|
+
uploading: "上传中...",
|
|
6
|
+
successful: '处理成功',
|
|
7
|
+
fail: '处理失败'
|
|
8
|
+
},
|
|
9
|
+
parsing: '解析文件...',
|
|
10
|
+
parseFailure: '无法解析文件{{name}}',
|
|
11
|
+
waitUploading: '上传过程中无法退出!',
|
|
12
|
+
button: {
|
|
13
|
+
upload: '上传',
|
|
14
|
+
save: '导出异常',
|
|
15
|
+
open: '文件'
|
|
16
|
+
},
|
|
17
|
+
errorTitle: '异常原因',
|
|
18
|
+
sheetName: '错误数据'
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export default langRes;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare const langRes: {
|
|
2
|
+
batchUploading: {
|
|
3
|
+
status: {
|
|
4
|
+
pending: string;
|
|
5
|
+
uploading: string;
|
|
6
|
+
successful: string;
|
|
7
|
+
fail: string;
|
|
8
|
+
};
|
|
9
|
+
parsing: string;
|
|
10
|
+
parseFailure: string;
|
|
11
|
+
waitUploading: string;
|
|
12
|
+
button: {
|
|
13
|
+
upload: string;
|
|
14
|
+
save: string;
|
|
15
|
+
open: string;
|
|
16
|
+
};
|
|
17
|
+
errorTitle: string;
|
|
18
|
+
sheetName: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export default langRes;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const langRes = {
|
|
2
|
+
batchUploading: {
|
|
3
|
+
status: {
|
|
4
|
+
pending: 'To upload',
|
|
5
|
+
uploading: "Uploading...",
|
|
6
|
+
successful: 'Success',
|
|
7
|
+
fail: 'Failure'
|
|
8
|
+
},
|
|
9
|
+
parsing: 'Parsing file...',
|
|
10
|
+
parseFailure: 'Cannot parse file: {{name}}',
|
|
11
|
+
waitUploading: 'Cannot exit on uploading!',
|
|
12
|
+
button: {
|
|
13
|
+
upload: 'Upload',
|
|
14
|
+
save: 'Error data',
|
|
15
|
+
open: 'File'
|
|
16
|
+
},
|
|
17
|
+
errorTitle: 'Error',
|
|
18
|
+
sheetName: 'Abnormal data'
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export default langRes;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
declare const i18nKeys: {
|
|
2
|
+
status: {
|
|
3
|
+
pending: {
|
|
4
|
+
key: string;
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
uploading: {
|
|
8
|
+
key: string;
|
|
9
|
+
text: string;
|
|
10
|
+
};
|
|
11
|
+
successful: {
|
|
12
|
+
key: string;
|
|
13
|
+
text: string;
|
|
14
|
+
};
|
|
15
|
+
fail: {
|
|
16
|
+
key: string;
|
|
17
|
+
text: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
parsing: {
|
|
21
|
+
key: string;
|
|
22
|
+
text: string;
|
|
23
|
+
};
|
|
24
|
+
parseFailure: {
|
|
25
|
+
key: string;
|
|
26
|
+
text: string;
|
|
27
|
+
};
|
|
28
|
+
waitUploading: {
|
|
29
|
+
key: string;
|
|
30
|
+
text: string;
|
|
31
|
+
};
|
|
32
|
+
button: {
|
|
33
|
+
upload: {
|
|
34
|
+
key: string;
|
|
35
|
+
text: string;
|
|
36
|
+
};
|
|
37
|
+
save: {
|
|
38
|
+
key: string;
|
|
39
|
+
text: string;
|
|
40
|
+
};
|
|
41
|
+
open: {
|
|
42
|
+
key: string;
|
|
43
|
+
text: string;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
errorTitle: {
|
|
47
|
+
key: string;
|
|
48
|
+
text: string;
|
|
49
|
+
};
|
|
50
|
+
sheetName: {
|
|
51
|
+
key: string;
|
|
52
|
+
text: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export default i18nKeys;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import langRes from "./batch_en_resource";
|
|
2
|
+
const i18nKeys = {
|
|
3
|
+
status: {
|
|
4
|
+
pending: {
|
|
5
|
+
key: "batchUploading.status.pending",
|
|
6
|
+
text: langRes.batchUploading.status.pending
|
|
7
|
+
},
|
|
8
|
+
uploading: {
|
|
9
|
+
key: "batchUploading.status.uploading",
|
|
10
|
+
text: langRes.batchUploading.status.uploading
|
|
11
|
+
},
|
|
12
|
+
successful: {
|
|
13
|
+
key: "batchUploading.status.successful",
|
|
14
|
+
text: langRes.batchUploading.status.successful
|
|
15
|
+
},
|
|
16
|
+
fail: {
|
|
17
|
+
key: "batchUploading.status.fail",
|
|
18
|
+
text: langRes.batchUploading.status.fail
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
parsing: {
|
|
22
|
+
key: 'batchUploading.parsing',
|
|
23
|
+
text: langRes.batchUploading.parsing
|
|
24
|
+
},
|
|
25
|
+
parseFailure: {
|
|
26
|
+
key: 'batchUploading.parseFailure',
|
|
27
|
+
text: langRes.batchUploading.parseFailure
|
|
28
|
+
},
|
|
29
|
+
waitUploading: {
|
|
30
|
+
key: 'batchUploading.waitUploading',
|
|
31
|
+
text: langRes.batchUploading.waitUploading
|
|
32
|
+
},
|
|
33
|
+
button: {
|
|
34
|
+
upload: {
|
|
35
|
+
key: 'batchUploading.button.upload',
|
|
36
|
+
text: langRes.batchUploading.button.upload
|
|
37
|
+
},
|
|
38
|
+
save: {
|
|
39
|
+
key: 'batchUploading.button.save',
|
|
40
|
+
text: langRes.batchUploading.button.save
|
|
41
|
+
},
|
|
42
|
+
open: {
|
|
43
|
+
key: 'batchUploading.button.open',
|
|
44
|
+
text: langRes.batchUploading.button.open
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
errorTitle: {
|
|
48
|
+
key: 'batchUploading.errorTitle',
|
|
49
|
+
text: langRes.batchUploading.errorTitle
|
|
50
|
+
},
|
|
51
|
+
sheetName: {
|
|
52
|
+
key: 'batchUploading.sheetName',
|
|
53
|
+
text: langRes.batchUploading.sheetName
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
export default i18nKeys;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const setNestedValue = (obj, path, value) => {
|
|
2
|
+
const keys = path.split('.');
|
|
3
|
+
let current = obj;
|
|
4
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
5
|
+
if (!current[keys[i]])
|
|
6
|
+
current[keys[i]] = {};
|
|
7
|
+
current = current[keys[i]];
|
|
8
|
+
}
|
|
9
|
+
current[keys[keys.length - 1]] = value;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* 获取嵌套属性的值
|
|
13
|
+
* @param obj
|
|
14
|
+
* @param path
|
|
15
|
+
*/
|
|
16
|
+
const getNestedValue = (obj, path) => {
|
|
17
|
+
return path.split('.').reduce((acc, key) => acc?.[key], obj);
|
|
18
|
+
};
|
|
19
|
+
export default {
|
|
20
|
+
setNestedValue,
|
|
21
|
+
getNestedValue
|
|
22
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ticatec/batch-data-uploader",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A reusable Svelte component for batch uploading Excel data with support for error handling, multi-language, and preprocessing.",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite dev",
|
|
7
|
+
"build": "svelte-kit sync && svelte-package",
|
|
8
|
+
"package": "svelte-kit sync && svelte-package && publint",
|
|
9
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
10
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
11
|
+
"publish:public": "npm run build && npm publish --access public"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"excel",
|
|
15
|
+
"upload",
|
|
16
|
+
"svelte",
|
|
17
|
+
"component",
|
|
18
|
+
"batch",
|
|
19
|
+
"uploader",
|
|
20
|
+
"xlsx",
|
|
21
|
+
"i18n"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"!dist/**/*.scss",
|
|
26
|
+
"!dist/**/*.test.*",
|
|
27
|
+
"!dist/**/*.spec.*"
|
|
28
|
+
],
|
|
29
|
+
"sideEffects": [
|
|
30
|
+
"**/*.css"
|
|
31
|
+
],
|
|
32
|
+
"main": "dist/index.js",
|
|
33
|
+
"svelte": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"type": "module",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./BaseTemplate": {
|
|
42
|
+
"types": "./dist/BaseTemplate.d.ts",
|
|
43
|
+
"import": "./dist/BaseTemplate.js"
|
|
44
|
+
},
|
|
45
|
+
"./DataColumn": {
|
|
46
|
+
"types": "./dist/DataColumn.d.ts",
|
|
47
|
+
"import": "./dist/DataColumn.js"
|
|
48
|
+
},
|
|
49
|
+
"./FileUploadWizard.svelte": {
|
|
50
|
+
"types": "./dist/FileUploadWizard.svelte.d.ts",
|
|
51
|
+
"import": "./dist/FileUploadWizard.svelte"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"svelte": "^5.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@sveltejs/adapter-auto": "^4.0.0",
|
|
59
|
+
"@sveltejs/adapter-static": "^3.0.8",
|
|
60
|
+
"@sveltejs/kit": "^2.0.0",
|
|
61
|
+
"@sveltejs/package": "^2.0.0",
|
|
62
|
+
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
|
63
|
+
"@ticatec/i18n": "^0.0.8",
|
|
64
|
+
"@ticatec/uniface-element": "^0.1.52",
|
|
65
|
+
"@ticatec/uniface-google-material-icons": "^0.1.2",
|
|
66
|
+
"dayjs": "^1.11.10",
|
|
67
|
+
"publint": "^0.3.2",
|
|
68
|
+
"sass": "^1.57.1",
|
|
69
|
+
"svelte": "^5.0.0",
|
|
70
|
+
"svelte-check": "^4.0.0",
|
|
71
|
+
"svelte-portal": "^2.2.1",
|
|
72
|
+
"svelte-preprocess": "^6.0.3",
|
|
73
|
+
"tslib": "^2.3.1",
|
|
74
|
+
"typescript": "^5.7.3",
|
|
75
|
+
"vite": "^5.4.11"
|
|
76
|
+
},
|
|
77
|
+
"author": "Henry Feng",
|
|
78
|
+
"license": "MIT",
|
|
79
|
+
"private": false,
|
|
80
|
+
"dependencies": {
|
|
81
|
+
"xlsx": "^0.18.5"
|
|
82
|
+
}
|
|
83
|
+
}
|