@skyfox2000/webui 1.3.4 → 1.3.6

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 (55) hide show
  1. package/.vscode/settings.json +1 -1
  2. package/lib/assets/modules/{file-upload-BBlFaIXB.js → file-upload-DhPgqGdk.js} +50 -50
  3. package/lib/assets/modules/index-02J2AYth.js +377 -0
  4. package/lib/assets/modules/{index-m5rogIyM.js → index-C31q4LHC.js} +2 -2
  5. package/lib/assets/modules/{index-BG1SqSVl.js → index-CCpTizF9.js} +1 -1
  6. package/lib/assets/modules/{menuTabs-tPIz4a89.js → menuTabs-DyhSKN9r.js} +2 -2
  7. package/lib/assets/modules/{toolIcon-DwWoD9TN.js → toolIcon-CqM4gBIc.js} +1 -1
  8. package/lib/assets/modules/{uploadList-D_Z-Y2tw.js → uploadList-DAVjJkqz.js} +511 -476
  9. package/lib/assets/modules/{uploadList-Da7mQUNK.js → uploadList-ZajZKqaS.js} +4 -4
  10. package/lib/components/common/alert/index.vue.d.ts +13 -0
  11. package/lib/components/common/icon/helper.vue.d.ts +1 -0
  12. package/lib/components/common/index.d.ts +2 -0
  13. package/lib/components/content/form/formItem.vue.d.ts +1 -0
  14. package/lib/components/content/table/index.vue.d.ts +95 -4
  15. package/lib/components/form/input/index.vue.d.ts +4 -1
  16. package/lib/components/form/select/index.vue.d.ts +2 -0
  17. package/lib/components/form/treeSelect/index.vue.d.ts +11 -2
  18. package/lib/components/index.d.ts +1 -1
  19. package/lib/es/AceEditor/index.js +3 -3
  20. package/lib/es/BasicLayout/index.js +3 -3
  21. package/lib/es/Error403/index.js +1 -1
  22. package/lib/es/Error404/index.js +1 -1
  23. package/lib/es/ExcelForm/index.js +317 -274
  24. package/lib/es/UploadForm/index.js +4 -4
  25. package/lib/index.d.ts +1 -1
  26. package/lib/typings/form.d.ts +2 -2
  27. package/lib/typings/option.d.ts +2 -2
  28. package/lib/utils/excel-preview.d.ts +24 -0
  29. package/lib/utils/form-excel.d.ts +17 -4
  30. package/lib/utils/options.d.ts +2 -2
  31. package/lib/webui.css +1 -1
  32. package/lib/webui.es.js +759 -747
  33. package/package.json +1 -1
  34. package/src/components/common/alert/index.vue +76 -0
  35. package/src/components/common/icon/helper.vue +7 -1
  36. package/src/components/common/index.ts +4 -1
  37. package/src/components/common/loading/index.vue +1 -1
  38. package/src/components/content/dialog/excelForm.vue +343 -313
  39. package/src/components/content/form/formItem.vue +6 -2
  40. package/src/components/content/table/index.vue +9 -6
  41. package/src/components/form/autoComplete/index.vue +9 -3
  42. package/src/components/form/cascader/index.vue +8 -6
  43. package/src/components/form/input/index.vue +16 -3
  44. package/src/components/form/select/index.vue +5 -11
  45. package/src/components/form/treeSelect/index.vue +22 -17
  46. package/src/components/index.ts +1 -0
  47. package/src/index.ts +1 -0
  48. package/src/typings/form.d.ts +2 -2
  49. package/src/typings/option.d.ts +2 -2
  50. package/src/utils/excel-preview.ts +188 -0
  51. package/src/utils/file-upload.ts +0 -2
  52. package/src/utils/form-excel.ts +132 -126
  53. package/src/utils/options.ts +80 -22
  54. package/src/utils/table.ts +15 -2
  55. package/lib/assets/modules/index-4kDAt8nS.js +0 -333
@@ -7,7 +7,7 @@ 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
+ import { toExcel, type ExcelMarkInfo, excelToNormalized } from './excel-view';
11
11
 
12
12
  // ExcelMarkCell 和 ExcelMarkInfo 类型已移至 excel-view.ts 统一管理
13
13
 
@@ -17,105 +17,31 @@ import { toExcel, type ExcelMarkInfo } from './excel-view';
17
17
  * @returns 包含工作簿、工作表、表头和数据的对象
18
18
  */
19
19
  export const processExcelFile = async (excelBuffer: ArrayBuffer) => {
20
- // 动态导入 exceljs
21
- const ExcelJS = await import('exceljs');
22
- const workbook = new ExcelJS.default.Workbook();
23
- await workbook.xlsx.load(excelBuffer);
24
-
25
- // 获取第一个工作表
26
- const worksheet = workbook.worksheets[0];
27
- if (!worksheet) {
20
+ // 使用excel-view.ts中的统一方法
21
+ const normalized = await excelToNormalized(excelBuffer);
22
+
23
+ if (normalized.headers.length === 0) {
28
24
  message.error('Excel文件不包含工作表');
29
25
  return null;
30
26
  }
31
27
 
32
- // 提取表头和数据
33
- const headers: string[] = [];
34
- // 表头对象数据行
28
+ // 转换为原有格式以保持兼容性
35
29
  const excelData: Record<string, any>[] = [];
36
- // 纯数据行
37
- const excelRows: any[][] = [];
38
-
39
- // 提取表头(第一行)
40
- worksheet.getRow(1).eachCell((cell) => {
41
- // 处理不同类型的单元格值,确保对象类型值被正确转换为字符串
42
- let headerValue = '';
43
- if (cell.value !== null && cell.value !== undefined) {
44
- // 处理不同类型的单元格值
45
- if (typeof cell.value === 'object') {
46
- // 处理RichText类型
47
- if ('richText' in cell.value && Array.isArray(cell.value.richText)) {
48
- headerValue = cell.value.richText.map((rt: any) => rt.text || '').join('');
49
- }
50
- // 处理带有text属性的对象
51
- else if ('text' in cell.value && typeof cell.value.text === 'string') {
52
- headerValue = cell.value.text;
53
- }
54
- // 处理日期类型
55
- else if (cell.value instanceof Date) {
56
- headerValue = cell.value.toLocaleDateString();
57
- }
58
- // 其他对象类型,尝试获取有意义的字符串表示
59
- else {
60
- try {
61
- headerValue = JSON.stringify(cell.value);
62
- } catch {
63
- headerValue = String(cell.value);
64
- }
65
- }
66
- } else {
67
- // 非对象类型直接转字符串
68
- headerValue = String(cell.value);
30
+ normalized.rows.forEach((row) => {
31
+ const rowData: Record<string, any> = {};
32
+ normalized.headers.forEach((header, idx) => {
33
+ if (header) {
34
+ rowData[header] = row[idx];
69
35
  }
70
- }
71
- headers.push(headerValue);
72
- });
73
-
74
- // 提取数据行
75
- worksheet.eachRow((row, rowNumber) => {
76
- if (rowNumber > 1) {
77
- // 跳过表头
78
- const rowData: Record<string, any> = {};
79
- const rowDataArray: any[] = [];
80
- headers.forEach((header, idx) => {
81
- if (header) {
82
- const cell = row.getCell(idx + 1);
83
- const cellValue = cell.value;
84
-
85
- // 使用与表头处理相同的逻辑来提取单元格值
86
- if (cellValue !== null && cellValue !== undefined) {
87
- if (typeof cellValue === 'object') {
88
- // 处理RichText类型
89
- if ('richText' in cellValue && Array.isArray(cellValue.richText)) {
90
- rowData[header] = cellValue.richText.map((rt: any) => rt.text || '').join('');
91
- }
92
- // 处理带有text属性的对象
93
- else if ('text' in cellValue && typeof cellValue.text === 'string') {
94
- rowData[header] = cellValue.text;
95
- }
96
- // 日期类型保留原样,便于验证处理
97
- else if (cellValue instanceof Date) {
98
- rowData[header] = cellValue;
99
- }
100
- // 其他对象类型
101
- else {
102
- rowData[header] = cellValue;
103
- }
104
- } else {
105
- rowData[header] = cellValue;
106
- }
107
- } else {
108
- rowData[header] = null;
109
- }
110
- rowDataArray.push(rowData[header]);
111
- }
112
- });
113
- excelData.push(rowData);
114
- excelRows.push(rowDataArray);
115
- }
36
+ });
37
+ excelData.push(rowData);
116
38
  });
117
39
 
118
- return { workbook, worksheet, headers, excelData, excelRows };
40
+ return {
41
+ headers: normalized.headers,
42
+ excelData,
43
+ excelRows: normalized.rows,
44
+ };
119
45
  };
120
46
 
121
47
  /**
@@ -170,7 +96,8 @@ export const validateExcel = async (
170
96
  rules?: Record<string, ValidateRule>,
171
97
  ): Promise<{
172
98
  hasError: boolean;
173
- errBlob?: Blob;
99
+ markCells?: Array<{ row: number; col: number; color: string }>;
100
+ markHeaders?: string[];
174
101
  }> => {
175
102
  if (!rules || isEmpty(rules)) {
176
103
  return { hasError: false };
@@ -203,7 +130,7 @@ export const validateExcel = async (
203
130
  // 验证数据(仅验证非缺失字段)
204
131
  const validationErrors = await validateExcelData(headers, excelJson, validator);
205
132
 
206
- // 如果有验证错误或缺失字段,标记错误单元格和表头
133
+ // 如果有验证错误或缺失字段,返回标记信息
207
134
  if (validationErrors.length > 0 || missingFields.length > 0) {
208
135
  // 准备需要标记的单元格
209
136
  const markCells = validationErrors.map((error) => ({
@@ -212,27 +139,14 @@ export const validateExcel = async (
212
139
  color: 'FFFF0000', // 红色
213
140
  }));
214
141
 
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
- }
142
+ return {
143
+ hasError: true,
144
+ markCells,
145
+ markHeaders: missingFields,
146
+ };
233
147
  }
234
148
 
235
- return { hasError: false }; // 没有错误时返回null
149
+ return { hasError: false };
236
150
  };
237
151
 
238
152
  type ExcelValidationError = ValidateError & {
@@ -296,7 +210,8 @@ export const checkExcelDuplicates = async (
296
210
  url?: IUrlInfo,
297
211
  ): Promise<{
298
212
  hasError: boolean;
299
- errBlob?: Blob;
213
+ markCells?: Array<{ row: number; col: number; color: string }>;
214
+ markHeaders?: string[];
300
215
  }> => {
301
216
  if (!duplicateRules || duplicateRules.length === 0) {
302
217
  return { hasError: false };
@@ -328,14 +243,18 @@ export const checkExcelDuplicates = async (
328
243
 
329
244
  rows.forEach((rowData, index) => {
330
245
  // 构建唯一键,将所有重复规则字段的值连接起来
331
- const uniqueKey = duplicateRules.map((field) => rowData[field]).join('|');
246
+ const keyValues = duplicateRules.map((field) => rowData[field]);
247
+ const uniqueKey = keyValues.join('|');
332
248
  allKeys.push(uniqueKey);
333
249
 
334
- if (uniqueValues.has(uniqueKey)) {
250
+ // 只检测非空值的重复,避免多个空行被误判为重复
251
+ const hasValidValue = keyValues.some((value) => value !== null && value !== undefined && value !== '');
252
+
253
+ if (hasValidValue && uniqueValues.has(uniqueKey)) {
335
254
  // 找到重复行,记录当前索引和第一次出现的索引
336
255
  duplicateIndices.add(index); // 添加当前重复行
337
256
  duplicateIndices.add(uniqueValues.get(uniqueKey)!); // 添加第一次出现的行
338
- } else {
257
+ } else if (hasValidValue) {
339
258
  uniqueValues.set(uniqueKey, index);
340
259
  }
341
260
  });
@@ -367,33 +286,120 @@ export const checkExcelDuplicates = async (
367
286
  markCells.push({
368
287
  row: rowIndex + 2, // Excel行号 = 数组索引 + 2(表头和1-based索引)
369
288
  col: colIndex + 1, // Excel列号 = 数组索引 + 1(1-based索引)
370
- color: 'FFFF0000', // 红色
289
+ color: 'FFA500', // 黄橙色
371
290
  });
372
291
  }
373
292
  });
374
293
  });
375
294
 
376
- // 使用excel-view统一创建标记Excel
295
+ return {
296
+ hasError: true,
297
+ markCells,
298
+ markHeaders: missingDuplicateFields,
299
+ };
300
+ }
301
+
302
+ return { hasError: false }; // 没有重复数据
303
+ };
304
+
305
+ /**
306
+ * 统一验证Excel数据(格式验证和重复验证)
307
+ * @param excelBuffer Excel文件的ArrayBuffer
308
+ * @param rules 验证规则
309
+ * @param duplicateRules 重复规则
310
+ * @param duplicateUrl 重复检查URL
311
+ * @returns 统一验证结果
312
+ */
313
+ export const validateExcelUnified = async (
314
+ excelBuffer: ArrayBuffer,
315
+ rules?: Record<string, ValidateRule>,
316
+ duplicateRules?: string[],
317
+ duplicateUrl?: IUrlInfo,
318
+ ): Promise<{
319
+ hasError: boolean;
320
+ errBlob?: Blob;
321
+ validationMsg: string;
322
+ duplicateMsg: string;
323
+ }> => {
324
+ const allMarkCells: Array<{ row: number; col: number; color: string }> = [];
325
+ const allMarkHeaders: string[] = [];
326
+ let hasValidationError = false;
327
+ let hasDuplicateError = false;
328
+ let validationMsg = '数据验证成功';
329
+ let duplicateMsg = '数据验证通过';
330
+
331
+ // 1. 格式验证
332
+ const validationResult = await validateExcel(excelBuffer, rules);
333
+ if (validationResult.hasError) {
334
+ hasValidationError = true;
335
+ validationMsg = '数据验证失败';
336
+ if (validationResult.markCells) {
337
+ allMarkCells.push(...validationResult.markCells);
338
+ }
339
+ if (validationResult.markHeaders) {
340
+ allMarkHeaders.push(...validationResult.markHeaders);
341
+ }
342
+ }
343
+
344
+ // 2. 重复验证
345
+ if (duplicateRules && duplicateRules.length > 0) {
346
+ const duplicateResult = await checkExcelDuplicates(excelBuffer, duplicateRules, duplicateUrl);
347
+ if (duplicateResult.hasError) {
348
+ hasDuplicateError = true;
349
+ duplicateMsg = '检测到重复数据';
350
+ if (duplicateResult.markCells) {
351
+ // 检查是否有重复标记的单元格,如果有则改为深红色
352
+ duplicateResult.markCells.forEach((duplicateCell) => {
353
+ const existingCell = allMarkCells.find(
354
+ (cell) => cell.row === duplicateCell.row && cell.col === duplicateCell.col,
355
+ );
356
+ if (existingCell) {
357
+ existingCell.color = '8B0000'; // 深红色
358
+ } else {
359
+ allMarkCells.push(duplicateCell);
360
+ }
361
+ });
362
+ }
363
+ if (duplicateResult.markHeaders) {
364
+ allMarkHeaders.push(...duplicateResult.markHeaders);
365
+ }
366
+ }
367
+ }
368
+
369
+ // 3. 如果有任何错误,创建统一的标记Excel
370
+ if (hasValidationError || hasDuplicateError) {
377
371
  const markResult = await createMarkedExcelView(
378
372
  excelBuffer,
379
373
  {
380
- markCells,
381
- markHeaders: missingDuplicateFields,
374
+ markCells: allMarkCells,
375
+ markHeaders: allMarkHeaders,
382
376
  },
383
- 'duplicate_errors.xlsx',
377
+ 'validation_errors.xlsx',
384
378
  );
385
379
 
386
380
  if (markResult.success && markResult.blobUrl) {
387
- // 从blob URL创建blob对象
388
381
  const response = await fetch(markResult.blobUrl);
389
382
  const errBlob = await response.blob();
390
- return { hasError: true, errBlob };
383
+ return {
384
+ hasError: true,
385
+ errBlob,
386
+ validationMsg,
387
+ duplicateMsg,
388
+ };
391
389
  } else {
392
- return { hasError: true };
390
+ return {
391
+ hasError: true,
392
+ validationMsg,
393
+ duplicateMsg,
394
+ };
393
395
  }
394
396
  }
395
397
 
396
- return { hasError: false }; // 没有重复数据
398
+ return {
399
+ hasError: false,
400
+ validationMsg,
401
+ duplicateMsg,
402
+ };
397
403
  };
398
404
 
399
405
  /**
@@ -179,25 +179,77 @@ const queryOptions = <T>(
179
179
 
180
180
  /**
181
181
  * 获取选中的选项对象或对象数组
182
+ * - 支持单选或者多选
183
+ * - 支持子级选项
182
184
  * @param values 当前选择的值
183
185
  * @param options 所有选项对象数组
186
+ * @param keepChildren 是否保留子选项,默认为 true
184
187
  * @returns 选中的选项对象或对象数组
185
188
  */
186
189
  export const getSelectedValues = (
187
190
  values: undefined | string | number | (string | number | undefined)[],
188
191
  options: OptionItemProps[],
192
+ keepChildren?: boolean,
189
193
  ): OptionItemProps | OptionItemProps[] | undefined => {
190
194
  // 如果 values 为 undefined,直接返回 undefined
191
195
  if (values === undefined) return undefined;
192
196
 
197
+ // 深拷贝选项以确保数据隔离
198
+ const deepCloneOption = (option: OptionItemProps): OptionItemProps => {
199
+ const cloned = { ...option };
200
+ if (keepChildren && option.children && option.children.length > 0) {
201
+ cloned.children = option.children.map((child: OptionItemProps) => deepCloneOption(child));
202
+ } else {
203
+ delete cloned.children;
204
+ }
205
+ return cloned;
206
+ };
207
+
208
+ // 递归查找匹配的选项
209
+ const findMatchedOptions = (
210
+ searchValues: (string | number | undefined)[],
211
+ searchOptions: OptionItemProps[],
212
+ ): OptionItemProps[] => {
213
+ const matched: OptionItemProps[] = [];
214
+
215
+ for (const option of searchOptions) {
216
+ // 检查当前选项是否匹配
217
+ if (searchValues.includes(option.value)) {
218
+ matched.push(deepCloneOption(option));
219
+ }
220
+
221
+ // 递归检查子选项
222
+ if (option.children && option.children.length > 0) {
223
+ const childMatched = findMatchedOptions(searchValues, option.children);
224
+ matched.push(...childMatched);
225
+ }
226
+ }
227
+
228
+ return matched;
229
+ };
230
+
193
231
  // 如果 values 是数组,返回所有匹配的选项对象数组
194
232
  if (Array.isArray(values)) {
195
- return options.filter((option) => values.includes(option.value));
233
+ return findMatchedOptions(values, options);
196
234
  }
197
235
  // 如果 values 是单个值,返回匹配的单个选项对象
198
236
  else {
199
- return options.find((option) => option.value === values);
237
+ const matched = findMatchedOptions([values], options);
238
+ return matched.length > 0 ? matched[0] : undefined;
239
+ }
240
+ };
241
+
242
+ /**
243
+ * 获取选中的选项文字内容
244
+ * @param selectedValues 选中的选项对象或对象数
245
+ * @returns 选中的选项文字内容
246
+ */
247
+ export const getSelectedLabels = (selectedValues: OptionItemProps | OptionItemProps[] | undefined): string[] => {
248
+ if (selectedValues === undefined) return [];
249
+ if (Array.isArray(selectedValues)) {
250
+ return selectedValues.map((option) => option.label);
200
251
  }
252
+ return [selectedValues.label];
201
253
  };
202
254
 
203
255
  // 辅助函数:根据路径设置值
@@ -216,7 +268,10 @@ const setNestedValue = (obj: Record<string, any>, path: string, value: any) => {
216
268
 
217
269
  /**
218
270
  * 将选中的值根据 outFields 映射到 formData 上
219
- * @param formData 需要更新的数据对象
271
+ * @param formData 需要更新的数据对
272
+ * 输出字段转换控制
273
+ * - Key:目的字段,支持 "." 嵌套
274
+ * - Value:源字段,支持模板 ${} 或者 ${index} 或者 ${index}.${key}
220
275
  * @param outFields 输出字段转换映射关系
221
276
  * @param selectedValues 选中的选项对象或对象数组
222
277
  */
@@ -235,12 +290,28 @@ export const outFormDataFields = (
235
290
 
236
291
  // 如果 selectedValues 是数组
237
292
  if (Array.isArray(selectedValues)) {
238
- // selectedValues.forEach((selectedValue) => {
239
- // Object.entries(outFields).forEach(([targetKey, sourceKey]) => {
240
- // const value = selectedValue[sourceKey];
241
- // setNestedValue(formData, targetKey, value);
242
- // });
243
- // });
293
+ Object.entries(outFields).forEach(([targetKey, sourceKey]) => {
294
+ // 必须是 ${index} 或者 ${index}.${key},否则输出错误并跳过
295
+ const reg = /^\$\{\d+\}/;
296
+ // 第一个必须是 ${index},否则输出错误并跳过
297
+ if (reg.test(sourceKey)) {
298
+ try {
299
+ const index = parseInt(sourceKey.match(/\$\{(\d+)\}/)?.[1] ?? '0');
300
+ const targetValue = selectedValues[index];
301
+ const restKey = sourceKey.replace(/\$\{\d+\}\./, '');
302
+ if (restKey && targetValue) {
303
+ const value = parseFieldTemplate(restKey, targetValue);
304
+ setNestedValue(formData, targetKey, value);
305
+ } else {
306
+ setNestedValue(formData, targetKey, targetValue);
307
+ }
308
+ } catch (error) {
309
+ console.error('outFields 格式错误:' + sourceKey, '必须是 ${index} 或者 ${index}.${key}');
310
+ }
311
+ } else {
312
+ console.error('outFields 格式错误:' + sourceKey, '必须是 ${index} 或者 ${index}.${key}');
313
+ }
314
+ });
244
315
  }
245
316
  // 如果 selectedValues 是单个对象
246
317
  else {
@@ -320,16 +391,3 @@ export const onOptionChanged = (
320
391
  }
321
392
  return selectedValues;
322
393
  };
323
-
324
- /**
325
- * 获取选中的选项文字内容
326
- * @param selectedValues 选中的选项对象或对象数
327
- * @returns 选中的选项文字内容
328
- */
329
- export const getSelectedLabels = (selectedValues: OptionItemProps | OptionItemProps[] | undefined): string[] => {
330
- if (selectedValues === undefined) return [];
331
- if (Array.isArray(selectedValues)) {
332
- return selectedValues.map((option) => option.label);
333
- }
334
- return [selectedValues.label];
335
- };
@@ -25,8 +25,9 @@ interface TableColumn {
25
25
  */
26
26
  export const filterColumns = (columns: TableColumn[], toolCtl?: boolean) => {
27
27
  const userInfoStore = useUserInfo();
28
-
29
- return columns.filter((column) => {
28
+ // 检查是否存在enabled:false的列
29
+ // 并且设置没有权限的列为enabled:false
30
+ const enabledColumns = columns.filter((column) => {
30
31
  // 角色权限检查
31
32
  if (column.role && !userInfoStore.hasRole(column.role)) {
32
33
  return false;
@@ -37,6 +38,18 @@ export const filterColumns = (columns: TableColumn[], toolCtl?: boolean) => {
37
38
  return false;
38
39
  }
39
40
 
41
+ if (column.enabled === false) {
42
+ return false;
43
+ }
44
+
45
+ if (typeof column.enabled === 'function') {
46
+ return column.enabled();
47
+ }
48
+
49
+ return true;
50
+ });
51
+
52
+ return enabledColumns.filter((column) => {
40
53
  if (!toolCtl) {
41
54
  // 可见性检查
42
55
  if (column.visible === false) {