@ticatec/batch-data-uploader 0.0.12 → 0.1.0

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.
@@ -1,6 +1,11 @@
1
1
  import BaseTemplate from "./BaseTemplate";
2
2
  import type DataColumn from "./DataColumn";
3
3
  import type { DataColumn as TableColumn } from "@ticatec/uniface-element/DataTable";
4
+ export interface ValidationResult {
5
+ valid: boolean;
6
+ hint?: string;
7
+ error?: string;
8
+ }
4
9
  export default abstract class BaseEncodingTemplate extends BaseTemplate {
5
10
  private hintColumn;
6
11
  private validColumn;
@@ -16,11 +21,15 @@ export default abstract class BaseEncodingTemplate extends BaseTemplate {
16
21
  * @param row
17
22
  * @protected
18
23
  */
19
- protected abstract validateData(row: any): Promise<any>;
24
+ protected abstract validateData(row: any): ValidationResult | Promise<ValidationResult>;
20
25
  /**
21
26
  * 数据集是否有效
22
27
  */
23
28
  get valid(): boolean;
29
+ /**
30
+ * 获取无效数据的数量
31
+ */
32
+ get invalidCount(): number;
24
33
  /**
25
34
  * 从服务器抓取数据,然后根据主键进行数据合并
26
35
  * @param rows
@@ -28,4 +37,12 @@ export default abstract class BaseEncodingTemplate extends BaseTemplate {
28
37
  */
29
38
  protected consolidateData(rows: Array<any>): Promise<Array<any>>;
30
39
  get columns(): Array<TableColumn>;
40
+ /**
41
+ * 获取所有有效的数据
42
+ */
43
+ get validDataList(): Array<any>;
44
+ /**
45
+ * 获取所有无效的数据
46
+ */
47
+ get invalidDataList(): Array<any>;
31
48
  }
@@ -1,21 +1,20 @@
1
1
  import BaseTemplate from "./BaseTemplate";
2
- import { getI18nText } from "@ticatec/i18n";
3
- import i18nKeys from "./i18n_resources/i18nKeys";
4
- const ValidData = `<span style="color: #76FF03">${getI18nText(i18nKeys.textValid)}</span>`;
5
- const InvalidData = `<span style="color: #ff3e00">${getI18nText(i18nKeys.textInvalid)}</span>`;
2
+ import i18nRes from "./i18n_resources/i18nRes";
3
+ const ValidData = `<span style="color: #76FF03">${i18nRes.textValid}</span>`;
4
+ const InvalidData = `<span style="color: #ff3e00">${i18nRes.textInvalid.key}</span>`;
6
5
  export default class BaseEncodingTemplate extends BaseTemplate {
7
6
  hintColumn = {
8
- text: getI18nText(i18nKeys.labelHint),
7
+ text: i18nRes.labelHint,
9
8
  field: "hint",
10
9
  width: 150,
11
10
  resizable: true
12
11
  };
13
12
  validColumn = {
14
- text: getI18nText(i18nKeys.labelValid),
13
+ text: i18nRes.labelValid,
15
14
  field: "valid",
16
15
  width: 90,
17
16
  align: 'center',
18
- escapeHTML: true,
17
+ escapeHTML: false, // 修复:需要渲染HTML
19
18
  formatter: valid => valid ? ValidData : InvalidData
20
19
  };
21
20
  constructor(columns, rowOffset = 1) {
@@ -27,25 +26,80 @@ export default class BaseEncodingTemplate extends BaseTemplate {
27
26
  get valid() {
28
27
  return this._list.filter(row => !row.valid).length == 0;
29
28
  }
29
+ /**
30
+ * 获取无效数据的数量
31
+ */
32
+ get invalidCount() {
33
+ return this._list.filter(row => row.valid !== true).length;
34
+ }
30
35
  /**
31
36
  * 从服务器抓取数据,然后根据主键进行数据合并
32
37
  * @param rows
33
38
  * @protected
34
39
  */
35
40
  async consolidateData(rows) {
36
- let list = await this.encodeData(this.extractData(rows));
37
- rows.forEach((item, idx) => {
38
- if (list[idx]) {
39
- let data = list[idx];
40
- item.data = { ...item.data, ...data };
41
- let result = this.validateData(data);
42
- item.valid = result.valid;
43
- item.hint = result.hint;
41
+ try {
42
+ let list = await this.encodeData(this.extractData(rows));
43
+ // 验证返回数据长度是否匹配
44
+ if (list.length !== rows.length) {
45
+ console.warn(`Encoded data length (${list.length}) doesn't match input length (${rows.length})`);
44
46
  }
45
- });
46
- return rows;
47
+ // 并行处理验证以提高性能
48
+ const validationPromises = rows.map(async (item, idx) => {
49
+ if (list[idx]) {
50
+ let data = list[idx];
51
+ item.data = { ...item.data, ...data };
52
+ try {
53
+ let result = await Promise.resolve(this.validateData(data));
54
+ item.valid = result.valid;
55
+ item.hint = result.hint || '';
56
+ item.error = result.error;
57
+ }
58
+ catch (validationError) {
59
+ console.error(`Validation error for row ${idx}:`, validationError);
60
+ item.valid = false;
61
+ item.hint = 'Validation failed';
62
+ item.error = validationError instanceof Error ? validationError.message : 'Unknown validation error';
63
+ }
64
+ }
65
+ else {
66
+ // 没有对应的编码数据
67
+ item.valid = false;
68
+ item.hint = 'No encoded data available';
69
+ item.error = 'Missing encoded data';
70
+ }
71
+ return item;
72
+ });
73
+ return await Promise.all(validationPromises);
74
+ }
75
+ catch (error) {
76
+ console.error('Error during data consolidation:', error);
77
+ // 标记所有行为无效
78
+ return rows.map(item => ({
79
+ ...item,
80
+ valid: false,
81
+ hint: 'Encoding failed',
82
+ error: error instanceof Error ? error.message : 'Unknown encoding error'
83
+ }));
84
+ }
47
85
  }
48
86
  get columns() {
49
87
  return [...super.columns, this.hintColumn, this.validColumn];
50
88
  }
89
+ /**
90
+ * 获取所有有效的数据
91
+ */
92
+ get validDataList() {
93
+ return this._list
94
+ .filter(row => row.valid === true)
95
+ .map(row => row.data);
96
+ }
97
+ /**
98
+ * 获取所有无效的数据
99
+ */
100
+ get invalidDataList() {
101
+ return this._list
102
+ .filter(row => row.valid !== true)
103
+ .map(row => ({ ...row.data, _error: row.error, _hint: row.hint }));
104
+ }
51
105
  }
@@ -27,31 +27,72 @@ export default class BaseTemplate {
27
27
  * @param file
28
28
  */
29
29
  async parseExcelFile(file) {
30
- const buffer = await file.arrayBuffer();
31
- const workbook = XLSX.read(buffer, { type: 'array' });
32
- const sheet = workbook.Sheets[workbook.SheetNames[0]];
33
- const range = XLSX.utils.decode_range(sheet['!ref'] || ''); // 获取范围
34
- const rows = [];
35
- for (let rowIndex = range.s.r + this.rowOffset; rowIndex <= range.e.r; rowIndex++) {
36
- const rowObject = {};
37
- let dummyCount = 0;
38
- for (let i = 0; i < this._columns.length; i++) {
39
- const colDef = this._columns[i];
40
- if (colDef.dummy) {
41
- dummyCount++;
30
+ try {
31
+ const buffer = await file.arrayBuffer();
32
+ const workbook = XLSX.read(buffer, { type: 'array' });
33
+ // 验证工作簿是否有工作表
34
+ if (!workbook.SheetNames || workbook.SheetNames.length === 0) {
35
+ throw new Error('Excel file contains no worksheets');
36
+ }
37
+ const sheet = workbook.Sheets[workbook.SheetNames[0]];
38
+ // 验证工作表是否存在且有数据
39
+ if (!sheet || !sheet['!ref']) {
40
+ throw new Error('Worksheet is empty or invalid');
41
+ }
42
+ const range = XLSX.utils.decode_range(sheet['!ref']); // 获取范围
43
+ // 验证是否有足够的行数
44
+ if (range.e.r < this.rowOffset) {
45
+ throw new Error(`Not enough rows in file. Expected at least ${this.rowOffset + 1} rows`);
46
+ }
47
+ const rows = [];
48
+ for (let rowIndex = range.s.r + this.rowOffset; rowIndex <= range.e.r; rowIndex++) {
49
+ const rowObject = {};
50
+ let dummyCount = 0;
51
+ let hasData = false;
52
+ for (let i = 0; i < this._columns.length; i++) {
53
+ const colDef = this._columns[i];
54
+ if (colDef.dummy) {
55
+ dummyCount++;
56
+ }
57
+ else {
58
+ // 确保列索引不会为负数
59
+ const actualColIndex = i - dummyCount;
60
+ if (actualColIndex < 0) {
61
+ console.warn(`Invalid column index for ${colDef.field}: ${actualColIndex}`);
62
+ continue;
63
+ }
64
+ const cellAddress = { r: rowIndex, c: actualColIndex };
65
+ const cellRef = XLSX.utils.encode_cell(cellAddress);
66
+ const cell = sheet[cellRef];
67
+ const rawValue = cell?.v;
68
+ // 检查是否有实际数据
69
+ if (rawValue !== undefined && rawValue !== null && rawValue !== '') {
70
+ hasData = true;
71
+ }
72
+ try {
73
+ const formattedValue = colDef.parser ? colDef.parser(rawValue) : rawValue;
74
+ utils.setNestedValue(rowObject, colDef.field, formattedValue);
75
+ }
76
+ catch (parseError) {
77
+ console.warn(`Failed to parse cell ${cellRef}:`, parseError);
78
+ utils.setNestedValue(rowObject, colDef.field, rawValue);
79
+ }
80
+ }
42
81
  }
43
- else {
44
- const cellAddress = { r: rowIndex, c: i - dummyCount };
45
- const cellRef = XLSX.utils.encode_cell(cellAddress);
46
- const cell = sheet[cellRef];
47
- const rawValue = cell?.v;
48
- const formattedValue = colDef.parser ? colDef.parser(rawValue) : rawValue;
49
- utils.setNestedValue(rowObject, colDef.field, formattedValue);
82
+ // 只添加有数据的行
83
+ if (hasData) {
84
+ rows.push(this.wrapData(rowObject));
50
85
  }
51
86
  }
52
- rows.push(this.wrapData(rowObject));
87
+ if (rows.length === 0) {
88
+ throw new Error('No valid data rows found in the file');
89
+ }
90
+ this._list = await this.consolidateData(rows);
91
+ }
92
+ catch (error) {
93
+ console.error('Failed to parse Excel file:', error);
94
+ throw new Error(`Failed to parse Excel file: ${error instanceof Error ? error.message : 'Unknown error'}`);
53
95
  }
54
- this._list = await this.consolidateData(rows);
55
96
  }
56
97
  /**
57
98
  * 获取实际待上传的数据
@@ -61,7 +102,8 @@ export default class BaseTemplate {
61
102
  let list = arr.map(item => {
62
103
  let result = {};
63
104
  for (let col of this._columns) {
64
- if (col.visible != false && col.ignore != false) {
105
+ // 修复逻辑错误:ignore应该为true时才忽略,visiblefalse时才隐藏
106
+ if (col.visible != false && col.ignore != true && !col.dummy) {
65
107
  let data = item.data;
66
108
  utils.setNestedValue(result, col.field, utils.getNestedValue(data, col.field));
67
109
  }
@@ -82,7 +124,9 @@ export default class BaseTemplate {
82
124
  * 获取表格的列定义
83
125
  */
84
126
  get columns() {
85
- return this._columns.map(col => ({ ...col, field: `data.${col.field}` }));
127
+ return this._columns
128
+ .filter(col => col.visible !== false)
129
+ .map(col => ({ ...col, field: `data.${col.field}` }));
86
130
  }
87
131
  /**
88
132
  * 获取数据
@@ -3,9 +3,20 @@ import type { DataColumn as TableColumn } from "@ticatec/uniface-element/DataTab
3
3
  import type DataColumn from "./DataColumn";
4
4
  export type UploadFun = (arr: Array<any>) => Promise<Array<any>>;
5
5
  export type UpdateProgressStatus = () => void;
6
+ export interface UploadResult {
7
+ error?: any;
8
+ errorText?: string;
9
+ success?: boolean;
10
+ }
11
+ export interface ExportErrorOptions {
12
+ includeAllData?: boolean;
13
+ separateSheets?: boolean;
14
+ originalFormat?: boolean;
15
+ }
6
16
  export default abstract class BaseUploadTemplate extends BaseTemplate {
7
17
  protected batchSize: number;
8
18
  protected updateProgressStatus: UpdateProgressStatus | null;
19
+ private _uploadAborted;
9
20
  protected constructor(columns: Array<DataColumn>, batchSize?: number, rowOffset?: number);
10
21
  /**
11
22
  * 状态更新的监听器
@@ -13,6 +24,14 @@ export default abstract class BaseUploadTemplate extends BaseTemplate {
13
24
  */
14
25
  setProgressStatusListener(value: UpdateProgressStatus): void;
15
26
  protected abstract uploadData(list: Array<any>): Promise<Array<any>>;
27
+ /**
28
+ * 中止上传
29
+ */
30
+ abortUpload(): void;
31
+ /**
32
+ * 重置上传状态
33
+ */
34
+ resetUploadStatus(): void;
16
35
  /**
17
36
  * 上传数据
18
37
  */
@@ -28,8 +47,55 @@ export default abstract class BaseUploadTemplate extends BaseTemplate {
28
47
  */
29
48
  get columns(): Array<TableColumn>;
30
49
  /**
31
- * 导出处理异常的数据
50
+ * 获取上传统计信息
51
+ */
52
+ get uploadStats(): {
53
+ total: number;
54
+ pending: number;
55
+ uploading: number;
56
+ completed: number;
57
+ success: number;
58
+ failed: number;
59
+ };
60
+ /**
61
+ * 导出处理异常的数据 - 基础版本(保持向后兼容)
32
62
  * @param filename
33
63
  */
34
64
  exportErrorRowsToExcel(filename: string): void;
65
+ /**
66
+ * 导出错误数据 - 增强版本
67
+ * @param filename
68
+ * @param options 导出选项
69
+ */
70
+ exportErrorData(filename: string, options?: ExportErrorOptions): void;
71
+ /**
72
+ * 导出原始格式 - 可以重新导入和上传
73
+ * @private
74
+ */
75
+ private _exportOriginalFormat;
76
+ /**
77
+ * 导出错误详情 - 作为第二个工作表
78
+ * @private
79
+ */
80
+ private _exportErrorDetails;
81
+ /**
82
+ * 格式化值用于导出
83
+ * @private
84
+ */
85
+ private _formatValueForExport;
86
+ /**
87
+ * 设置列宽
88
+ * @private
89
+ */
90
+ private _setColumnWidths;
91
+ /**
92
+ * 快速导出错误数据用于重新上传
93
+ * @param filename
94
+ */
95
+ exportErrorsForReupload(filename: string): void;
96
+ /**
97
+ * 导出完整报告(包含所有数据和详情)
98
+ * @param filename
99
+ */
100
+ exportFullReport(filename: string): void;
35
101
  }
@@ -1,25 +1,26 @@
1
+ // 改进的BaseUploadTemplate.ts - 完全清理元数据相关代码
1
2
  import BaseTemplate from "./BaseTemplate";
2
- import { getI18nText } from "@ticatec/i18n";
3
3
  import i18nKeys from "./i18n_resources/i18nKeys";
4
4
  import utils from "./utils";
5
5
  import * as XLSX from 'xlsx';
6
+ import i18nRes from "./i18n_resources/i18nRes";
6
7
  const statusColumn = {
7
- text: getI18nText(i18nKeys.labelStatus),
8
+ text: i18nRes.labelStatus,
8
9
  width: 240,
9
10
  resizable: true,
10
11
  formatter: row => {
11
12
  if (row.status == 'P') {
12
- return getI18nText(i18nKeys.status.pending);
13
+ return i18nRes.status.pending;
13
14
  }
14
15
  else if (row.status == 'U') {
15
- return getI18nText(i18nKeys.status.uploading);
16
+ return i18nRes.status.uploading;
16
17
  }
17
18
  else {
18
19
  if (row.error) {
19
20
  return row.errorText;
20
21
  }
21
22
  else {
22
- return getI18nText(i18nKeys.status.successful);
23
+ return i18nRes.status.successful;
23
24
  }
24
25
  }
25
26
  }
@@ -27,9 +28,10 @@ const statusColumn = {
27
28
  export default class BaseUploadTemplate extends BaseTemplate {
28
29
  batchSize;
29
30
  updateProgressStatus = null;
31
+ _uploadAborted = false;
30
32
  constructor(columns, batchSize = 50, rowOffset = 1) {
31
33
  super(columns, rowOffset);
32
- this.batchSize = batchSize;
34
+ this.batchSize = Math.max(1, batchSize);
33
35
  }
34
36
  /**
35
37
  * 状态更新的监听器
@@ -38,25 +40,83 @@ export default class BaseUploadTemplate extends BaseTemplate {
38
40
  setProgressStatusListener(value) {
39
41
  this.updateProgressStatus = value;
40
42
  }
43
+ /**
44
+ * 中止上传
45
+ */
46
+ abortUpload() {
47
+ this._uploadAborted = true;
48
+ }
49
+ /**
50
+ * 重置上传状态
51
+ */
52
+ resetUploadStatus() {
53
+ this._uploadAborted = false;
54
+ this._list.forEach(item => {
55
+ if (item.status !== 'D') {
56
+ item.status = 'P';
57
+ delete item.error;
58
+ delete item.errorText;
59
+ }
60
+ });
61
+ }
41
62
  /**
42
63
  * 上传数据
43
64
  */
44
65
  async upload() {
66
+ this._uploadAborted = false;
45
67
  for (let i = 0; i < this._list.length; i += this.batchSize) {
68
+ // 检查是否需要中止
69
+ if (this._uploadAborted) {
70
+ console.log('Upload aborted by user');
71
+ break;
72
+ }
46
73
  const chunk = this._list.slice(i, i + this.batchSize);
47
- chunk.forEach(item => item.status = 'U');
74
+ chunk.forEach(item => {
75
+ if (item.status !== 'D') { // 不重复处理已完成的项目
76
+ item.status = 'U';
77
+ delete item.error;
78
+ delete item.errorText;
79
+ }
80
+ });
48
81
  this.updateProgressStatus?.();
49
- let list = await this.uploadData(this.extractData(chunk));
50
- for (let j = 0; j < chunk.length; j++) {
51
- this._list[i + j].status = 'D';
52
- if (list[j]) {
53
- if (list[j].error) {
54
- this._list[i + j].error = list[j].error;
55
- this._list[i + j].errorText = list[j].errorText;
82
+ try {
83
+ // 只上传未完成的项目
84
+ const pendingItems = chunk.filter(item => item.status === 'U');
85
+ if (pendingItems.length === 0) {
86
+ continue; // 跳过已完成的批次
87
+ }
88
+ let list = await this.uploadData(this.extractData(pendingItems));
89
+ // 验证结果长度是否匹配
90
+ if (list.length !== pendingItems.length) {
91
+ console.warn(`Upload results length (${list.length}) doesn't match pending items (${pendingItems.length})`);
92
+ }
93
+ for (let j = 0; j < pendingItems.length; j++) {
94
+ const item = pendingItems[j];
95
+ item.status = 'D';
96
+ if (list[j]) {
97
+ if (list[j].error) {
98
+ item.error = list[j].error;
99
+ item.errorText = list[j].errorText;
100
+ }
56
101
  }
57
102
  }
58
103
  }
104
+ catch (batchError) {
105
+ console.error(`Batch upload error for items ${i}-${i + this.batchSize - 1}:`, batchError);
106
+ // 标记整个批次为失败
107
+ chunk.forEach(item => {
108
+ if (item.status === 'U') {
109
+ item.status = 'D';
110
+ item.error = batchError;
111
+ item.errorText = batchError instanceof Error ? batchError.message : 'Batch upload failed';
112
+ }
113
+ });
114
+ }
59
115
  this.updateProgressStatus?.();
116
+ // 添加小延迟以避免过快的请求
117
+ if (i + this.batchSize < this._list.length && !this._uploadAborted) {
118
+ await new Promise(resolve => setTimeout(resolve, 100));
119
+ }
60
120
  }
61
121
  }
62
122
  /**
@@ -74,40 +134,212 @@ export default class BaseUploadTemplate extends BaseTemplate {
74
134
  return [...super.columns, statusColumn];
75
135
  }
76
136
  /**
77
- * 导出处理异常的数据
137
+ * 获取上传统计信息
138
+ */
139
+ get uploadStats() {
140
+ const pending = this._list.filter(item => item.status === 'P').length;
141
+ const uploading = this._list.filter(item => item.status === 'U').length;
142
+ const completed = this._list.filter(item => item.status === 'D').length;
143
+ const success = this._list.filter(item => item.status === 'D' && !item.error).length;
144
+ const failed = this._list.filter(item => item.status === 'D' && item.error).length;
145
+ return {
146
+ total: this._list.length,
147
+ pending,
148
+ uploading,
149
+ completed,
150
+ success,
151
+ failed
152
+ };
153
+ }
154
+ /**
155
+ * 导出处理异常的数据 - 基础版本(保持向后兼容)
78
156
  * @param filename
79
157
  */
80
158
  exportErrorRowsToExcel(filename) {
81
- // 筛选出有错误的行
82
- const errorRows = this._list.filter(row => row.error != null);
83
- // 生成 Excel 数据(第一行为标题)
84
- const header = [...this._columns.map(col => col.text), getI18nText(i18nKeys.errorTitle)];
85
- const data = errorRows.map(row => {
86
- const values = this._columns.map(col => {
159
+ this.exportErrorData(filename, { originalFormat: false });
160
+ }
161
+ /**
162
+ * 导出错误数据 - 增强版本
163
+ * @param filename
164
+ * @param options 导出选项
165
+ */
166
+ exportErrorData(filename, options = {}) {
167
+ const { includeAllData = false, separateSheets = true, // 默认分离工作表
168
+ originalFormat = true } = options;
169
+ try {
170
+ const workbook = XLSX.utils.book_new();
171
+ // 第一页:重传数据(原始格式)- 用户可以直接修改和重新导入
172
+ if (originalFormat) {
173
+ this._exportOriginalFormat(workbook, includeAllData);
174
+ }
175
+ // 第二页:异常详情 - 用于查看错误信息
176
+ if (separateSheets) {
177
+ this._exportErrorDetails(workbook, includeAllData);
178
+ }
179
+ const wbout = XLSX.write(workbook, {
180
+ bookType: 'xlsx',
181
+ type: 'array'
182
+ });
183
+ // 创建 Blob 并触发下载
184
+ const blob = new Blob([wbout], {
185
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
186
+ });
187
+ const url = URL.createObjectURL(blob);
188
+ const a = document.createElement('a');
189
+ a.href = url;
190
+ a.download = filename.endsWith('.xlsx') ? filename : `${filename}.xlsx`;
191
+ a.style.display = 'none';
192
+ document.body.appendChild(a);
193
+ a.click();
194
+ document.body.removeChild(a);
195
+ URL.revokeObjectURL(url);
196
+ }
197
+ catch (error) {
198
+ console.error('Failed to export error data:', error);
199
+ throw new Error(`Failed to export error data: ${error instanceof Error ? error.message : 'Unknown error'}`);
200
+ }
201
+ }
202
+ /**
203
+ * 导出原始格式 - 可以重新导入和上传
204
+ * @private
205
+ */
206
+ _exportOriginalFormat(workbook, includeAllData) {
207
+ // 获取需要导出的数据
208
+ const dataToExport = includeAllData
209
+ ? this._list
210
+ : this._list.filter(row => row.status === 'D' && row.error);
211
+ if (dataToExport.length === 0) {
212
+ console.warn('No data to export in original format');
213
+ return;
214
+ }
215
+ // 获取可见且非虚拟的列
216
+ const exportColumns = this._columns.filter(col => col.visible !== false && !col.dummy && !col.ignore);
217
+ // 创建标题行(与原始导入格式一致)
218
+ const headers = exportColumns.map(col => col.text || col.field);
219
+ // 创建数据行
220
+ const rows = dataToExport.map(item => {
221
+ return exportColumns.map(col => {
222
+ const value = utils.getNestedValue(item.data, col.field);
223
+ // 如果有格式化函数的逆向操作,在这里处理
224
+ return this._formatValueForExport(value, col);
225
+ });
226
+ });
227
+ const worksheetData = [headers, ...rows];
228
+ const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
229
+ // 设置列宽
230
+ this._setColumnWidths(worksheet, [headers, ...rows]);
231
+ // 添加到工作簿 - 重传数据作为第一个工作表
232
+ const sheetName = includeAllData ? '全部数据重传' : '失败数据重传';
233
+ XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
234
+ }
235
+ /**
236
+ * 导出错误详情 - 作为第二个工作表
237
+ * @private
238
+ */
239
+ _exportErrorDetails(workbook, includeAllData) {
240
+ const errorRows = includeAllData
241
+ ? this._list
242
+ : this._list.filter(row => row.status === 'D' && row.error);
243
+ if (errorRows.length === 0)
244
+ return;
245
+ const visibleColumns = this._columns.filter(col => col.visible !== false && !col.dummy);
246
+ const headers = [
247
+ '行号',
248
+ ...visibleColumns.map(col => col.text || col.field),
249
+ '结果',
250
+ '错误原因'
251
+ ];
252
+ const rows = errorRows.map((row, index) => {
253
+ const values = visibleColumns.map(col => {
87
254
  return utils.getNestedValue(row.data, col.field);
88
255
  });
89
- return [...values, row.error];
256
+ // 简化状态:只有成功或失败
257
+ const result = row.error ? '✗ 失败' : '✓ 成功';
258
+ return [
259
+ index + 1, // 行号
260
+ ...values,
261
+ result,
262
+ row.errorText || row.error || ''
263
+ ];
90
264
  });
91
- const worksheetData = [header, ...data];
265
+ const worksheetData = [headers, ...rows];
92
266
  const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
93
- const workbook = XLSX.utils.book_new();
94
- XLSX.utils.book_append_sheet(workbook, worksheet, getI18nText(i18nKeys.sheetName));
95
- const wbout = XLSX.write(workbook, {
96
- bookType: 'xlsx',
97
- type: 'array'
267
+ // 设置列宽 - 错误原因列设置更宽
268
+ const colWidths = headers.map((header, colIndex) => {
269
+ let maxLength = header.length;
270
+ // 计算该列的最大长度
271
+ rows.forEach(row => {
272
+ const cellValue = String(row[colIndex] || '');
273
+ maxLength = Math.max(maxLength, cellValue.length);
274
+ });
275
+ // 错误原因列设置更宽
276
+ if (header === '错误原因') {
277
+ return { wch: Math.min(Math.max(maxLength, 20), 60) };
278
+ }
279
+ return { wch: Math.min(Math.max(maxLength + 2, 10), 30) };
280
+ });
281
+ worksheet['!cols'] = colWidths;
282
+ const sheetName = includeAllData ? '上传详情' : '异常详情';
283
+ XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
284
+ }
285
+ /**
286
+ * 格式化值用于导出
287
+ * @private
288
+ */
289
+ _formatValueForExport(value, column) {
290
+ // 如果列有解析器,可能需要逆向格式化
291
+ // 这里可以根据具体的parser类型进行逆向处理
292
+ if (value === null || value === undefined) {
293
+ return '';
294
+ }
295
+ // 日期格式化
296
+ if (value instanceof Date) {
297
+ return value.toISOString().split('T')[0]; // YYYY-MM-DD格式
298
+ }
299
+ // 数字格式化
300
+ if (typeof value === 'number') {
301
+ return value;
302
+ }
303
+ // 布尔值格式化
304
+ if (typeof value === 'boolean') {
305
+ return value ? '是' : '否';
306
+ }
307
+ return String(value);
308
+ }
309
+ /**
310
+ * 设置列宽
311
+ * @private
312
+ */
313
+ _setColumnWidths(worksheet, data) {
314
+ const colWidths = data[0]?.map((_, colIndex) => {
315
+ const maxLength = Math.max(...data.map(row => {
316
+ const cellValue = row[colIndex];
317
+ return String(cellValue || '').length;
318
+ }));
319
+ return { wch: Math.min(Math.max(maxLength + 2, 10), 50) };
320
+ }) || [];
321
+ worksheet['!cols'] = colWidths;
322
+ }
323
+ /**
324
+ * 快速导出错误数据用于重新上传
325
+ * @param filename
326
+ */
327
+ exportErrorsForReupload(filename) {
328
+ this.exportErrorData(filename, {
329
+ includeAllData: false,
330
+ separateSheets: false,
331
+ originalFormat: true
98
332
  });
99
- // 创建 Blob 并触发下载
100
- const blob = new Blob([wbout], {
101
- type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
333
+ }
334
+ /**
335
+ * 导出完整报告(包含所有数据和详情)
336
+ * @param filename
337
+ */
338
+ exportFullReport(filename) {
339
+ this.exportErrorData(filename, {
340
+ includeAllData: true,
341
+ separateSheets: true,
342
+ originalFormat: true
102
343
  });
103
- const url = URL.createObjectURL(blob);
104
- const a = document.createElement('a');
105
- a.href = url;
106
- a.download = filename;
107
- a.style.display = 'none';
108
- document.body.appendChild(a);
109
- a.click();
110
- document.body.removeChild(a);
111
- URL.revokeObjectURL(url);
112
344
  }
113
345
  }
@@ -2,12 +2,12 @@
2
2
  import Dialog from "@ticatec/uniface-element/Dialog";
3
3
  import type {ButtonAction, ButtonActions} from "@ticatec/uniface-element/ActionBar";
4
4
  import DataTable, {type IndicatorColumn} from "@ticatec/uniface-element/DataTable";
5
- import {getI18nText} from "@ticatec/i18n";
6
5
  import Box from "@ticatec/uniface-element/Box"
7
6
  import {onMount} from "svelte";
8
7
  import type DataColumn from "./DataColumn";
9
- import i18nKeys from "./i18n_resources/i18nKeys";
10
8
  import type BaseEncodingTemplate from "./BaseEncodingTemplate";
9
+ import i18nRes from "./i18n_resources/i18nRes";
10
+ import {i18nUtils} from "@ticatec/i18n";
11
11
 
12
12
  export let title: string;
13
13
  export let width: string = "800px";
@@ -18,7 +18,7 @@
18
18
 
19
19
 
20
20
  const btnChoose: ButtonAction = {
21
- label: getI18nText(i18nKeys.button.open),
21
+ label: i18nRes.button.open,
22
22
  type: 'primary',
23
23
  handler: () => {
24
24
  uploadField.value = '';
@@ -27,7 +27,7 @@
27
27
  }
28
28
 
29
29
  const btnConfirm: ButtonAction = {
30
- label: getI18nText(i18nKeys.button.confirm),
30
+ label: i18nRes.button.confirm,
31
31
  type: 'primary',
32
32
  handler: ()=> {
33
33
  confirmCallback?.(template.dataList);
@@ -43,7 +43,7 @@
43
43
  const parseExcelFile = async (excelFile: File) => {
44
44
  if (excelFile) {
45
45
  filename = excelFile.name;
46
- window.Indicator.show(getI18nText(i18nKeys.parsing));
46
+ window.Indicator.show(i18nRes.parsing);
47
47
  try {
48
48
  await template.parseExcelFile(excelFile);
49
49
  list = template.list;
@@ -52,7 +52,7 @@
52
52
  }
53
53
  } catch (ex) {
54
54
  console.error(ex);
55
- window.Toast.show(getI18nText(i18nKeys.parseFailure, {name: excelFile.name}));
55
+ window.Toast.show(i18nUtils.formatText(i18nRes.parseFailure, {name: excelFile.name}));
56
56
  } finally {
57
57
  window.Indicator.hide();
58
58
  }
@@ -1,13 +1,14 @@
1
+ <!-- 更新的FileUploadWizard.svelte - 彻底清理includeMetadata -->
1
2
  <script lang="ts">
2
3
  import Dialog from "@ticatec/uniface-element/Dialog";
3
4
  import type {ButtonAction, ButtonActions} from "@ticatec/uniface-element/ActionBar";
4
5
  import DataTable, {type IndicatorColumn} from "@ticatec/uniface-element/DataTable";
5
- import {getI18nText} from "@ticatec/i18n";
6
6
  import Box from "@ticatec/uniface-element/Box"
7
7
  import {onMount} from "svelte";
8
8
  import type DataColumn from "./DataColumn";
9
- import i18nKeys from "./i18n_resources/i18nKeys";
10
9
  import type BaseUploadTemplate from "./BaseUploadTemplate.js";
10
+ import i18nRes from "./i18n_resources/i18nRes";
11
+ import {i18nUtils} from "@ticatec/i18n";
11
12
 
12
13
  export let title: string;
13
14
  export let width: string = "800px";
@@ -20,9 +21,8 @@
20
21
 
21
22
  let status: ProcessStatus = 'Init';
22
23
 
23
-
24
24
  const btnChoose: ButtonAction = {
25
- label: getI18nText(i18nKeys.button.open),
25
+ label: i18nRes.button.open,
26
26
  type: 'primary',
27
27
  handler: () => {
28
28
  uploadField.value = '';
@@ -31,7 +31,7 @@
31
31
  }
32
32
 
33
33
  const btnUpload: ButtonAction = {
34
- label: getI18nText(i18nKeys.button.upload),
34
+ label: i18nRes.button.upload,
35
35
  type: 'primary',
36
36
  handler: async ()=> {
37
37
  status = 'Uploading';
@@ -44,11 +44,52 @@
44
44
  }
45
45
  }
46
46
 
47
+ // 原有的保存按钮(向后兼容)
47
48
  const btnSave: ButtonAction = {
48
- label: getI18nText(i18nKeys.button.save),
49
+ label: i18nRes.button.save,
49
50
  type: 'primary',
50
51
  handler: async ()=> {
51
- template.exportErrorRowsToExcel(`error-${filename}`);
52
+ const baseFilename = filename.replace(/\.[^/.]+$/, ""); // 移除扩展名
53
+ template.exportErrorRowsToExcel(`error-${baseFilename}.xlsx`);
54
+ }
55
+ }
56
+
57
+ // 新增:导出用于重新上传的数据
58
+ const btnExportForReupload: ButtonAction = {
59
+ label: '导出失败数据',
60
+ type: 'secondary',
61
+ handler: async ()=> {
62
+ const baseFilename = filename.replace(/\.[^/.]+$/, ""); // 移除扩展名
63
+ template.exportErrorData(`重传-${baseFilename}.xlsx`, {
64
+ includeAllData: false,
65
+ separateSheets: true,
66
+ originalFormat: true
67
+ });
68
+ }
69
+ }
70
+
71
+ // 新增:导出完整报告
72
+ const btnExportFullReport: ButtonAction = {
73
+ label: '导出完整报告',
74
+ type: 'secondary',
75
+ handler: async ()=> {
76
+ const baseFilename = filename.replace(/\.[^/.]+$/, ""); // 移除扩展名
77
+ template.exportErrorData(`报告-${baseFilename}.xlsx`, {
78
+ includeAllData: true,
79
+ separateSheets: true,
80
+ originalFormat: true
81
+ });
82
+ }
83
+ }
84
+
85
+ // 新增:重置失败数据状态
86
+ const btnResetErrors: ButtonAction = {
87
+ label: '重置失败数据',
88
+ type: 'secondary',
89
+ handler: async ()=> {
90
+ template.resetUploadStatus();
91
+ list = template.list;
92
+ status = 'Pending';
52
93
  }
53
94
  }
54
95
 
@@ -60,20 +101,21 @@
60
101
  const parseExcelFile = async (excelFile: File) => {
61
102
  if (excelFile) {
62
103
  filename = excelFile.name;
63
- window.Indicator.show(getI18nText(i18nKeys.parsing));
104
+ window.Indicator.show(i18nRes.parsing);
64
105
  try {
65
106
  await template.parseExcelFile(excelFile);
66
107
  list = template.list;
67
108
  status = list.length > 0 ? 'Pending' : 'Init';
68
109
  } catch (ex) {
69
- window.Toast.show(getI18nText(i18nKeys.parseFailure, {name: excelFile.name}));
110
+ console.error('Parse file error:', ex);
111
+ window.Toast.show(i18nUtils.formatText(i18nRes.parseFailure, {name: excelFile.name}));
112
+ status = 'Init'; // 确保解析失败时重置状态
70
113
  } finally {
71
114
  window.Indicator.hide();
72
115
  }
73
116
  }
74
117
  }
75
118
 
76
-
77
119
  let columns: Array<DataColumn>;
78
120
 
79
121
  onMount(async () => {
@@ -83,15 +125,12 @@
83
125
  })
84
126
  });
85
127
 
86
-
87
-
88
128
  const indicatorColumn: IndicatorColumn = {
89
129
  width: 40,
90
130
  selectable: false,
91
131
  displayNo: true
92
132
  }
93
133
 
94
-
95
134
  $: {
96
135
  switch (status) {
97
136
  case 'Init':
@@ -108,32 +147,76 @@
108
147
  case 'Done':
109
148
  btnUpload.disabled = false;
110
149
  btnChoose.disabled = false;
111
- const hasError = list.filter(item => item.error == null).length != list.length;
112
- actions = hasError ? [btnSave, btnChoose] : [btnChoose];
150
+
151
+ // 只有在Done状态时才获取统计信息并判断按钮显示
152
+ const stats = template.uploadStats;
153
+ const hasError = stats.failed > 0;
154
+ const hasSuccess = stats.success > 0;
155
+
156
+ // 根据上传结果提供不同的操作选项
157
+ if (hasError && hasSuccess) {
158
+ // 部分成功:提供重传错误数据、导出报告等选项
159
+ actions = [
160
+ btnExportForReupload,
161
+ btnExportFullReport,
162
+ btnResetErrors,
163
+ btnSave, // 保持向后兼容
164
+ btnChoose
165
+ ];
166
+ } else if (hasError && !hasSuccess) {
167
+ // 全部失败:提供重传和导出选项
168
+ actions = [
169
+ btnExportForReupload,
170
+ btnSave,
171
+ btnChoose
172
+ ];
173
+ } else if (hasSuccess && !hasError) {
174
+ // 全部成功:只提供导出报告和重新上传
175
+ actions = [btnExportFullReport, btnChoose];
176
+ } else {
177
+ // 没有数据或其他情况
178
+ actions = [btnChoose];
179
+ }
113
180
  break;
114
181
  }
115
182
  }
116
183
 
117
-
118
184
  const confirmCloseDialog = async ():Promise<boolean> => {
119
185
  if (status == 'Uploading') {
120
- window.Toast.show(getI18nText(i18nKeys.waitUploading));
186
+ window.Toast.show(i18nRes.waitUploading);
121
187
  return false;
122
188
  } else {
123
189
  return true;
124
190
  }
125
191
  }
126
192
 
193
+ // 显示上传统计信息
194
+ $: uploadStatsText = (() => {
195
+ if (status === 'Done' && list.length > 0) {
196
+ const stats = template.uploadStats;
197
+ return `总计: ${stats.total}, 成功: ${stats.success}, 失败: ${stats.failed}`;
198
+ }
199
+ return '';
200
+ })();
201
+
127
202
  </script>
128
203
 
129
204
  <Dialog {title} {closeHandler} {actions} closeConfirm={confirmCloseDialog}
130
205
  content$style="width: {width}; height: {height}; padding: 12px;">
206
+
207
+ <!-- 添加状态信息显示 -->
208
+ {#if uploadStatsText}
209
+ <div style="margin-bottom: 8px; padding: 8px; background: #f5f5f5; border-radius: 4px; font-size: 14px;">
210
+ {uploadStatsText}
211
+ </div>
212
+ {/if}
213
+
131
214
  <Box style="border: 1px solid var(--uniface-editor-border-color, #F8FAFC); width: 100%; height: 100%; cursor: {status == 'Uploading' ? 'progress' : 'default'}" round>
132
215
  <DataTable style="width: 100%; height: 100%" {list} {indicatorColumn} {columns}>
133
-
134
216
  </DataTable>
135
217
  </Box>
218
+
136
219
  <input type="file" bind:this={uploadField} on:change={(e) => parseExcelFile(e.target.files?.[0])} style="display: none"
137
220
  accept=".xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
138
221
 
139
- </Dialog>
222
+ </Dialog>
@@ -0,0 +1,2 @@
1
+ declare const i18nRes: any;
2
+ export default i18nRes;
@@ -0,0 +1,29 @@
1
+ import { i18nUtils } from "@ticatec/i18n";
2
+ const langRes = {
3
+ batchUploading: {
4
+ status: {
5
+ pending: "To upload",
6
+ uploading: "Uploading...",
7
+ successful: "Success",
8
+ fail: "Failure"
9
+ },
10
+ parsing: "Parsing file...",
11
+ parseFailure: "Cannot parse file: {{name}}",
12
+ waitUploading: "Cannot exit during uploading!",
13
+ button: {
14
+ upload: "Upload",
15
+ save: "Save error data",
16
+ open: "Open",
17
+ confirm: "Confirm"
18
+ },
19
+ errorTitle: "Error",
20
+ sheetName: "Abnormal data",
21
+ labelStatus: "Status",
22
+ labelValid: "Validity",
23
+ textValid: "Yes",
24
+ textInvalid: "No",
25
+ labelHint: "Hint"
26
+ }
27
+ };
28
+ const i18nRes = i18nUtils.createResourceProxy(langRes, 'omni');
29
+ export default i18nRes;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticatec/batch-data-uploader",
3
- "version": "0.0.12",
3
+ "version": "0.1.0",
4
4
  "description": "A reusable Svelte component for batch uploading Excel data with support for error handling, multi-language, and preprocessing.",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -73,8 +73,8 @@
73
73
  "@sveltejs/kit": "^2.0.0",
74
74
  "@sveltejs/package": "^2.0.0",
75
75
  "@sveltejs/vite-plugin-svelte": "^4.0.0",
76
- "@ticatec/i18n": "^0.0.8",
77
- "@ticatec/uniface-element": "^0.1.52",
76
+ "@ticatec/i18n": "^0.2.0",
77
+ "@ticatec/uniface-element": "^0.1.71",
78
78
  "@ticatec/uniface-google-material-icons": "^0.1.2",
79
79
  "dayjs": "^1.11.10",
80
80
  "publint": "^0.3.2",