@skyfox2000/webui 1.3.3 → 1.3.5

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 (38) hide show
  1. package/lib/assets/modules/{file-upload-D4Pqs8h3.js → file-upload-C0twqMV5.js} +1 -1
  2. package/lib/assets/modules/{index-V1j9haWy.js → index-C4CryM-R.js} +1 -1
  3. package/lib/assets/modules/index-CKJIxasX.js +333 -0
  4. package/lib/assets/modules/{index-CSnwbbQT.js → index-D1XAa1Uo.js} +2 -2
  5. package/lib/assets/modules/{menuTabs-e8XoJN7m.js → menuTabs-BrYQa4UO.js} +2 -2
  6. package/lib/assets/modules/{toolIcon-BSF7eiPf.js → toolIcon-B-g9pyE4.js} +1 -1
  7. package/lib/assets/modules/{uploadList-DA4TRDWR.js → uploadList-0f2FA_5s.js} +490 -455
  8. package/lib/assets/modules/{uploadList-Bcf7g1bf.js → uploadList-DCWRIxPJ.js} +4 -4
  9. package/lib/components/content/table/index.vue.d.ts +95 -4
  10. package/lib/es/AceEditor/index.js +3 -3
  11. package/lib/es/BasicLayout/index.js +3 -3
  12. package/lib/es/Error403/index.js +1 -1
  13. package/lib/es/Error404/index.js +1 -1
  14. package/lib/es/ExcelForm/index.js +339 -202
  15. package/lib/es/UploadForm/index.js +4 -4
  16. package/lib/index.d.ts +3 -2
  17. package/lib/typings/option.d.ts +2 -2
  18. package/lib/utils/excel-view.d.ts +25 -0
  19. package/lib/utils/form-csv.d.ts +18 -0
  20. package/lib/utils/form-excel.d.ts +2 -13
  21. package/lib/utils/options.d.ts +2 -2
  22. package/lib/webui.css +1 -1
  23. package/lib/webui.es.js +897 -871
  24. package/package.json +2 -2
  25. package/src/components/common/loading/index.vue +1 -1
  26. package/src/components/content/dialog/excelForm.vue +386 -107
  27. package/src/components/content/table/index.vue +9 -6
  28. package/src/components/form/autoComplete/index.vue +9 -3
  29. package/src/components/form/cascader/index.vue +8 -6
  30. package/src/index.ts +25 -2
  31. package/src/typings/option.d.ts +2 -2
  32. package/src/utils/excel-view.ts +340 -0
  33. package/src/utils/form-csv.ts +55 -0
  34. package/src/utils/form-excel.ts +59 -192
  35. package/src/utils/options.ts +80 -22
  36. package/src/utils/table.ts +15 -2
  37. package/vite.config.ts +0 -1
  38. package/lib/assets/modules/form-excel-D1vXB4c4.js +0 -235
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { watch, ref, onMounted } from 'vue';
3
- import { Button } from '../../common';
2
+ import { watch, ref, onMounted, computed } from 'vue';
3
+ import { Button, Loading } from '../../common';
4
4
  import { Modal, Space, Upload, Alert } from 'ant-design-vue';
5
5
  import {
6
6
  EditorControl,
@@ -11,12 +11,14 @@ import {
11
11
  AsyncUploader,
12
12
  UploadStatus,
13
13
  doSave,
14
+ doQuery,
14
15
  ExcelFileParams,
15
16
  processExcelFile,
16
17
  path,
17
18
  UploadFile,
18
19
  } from '@/index';
19
- import { AnyData, ResStatus } from '@skyfox2000/fapi';
20
+ import { csvToExcelView } from '@/utils/excel-view';
21
+ import { AnyData, ResStatus, IUrlInfo, httpGet, ApiResponse } from '@skyfox2000/fapi';
20
22
  import message from 'vue-m-message';
21
23
  //引入相关样式
22
24
  import type { UploadProps } from 'ant-design-vue';
@@ -24,6 +26,7 @@ import VueOfficeExcel from '@vue-office/excel';
24
26
  import '@vue-office/excel/lib/index.css';
25
27
 
26
28
  type AlertType = 'success' | 'info' | 'warning' | 'error';
29
+ type FileType = 'excel' | 'csv' | 'both';
27
30
 
28
31
  const props = defineProps<{
29
32
  /**
@@ -31,22 +34,22 @@ const props = defineProps<{
31
34
  */
32
35
  title: String;
33
36
  /**
34
- * 表格控制器
37
+ * 来源表格控制器
35
38
  */
36
- gridCtrl: GridControl<AnyData>;
39
+ gridCtrl?: GridControl<AnyData>;
37
40
  /**
38
- * 表格控制器
41
+ * 当前表单控制器
39
42
  */
40
43
  excelCtrl: EditorControl<AnyData>;
41
44
  /**
42
45
  * 文件上传参数
43
46
  */
44
- uploadParams: ExcelFileParams;
47
+ uploadParams?: ExcelFileParams;
45
48
  /**
46
49
  * 表格字段映射
47
50
  * - 表头映射字段
48
51
  */
49
- excelFieldMap: Record<string, string>;
52
+ excelFieldMap?: Record<string, string>;
50
53
  /**
51
54
  * Excel文件信息字段
52
55
  */
@@ -60,6 +63,18 @@ const props = defineProps<{
60
63
  * 确认按钮文字,空字符串则不显示
61
64
  */
62
65
  saveText?: string;
66
+ /**
67
+ * 取消按钮文字,空字符串则不显示
68
+ */
69
+ cancelText?: string;
70
+ /**
71
+ * 外部预览地址
72
+ */
73
+ previewUrl?: IUrlInfo;
74
+ /**
75
+ * 允许的文件类型:excel(仅Excel) | csv(仅CSV) | both(都允许)
76
+ */
77
+ fileType?: FileType;
63
78
  }>();
64
79
 
65
80
  const excelCtrl = props.excelCtrl;
@@ -68,6 +83,36 @@ const excelUrl = ref('');
68
83
  const fileList = ref<any[]>([]);
69
84
  const fileName = ref('');
70
85
  const validating = ref(true); // 表示正在验证状态
86
+ // 是否为预览模式 - 改为计算属性
87
+ const isPreviewMode = computed(() => !!props.previewUrl);
88
+
89
+ // 计算文件类型限制
90
+ const fileAccept = computed(() => {
91
+ const fileType = props.fileType || 'both';
92
+ switch (fileType) {
93
+ case 'excel':
94
+ return '.xlsx,.xls';
95
+ case 'csv':
96
+ return '.csv';
97
+ case 'both':
98
+ default:
99
+ return '.xlsx,.xls,.csv';
100
+ }
101
+ });
102
+
103
+ // 计算文件类型提示
104
+ const fileTypeTip = computed(() => {
105
+ const fileType = props.fileType || 'both';
106
+ switch (fileType) {
107
+ case 'excel':
108
+ return '请选择Excel文件';
109
+ case 'csv':
110
+ return '请选择CSV文件';
111
+ case 'both':
112
+ default:
113
+ return '请选择Excel或CSV文件';
114
+ }
115
+ });
71
116
 
72
117
  // Alert状态变量
73
118
  const validationMsg = ref('待验证数据规则');
@@ -79,6 +124,24 @@ watch(
79
124
  () => excelCtrl.visible.value,
80
125
  () => {
81
126
  open.value = excelCtrl.visible.value;
127
+ // 当对话框打开时,先清空表格内容
128
+ if (open.value) {
129
+ // 清空之前的数据
130
+ excelUrl.value = '';
131
+ fileName.value = '';
132
+ validating.value = true;
133
+ validationMsg.value = '待验证数据规则';
134
+ validationType.value = 'warning';
135
+ duplicateMsg.value = '待验证重复数据';
136
+ duplicateType.value = 'warning';
137
+ excelError.value = false;
138
+ duplicateError.value = false;
139
+
140
+ // 然后检查是否有预览地址
141
+ if (props.previewUrl) {
142
+ loadPreviewFile();
143
+ }
144
+ }
82
145
  },
83
146
  );
84
147
  watch(
@@ -88,10 +151,144 @@ watch(
88
151
  },
89
152
  );
90
153
 
154
+ watch(
155
+ () => props.previewUrl,
156
+ () => {
157
+ // 当预览地址变化且对话框是打开状态时,重新加载预览文件
158
+ if (open.value && props.previewUrl) {
159
+ loadPreviewFile();
160
+ }
161
+ },
162
+ { deep: true },
163
+ );
164
+
91
165
  const uploadParams = ref(props.uploadParams);
92
- const uploadUrl = ref(uploadParams.value.uploadUrl);
93
- const duplicateRules = ref(uploadParams.value.duplicateRules);
94
- const duplicateUrl = ref(uploadParams.value.duplicateUrl);
166
+ const uploadUrl = ref(uploadParams.value?.uploadUrl);
167
+ const duplicateRules = ref(uploadParams.value?.duplicateRules);
168
+ const duplicateUrl = ref(uploadParams.value?.duplicateUrl);
169
+
170
+ // 加载预览文件
171
+ const loadPreviewFile = async () => {
172
+ if (!props.previewUrl || !excelCtrl) return;
173
+
174
+ excelCtrl.isFormLoading.value = true;
175
+ try {
176
+ let result: ApiResponse<AnyData> | null = null;
177
+
178
+ // 根据请求方法选择不同的处理方式
179
+ if (props.previewUrl.method === 'GET') {
180
+ // 使用 httpGet 方法处理 GET 请求
181
+ const getUrl: IUrlInfo = {
182
+ ...props.previewUrl,
183
+ method: 'GET' as const,
184
+ api: props.previewUrl.api || excelCtrl.page.api,
185
+ authorize: props.previewUrl.authorize ?? excelCtrl.page.authorize,
186
+ };
187
+
188
+ result = await httpGet(getUrl);
189
+ } else {
190
+ // 使用 doQuery 处理 POST 请求
191
+ const queryParams = props.previewUrl.params;
192
+ result = await doQuery(excelCtrl, {
193
+ url: props.previewUrl,
194
+ urlKey: 'preview',
195
+ params: queryParams,
196
+ hideErrorToast: true,
197
+ });
198
+ }
199
+
200
+ // 原始模式下,result 就是原始数据,不是 ApiResponse 格式
201
+ if (props.previewUrl.raw) {
202
+ // 原始模式:直接处理返回的数据
203
+ await handleFileData(result);
204
+ } else {
205
+ // 标准模式:检查 ApiResponse 格式
206
+ if (result?.status === ResStatus.SUCCESS && result.data) {
207
+ const data = result.data;
208
+ // 处理返回的文件数据
209
+ await handleFileData(data);
210
+ } else {
211
+ throw new Error(result?.msg || '文件加载失败');
212
+ }
213
+ }
214
+ } catch (error: any) {
215
+ console.error('预览文件加载错误:', error);
216
+ message.error('文件加载失败:' + (error?.message || '未知错误'));
217
+ } finally {
218
+ setTimeout(() => {
219
+ excelCtrl.isFormLoading.value = false;
220
+ }, 1000);
221
+ }
222
+ };
223
+
224
+ // CSV内容处理的公共方法
225
+ const processCsvContent = async (content: string, filename: string = 'preview.csv') => {
226
+ const csvResult = await csvToExcelView(content, filename);
227
+ if (csvResult.success) {
228
+ excelUrl.value = csvResult.blobUrl!;
229
+ fileName.value = csvResult.fileName!;
230
+ } else {
231
+ throw new Error(csvResult.error || 'CSV格式处理失败');
232
+ }
233
+ };
234
+
235
+ // Excel内容处理的公共方法
236
+ const processExcelContent = (content: string, mimeType?: string, filename: string = '预览文件.xlsx') => {
237
+ const type = mimeType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
238
+ excelUrl.value = `data:${type};base64,${content}`;
239
+ fileName.value = filename;
240
+ };
241
+
242
+ // 判断是否为CSV格式的内容
243
+ const isCsvContent = (data: string) => data.includes(',') || data.includes('\n');
244
+
245
+ // 判断是否为CSV类型
246
+ const isCsvType = (type?: string, filename?: string) => type === 'text/csv' || filename?.toLowerCase().includes('.csv');
247
+
248
+ // 判断是否为Excel类型
249
+ const isExcelType = (type?: string, filename?: string) =>
250
+ type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
251
+ type === 'application/vnd.ms-excel' ||
252
+ filename?.toLowerCase().match(/\.(xlsx|xls)$/);
253
+
254
+ // 处理文件数据的统一方法
255
+ const handleFileData = async (data: any) => {
256
+ // 原始模式:直接处理文件内容
257
+ if (props.previewUrl?.raw) {
258
+ if (typeof data === 'string') {
259
+ if (isCsvContent(data)) {
260
+ await processCsvContent(data);
261
+ } else {
262
+ processExcelContent(data);
263
+ }
264
+ } else if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
265
+ const blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
266
+ excelUrl.value = URL.createObjectURL(blob);
267
+ fileName.value = '预览文件.xlsx';
268
+ } else {
269
+ throw new Error('不支持的原始文件格式');
270
+ }
271
+ return;
272
+ }
273
+
274
+ // 标准模式:处理结构化数据
275
+ if (data.Content && data.Type) {
276
+ if (isCsvType(data.Type, data.FileName)) {
277
+ await processCsvContent(data.Content, data.FileName || 'preview.csv');
278
+ } else if (isExcelType(data.Type, data.FileName)) {
279
+ processExcelContent(data.Content, data.Type, data.FileName || '预览文件.xlsx');
280
+ } else {
281
+ processExcelContent(data.Content);
282
+ }
283
+ } else if (data.url) {
284
+ excelUrl.value = data.url;
285
+ fileName.value = data.fileName || '预览文件.xlsx';
286
+ } else if (typeof data === 'string') {
287
+ await processCsvContent(data);
288
+ } else {
289
+ throw new Error('不支持的文件格式');
290
+ }
291
+ };
95
292
 
96
293
  const dialogUpload = async () => {
97
294
  const url = uploadUrl.value;
@@ -105,23 +302,24 @@ const dialogUpload = async () => {
105
302
  return;
106
303
  }
107
304
 
108
- excelCtrl.isFormLoading.value = true;
305
+ excelCtrl.isFormSaving.value = true;
109
306
  try {
110
307
  if (!excelBuffer.value || !fileName.value) {
111
- message.warning('请先选择Excel文件!');
112
- excelCtrl.isFormLoading.value = false;
308
+ message.warning('请先选择文件!');
309
+ excelCtrl.isFormSaving.value = false;
113
310
  return;
114
311
  }
115
312
 
116
313
  // 创建文件对象和上传器
117
314
  const uploader = new AsyncUploader(url, 1);
118
315
 
119
- // 创建上传文件对象
316
+ // 创建上传文件对象 - 根据原始文件类型设置正确的MIME类型
317
+ const fileType = originalFileType.value || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
120
318
  const file = new File([excelBuffer.value], fileName.value, {
121
- type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
319
+ type: fileType,
122
320
  });
123
321
 
124
- const fileKey = path.join(uploadParams.value.basePath, fileName.value);
322
+ const fileKey = path.join(uploadParams.value?.basePath ?? '', fileName.value);
125
323
  const uploadFile: UploadFile = {
126
324
  uid: '1',
127
325
  name: fileName.value,
@@ -152,7 +350,7 @@ const dialogUpload = async () => {
152
350
  console.error('上传处理错误:', error);
153
351
  message.error('上传处理失败:' + (error?.message || '未知错误'));
154
352
  } finally {
155
- excelCtrl.isFormLoading.value = false;
353
+ excelCtrl.isFormSaving.value = false;
156
354
  }
157
355
  };
158
356
 
@@ -167,7 +365,7 @@ const dialogSave = async (uploadFile: UploadFile) => {
167
365
  const excelDataList = excelData.map((row: Record<string, AnyData>) => {
168
366
  const result: Record<string, AnyData> = {};
169
367
  for (const key in row) {
170
- const field = props.excelFieldMap[key];
368
+ const field = props.excelFieldMap?.[key];
171
369
  if (field) {
172
370
  result[field] = row[key];
173
371
  }
@@ -193,7 +391,7 @@ const dialogSave = async (uploadFile: UploadFile) => {
193
391
  },
194
392
  };
195
393
 
196
- excelCtrl.isFormLoading.value = true;
394
+ excelCtrl.isFormSaving.value = true;
197
395
  try {
198
396
  const result = await doSave(props.excelCtrl, {
199
397
  params: postData,
@@ -214,7 +412,7 @@ const dialogSave = async (uploadFile: UploadFile) => {
214
412
  console.error('保存错误:', error);
215
413
  message.error('数据保存失败,请稍后再试!');
216
414
  } finally {
217
- excelCtrl.isFormLoading.value = false;
415
+ excelCtrl.isFormSaving.value = false;
218
416
  }
219
417
  }
220
418
  };
@@ -222,20 +420,114 @@ const dialogSave = async (uploadFile: UploadFile) => {
222
420
  const excelError = ref(false);
223
421
  const duplicateError = ref(false);
224
422
  const excelBuffer = ref<ArrayBuffer | null>(null);
423
+ const originalFileType = ref<string>(''); // 跟踪原始文件类型
424
+
425
+ /**
426
+ * 执行数据验证逻辑
427
+ * @param buffer 要验证的ArrayBuffer
428
+ * @returns 验证是否通过
429
+ */
430
+ const performDataValidation = async (buffer: ArrayBuffer): Promise<boolean> => {
431
+ const gridCtrl = props.gridCtrl;
432
+ if (!gridCtrl) return false;
433
+
434
+ // 先进行数据验证
435
+ const { hasError, errBlob } = await validateExcel(buffer, excelCtrl.formRules.value);
436
+
437
+ // 有验证错误
438
+ if (hasError) {
439
+ if (errBlob) {
440
+ excelError.value = true;
441
+ validating.value = false;
442
+ validationMsg.value = '数据验证失败';
443
+ validationType.value = 'error';
444
+ const blobUrl = URL.createObjectURL(errBlob);
445
+ excelUrl.value = blobUrl;
446
+ }
447
+ return false; // 验证失败则结束
448
+ } else {
449
+ // 验证成功
450
+ validationMsg.value = '数据验证成功';
451
+ validationType.value = 'success';
452
+ }
453
+
454
+ // 无验证错误,继续验证重复数据
455
+ if (duplicateRules.value && duplicateRules.value.length > 0 && duplicateUrl.value) {
456
+ try {
457
+ // 检测重复数据
458
+ if (!duplicateUrl.value.api) duplicateUrl.value.api = gridCtrl.page.api;
459
+ if (duplicateUrl.value.authorize === undefined) duplicateUrl.value.authorize = gridCtrl.page.authorize;
460
+
461
+ const { hasError, errBlob } = await checkExcelDuplicates(buffer, duplicateRules.value, duplicateUrl.value);
462
+ if (hasError) {
463
+ // 有重复数据
464
+ if (errBlob) {
465
+ duplicateError.value = true;
466
+ duplicateMsg.value = '检测到重复数据';
467
+ duplicateType.value = 'error';
468
+ const blobUrl = URL.createObjectURL(errBlob);
469
+ excelUrl.value = blobUrl;
470
+ }
471
+ return false;
472
+ } else {
473
+ // 无重复数据
474
+ duplicateError.value = false;
475
+ duplicateMsg.value = '数据验证通过';
476
+ duplicateType.value = 'success';
477
+ }
478
+ } catch (error) {
479
+ duplicateMsg.value = '重复检测异常';
480
+ duplicateType.value = 'error';
481
+ return false;
482
+ }
483
+ }
484
+
485
+ return true; // 验证通过
486
+ };
225
487
 
226
488
  // 上传前处理函数
227
489
  const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
490
+ const gridCtrl = props.gridCtrl;
491
+ if (!gridCtrl) {
492
+ message.error('未配置表格控制器!');
493
+ return;
494
+ }
495
+
228
496
  const isExcel =
229
497
  file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
230
498
  file.type === 'application/vnd.ms-excel';
231
- if (!isExcel) {
232
- message.error('只能上传Excel文件!');
499
+ const isCsv = file.type === 'text/csv' || file.name.toLowerCase().endsWith('.csv');
500
+
501
+ // 根据配置的文件类型检查
502
+ const allowedType = props.fileType || 'both';
503
+ let isValidType = false;
504
+ let errorMsg = '';
505
+
506
+ switch (allowedType) {
507
+ case 'excel':
508
+ isValidType = isExcel;
509
+ errorMsg = '只能上传Excel文件!';
510
+ break;
511
+ case 'csv':
512
+ isValidType = isCsv;
513
+ errorMsg = '只能上传CSV文件!';
514
+ break;
515
+ case 'both':
516
+ default:
517
+ isValidType = isExcel || isCsv;
518
+ errorMsg = '只能上传Excel文件或CSV文件!';
519
+ break;
520
+ }
521
+
522
+ if (!isValidType) {
523
+ message.error(errorMsg);
233
524
  return Upload.LIST_IGNORE;
234
525
  }
235
526
 
236
527
  try {
237
- // 设置文件名
528
+ // 设置文件名和原始文件类型
238
529
  fileName.value = file.name;
530
+ originalFileType.value = isCsv ? 'text/csv' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
239
531
 
240
532
  // 清除之前的错误状态并设置为验证中
241
533
  excelError.value = false;
@@ -246,7 +538,40 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
246
538
  duplicateMsg.value = '待验证重复数据';
247
539
  duplicateType.value = 'warning';
248
540
 
249
- // 使用ArrayBuffer读取文件
541
+ if (isCsv) {
542
+ // 处理CSV文件 - 完整验证流程同Excel
543
+ const csvBuffer = await file.arrayBuffer();
544
+
545
+ // 先将CSV转换为Excel格式的ArrayBuffer
546
+ const decoder = new TextDecoder('utf-8');
547
+ const csvText = decoder.decode(csvBuffer);
548
+
549
+ // 转换CSV为Excel格式用于验证
550
+ const { csvToExcelView } = await import('@/utils/excel-view');
551
+ const excelResult = await csvToExcelView(csvText, file.name);
552
+ if (!excelResult.success) {
553
+ throw new Error(excelResult.error || 'CSV文件处理失败');
554
+ }
555
+
556
+ // 获取Excel格式的ArrayBuffer用于验证
557
+ const response = await fetch(excelResult.blobUrl!);
558
+ const convertedExcelBuffer = await response.arrayBuffer();
559
+ excelBuffer.value = convertedExcelBuffer;
560
+
561
+ // 执行统一的验证流程
562
+ const isValidationPassed = await performDataValidation(convertedExcelBuffer);
563
+ if (!isValidationPassed) {
564
+ return false; // 验证失败则结束
565
+ }
566
+
567
+ // 所有验证通过,使用已生成的Excel预览
568
+ excelUrl.value = excelResult.blobUrl!;
569
+ fileName.value = excelResult.fileName!;
570
+ validating.value = false;
571
+ return false;
572
+ }
573
+
574
+ // 使用ArrayBuffer读取Excel文件
250
575
  const buffer = await file.arrayBuffer();
251
576
  excelBuffer.value = buffer;
252
577
 
@@ -254,73 +579,24 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
254
579
  reader.readAsDataURL(file);
255
580
  reader.onload = async (loadEvent) => {
256
581
  if (loadEvent.target) {
257
- // 读取完成后先进行普通验证
258
- const { hasError, errBlob } = await validateExcel(buffer, excelCtrl.formRules.value);
259
-
260
- // 有验证错误
261
- if (hasError) {
262
- if (errBlob) {
263
- excelError.value = true;
264
- validating.value = false;
265
- validationMsg.value = '数据验证失败';
266
- validationType.value = 'error';
267
- const blobUrl = URL.createObjectURL(errBlob);
268
- excelUrl.value = blobUrl;
269
- }
582
+ // 执行统一的验证流程
583
+ const isValidationPassed = await performDataValidation(buffer);
584
+ if (!isValidationPassed) {
270
585
  return; // 验证失败则结束
271
- } else {
272
- // 验证成功
273
- validationMsg.value = '数据验证成功';
274
- validationType.value = 'success';
275
586
  }
276
587
 
277
- // 无验证错误,继续验证重复数据
278
- if (duplicateRules.value && duplicateRules.value.length > 0 && duplicateUrl.value) {
279
- try {
280
- // 检测重复数据
281
- if (!duplicateUrl.value.api) duplicateUrl.value.api = props.gridCtrl.page.api;
282
- if (duplicateUrl.value.authorize === undefined)
283
- duplicateUrl.value.authorize = props.gridCtrl.page.authorize;
284
-
285
- const { hasError, errBlob } = await checkExcelDuplicates(
286
- buffer,
287
- duplicateRules.value,
288
- duplicateUrl.value,
289
- );
290
- if (hasError) {
291
- // 有重复数据
292
- if (errBlob) {
293
- duplicateError.value = true;
294
- duplicateMsg.value = '检测到重复数据';
295
- duplicateType.value = 'error';
296
- const blobUrl = URL.createObjectURL(errBlob);
297
- excelUrl.value = blobUrl;
298
- }
299
- return;
300
- } else {
301
- // 无重复数据
302
- duplicateError.value = false;
303
- duplicateMsg.value = '数据验证通过';
304
- duplicateType.value = 'success';
305
- }
306
- } catch (error) {
307
- duplicateMsg.value = '重复检测异常';
308
- duplicateType.value = 'error';
309
- }
310
- }
588
+ // 验证通过,显示原始Excel文件
311
589
  excelUrl.value = loadEvent.target.result as string;
312
-
313
- // 验证完成
314
590
  validating.value = false;
315
591
  } else {
316
- message.error('加载Excel文件失败,请检查文件格式!');
592
+ message.error('加载文件失败,请检查文件格式!');
317
593
  }
318
594
  };
319
595
  } catch (error) {
320
- console.error('Excel处理错误:', error);
321
- message.error('Excel文件处理失败,请检查文件格式!');
596
+ console.error('文件处理错误:', error);
597
+ message.error('文件处理失败,请检查文件格式!');
322
598
  validating.value = false;
323
- validationMsg.value = 'Excel处理错误';
599
+ validationMsg.value = '文件处理错误';
324
600
  validationType.value = 'error';
325
601
  }
326
602
 
@@ -331,21 +607,29 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
331
607
  const validationRules: { field: string; rules: string[] }[] = getRuleTexts(excelCtrl.formRules.value);
332
608
 
333
609
  onMounted(() => {
334
- const pageCtrl = props.gridCtrl.page;
335
- uploadUrl.value = uploadUrl.value ?? pageCtrl.urls.upload;
336
- if (!uploadUrl.value) {
337
- message.error('未配置文件上传地址!');
338
- return;
339
- }
610
+ const pageCtrl = props.gridCtrl?.page;
611
+ if (pageCtrl && !isPreviewMode.value) {
612
+ uploadUrl.value = uploadUrl.value ?? pageCtrl.urls.upload;
613
+ // 只有在非预览模式下才检查上传地址
614
+ if (!uploadUrl.value) {
615
+ message.error('未配置文件上传地址!');
616
+ return;
617
+ }
340
618
 
341
- if (!uploadUrl.value.api) uploadUrl.value.api = pageCtrl.api;
342
- if (uploadUrl.value.authorize === undefined) uploadUrl.value.authorize = pageCtrl.authorize;
619
+ if (!uploadUrl.value.api) uploadUrl.value.api = pageCtrl.api;
620
+ if (uploadUrl.value.authorize === undefined) uploadUrl.value.authorize = pageCtrl.authorize;
343
621
 
344
- for (const key in excelCtrl.formData.value) {
345
- if (props.gridCtrl.rowData.value) excelCtrl.formData.value[key] = props.gridCtrl.rowData.value[key];
622
+ for (const key in excelCtrl.formData.value) {
623
+ if (props.gridCtrl.rowData.value) excelCtrl.formData.value[key] = props.gridCtrl.rowData.value[key];
624
+ }
346
625
  }
347
626
 
348
627
  open.value = excelCtrl.visible.value;
628
+
629
+ // 如果在挂载时对话框已经是打开状态且是预览模式,则立即加载预览
630
+ if (open.value && props.previewUrl) {
631
+ loadPreviewFile();
632
+ }
349
633
  });
350
634
 
351
635
  const dialogClose = () => {
@@ -357,26 +641,27 @@ const handleError = () => {
357
641
  </script>
358
642
  <template>
359
643
  <Modal
360
- :title="title ?? 'Excel文件上传'"
644
+ :title="title ?? '文件上传'"
361
645
  v-model:open="open"
362
646
  :wrapClassName="['modal', 'mx-auto', $attrs.width ? 'w-[' + $attrs.width + ']' : ''].join(' ')"
363
647
  :width="940"
364
648
  @close="dialogClose"
365
649
  >
366
650
  <slot></slot>
367
- <div class="mb-4 flex items-center">
368
- <Upload :file-list="fileList" :before-upload="beforeUpload" accept=".xlsx,.xls" :showUploadList="true">
369
- <Button type="primary">选择Excel文件</Button>
651
+ <div v-if="!isPreviewMode" class="mb-4 flex items-center">
652
+ <Upload :file-list="fileList" :before-upload="beforeUpload" :accept="fileAccept" :showUploadList="true">
653
+ <Button type="primary">{{ fileTypeTip }}</Button>
370
654
  </Upload>
371
655
  <div v-if="excelUrl && fileName" class="ml-3 text-gray-600">
372
656
  <span>{{ fileName }}</span>
373
657
  </div>
374
658
  </div>
375
659
 
376
- <div class="flex gap-4">
660
+ <div class="flex gap-4 relative">
377
661
  <!-- 左侧Excel显示区域 -->
662
+ <Loading size="large" v-if="excelCtrl.isFormLoading.value" />
378
663
  <div
379
- class="flex-shrink-0 excel-container"
664
+ class="flex-shrink-0 relative border border-gray-200 rounded-md overflow-hidden"
380
665
  :class="[validationRules.length === 0 ? 'w-[100%]' : 'w-[80%]']"
381
666
  style="height: 430px"
382
667
  >
@@ -434,11 +719,12 @@ const handleError = () => {
434
719
 
435
720
  <template #footer>
436
721
  <Space>
437
- <Button @click="dialogClose">取消</Button>
722
+ <Button @click="dialogClose">{{ cancelText ?? (isPreviewMode ? '关闭' : '取消') }}</Button>
438
723
  <Button
724
+ v-if="!isPreviewMode"
439
725
  @click="dialogUpload"
440
726
  type="primary"
441
- :loading="excelCtrl.isFormSaving.value"
727
+ :loading="excelCtrl?.isFormSaving.value ?? false"
442
728
  :disabled="!excelUrl || excelError || duplicateError || validating"
443
729
  >
444
730
  {{ saveText ?? '上传文件' }}
@@ -469,11 +755,4 @@ const handleError = () => {
469
755
  .full-modal .ant-modal-body {
470
756
  flex: 1;
471
757
  }
472
-
473
- .excel-container {
474
- position: relative;
475
- border: 1px solid #f0f0f0;
476
- border-radius: 4px;
477
- overflow: hidden;
478
- }
479
758
  </style>