@skyfox2000/webui 1.0.13 → 1.2.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.
Files changed (162) hide show
  1. package/lib/assets/modules/file-upload-CBUcsUnR.js +170 -0
  2. package/lib/assets/modules/form-validate-CgX7aR7T.js +297 -0
  3. package/lib/assets/modules/index-Civhd8xG.js +112 -0
  4. package/lib/assets/modules/index-DQMdt51R.js +726 -0
  5. package/lib/assets/modules/{index-BEWJ_qAH.js → index-DmWrkTXX.js} +1 -1
  6. package/lib/assets/modules/{menuTabs-BXdbFZor.js → menuTabs-BRYvFWA-.js} +131 -121
  7. package/lib/assets/modules/settingInfo-BZakNKIN.js +999 -0
  8. package/lib/assets/modules/uploadList-B7XoxGOh.js +278 -0
  9. package/lib/components/common/icon/index.vue.d.ts +1 -1
  10. package/lib/components/content/dialog/index.vue.d.ts +1 -1
  11. package/lib/components/content/drawer/index.vue.d.ts +1 -1
  12. package/lib/components/content/form/index.vue.d.ts +1 -1
  13. package/lib/components/content/search/index.vue.d.ts +1 -1
  14. package/lib/components/content/table/index.vue.d.ts +1 -1
  15. package/lib/components/content/table/tableOperate.vue.d.ts +1 -1
  16. package/lib/components/content/toolbar/icontool.vue.d.ts +1 -1
  17. package/lib/components/content/toolbar/index.vue.d.ts +1 -1
  18. package/lib/components/content/tree/index.vue.d.ts +1 -1
  19. package/lib/components/form/transfer/transferTable.vue.d.ts +1 -1
  20. package/lib/components/form/treeSelect/index.vue.d.ts +1 -1
  21. package/lib/components/form/upload/uploadList.vue.d.ts +1 -1
  22. package/lib/const/options.d.ts +32 -0
  23. package/lib/directives/enter-submit.d.ts +4 -0
  24. package/lib/directives/index.d.ts +2 -0
  25. package/lib/directives/permission.d.ts +5 -0
  26. package/lib/es/AceEditor/index.js +9 -8
  27. package/lib/es/BasicLayout/index.js +28 -24
  28. package/lib/es/Error403/index.js +15 -10
  29. package/lib/es/Error404/index.js +15 -10
  30. package/lib/es/ExcelForm/index.js +380 -175
  31. package/lib/es/UploadForm/index.js +23 -20
  32. package/lib/index.d.ts +42 -2
  33. package/lib/router/index.d.ts +16 -0
  34. package/lib/stores/appInfo.d.ts +34 -0
  35. package/lib/stores/hostInfo.d.ts +9 -0
  36. package/lib/stores/pageInfo.d.ts +18 -0
  37. package/lib/stores/pinia.d.ts +3 -0
  38. package/lib/stores/settingInfo.d.ts +8 -0
  39. package/lib/stores/userInfo.d.ts +21 -0
  40. package/lib/typings/data.d.ts +80 -0
  41. package/lib/typings/form.d.ts +171 -0
  42. package/lib/typings/menu.d.ts +7 -0
  43. package/lib/typings/option.d.ts +175 -0
  44. package/lib/typings/page.d.ts +69 -0
  45. package/lib/typings/table.d.ts +181 -0
  46. package/lib/typings/tools.d.ts +130 -0
  47. package/lib/typings/tree.d.ts +72 -0
  48. package/lib/typings/upload.d.ts +161 -0
  49. package/lib/typings/urls.d.ts +69 -0
  50. package/lib/utils/cache.d.ts +23 -0
  51. package/lib/utils/data.d.ts +6 -0
  52. package/lib/utils/download.d.ts +4 -0
  53. package/lib/utils/eventbus.d.ts +16 -0
  54. package/lib/utils/export-table.d.ts +12 -0
  55. package/lib/utils/file-upload.d.ts +15 -0
  56. package/lib/utils/form-excel.d.ts +30 -0
  57. package/lib/utils/form-validate.d.ts +29 -0
  58. package/lib/utils/form.d.ts +9 -0
  59. package/lib/utils/icon-loader.d.ts +125 -0
  60. package/lib/utils/isEmpty.d.ts +1 -0
  61. package/lib/utils/main-openapis.d.ts +9 -0
  62. package/lib/utils/menu.d.ts +6 -0
  63. package/lib/utils/options.d.ts +10 -0
  64. package/lib/utils/page.d.ts +25 -0
  65. package/lib/utils/table.d.ts +21 -0
  66. package/lib/utils/tools.d.ts +18 -0
  67. package/lib/utils/tree.d.ts +3 -0
  68. package/lib/vite-env.d.ts +8 -0
  69. package/lib/webui.css +1 -1
  70. package/lib/webui.es.js +1020 -854
  71. package/package.json +7 -6
  72. package/src/components/common/icon/appicon.vue +1 -1
  73. package/src/components/common/icon/fullscreen.vue +2 -1
  74. package/src/components/common/icon/index.vue +1 -1
  75. package/src/components/common/icon/layoutIcon.vue +1 -1
  76. package/src/components/common/icon/projectIcon.vue +1 -1
  77. package/src/components/common/icon/toolIcon.vue +1 -1
  78. package/src/components/content/dialog/excelForm.vue +2 -2
  79. package/src/components/content/dialog/index.vue +1 -1
  80. package/src/components/content/dialog/uploadForm.vue +7 -6
  81. package/src/components/content/drawer/index.vue +43 -18
  82. package/src/components/content/form/formItem.vue +1 -1
  83. package/src/components/content/form/index.vue +1 -1
  84. package/src/components/content/search/index.vue +1 -1
  85. package/src/components/content/search/searchItem.vue +1 -1
  86. package/src/components/content/table/index.vue +8 -5
  87. package/src/components/content/table/tableOperate.vue +8 -4
  88. package/src/components/content/toolbar/icontool.vue +2 -2
  89. package/src/components/content/toolbar/index.vue +9 -5
  90. package/src/components/content/tree/index.vue +1 -1
  91. package/src/components/error/error403.vue +2 -2
  92. package/src/components/error/error404.vue +2 -2
  93. package/src/components/form/autoComplete/index.vue +1 -1
  94. package/src/components/form/cascader/index.vue +1 -2
  95. package/src/components/form/checkbox/index.vue +11 -5
  96. package/src/components/form/datePicker/index.vue +1 -1
  97. package/src/components/form/input/index.vue +1 -1
  98. package/src/components/form/input/inputNumber.vue +1 -1
  99. package/src/components/form/input/inputPassword.vue +1 -1
  100. package/src/components/form/radio/index.vue +1 -1
  101. package/src/components/form/radio/radioStatus.vue +1 -1
  102. package/src/components/form/rangePicker/index.vue +1 -1
  103. package/src/components/form/select/index.vue +1 -1
  104. package/src/components/form/switch/index.vue +7 -3
  105. package/src/components/form/textarea/index.vue +1 -1
  106. package/src/components/form/transfer/index.vue +1 -1
  107. package/src/components/form/transfer/transferTable.vue +42 -22
  108. package/src/components/form/treeSelect/index.vue +2 -3
  109. package/src/components/form/upload/uploadList.vue +1 -1
  110. package/src/components/layout/breadcrumb/index.vue +1 -1
  111. package/src/components/layout/header/headerExits.vue +1 -1
  112. package/src/components/layout/header/index.vue +1 -1
  113. package/src/components/layout/header/user.vue +2 -1
  114. package/src/components/layout/menu/index.vue +9 -3
  115. package/src/components/layout/menu/menuTabs.vue +10 -12
  116. package/src/components/layout/page/basicLayout.vue +1 -1
  117. package/src/const/options.ts +114 -0
  118. package/src/directives/enter-submit.ts +13 -0
  119. package/src/directives/index.ts +26 -0
  120. package/src/directives/permission.ts +144 -0
  121. package/src/index.ts +201 -0
  122. package/src/router/index.ts +196 -0
  123. package/src/stores/appInfo.ts +471 -0
  124. package/src/stores/hostInfo.ts +117 -0
  125. package/src/stores/pageInfo.ts +131 -0
  126. package/src/stores/pinia.ts +10 -0
  127. package/src/stores/settingInfo.ts +53 -0
  128. package/src/stores/userInfo.ts +392 -0
  129. package/src/typings/data.d.ts +81 -0
  130. package/src/typings/form.d.ts +172 -0
  131. package/src/typings/menu.d.ts +7 -0
  132. package/src/typings/option.d.ts +177 -0
  133. package/src/typings/page.d.ts +70 -0
  134. package/src/typings/table.d.ts +182 -0
  135. package/src/typings/tools.d.ts +131 -0
  136. package/src/typings/tree.d.ts +73 -0
  137. package/src/typings/upload.d.ts +162 -0
  138. package/src/typings/urls.d.ts +70 -0
  139. package/src/utils/cache.ts +175 -0
  140. package/src/utils/data.ts +189 -0
  141. package/src/utils/download.ts +80 -0
  142. package/src/utils/eventbus.ts +78 -0
  143. package/src/utils/export-table.ts +155 -0
  144. package/src/utils/file-upload.ts +304 -0
  145. package/src/utils/form-excel.ts +523 -0
  146. package/src/utils/form-validate.ts +368 -0
  147. package/src/utils/form.ts +188 -0
  148. package/src/utils/icon-loader.ts +412 -0
  149. package/src/utils/isEmpty.ts +18 -0
  150. package/src/utils/main-openapis.ts +72 -0
  151. package/src/utils/menu.ts +89 -0
  152. package/src/utils/options.ts +324 -0
  153. package/src/utils/page.ts +262 -0
  154. package/src/utils/table.ts +274 -0
  155. package/src/utils/tools.ts +362 -0
  156. package/src/utils/tree.ts +28 -0
  157. package/tsconfig.json +1 -8
  158. package/vite.config.ts +7 -4
  159. package/lib/assets/modules/index-BahGnrAq.js +0 -415
  160. package/lib/assets/modules/index-BoKIa2sr.js +0 -109
  161. package/lib/assets/modules/index-D47Ci-T3.js +0 -107
  162. package/lib/assets/modules/uploadList-Dzlg47V0.js +0 -182
@@ -0,0 +1,523 @@
1
+ import Validator, { ValidateError } from 'async-validator';
2
+
3
+ import { httpPost, IUrlInfo, ResStatus } from '@skyfox2000/fapi';
4
+ import { isEmpty } from './isEmpty';
5
+
6
+ import message from 'vue-m-message';
7
+ import { ValidateRule } from '@/typings/form';
8
+ import { validMessages } from './form-validate';
9
+
10
+ /**
11
+ * Excel数据处理需要标记的单元格
12
+ */
13
+ export type ExcelMarkCell = {
14
+ row: number;
15
+ col: number;
16
+ color?: string;
17
+ };
18
+
19
+ /**
20
+ * Excel标记类型
21
+ */
22
+ export type ExcelMarkInfo = {
23
+ markCells: ExcelMarkCell[];
24
+ markHeaders?: string[];
25
+ };
26
+
27
+ /**
28
+ * 处理Excel文件的通用函数,用于提取表头和数据
29
+ * @param excelBuffer Excel文件的ArrayBuffer
30
+ * @returns 包含工作簿、工作表、表头和数据的对象
31
+ */
32
+ export const processExcelFile = async (excelBuffer: ArrayBuffer) => {
33
+ // 动态导入 exceljs
34
+ const ExcelJS = await import('exceljs');
35
+ const workbook = new ExcelJS.Workbook();
36
+ await workbook.xlsx.load(excelBuffer);
37
+
38
+ // 获取第一个工作表
39
+ const worksheet = workbook.worksheets[0];
40
+ if (!worksheet) {
41
+ message.error('Excel文件不包含工作表');
42
+ return null;
43
+ }
44
+
45
+ // 提取表头和数据
46
+ const headers: string[] = [];
47
+ const excelData: Record<string, any>[] = [];
48
+
49
+ // 提取表头(第一行)
50
+ worksheet.getRow(1).eachCell((cell) => {
51
+ // 处理不同类型的单元格值,确保对象类型值被正确转换为字符串
52
+ let headerValue = '';
53
+ if (cell.value !== null && cell.value !== undefined) {
54
+ // 处理不同类型的单元格值
55
+ if (typeof cell.value === 'object') {
56
+ // 处理RichText类型
57
+ if ('richText' in cell.value && Array.isArray(cell.value.richText)) {
58
+ headerValue = cell.value.richText.map((rt: any) => rt.text || '').join('');
59
+ }
60
+ // 处理带有text属性的对象
61
+ else if ('text' in cell.value && typeof cell.value.text === 'string') {
62
+ headerValue = cell.value.text;
63
+ }
64
+ // 处理日期类型
65
+ else if (cell.value instanceof Date) {
66
+ headerValue = cell.value.toLocaleDateString();
67
+ }
68
+ // 其他对象类型,尝试获取有意义的字符串表示
69
+ else {
70
+ try {
71
+ headerValue = JSON.stringify(cell.value);
72
+ } catch {
73
+ headerValue = String(cell.value);
74
+ }
75
+ }
76
+ } else {
77
+ // 非对象类型直接转字符串
78
+ headerValue = String(cell.value);
79
+ }
80
+ }
81
+ headers.push(headerValue);
82
+ });
83
+
84
+ // 提取数据行
85
+ worksheet.eachRow((row, rowNumber) => {
86
+ if (rowNumber > 1) {
87
+ // 跳过表头
88
+ const rowData: Record<string, any> = {};
89
+ headers.forEach((header, idx) => {
90
+ if (header) {
91
+ const cell = row.getCell(idx + 1);
92
+ const cellValue = cell.value;
93
+
94
+ // 使用与表头处理相同的逻辑来提取单元格值
95
+ if (cellValue !== null && cellValue !== undefined) {
96
+ if (typeof cellValue === 'object') {
97
+ // 处理RichText类型
98
+ if ('richText' in cellValue && Array.isArray(cellValue.richText)) {
99
+ rowData[header] = cellValue.richText.map((rt: any) => rt.text || '').join('');
100
+ }
101
+ // 处理带有text属性的对象
102
+ else if ('text' in cellValue && typeof cellValue.text === 'string') {
103
+ rowData[header] = cellValue.text;
104
+ }
105
+ // 日期类型保留原样,便于验证处理
106
+ else if (cellValue instanceof Date) {
107
+ rowData[header] = cellValue;
108
+ }
109
+ // 其他对象类型
110
+ else {
111
+ rowData[header] = cellValue;
112
+ }
113
+ } else {
114
+ rowData[header] = cellValue;
115
+ }
116
+ } else {
117
+ rowData[header] = null;
118
+ }
119
+ }
120
+ });
121
+ excelData.push(rowData);
122
+ }
123
+ });
124
+
125
+ return { workbook, worksheet, headers, excelData };
126
+ };
127
+
128
+ /**
129
+ * 生成标记了错误/重复数据的Excel文件
130
+ * @param excelBuffer 原始Excel文件的ArrayBuffer
131
+ * @param markInfo 标记信息,包含要标记的单元格和表头
132
+ * @returns 标记后的Excel文件Blob
133
+ */
134
+ export const createMarkedExcelBlob = async (
135
+ excelBuffer: ArrayBuffer,
136
+ markInfo: ExcelMarkInfo,
137
+ ): Promise<{
138
+ hasError: boolean;
139
+ errBlob?: Blob;
140
+ }> => {
141
+ const ExcelJS = await import('exceljs');
142
+ // 处理Excel文件
143
+ const excelData = await processExcelFile(excelBuffer);
144
+ if (!excelData) return { hasError: true };
145
+
146
+ const { worksheet, headers: originalHeaders } = excelData;
147
+ const { markCells, markHeaders } = markInfo;
148
+
149
+ if (markCells.length === 0 && (!markHeaders || markHeaders.length === 0)) {
150
+ return { hasError: false }; // 没有需要标记的内容
151
+ }
152
+
153
+ // 处理缺失字段,将缺失字段添加到headers中
154
+ const headers = [...originalHeaders];
155
+ if (markHeaders && markHeaders.length > 0) {
156
+ markHeaders.forEach((field) => {
157
+ if (!headers.includes(field)) {
158
+ headers.push(field);
159
+ }
160
+ });
161
+ }
162
+
163
+ // 创建一个新的工作簿和工作表
164
+ const newWorkbook = new ExcelJS.Workbook();
165
+ const newWorksheet = newWorkbook.addWorksheet('Sheet1');
166
+
167
+ // 默认列宽
168
+ const defaultColumnWidth = 15;
169
+
170
+ // 创建错误单元格位置的查询表,用于快速检查
171
+ const cellMarkMap = new Map<string, string>();
172
+ markCells.forEach(({ row, col, color }) => {
173
+ const cellKey = `${row}-${col}`;
174
+ cellMarkMap.set(cellKey, color || 'FFFF0000'); // 默认红色
175
+ });
176
+
177
+ // 复制列宽并应用于新工作表
178
+ for (let i = 0; i < headers.length; i++) {
179
+ const column = newWorksheet.getColumn(i + 1);
180
+
181
+ if (i < worksheet.columnCount && i < originalHeaders.length) {
182
+ const originalCol = worksheet.getColumn(i + 1);
183
+ if (originalCol && originalCol.width) {
184
+ column.width = originalCol.width;
185
+ } else {
186
+ column.width = defaultColumnWidth;
187
+ }
188
+ } else {
189
+ column.width = defaultColumnWidth;
190
+ }
191
+ }
192
+
193
+ // 复制表头行
194
+ const headerRow = newWorksheet.getRow(1);
195
+ headers.forEach((header, index) => {
196
+ const headerCell = headerRow.getCell(index + 1);
197
+ headerCell.value = header;
198
+
199
+ const isOriginalHeader = index < originalHeaders.length;
200
+ if (isOriginalHeader && index < worksheet.columnCount) {
201
+ // 复制原表头样式
202
+ const originalCell = worksheet.getRow(1).getCell(index + 1);
203
+
204
+ // 复制样式
205
+ if (originalCell.style) headerCell.style = JSON.parse(JSON.stringify(originalCell.style));
206
+ if (originalCell.font) headerCell.font = JSON.parse(JSON.stringify(originalCell.font));
207
+ if (originalCell.alignment) headerCell.alignment = JSON.parse(JSON.stringify(originalCell.alignment));
208
+ if (originalCell.border) headerCell.border = JSON.parse(JSON.stringify(originalCell.border));
209
+ if (originalCell.numFmt) headerCell.numFmt = originalCell.numFmt;
210
+ if (originalCell.fill) headerCell.fill = JSON.parse(JSON.stringify(originalCell.fill));
211
+ }
212
+
213
+ // 仅标记缺失的表头字段,不标记数据重复的表头
214
+ if (markHeaders && markHeaders.includes(header) && !isOriginalHeader) {
215
+ headerCell.fill = {
216
+ type: 'pattern',
217
+ pattern: 'solid',
218
+ fgColor: { argb: 'FFFF0000' }, // 红色背景
219
+ };
220
+
221
+ // 添加白色文字
222
+ headerCell.font = {
223
+ name: 'Arial',
224
+ size: 10,
225
+ bold: true,
226
+ color: { argb: 'FFFFFFFF' }, // 白色文字
227
+ };
228
+ }
229
+ });
230
+ headerRow.commit();
231
+
232
+ // 处理数据行
233
+ worksheet.eachRow((row, rowNumber) => {
234
+ if (rowNumber > 1) {
235
+ // 跳过表头行
236
+ const newRow = newWorksheet.getRow(rowNumber);
237
+
238
+ // 使用表头长度来循环每个单元格
239
+ for (let colIndex = 0; colIndex < headers.length; colIndex++) {
240
+ const newCell = newRow.getCell(colIndex + 1);
241
+ const isOriginalHeader = colIndex < originalHeaders.length;
242
+
243
+ if (isOriginalHeader && colIndex < worksheet.columnCount) {
244
+ // 对原始数据中存在的列
245
+ const originalCell = row.getCell(colIndex + 1);
246
+
247
+ // 复制值
248
+ newCell.value = originalCell.value;
249
+
250
+ // 处理对象类型的单元格值,确保正确显示
251
+ if (newCell.value !== null && newCell.value !== undefined && typeof newCell.value === 'object') {
252
+ // 对于RichText,保留原格式
253
+ if (
254
+ !('richText' in newCell.value) &&
255
+ !('formula' in newCell.value) &&
256
+ !('hyperlink' in newCell.value) &&
257
+ !(newCell.value instanceof Date)
258
+ ) {
259
+ // 尝试转换为字符串显示
260
+ try {
261
+ if ('text' in newCell.value && typeof newCell.value.text === 'string') {
262
+ newCell.value = newCell.value.text;
263
+ } else {
264
+ // 其他对象类型尝试JSON序列化
265
+ newCell.value = JSON.stringify(newCell.value);
266
+ }
267
+ } catch {
268
+ // 如果转换失败,使用toString
269
+ newCell.value = String(newCell.value);
270
+ }
271
+ }
272
+ }
273
+
274
+ // 复制样式
275
+ if (originalCell.style) newCell.style = JSON.parse(JSON.stringify(originalCell.style));
276
+ if (originalCell.font) newCell.font = JSON.parse(JSON.stringify(originalCell.font));
277
+ if (originalCell.alignment) newCell.alignment = JSON.parse(JSON.stringify(originalCell.alignment));
278
+ if (originalCell.border) newCell.border = JSON.parse(JSON.stringify(originalCell.border));
279
+ if (originalCell.numFmt) newCell.numFmt = originalCell.numFmt;
280
+ if (originalCell.fill) newCell.fill = JSON.parse(JSON.stringify(originalCell.fill));
281
+
282
+ // 检查是否为需要标记的单元格
283
+ const cellKey = `${rowNumber}-${colIndex + 1}`;
284
+ if (cellMarkMap.has(cellKey)) {
285
+ // 设置背景颜色
286
+ newCell.fill = {
287
+ type: 'pattern',
288
+ pattern: 'solid',
289
+ fgColor: { argb: cellMarkMap.get(cellKey) },
290
+ };
291
+ }
292
+ } else {
293
+ // 为新增列设置空值
294
+ newCell.value = null;
295
+ }
296
+ }
297
+
298
+ // 复制行属性
299
+ if (row.height) newRow.height = row.height;
300
+ if (row.outlineLevel) newRow.outlineLevel = row.outlineLevel;
301
+
302
+ newRow.commit();
303
+ }
304
+ });
305
+
306
+ // 生成Excel文件Buffer
307
+ const excelOutput = await newWorkbook.xlsx.writeBuffer();
308
+
309
+ // 创建Blob对象
310
+ const excelBlob = new Blob([excelOutput], {
311
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
312
+ });
313
+
314
+ return { hasError: true, errBlob: excelBlob };
315
+ };
316
+
317
+ /**
318
+ * 验证Excel数据
319
+ * @param excelBuffer Excel数据
320
+ * @param rules 规则
321
+ * @returns 验证结果
322
+ */
323
+ export const validateExcel = async (
324
+ excelBuffer: ArrayBuffer,
325
+ rules?: Record<string, ValidateRule>,
326
+ ): Promise<{
327
+ hasError: boolean;
328
+ errBlob?: Blob;
329
+ }> => {
330
+ if (!rules || isEmpty(rules)) {
331
+ return { hasError: false };
332
+ }
333
+
334
+ // 处理Excel文件
335
+ const excelData = await processExcelFile(excelBuffer);
336
+ if (!excelData) return { hasError: true };
337
+
338
+ const { headers, excelData: excelJson } = excelData;
339
+
340
+ // 检查rules中的字段是否在表头中存在
341
+ const missingFields: string[] = [];
342
+ Object.keys(rules).forEach((field) => {
343
+ if (!headers.includes(field)) {
344
+ missingFields.push(field);
345
+ }
346
+ });
347
+
348
+ if (headers.length === 0 || excelJson.length === 0) {
349
+ message.error('Excel文件不包含足够的数据');
350
+ return { hasError: true };
351
+ }
352
+
353
+ // 设置验证器
354
+ const validator = new Validator({});
355
+ validator.messages(validMessages.messages());
356
+ validator.define(rules);
357
+
358
+ // 验证数据(仅验证非缺失字段)
359
+ const validationErrors = await validateExcelData(headers, excelJson, validator);
360
+
361
+ // 如果有验证错误或缺失字段,标记错误单元格和表头
362
+ if (validationErrors.length > 0 || missingFields.length > 0) {
363
+ // 准备需要标记的单元格
364
+ const markCells = validationErrors.map((error) => ({
365
+ row: error.row + 2, // 转为Excel行号(+2是因为表头占一行,且是1-based索引)
366
+ col: error.col + 1, // 转为Excel列号(+1是因为是1-based索引)
367
+ color: 'FFFF0000', // 红色
368
+ }));
369
+
370
+ // 创建标记过的Excel文件,确保传入所有表头(包括缺失的)
371
+ return createMarkedExcelBlob(excelBuffer, {
372
+ markCells,
373
+ markHeaders: missingFields,
374
+ });
375
+ }
376
+
377
+ return { hasError: false }; // 没有错误时返回null
378
+ };
379
+
380
+ type ExcelValidationError = ValidateError & {
381
+ row: number;
382
+ col: number;
383
+ header: string;
384
+ };
385
+
386
+ // 保留原有的validateExcelData方法,但根据规则进行调整
387
+ const validateExcelData = async (headers: string[], excelJson: Record<string, any>[], validator: Validator) => {
388
+ const validationErrors: ExcelValidationError[] = [];
389
+
390
+ // 遍历数据行
391
+ for (let i = 0; i < excelJson.length; i++) {
392
+ const rowData = excelJson[i];
393
+
394
+ try {
395
+ // 验证行数据
396
+ await validator.validate(rowData).catch(({ errors }) => {
397
+ // 记录验证错误,过滤掉重复的错误
398
+ const rowErrors: ExcelValidationError[] = [];
399
+
400
+ errors.forEach((err: any) => {
401
+ // 确保只有当列索引有效时才添加到验证错误中
402
+ const colIndex = headers.indexOf(err.field);
403
+ if (colIndex >= 0) {
404
+ // 检查是否已经有相同位置的错误
405
+ const exists = rowErrors.some((e) => e.row === i && e.col === colIndex);
406
+
407
+ if (!exists) {
408
+ rowErrors.push({
409
+ row: i,
410
+ col: colIndex,
411
+ header: err.field,
412
+ message: err.message.replace('${label}', headers[colIndex]),
413
+ });
414
+ }
415
+ }
416
+ });
417
+
418
+ validationErrors.push(...rowErrors);
419
+ });
420
+ } catch (error) {
421
+ console.error('验证表格数据时发生错误:', error);
422
+ message.error('验证表格数据时发生错误:' + error);
423
+ }
424
+ }
425
+
426
+ return validationErrors;
427
+ };
428
+
429
+ /**
430
+ * 检测Excel数据重复项
431
+ * @param excelBuffer Excel文件的ArrayBuffer
432
+ * @param duplicateRules 重复规则配置,字段名数组,必须在这些字段组合上唯一
433
+ * @returns 如果有重复项,返回标记了重复项的Excel Blob,否则返回null
434
+ */
435
+ export const checkExcelDuplicates = async (
436
+ excelBuffer: ArrayBuffer,
437
+ duplicateRules: string[],
438
+ url?: IUrlInfo,
439
+ ): Promise<{
440
+ hasError: boolean;
441
+ errBlob?: Blob;
442
+ }> => {
443
+ if (!duplicateRules || duplicateRules.length === 0) {
444
+ return { hasError: false };
445
+ }
446
+
447
+ // 处理Excel文件
448
+ const excelData = await processExcelFile(excelBuffer);
449
+ if (!excelData) return { hasError: true };
450
+
451
+ const { headers, excelData: rows } = excelData;
452
+
453
+ // 检查duplicateRules中的字段是否在表头中存在
454
+ const missingDuplicateFields: string[] = [];
455
+ duplicateRules.forEach((field) => {
456
+ if (!headers.includes(field)) {
457
+ missingDuplicateFields.push(field);
458
+ }
459
+ });
460
+
461
+ if (missingDuplicateFields.length > 0) {
462
+ message.error(`表头缺少重复检测所需字段: ${missingDuplicateFields.join(', ')}`);
463
+ return { hasError: true };
464
+ }
465
+
466
+ // 检测重复行
467
+ const uniqueValues = new Map<string, number>(); // 记录唯一键和它第一次出现的行索引
468
+ const duplicateIndices = new Set<number>(); // 记录所有重复行的索引
469
+ const allKeys = new Array<string>();
470
+
471
+ rows.forEach((rowData, index) => {
472
+ // 构建唯一键,将所有重复规则字段的值连接起来
473
+ const uniqueKey = duplicateRules.map((field) => rowData[field]).join('|');
474
+ allKeys.push(uniqueKey);
475
+
476
+ if (uniqueValues.has(uniqueKey)) {
477
+ // 找到重复行,记录当前索引和第一次出现的索引
478
+ duplicateIndices.add(index); // 添加当前重复行
479
+ duplicateIndices.add(uniqueValues.get(uniqueKey)!); // 添加第一次出现的行
480
+ } else {
481
+ uniqueValues.set(uniqueKey, index);
482
+ }
483
+ });
484
+
485
+ if (url) {
486
+ const duplicateRows = await httpPost(url, {
487
+ Data: allKeys,
488
+ });
489
+ if (duplicateRows?.data) {
490
+ (duplicateRows.data as number[]).forEach((index) => {
491
+ duplicateIndices.add(index);
492
+ });
493
+ }
494
+ if (duplicateRows?.status === ResStatus.ERROR) {
495
+ throw new Error(duplicateRows.msg);
496
+ }
497
+ }
498
+
499
+ // 如果有重复行,标记这些行中的重复字段
500
+ if (duplicateIndices.size > 0) {
501
+ // 准备需要标记的单元格
502
+ const markCells: Array<{ row: number; col: number; color: string }> = [];
503
+
504
+ // 对于所有重复行,标记其中的重复字段列
505
+ duplicateIndices.forEach((rowIndex) => {
506
+ duplicateRules.forEach((field) => {
507
+ const colIndex = headers.indexOf(field);
508
+ if (colIndex >= 0) {
509
+ markCells.push({
510
+ row: rowIndex + 2, // Excel行号 = 数组索引 + 2(表头和1-based索引)
511
+ col: colIndex + 1, // Excel列号 = 数组索引 + 1(1-based索引)
512
+ color: 'FFFF0000', // 红色
513
+ });
514
+ }
515
+ });
516
+ });
517
+
518
+ // 创建标记过的Excel文件
519
+ return createMarkedExcelBlob(excelBuffer, { markCells, markHeaders: missingDuplicateFields });
520
+ }
521
+
522
+ return { hasError: false }; // 没有重复数据
523
+ };