@skyfox2000/webui 1.3.3 → 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.
Files changed (30) hide show
  1. package/lib/assets/modules/{file-upload-D4Pqs8h3.js → file-upload-BBlFaIXB.js} +1 -1
  2. package/lib/assets/modules/index-4kDAt8nS.js +333 -0
  3. package/lib/assets/modules/{index-V1j9haWy.js → index-BG1SqSVl.js} +1 -1
  4. package/lib/assets/modules/{index-CSnwbbQT.js → index-m5rogIyM.js} +2 -2
  5. package/lib/assets/modules/{menuTabs-e8XoJN7m.js → menuTabs-tPIz4a89.js} +2 -2
  6. package/lib/assets/modules/{toolIcon-BSF7eiPf.js → toolIcon-DwWoD9TN.js} +1 -1
  7. package/lib/assets/modules/{uploadList-DA4TRDWR.js → uploadList-D_Z-Y2tw.js} +2 -2
  8. package/lib/assets/modules/{uploadList-Bcf7g1bf.js → uploadList-Da7mQUNK.js} +4 -4
  9. package/lib/es/AceEditor/index.js +3 -3
  10. package/lib/es/BasicLayout/index.js +3 -3
  11. package/lib/es/Error403/index.js +1 -1
  12. package/lib/es/Error404/index.js +1 -1
  13. package/lib/es/ExcelForm/index.js +332 -202
  14. package/lib/es/UploadForm/index.js +4 -4
  15. package/lib/index.d.ts +3 -2
  16. package/lib/utils/excel-view.d.ts +25 -0
  17. package/lib/utils/form-csv.d.ts +18 -0
  18. package/lib/utils/form-excel.d.ts +2 -13
  19. package/lib/webui.css +1 -1
  20. package/lib/webui.es.js +862 -833
  21. package/package.json +2 -2
  22. package/src/components/common/loading/index.vue +1 -1
  23. package/src/components/content/dialog/excelForm.vue +384 -106
  24. package/src/components/form/autoComplete/index.vue +1 -1
  25. package/src/index.ts +25 -2
  26. package/src/utils/excel-view.ts +340 -0
  27. package/src/utils/form-csv.ts +55 -0
  28. package/src/utils/form-excel.ts +59 -192
  29. package/vite.config.ts +0 -1
  30. 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,143 @@ 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
+ try {
175
+ let result: ApiResponse<AnyData> | null = null;
176
+ excelCtrl.isFormLoading.value = true;
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
+ excelCtrl.isFormLoading.value = false;
219
+ }
220
+ };
221
+
222
+ // CSV内容处理的公共方法
223
+ const processCsvContent = async (content: string, filename: string = 'preview.csv') => {
224
+ const csvResult = await csvToExcelView(content, filename);
225
+ if (csvResult.success) {
226
+ excelUrl.value = csvResult.blobUrl!;
227
+ fileName.value = csvResult.fileName!;
228
+ } else {
229
+ throw new Error(csvResult.error || 'CSV格式处理失败');
230
+ }
231
+ };
232
+
233
+ // Excel内容处理的公共方法
234
+ const processExcelContent = (content: string, mimeType?: string, filename: string = '预览文件.xlsx') => {
235
+ const type = mimeType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
236
+ excelUrl.value = `data:${type};base64,${content}`;
237
+ fileName.value = filename;
238
+ };
239
+
240
+ // 判断是否为CSV格式的内容
241
+ const isCsvContent = (data: string) => data.includes(',') || data.includes('\n');
242
+
243
+ // 判断是否为CSV类型
244
+ const isCsvType = (type?: string, filename?: string) =>
245
+ type === 'text/csv' || filename?.toLowerCase().includes('.csv');
246
+
247
+ // 判断是否为Excel类型
248
+ const isExcelType = (type?: string, filename?: string) =>
249
+ type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
250
+ type === 'application/vnd.ms-excel' ||
251
+ filename?.toLowerCase().match(/\.(xlsx|xls)$/);
252
+
253
+ // 处理文件数据的统一方法
254
+ const handleFileData = async (data: any) => {
255
+ // 原始模式:直接处理文件内容
256
+ if (props.previewUrl?.raw) {
257
+ if (typeof data === 'string') {
258
+ if (isCsvContent(data)) {
259
+ await processCsvContent(data);
260
+ } else {
261
+ processExcelContent(data);
262
+ }
263
+ } else if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
264
+ const blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
265
+ excelUrl.value = URL.createObjectURL(blob);
266
+ fileName.value = '预览文件.xlsx';
267
+ } else {
268
+ throw new Error('不支持的原始文件格式');
269
+ }
270
+ return;
271
+ }
272
+
273
+ // 标准模式:处理结构化数据
274
+ if (data.Content && data.Type) {
275
+ if (isCsvType(data.Type, data.FileName)) {
276
+ await processCsvContent(data.Content, data.FileName || 'preview.csv');
277
+ } else if (isExcelType(data.Type, data.FileName)) {
278
+ processExcelContent(data.Content, data.Type, data.FileName || '预览文件.xlsx');
279
+ } else {
280
+ processExcelContent(data.Content);
281
+ }
282
+ } else if (data.url) {
283
+ excelUrl.value = data.url;
284
+ fileName.value = data.fileName || '预览文件.xlsx';
285
+ } else if (typeof data === 'string') {
286
+ await processCsvContent(data);
287
+ } else {
288
+ throw new Error('不支持的文件格式');
289
+ }
290
+ };
95
291
 
96
292
  const dialogUpload = async () => {
97
293
  const url = uploadUrl.value;
@@ -105,23 +301,24 @@ const dialogUpload = async () => {
105
301
  return;
106
302
  }
107
303
 
108
- excelCtrl.isFormLoading.value = true;
304
+ excelCtrl.isFormSaving.value = true;
109
305
  try {
110
306
  if (!excelBuffer.value || !fileName.value) {
111
- message.warning('请先选择Excel文件!');
112
- excelCtrl.isFormLoading.value = false;
307
+ message.warning('请先选择文件!');
308
+ excelCtrl.isFormSaving.value = false;
113
309
  return;
114
310
  }
115
311
 
116
312
  // 创建文件对象和上传器
117
313
  const uploader = new AsyncUploader(url, 1);
118
314
 
119
- // 创建上传文件对象
315
+ // 创建上传文件对象 - 根据原始文件类型设置正确的MIME类型
316
+ const fileType = originalFileType.value || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
120
317
  const file = new File([excelBuffer.value], fileName.value, {
121
- type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
318
+ type: fileType,
122
319
  });
123
320
 
124
- const fileKey = path.join(uploadParams.value.basePath, fileName.value);
321
+ const fileKey = path.join(uploadParams.value?.basePath ?? '', fileName.value);
125
322
  const uploadFile: UploadFile = {
126
323
  uid: '1',
127
324
  name: fileName.value,
@@ -152,7 +349,7 @@ const dialogUpload = async () => {
152
349
  console.error('上传处理错误:', error);
153
350
  message.error('上传处理失败:' + (error?.message || '未知错误'));
154
351
  } finally {
155
- excelCtrl.isFormLoading.value = false;
352
+ excelCtrl.isFormSaving.value = false;
156
353
  }
157
354
  };
158
355
 
@@ -167,7 +364,7 @@ const dialogSave = async (uploadFile: UploadFile) => {
167
364
  const excelDataList = excelData.map((row: Record<string, AnyData>) => {
168
365
  const result: Record<string, AnyData> = {};
169
366
  for (const key in row) {
170
- const field = props.excelFieldMap[key];
367
+ const field = props.excelFieldMap?.[key];
171
368
  if (field) {
172
369
  result[field] = row[key];
173
370
  }
@@ -193,7 +390,7 @@ const dialogSave = async (uploadFile: UploadFile) => {
193
390
  },
194
391
  };
195
392
 
196
- excelCtrl.isFormLoading.value = true;
393
+ excelCtrl.isFormSaving.value = true;
197
394
  try {
198
395
  const result = await doSave(props.excelCtrl, {
199
396
  params: postData,
@@ -214,7 +411,7 @@ const dialogSave = async (uploadFile: UploadFile) => {
214
411
  console.error('保存错误:', error);
215
412
  message.error('数据保存失败,请稍后再试!');
216
413
  } finally {
217
- excelCtrl.isFormLoading.value = false;
414
+ excelCtrl.isFormSaving.value = false;
218
415
  }
219
416
  }
220
417
  };
@@ -222,20 +419,114 @@ const dialogSave = async (uploadFile: UploadFile) => {
222
419
  const excelError = ref(false);
223
420
  const duplicateError = ref(false);
224
421
  const excelBuffer = ref<ArrayBuffer | null>(null);
422
+ const originalFileType = ref<string>(''); // 跟踪原始文件类型
423
+
424
+ /**
425
+ * 执行数据验证逻辑
426
+ * @param buffer 要验证的ArrayBuffer
427
+ * @returns 验证是否通过
428
+ */
429
+ const performDataValidation = async (buffer: ArrayBuffer): Promise<boolean> => {
430
+ const gridCtrl = props.gridCtrl;
431
+ if (!gridCtrl) return false;
432
+
433
+ // 先进行数据验证
434
+ const { hasError, errBlob } = await validateExcel(buffer, excelCtrl.formRules.value);
435
+
436
+ // 有验证错误
437
+ if (hasError) {
438
+ if (errBlob) {
439
+ excelError.value = true;
440
+ validating.value = false;
441
+ validationMsg.value = '数据验证失败';
442
+ validationType.value = 'error';
443
+ const blobUrl = URL.createObjectURL(errBlob);
444
+ excelUrl.value = blobUrl;
445
+ }
446
+ return false; // 验证失败则结束
447
+ } else {
448
+ // 验证成功
449
+ validationMsg.value = '数据验证成功';
450
+ validationType.value = 'success';
451
+ }
452
+
453
+ // 无验证错误,继续验证重复数据
454
+ if (duplicateRules.value && duplicateRules.value.length > 0 && duplicateUrl.value) {
455
+ try {
456
+ // 检测重复数据
457
+ if (!duplicateUrl.value.api) duplicateUrl.value.api = gridCtrl.page.api;
458
+ if (duplicateUrl.value.authorize === undefined) duplicateUrl.value.authorize = gridCtrl.page.authorize;
459
+
460
+ const { hasError, errBlob } = await checkExcelDuplicates(buffer, duplicateRules.value, duplicateUrl.value);
461
+ if (hasError) {
462
+ // 有重复数据
463
+ if (errBlob) {
464
+ duplicateError.value = true;
465
+ duplicateMsg.value = '检测到重复数据';
466
+ duplicateType.value = 'error';
467
+ const blobUrl = URL.createObjectURL(errBlob);
468
+ excelUrl.value = blobUrl;
469
+ }
470
+ return false;
471
+ } else {
472
+ // 无重复数据
473
+ duplicateError.value = false;
474
+ duplicateMsg.value = '数据验证通过';
475
+ duplicateType.value = 'success';
476
+ }
477
+ } catch (error) {
478
+ duplicateMsg.value = '重复检测异常';
479
+ duplicateType.value = 'error';
480
+ return false;
481
+ }
482
+ }
483
+
484
+ return true; // 验证通过
485
+ };
225
486
 
226
487
  // 上传前处理函数
227
488
  const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
489
+ const gridCtrl = props.gridCtrl;
490
+ if (!gridCtrl) {
491
+ message.error('未配置表格控制器!');
492
+ return;
493
+ }
494
+
228
495
  const isExcel =
229
496
  file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
230
497
  file.type === 'application/vnd.ms-excel';
231
- if (!isExcel) {
232
- message.error('只能上传Excel文件!');
498
+ const isCsv = file.type === 'text/csv' || file.name.toLowerCase().endsWith('.csv');
499
+
500
+ // 根据配置的文件类型检查
501
+ const allowedType = props.fileType || 'both';
502
+ let isValidType = false;
503
+ let errorMsg = '';
504
+
505
+ switch (allowedType) {
506
+ case 'excel':
507
+ isValidType = isExcel;
508
+ errorMsg = '只能上传Excel文件!';
509
+ break;
510
+ case 'csv':
511
+ isValidType = isCsv;
512
+ errorMsg = '只能上传CSV文件!';
513
+ break;
514
+ case 'both':
515
+ default:
516
+ isValidType = isExcel || isCsv;
517
+ errorMsg = '只能上传Excel文件或CSV文件!';
518
+ break;
519
+ }
520
+
521
+ if (!isValidType) {
522
+ message.error(errorMsg);
233
523
  return Upload.LIST_IGNORE;
234
524
  }
235
525
 
236
526
  try {
237
- // 设置文件名
527
+ // 设置文件名和原始文件类型
238
528
  fileName.value = file.name;
529
+ originalFileType.value = isCsv ? 'text/csv' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
239
530
 
240
531
  // 清除之前的错误状态并设置为验证中
241
532
  excelError.value = false;
@@ -246,7 +537,40 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
246
537
  duplicateMsg.value = '待验证重复数据';
247
538
  duplicateType.value = 'warning';
248
539
 
249
- // 使用ArrayBuffer读取文件
540
+ if (isCsv) {
541
+ // 处理CSV文件 - 完整验证流程同Excel
542
+ const csvBuffer = await file.arrayBuffer();
543
+
544
+ // 先将CSV转换为Excel格式的ArrayBuffer
545
+ const decoder = new TextDecoder('utf-8');
546
+ const csvText = decoder.decode(csvBuffer);
547
+
548
+ // 转换CSV为Excel格式用于验证
549
+ const { csvToExcelView } = await import('@/utils/excel-view');
550
+ const excelResult = await csvToExcelView(csvText, file.name);
551
+ if (!excelResult.success) {
552
+ throw new Error(excelResult.error || 'CSV文件处理失败');
553
+ }
554
+
555
+ // 获取Excel格式的ArrayBuffer用于验证
556
+ const response = await fetch(excelResult.blobUrl!);
557
+ const convertedExcelBuffer = await response.arrayBuffer();
558
+ excelBuffer.value = convertedExcelBuffer;
559
+
560
+ // 执行统一的验证流程
561
+ const isValidationPassed = await performDataValidation(convertedExcelBuffer);
562
+ if (!isValidationPassed) {
563
+ return false; // 验证失败则结束
564
+ }
565
+
566
+ // 所有验证通过,使用已生成的Excel预览
567
+ excelUrl.value = excelResult.blobUrl!;
568
+ fileName.value = excelResult.fileName!;
569
+ validating.value = false;
570
+ return false;
571
+ }
572
+
573
+ // 使用ArrayBuffer读取Excel文件
250
574
  const buffer = await file.arrayBuffer();
251
575
  excelBuffer.value = buffer;
252
576
 
@@ -254,73 +578,24 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
254
578
  reader.readAsDataURL(file);
255
579
  reader.onload = async (loadEvent) => {
256
580
  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
- }
581
+ // 执行统一的验证流程
582
+ const isValidationPassed = await performDataValidation(buffer);
583
+ if (!isValidationPassed) {
270
584
  return; // 验证失败则结束
271
- } else {
272
- // 验证成功
273
- validationMsg.value = '数据验证成功';
274
- validationType.value = 'success';
275
585
  }
276
586
 
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
- }
587
+ // 验证通过,显示原始Excel文件
311
588
  excelUrl.value = loadEvent.target.result as string;
312
-
313
- // 验证完成
314
589
  validating.value = false;
315
590
  } else {
316
- message.error('加载Excel文件失败,请检查文件格式!');
591
+ message.error('加载文件失败,请检查文件格式!');
317
592
  }
318
593
  };
319
594
  } catch (error) {
320
- console.error('Excel处理错误:', error);
321
- message.error('Excel文件处理失败,请检查文件格式!');
595
+ console.error('文件处理错误:', error);
596
+ message.error('文件处理失败,请检查文件格式!');
322
597
  validating.value = false;
323
- validationMsg.value = 'Excel处理错误';
598
+ validationMsg.value = '文件处理错误';
324
599
  validationType.value = 'error';
325
600
  }
326
601
 
@@ -331,21 +606,29 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
331
606
  const validationRules: { field: string; rules: string[] }[] = getRuleTexts(excelCtrl.formRules.value);
332
607
 
333
608
  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
- }
609
+ const pageCtrl = props.gridCtrl?.page;
610
+ if (pageCtrl && !isPreviewMode.value) {
611
+ uploadUrl.value = uploadUrl.value ?? pageCtrl.urls.upload;
612
+ // 只有在非预览模式下才检查上传地址
613
+ if (!uploadUrl.value) {
614
+ message.error('未配置文件上传地址!');
615
+ return;
616
+ }
340
617
 
341
- if (!uploadUrl.value.api) uploadUrl.value.api = pageCtrl.api;
342
- if (uploadUrl.value.authorize === undefined) uploadUrl.value.authorize = pageCtrl.authorize;
618
+ if (!uploadUrl.value.api) uploadUrl.value.api = pageCtrl.api;
619
+ if (uploadUrl.value.authorize === undefined) uploadUrl.value.authorize = pageCtrl.authorize;
343
620
 
344
- for (const key in excelCtrl.formData.value) {
345
- if (props.gridCtrl.rowData.value) excelCtrl.formData.value[key] = props.gridCtrl.rowData.value[key];
621
+ for (const key in excelCtrl.formData.value) {
622
+ if (props.gridCtrl.rowData.value) excelCtrl.formData.value[key] = props.gridCtrl.rowData.value[key];
623
+ }
346
624
  }
347
625
 
348
626
  open.value = excelCtrl.visible.value;
627
+
628
+ // 如果在挂载时对话框已经是打开状态且是预览模式,则立即加载预览
629
+ if (open.value && props.previewUrl) {
630
+ loadPreviewFile();
631
+ }
349
632
  });
350
633
 
351
634
  const dialogClose = () => {
@@ -357,16 +640,16 @@ const handleError = () => {
357
640
  </script>
358
641
  <template>
359
642
  <Modal
360
- :title="title ?? 'Excel文件上传'"
643
+ :title="title ?? '文件上传'"
361
644
  v-model:open="open"
362
645
  :wrapClassName="['modal', 'mx-auto', $attrs.width ? 'w-[' + $attrs.width + ']' : ''].join(' ')"
363
646
  :width="940"
364
647
  @close="dialogClose"
365
648
  >
366
649
  <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>
650
+ <div v-if="!isPreviewMode" class="mb-4 flex items-center">
651
+ <Upload :file-list="fileList" :before-upload="beforeUpload" :accept="fileAccept" :showUploadList="true">
652
+ <Button type="primary">{{ fileTypeTip }}</Button>
370
653
  </Upload>
371
654
  <div v-if="excelUrl && fileName" class="ml-3 text-gray-600">
372
655
  <span>{{ fileName }}</span>
@@ -375,8 +658,9 @@ const handleError = () => {
375
658
 
376
659
  <div class="flex gap-4">
377
660
  <!-- 左侧Excel显示区域 -->
661
+ <Loading v-if="excelCtrl.isFormLoading" />
378
662
  <div
379
- class="flex-shrink-0 excel-container"
663
+ class="flex-shrink-0 relative border border-gray-200 rounded-md overflow-hidden"
380
664
  :class="[validationRules.length === 0 ? 'w-[100%]' : 'w-[80%]']"
381
665
  style="height: 430px"
382
666
  >
@@ -434,11 +718,12 @@ const handleError = () => {
434
718
 
435
719
  <template #footer>
436
720
  <Space>
437
- <Button @click="dialogClose">取消</Button>
721
+ <Button @click="dialogClose">{{ cancelText ?? (isPreviewMode ? '关闭' : '取消') }}</Button>
438
722
  <Button
723
+ v-if="!isPreviewMode"
439
724
  @click="dialogUpload"
440
725
  type="primary"
441
- :loading="excelCtrl.isFormSaving.value"
726
+ :loading="excelCtrl?.isFormSaving.value ?? false"
442
727
  :disabled="!excelUrl || excelError || duplicateError || validating"
443
728
  >
444
729
  {{ saveText ?? '上传文件' }}
@@ -469,11 +754,4 @@ const handleError = () => {
469
754
  .full-modal .ant-modal-body {
470
755
  flex: 1;
471
756
  }
472
-
473
- .excel-container {
474
- position: relative;
475
- border: 1px solid #f0f0f0;
476
- border-radius: 4px;
477
- overflow: hidden;
478
- }
479
757
  </style>