@skyfox2000/webui 1.3.2 → 1.3.4
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/lib/assets/modules/{file-upload-BVB9c-eZ.js → file-upload-BBlFaIXB.js} +1 -1
- package/lib/assets/modules/index-4kDAt8nS.js +333 -0
- package/lib/assets/modules/{index-CaaMz5sz.js → index-BG1SqSVl.js} +1 -1
- package/lib/assets/modules/{index-k_AnrbPY.js → index-m5rogIyM.js} +2 -2
- package/lib/assets/modules/{menuTabs-_Ph7P8ES.js → menuTabs-tPIz4a89.js} +2 -2
- package/lib/assets/modules/{toolIcon-QMXCkImG.js → toolIcon-DwWoD9TN.js} +1 -1
- package/lib/assets/modules/{uploadList-D-FOtndj.js → uploadList-D_Z-Y2tw.js} +482 -508
- package/lib/assets/modules/uploadList-Da7mQUNK.js +382 -0
- package/lib/components/form/autoComplete/index.vue.d.ts +10 -48
- package/lib/components/form/upload/uploadList.vue.d.ts +1 -0
- package/lib/es/AceEditor/index.js +3 -3
- package/lib/es/BasicLayout/index.js +3 -3
- package/lib/es/Error403/index.js +1 -1
- package/lib/es/Error404/index.js +1 -1
- package/lib/es/ExcelForm/index.js +332 -202
- package/lib/es/UploadForm/index.js +4 -4
- package/lib/index.d.ts +4 -3
- package/lib/utils/download.d.ts +2 -0
- package/lib/utils/excel-view.d.ts +25 -0
- package/lib/utils/form-csv.d.ts +18 -0
- package/lib/utils/form-excel.d.ts +2 -13
- package/lib/webui.css +1 -1
- package/lib/webui.es.js +773 -742
- package/package.json +2 -2
- package/src/components/common/loading/index.vue +1 -1
- package/src/components/content/dialog/excelForm.vue +384 -106
- package/src/components/content/table/index.vue +22 -10
- package/src/components/form/autoComplete/index.vue +29 -60
- package/src/components/form/upload/uploadList.vue +50 -11
- package/src/index.ts +26 -3
- package/src/utils/download.ts +31 -0
- package/src/utils/excel-view.ts +340 -0
- package/src/utils/form-csv.ts +55 -0
- package/src/utils/form-excel.ts +59 -192
- package/src/utils/options.ts +0 -1
- package/vite.config.ts +0 -1
- package/lib/assets/modules/form-excel-CsQBtfkA.js +0 -235
- package/lib/assets/modules/uploadList-CXa3siDj.js +0 -327
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Excel视图预处理工具
|
|
3
|
+
* 统一处理Excel和CSV数据的显示预处理,支持错误标记
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Excel错误标记单元格
|
|
8
|
+
*/
|
|
9
|
+
export interface ExcelMarkCell {
|
|
10
|
+
/** 行号(从1开始) */
|
|
11
|
+
row: number;
|
|
12
|
+
/** 列号(从1开始) */
|
|
13
|
+
col: number;
|
|
14
|
+
/** 标记颜色,默认红色 */
|
|
15
|
+
color?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Excel标记信息
|
|
20
|
+
*/
|
|
21
|
+
export interface ExcelMarkInfo {
|
|
22
|
+
/** 要标记的单元格列表 */
|
|
23
|
+
markCells: ExcelMarkCell[];
|
|
24
|
+
/** 要标记的表头字段列表 */
|
|
25
|
+
markHeaders?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 规范数组数据格式
|
|
30
|
+
* @interface NormalizedData
|
|
31
|
+
* @property {string[]} headers - 表头数组,如: ["姓名", "年龄", "地址"]
|
|
32
|
+
* @property {(string|number|null)[][]} rows - 数据行数组,每行对应headers的值
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* {
|
|
36
|
+
* headers: ["姓名", "年龄", "地址"],
|
|
37
|
+
* rows: [
|
|
38
|
+
* ["张三", 25, "北京市"],
|
|
39
|
+
* ["李四", 30, "上海市"],
|
|
40
|
+
* ["王五", null, "广州市"]
|
|
41
|
+
* ]
|
|
42
|
+
* }
|
|
43
|
+
*/
|
|
44
|
+
export interface NormalizedData {
|
|
45
|
+
/** 表头数组 */
|
|
46
|
+
headers: string[];
|
|
47
|
+
/** 数据行数组,每行为一个数组,对应headers的顺序 */
|
|
48
|
+
rows: any[][];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Excel预览结果
|
|
53
|
+
*/
|
|
54
|
+
export interface ExcelViewResult {
|
|
55
|
+
/** 是否成功 */
|
|
56
|
+
success: boolean;
|
|
57
|
+
/** blob URL用于预览 */
|
|
58
|
+
blobUrl?: string;
|
|
59
|
+
/** 处理后的文件名 */
|
|
60
|
+
fileName?: string;
|
|
61
|
+
/** 错误信息 */
|
|
62
|
+
error?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 将CSV文本转换为规范数组格式
|
|
67
|
+
* @param csvContent CSV文本内容
|
|
68
|
+
* @returns 规范数组数据
|
|
69
|
+
*/
|
|
70
|
+
export const csvToNormalized = (csvContent: string): NormalizedData => {
|
|
71
|
+
const lines = csvContent.split('\n').filter((line) => line.trim() !== '');
|
|
72
|
+
|
|
73
|
+
if (lines.length === 0) {
|
|
74
|
+
return { headers: [], rows: [] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 简单的CSV解析,处理逗号分隔
|
|
78
|
+
const allRows = lines.map((line) => {
|
|
79
|
+
return line.split(',').map((cell) => {
|
|
80
|
+
const trimmed = cell.trim();
|
|
81
|
+
// 尝试转换为数字
|
|
82
|
+
if (trimmed === '' || trimmed === 'null') return null;
|
|
83
|
+
const num = Number(trimmed);
|
|
84
|
+
return isNaN(num) ? trimmed : num;
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const headers = allRows[0].map((h) => String(h || ''));
|
|
89
|
+
const rows = allRows.slice(1);
|
|
90
|
+
|
|
91
|
+
return { headers, rows };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 将Excel ArrayBuffer转换为规范数组格式
|
|
96
|
+
* @param excelBuffer Excel文件的ArrayBuffer
|
|
97
|
+
* @returns 规范数组数据
|
|
98
|
+
*/
|
|
99
|
+
export const excelToNormalized = async (excelBuffer: ArrayBuffer): Promise<NormalizedData> => {
|
|
100
|
+
try {
|
|
101
|
+
// 动态导入 exceljs
|
|
102
|
+
const ExcelJS = await import('exceljs');
|
|
103
|
+
const workbook = new ExcelJS.default.Workbook();
|
|
104
|
+
await workbook.xlsx.load(excelBuffer);
|
|
105
|
+
|
|
106
|
+
// 获取第一个工作表
|
|
107
|
+
const worksheet = workbook.worksheets[0];
|
|
108
|
+
if (!worksheet) {
|
|
109
|
+
return { headers: [], rows: [] };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const headers: string[] = [];
|
|
113
|
+
const rows: any[][] = [];
|
|
114
|
+
|
|
115
|
+
// 提取表头(第一行)
|
|
116
|
+
worksheet.getRow(1).eachCell((cell) => {
|
|
117
|
+
let headerValue = '';
|
|
118
|
+
if (cell.value !== null && cell.value !== undefined) {
|
|
119
|
+
if (typeof cell.value === 'object') {
|
|
120
|
+
// 处理RichText类型
|
|
121
|
+
if ('richText' in cell.value && Array.isArray(cell.value.richText)) {
|
|
122
|
+
headerValue = cell.value.richText.map((rt: any) => rt.text || '').join('');
|
|
123
|
+
} else if ('text' in cell.value && typeof cell.value.text === 'string') {
|
|
124
|
+
headerValue = cell.value.text;
|
|
125
|
+
} else if (cell.value instanceof Date) {
|
|
126
|
+
headerValue = cell.value.toLocaleDateString();
|
|
127
|
+
} else {
|
|
128
|
+
headerValue = String(cell.value);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
headerValue = String(cell.value);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
headers.push(headerValue);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 提取数据行
|
|
138
|
+
worksheet.eachRow((row, rowNumber) => {
|
|
139
|
+
if (rowNumber > 1) {
|
|
140
|
+
// 跳过表头
|
|
141
|
+
const rowData: any[] = [];
|
|
142
|
+
headers.forEach((_, idx) => {
|
|
143
|
+
const cell = row.getCell(idx + 1);
|
|
144
|
+
const cellValue = cell.value;
|
|
145
|
+
|
|
146
|
+
if (cellValue !== null && cellValue !== undefined) {
|
|
147
|
+
if (typeof cellValue === 'object') {
|
|
148
|
+
if ('richText' in cellValue && Array.isArray(cellValue.richText)) {
|
|
149
|
+
rowData.push(cellValue.richText.map((rt: any) => rt.text || '').join(''));
|
|
150
|
+
} else if ('text' in cellValue && typeof cellValue.text === 'string') {
|
|
151
|
+
rowData.push(cellValue.text);
|
|
152
|
+
} else if (cellValue instanceof Date) {
|
|
153
|
+
rowData.push(cellValue);
|
|
154
|
+
} else {
|
|
155
|
+
rowData.push(cellValue);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
rowData.push(cellValue);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
rowData.push(null);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
rows.push(rowData);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return { headers, rows };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Excel解析失败:', error);
|
|
171
|
+
return { headers: [], rows: [] };
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 将规范数组数据转换为Excel预览,支持错误标记
|
|
177
|
+
* @param data 规范数组数据
|
|
178
|
+
* @param fileName 文件名
|
|
179
|
+
* @param markInfo 可选的标记信息
|
|
180
|
+
* @returns Excel预览结果
|
|
181
|
+
*/
|
|
182
|
+
export const toExcel = async (
|
|
183
|
+
data: NormalizedData,
|
|
184
|
+
fileName: string,
|
|
185
|
+
markInfo?: ExcelMarkInfo,
|
|
186
|
+
): Promise<ExcelViewResult> => {
|
|
187
|
+
try {
|
|
188
|
+
// 动态导入 exceljs
|
|
189
|
+
const ExcelJS = await import('exceljs');
|
|
190
|
+
const workbook = new ExcelJS.default.Workbook();
|
|
191
|
+
const worksheet = workbook.addWorksheet('Sheet1');
|
|
192
|
+
|
|
193
|
+
const { headers, rows } = data;
|
|
194
|
+
|
|
195
|
+
if (headers.length === 0) {
|
|
196
|
+
throw new Error('数据为空');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 创建错误单元格映射
|
|
200
|
+
const cellMarkMap = new Map<string, string>();
|
|
201
|
+
if (markInfo?.markCells) {
|
|
202
|
+
markInfo.markCells.forEach(({ row, col, color }) => {
|
|
203
|
+
const cellKey = `${row}-${col}`;
|
|
204
|
+
cellMarkMap.set(cellKey, color || 'FFFF0000');
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 添加表头
|
|
209
|
+
const headerRow = worksheet.getRow(1);
|
|
210
|
+
headers.forEach((header, index) => {
|
|
211
|
+
const headerCell = headerRow.getCell(index + 1);
|
|
212
|
+
headerCell.value = header;
|
|
213
|
+
|
|
214
|
+
// 设置默认表头样式
|
|
215
|
+
headerCell.font = { bold: true };
|
|
216
|
+
headerCell.fill = {
|
|
217
|
+
type: 'pattern',
|
|
218
|
+
pattern: 'solid',
|
|
219
|
+
fgColor: { argb: 'FFE0E0E0' },
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// 标记缺失的表头字段
|
|
223
|
+
if (markInfo?.markHeaders && markInfo.markHeaders.includes(header)) {
|
|
224
|
+
headerCell.fill = {
|
|
225
|
+
type: 'pattern',
|
|
226
|
+
pattern: 'solid',
|
|
227
|
+
fgColor: { argb: 'FFFF0000' },
|
|
228
|
+
};
|
|
229
|
+
headerCell.font = {
|
|
230
|
+
name: 'Arial',
|
|
231
|
+
size: 10,
|
|
232
|
+
bold: true,
|
|
233
|
+
color: { argb: 'FFFFFFFF' },
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// 添加数据行
|
|
239
|
+
rows.forEach((row, index) => {
|
|
240
|
+
const worksheetRow = worksheet.getRow(index + 2);
|
|
241
|
+
row.forEach((cell, cellIndex) => {
|
|
242
|
+
const worksheetCell = worksheetRow.getCell(cellIndex + 1);
|
|
243
|
+
worksheetCell.value = cell;
|
|
244
|
+
|
|
245
|
+
// 检查是否需要标记单元格
|
|
246
|
+
const cellKey = `${index + 2}-${cellIndex + 1}`;
|
|
247
|
+
if (cellMarkMap.has(cellKey)) {
|
|
248
|
+
worksheetCell.fill = {
|
|
249
|
+
type: 'pattern',
|
|
250
|
+
pattern: 'solid',
|
|
251
|
+
fgColor: { argb: cellMarkMap.get(cellKey) },
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// 自动调整列宽
|
|
258
|
+
worksheet.columns.forEach((column) => {
|
|
259
|
+
column.width = 15;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// 生成Excel文件Buffer
|
|
263
|
+
const excelBuffer = await workbook.xlsx.writeBuffer();
|
|
264
|
+
|
|
265
|
+
// 创建Blob对象
|
|
266
|
+
const blob = new Blob([excelBuffer], {
|
|
267
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// 创建blob URL
|
|
271
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
success: true,
|
|
275
|
+
blobUrl,
|
|
276
|
+
fileName: fileName.replace(/\.(csv|xlsx?)$/i, '.xlsx'),
|
|
277
|
+
};
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.error('转换Excel失败:', error);
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
error: error instanceof Error ? error.message : '未知错误',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* CSV文本转Excel预览
|
|
289
|
+
* @param csvContent CSV文本内容
|
|
290
|
+
* @param fileName 文件名
|
|
291
|
+
* @returns Excel预览结果
|
|
292
|
+
*/
|
|
293
|
+
export const csvToExcelView = async (csvContent: string, fileName: string): Promise<ExcelViewResult> => {
|
|
294
|
+
try {
|
|
295
|
+
const normalized = csvToNormalized(csvContent);
|
|
296
|
+
return await toExcel(normalized, fileName);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
error: error instanceof Error ? error.message : 'CSV处理失败',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Excel ArrayBuffer转Excel预览
|
|
307
|
+
* @param excelBuffer Excel文件的ArrayBuffer
|
|
308
|
+
* @param fileName 文件名
|
|
309
|
+
* @returns Excel预览结果
|
|
310
|
+
*/
|
|
311
|
+
export const excelToExcelView = async (excelBuffer: ArrayBuffer, fileName: string): Promise<ExcelViewResult> => {
|
|
312
|
+
try {
|
|
313
|
+
// Excel文件直接创建blob URL
|
|
314
|
+
const blob = new Blob([excelBuffer], {
|
|
315
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
316
|
+
});
|
|
317
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
success: true,
|
|
321
|
+
blobUrl,
|
|
322
|
+
fileName,
|
|
323
|
+
};
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return {
|
|
326
|
+
success: false,
|
|
327
|
+
error: error instanceof Error ? error.message : 'Excel处理失败',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 规范数组转Excel预览
|
|
334
|
+
* @param data 规范数组数据
|
|
335
|
+
* @param fileName 文件名
|
|
336
|
+
* @returns Excel预览结果
|
|
337
|
+
*/
|
|
338
|
+
export const normalizedToExcelView = async (data: NormalizedData, fileName: string): Promise<ExcelViewResult> => {
|
|
339
|
+
return await toExcel(data, fileName);
|
|
340
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV数据处理工具 - 统一使用excel-view处理
|
|
3
|
+
*/
|
|
4
|
+
import { csvToExcelView } from './excel-view';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 将CSV文本内容转换为Excel blob URL用于预览
|
|
8
|
+
* @param csvContent CSV文本内容
|
|
9
|
+
* @param fileName 文件名
|
|
10
|
+
* @returns blob URL和文件信息
|
|
11
|
+
* @deprecated 使用 csvToExcelView 替代
|
|
12
|
+
*/
|
|
13
|
+
export const csvToExcelBlob = async (csvContent: string, fileName: string) => {
|
|
14
|
+
const result = await csvToExcelView(csvContent, fileName);
|
|
15
|
+
if (result.success) {
|
|
16
|
+
return {
|
|
17
|
+
blobUrl: result.blobUrl!,
|
|
18
|
+
fileName: result.fileName!,
|
|
19
|
+
blob: null, // 不再返回blob对象,使用URL即可
|
|
20
|
+
};
|
|
21
|
+
} else {
|
|
22
|
+
throw new Error(result.error || 'CSV转Excel失败');
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 处理CSV文件上传前的预览
|
|
28
|
+
* @param csvBuffer CSV文件的ArrayBuffer
|
|
29
|
+
* @param fileName 文件名
|
|
30
|
+
* @returns Promise包含处理结果
|
|
31
|
+
*/
|
|
32
|
+
export const processCsvFile = async (csvBuffer: ArrayBuffer, fileName: string) => {
|
|
33
|
+
try {
|
|
34
|
+
// 将ArrayBuffer转换为文本
|
|
35
|
+
const decoder = new TextDecoder('utf-8');
|
|
36
|
+
const text = decoder.decode(csvBuffer);
|
|
37
|
+
|
|
38
|
+
// 使用excel-view统一处理
|
|
39
|
+
const result = await csvToExcelView(text, fileName);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
success: result.success,
|
|
43
|
+
blobUrl: result.blobUrl,
|
|
44
|
+
fileName: result.fileName,
|
|
45
|
+
error: result.error,
|
|
46
|
+
csvContent: text, // 保留原始CSV内容
|
|
47
|
+
};
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('CSV文件处理失败:', error);
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
error: error instanceof Error ? error.message : '未知错误',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
package/src/utils/form-excel.ts
CHANGED
|
@@ -7,23 +7,9 @@ import message from 'vue-m-message';
|
|
|
7
7
|
import { ValidateRule } from '@/typings/form';
|
|
8
8
|
import { validMessages } from './form-validate';
|
|
9
9
|
import { UploadFile } from '@/typings/upload';
|
|
10
|
+
import { toExcel, type ExcelMarkInfo } from './excel-view';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
* Excel数据处理需要标记的单元格
|
|
13
|
-
*/
|
|
14
|
-
export type ExcelMarkCell = {
|
|
15
|
-
row: number;
|
|
16
|
-
col: number;
|
|
17
|
-
color?: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Excel标记类型
|
|
22
|
-
*/
|
|
23
|
-
export type ExcelMarkInfo = {
|
|
24
|
-
markCells: ExcelMarkCell[];
|
|
25
|
-
markHeaders?: string[];
|
|
26
|
-
};
|
|
12
|
+
// ExcelMarkCell 和 ExcelMarkInfo 类型已移至 excel-view.ts 统一管理
|
|
27
13
|
|
|
28
14
|
/**
|
|
29
15
|
* 处理Excel文件的通用函数,用于提取表头和数据
|
|
@@ -133,31 +119,23 @@ export const processExcelFile = async (excelBuffer: ArrayBuffer) => {
|
|
|
133
119
|
};
|
|
134
120
|
|
|
135
121
|
/**
|
|
136
|
-
*
|
|
122
|
+
* 创建带错误标记的Excel预览
|
|
137
123
|
* @param excelBuffer 原始Excel文件的ArrayBuffer
|
|
138
|
-
* @param markInfo
|
|
139
|
-
* @
|
|
124
|
+
* @param markInfo 标记信息
|
|
125
|
+
* @param fileName 文件名
|
|
126
|
+
* @returns 标记后的Excel预览结果
|
|
140
127
|
*/
|
|
141
|
-
export const
|
|
142
|
-
|
|
143
|
-
markInfo: ExcelMarkInfo,
|
|
144
|
-
): Promise<{
|
|
145
|
-
hasError: boolean;
|
|
146
|
-
errBlob?: Blob;
|
|
147
|
-
}> => {
|
|
148
|
-
const ExcelJS = await import('exceljs');
|
|
149
|
-
// 处理Excel文件
|
|
128
|
+
export const createMarkedExcelView = async (excelBuffer: ArrayBuffer, markInfo: ExcelMarkInfo, fileName: string) => {
|
|
129
|
+
// 处理Excel文件获取数据
|
|
150
130
|
const excelData = await processExcelFile(excelBuffer);
|
|
151
|
-
if (!excelData)
|
|
152
|
-
|
|
153
|
-
const { worksheet, headers: originalHeaders } = excelData;
|
|
154
|
-
const { markCells, markHeaders } = markInfo;
|
|
155
|
-
|
|
156
|
-
if (markCells.length === 0 && (!markHeaders || markHeaders.length === 0)) {
|
|
157
|
-
return { hasError: false }; // 没有需要标记的内容
|
|
131
|
+
if (!excelData) {
|
|
132
|
+
return { success: false, error: 'Excel文件处理失败' };
|
|
158
133
|
}
|
|
159
134
|
|
|
160
|
-
|
|
135
|
+
const { headers: originalHeaders, excelRows } = excelData;
|
|
136
|
+
const { markHeaders } = markInfo;
|
|
137
|
+
|
|
138
|
+
// 处理缺失字段,添加到表头
|
|
161
139
|
const headers = [...originalHeaders];
|
|
162
140
|
if (markHeaders && markHeaders.length > 0) {
|
|
163
141
|
markHeaders.forEach((field) => {
|
|
@@ -167,158 +145,18 @@ export const createMarkedExcelBlob = async (
|
|
|
167
145
|
});
|
|
168
146
|
}
|
|
169
147
|
|
|
170
|
-
//
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// 创建错误单元格位置的查询表,用于快速检查
|
|
178
|
-
const cellMarkMap = new Map<string, string>();
|
|
179
|
-
markCells.forEach(({ row, col, color }) => {
|
|
180
|
-
const cellKey = `${row}-${col}`;
|
|
181
|
-
cellMarkMap.set(cellKey, color || 'FFFF0000'); // 默认红色
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// 复制列宽并应用于新工作表
|
|
185
|
-
for (let i = 0; i < headers.length; i++) {
|
|
186
|
-
const column = newWorksheet.getColumn(i + 1);
|
|
187
|
-
|
|
188
|
-
if (i < worksheet.columnCount && i < originalHeaders.length) {
|
|
189
|
-
const originalCol = worksheet.getColumn(i + 1);
|
|
190
|
-
if (originalCol && originalCol.width) {
|
|
191
|
-
column.width = originalCol.width;
|
|
192
|
-
} else {
|
|
193
|
-
column.width = defaultColumnWidth;
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
column.width = defaultColumnWidth;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// 复制表头行
|
|
201
|
-
const headerRow = newWorksheet.getRow(1);
|
|
202
|
-
headers.forEach((header, index) => {
|
|
203
|
-
const headerCell = headerRow.getCell(index + 1);
|
|
204
|
-
headerCell.value = header;
|
|
205
|
-
|
|
206
|
-
const isOriginalHeader = index < originalHeaders.length;
|
|
207
|
-
if (isOriginalHeader && index < worksheet.columnCount) {
|
|
208
|
-
// 复制原表头样式
|
|
209
|
-
const originalCell = worksheet.getRow(1).getCell(index + 1);
|
|
210
|
-
|
|
211
|
-
// 复制样式
|
|
212
|
-
if (originalCell.style) headerCell.style = JSON.parse(JSON.stringify(originalCell.style));
|
|
213
|
-
if (originalCell.font) headerCell.font = JSON.parse(JSON.stringify(originalCell.font));
|
|
214
|
-
if (originalCell.alignment) headerCell.alignment = JSON.parse(JSON.stringify(originalCell.alignment));
|
|
215
|
-
if (originalCell.border) headerCell.border = JSON.parse(JSON.stringify(originalCell.border));
|
|
216
|
-
if (originalCell.numFmt) headerCell.numFmt = originalCell.numFmt;
|
|
217
|
-
if (originalCell.fill) headerCell.fill = JSON.parse(JSON.stringify(originalCell.fill));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// 仅标记缺失的表头字段,不标记数据重复的表头
|
|
221
|
-
if (markHeaders && markHeaders.includes(header) && !isOriginalHeader) {
|
|
222
|
-
headerCell.fill = {
|
|
223
|
-
type: 'pattern',
|
|
224
|
-
pattern: 'solid',
|
|
225
|
-
fgColor: { argb: 'FFFF0000' }, // 红色背景
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
// 添加白色文字
|
|
229
|
-
headerCell.font = {
|
|
230
|
-
name: 'Arial',
|
|
231
|
-
size: 10,
|
|
232
|
-
bold: true,
|
|
233
|
-
color: { argb: 'FFFFFFFF' }, // 白色文字
|
|
234
|
-
};
|
|
148
|
+
// 扩展数据行以匹配新的表头长度
|
|
149
|
+
const rows = excelRows.map((row) => {
|
|
150
|
+
const newRow = [...row];
|
|
151
|
+
// 为新增的表头字段添加空值
|
|
152
|
+
while (newRow.length < headers.length) {
|
|
153
|
+
newRow.push(null);
|
|
235
154
|
}
|
|
155
|
+
return newRow;
|
|
236
156
|
});
|
|
237
|
-
headerRow.commit();
|
|
238
|
-
|
|
239
|
-
// 处理数据行
|
|
240
|
-
worksheet.eachRow((row, rowNumber) => {
|
|
241
|
-
if (rowNumber > 1) {
|
|
242
|
-
// 跳过表头行
|
|
243
|
-
const newRow = newWorksheet.getRow(rowNumber);
|
|
244
|
-
|
|
245
|
-
// 使用表头长度来循环每个单元格
|
|
246
|
-
for (let colIndex = 0; colIndex < headers.length; colIndex++) {
|
|
247
|
-
const newCell = newRow.getCell(colIndex + 1);
|
|
248
|
-
const isOriginalHeader = colIndex < originalHeaders.length;
|
|
249
|
-
|
|
250
|
-
if (isOriginalHeader && colIndex < worksheet.columnCount) {
|
|
251
|
-
// 对原始数据中存在的列
|
|
252
|
-
const originalCell = row.getCell(colIndex + 1);
|
|
253
|
-
|
|
254
|
-
// 复制值
|
|
255
|
-
newCell.value = originalCell.value;
|
|
256
|
-
|
|
257
|
-
// 处理对象类型的单元格值,确保正确显示
|
|
258
|
-
if (newCell.value !== null && newCell.value !== undefined && typeof newCell.value === 'object') {
|
|
259
|
-
// 对于RichText,保留原格式
|
|
260
|
-
if (
|
|
261
|
-
!('richText' in newCell.value) &&
|
|
262
|
-
!('formula' in newCell.value) &&
|
|
263
|
-
!('hyperlink' in newCell.value) &&
|
|
264
|
-
!(newCell.value instanceof Date)
|
|
265
|
-
) {
|
|
266
|
-
// 尝试转换为字符串显示
|
|
267
|
-
try {
|
|
268
|
-
if ('text' in newCell.value && typeof newCell.value.text === 'string') {
|
|
269
|
-
newCell.value = newCell.value.text;
|
|
270
|
-
} else {
|
|
271
|
-
// 其他对象类型尝试JSON序列化
|
|
272
|
-
newCell.value = JSON.stringify(newCell.value);
|
|
273
|
-
}
|
|
274
|
-
} catch {
|
|
275
|
-
// 如果转换失败,使用toString
|
|
276
|
-
newCell.value = String(newCell.value);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
157
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (originalCell.font) newCell.font = JSON.parse(JSON.stringify(originalCell.font));
|
|
284
|
-
if (originalCell.alignment) newCell.alignment = JSON.parse(JSON.stringify(originalCell.alignment));
|
|
285
|
-
if (originalCell.border) newCell.border = JSON.parse(JSON.stringify(originalCell.border));
|
|
286
|
-
if (originalCell.numFmt) newCell.numFmt = originalCell.numFmt;
|
|
287
|
-
if (originalCell.fill) newCell.fill = JSON.parse(JSON.stringify(originalCell.fill));
|
|
288
|
-
|
|
289
|
-
// 检查是否为需要标记的单元格
|
|
290
|
-
const cellKey = `${rowNumber}-${colIndex + 1}`;
|
|
291
|
-
if (cellMarkMap.has(cellKey)) {
|
|
292
|
-
// 设置背景颜色
|
|
293
|
-
newCell.fill = {
|
|
294
|
-
type: 'pattern',
|
|
295
|
-
pattern: 'solid',
|
|
296
|
-
fgColor: { argb: cellMarkMap.get(cellKey) },
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
} else {
|
|
300
|
-
// 为新增列设置空值
|
|
301
|
-
newCell.value = null;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// 复制行属性
|
|
306
|
-
if (row.height) newRow.height = row.height;
|
|
307
|
-
if (row.outlineLevel) newRow.outlineLevel = row.outlineLevel;
|
|
308
|
-
|
|
309
|
-
newRow.commit();
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// 生成Excel文件Buffer
|
|
314
|
-
const excelOutput = await newWorkbook.xlsx.writeBuffer();
|
|
315
|
-
|
|
316
|
-
// 创建Blob对象
|
|
317
|
-
const excelBlob = new Blob([excelOutput], {
|
|
318
|
-
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
return { hasError: true, errBlob: excelBlob };
|
|
158
|
+
// 调用excel-view的方法生成带标记的Excel
|
|
159
|
+
return await toExcel({ headers, rows }, fileName, markInfo);
|
|
322
160
|
};
|
|
323
161
|
|
|
324
162
|
/**
|
|
@@ -374,11 +212,24 @@ export const validateExcel = async (
|
|
|
374
212
|
color: 'FFFF0000', // 红色
|
|
375
213
|
}));
|
|
376
214
|
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
215
|
+
// 使用excel-view统一创建标记Excel
|
|
216
|
+
const markResult = await createMarkedExcelView(
|
|
217
|
+
excelBuffer,
|
|
218
|
+
{
|
|
219
|
+
markCells,
|
|
220
|
+
markHeaders: missingFields,
|
|
221
|
+
},
|
|
222
|
+
'validation_errors.xlsx',
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (markResult.success && markResult.blobUrl) {
|
|
226
|
+
// 从blob URL创建blob对象
|
|
227
|
+
const response = await fetch(markResult.blobUrl);
|
|
228
|
+
const errBlob = await response.blob();
|
|
229
|
+
return { hasError: true, errBlob };
|
|
230
|
+
} else {
|
|
231
|
+
return { hasError: true };
|
|
232
|
+
}
|
|
382
233
|
}
|
|
383
234
|
|
|
384
235
|
return { hasError: false }; // 没有错误时返回null
|
|
@@ -522,8 +373,24 @@ export const checkExcelDuplicates = async (
|
|
|
522
373
|
});
|
|
523
374
|
});
|
|
524
375
|
|
|
525
|
-
//
|
|
526
|
-
|
|
376
|
+
// 使用excel-view统一创建标记Excel
|
|
377
|
+
const markResult = await createMarkedExcelView(
|
|
378
|
+
excelBuffer,
|
|
379
|
+
{
|
|
380
|
+
markCells,
|
|
381
|
+
markHeaders: missingDuplicateFields,
|
|
382
|
+
},
|
|
383
|
+
'duplicate_errors.xlsx',
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (markResult.success && markResult.blobUrl) {
|
|
387
|
+
// 从blob URL创建blob对象
|
|
388
|
+
const response = await fetch(markResult.blobUrl);
|
|
389
|
+
const errBlob = await response.blob();
|
|
390
|
+
return { hasError: true, errBlob };
|
|
391
|
+
} else {
|
|
392
|
+
return { hasError: true };
|
|
393
|
+
}
|
|
527
394
|
}
|
|
528
395
|
|
|
529
396
|
return { hasError: false }; // 没有重复数据
|
package/src/utils/options.ts
CHANGED