@skyfox2000/webui 1.3.5 → 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 (45) hide show
  1. package/.vscode/settings.json +1 -1
  2. package/lib/assets/modules/{file-upload-C0twqMV5.js → file-upload-DhPgqGdk.js} +50 -50
  3. package/lib/assets/modules/index-02J2AYth.js +377 -0
  4. package/lib/assets/modules/{index-D1XAa1Uo.js → index-C31q4LHC.js} +2 -2
  5. package/lib/assets/modules/{index-C4CryM-R.js → index-CCpTizF9.js} +1 -1
  6. package/lib/assets/modules/{menuTabs-BrYQa4UO.js → menuTabs-DyhSKN9r.js} +2 -2
  7. package/lib/assets/modules/{toolIcon-B-g9pyE4.js → toolIcon-CqM4gBIc.js} +1 -1
  8. package/lib/assets/modules/{uploadList-0f2FA_5s.js → uploadList-DAVjJkqz.js} +127 -127
  9. package/lib/assets/modules/{uploadList-DCWRIxPJ.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/form/input/index.vue.d.ts +4 -1
  15. package/lib/components/form/select/index.vue.d.ts +2 -0
  16. package/lib/components/form/treeSelect/index.vue.d.ts +11 -2
  17. package/lib/components/index.d.ts +1 -1
  18. package/lib/es/AceEditor/index.js +3 -3
  19. package/lib/es/BasicLayout/index.js +3 -3
  20. package/lib/es/Error403/index.js +1 -1
  21. package/lib/es/Error404/index.js +1 -1
  22. package/lib/es/ExcelForm/index.js +311 -275
  23. package/lib/es/UploadForm/index.js +4 -4
  24. package/lib/index.d.ts +1 -1
  25. package/lib/typings/form.d.ts +2 -2
  26. package/lib/utils/excel-preview.d.ts +24 -0
  27. package/lib/utils/form-excel.d.ts +17 -4
  28. package/lib/webui.css +1 -1
  29. package/lib/webui.es.js +677 -662
  30. package/package.json +1 -1
  31. package/src/components/common/alert/index.vue +76 -0
  32. package/src/components/common/icon/helper.vue +7 -1
  33. package/src/components/common/index.ts +4 -1
  34. package/src/components/content/dialog/excelForm.vue +337 -308
  35. package/src/components/content/form/formItem.vue +6 -2
  36. package/src/components/form/input/index.vue +16 -3
  37. package/src/components/form/select/index.vue +5 -11
  38. package/src/components/form/treeSelect/index.vue +22 -17
  39. package/src/components/index.ts +1 -0
  40. package/src/index.ts +1 -0
  41. package/src/typings/form.d.ts +2 -2
  42. package/src/utils/excel-preview.ts +188 -0
  43. package/src/utils/file-upload.ts +0 -2
  44. package/src/utils/form-excel.ts +132 -126
  45. package/lib/assets/modules/index-CKJIxasX.js +0 -333
@@ -1,24 +1,124 @@
1
+ <!--
2
+ ExcelForm 组件功能说明
3
+ ====================
4
+
5
+ 功能概述:
6
+ ---------
7
+ ExcelForm是一个用于Excel文件上传、预览、验证和批量数据处理的Vue组件。
8
+ 支持Excel和CSV文件格式,提供完整的文件验证、错误标记和数据上传功能。
9
+
10
+ 主要功能:
11
+ ---------
12
+ 1. 文件上传
13
+ - 支持Excel(.xlsx/.xls)和CSV文件上传
14
+ - 可配置允许的文件类型(excel/csv/both)
15
+ - 文件类型自动检测和验证
16
+
17
+ 2. Excel预览
18
+ - 实时预览上传的Excel文件内容
19
+ - 支持Excel和CSV文件的预览显示
20
+ - 使用VueOfficeExcel组件进行渲染
21
+
22
+ 3. 数据验证
23
+ - 格式验证:根据配置的验证规则检查数据格式
24
+ - 重复验证:检查指定字段的重复数据
25
+ - 本地验证:在客户端进行基础验证
26
+ - 服务器验证:调用后端API进行深度验证
27
+
28
+ 4. 错误标记
29
+ - 格式错误:用红色标记异常单元格
30
+ - 重复错误:用黄橙色标记重复数据
31
+ - 双重错误:用深红色标记同时存在格式和重复问题的单元格
32
+ - 生成带标记的Excel文件供用户查看
33
+
34
+ 5. 批量数据处理
35
+ - 将Excel数据转换为JSON格式
36
+ - 支持字段映射和数据转换
37
+ - 批量上传到服务器
38
+
39
+ 组件属性:
40
+ ----------
41
+ - title: 对话框标题
42
+ - gridCtrl: 表格控制器,用于数据管理
43
+ - excelCtrl: Excel表单控制器,管理表单状态
44
+ - enableUpload: 是否启用文件上传功能
45
+ - uploadParams: 文件上传参数配置
46
+ - excelFieldMap: Excel字段映射配置
47
+ - fileField: 文件信息字段名
48
+ - excelBatchField: Excel批量数据字段名
49
+ - saveText: 确认按钮文字
50
+ - cancelText: 取消按钮文字
51
+ - previewUrl: 外部预览地址
52
+ - fileType: 允许的文件类型(excel/csv/both)
53
+
54
+ 验证规则:
55
+ ----------
56
+ - 格式验证:支持多种验证规则(required, email, phone等)
57
+ - 重复验证:支持单字段或多字段组合的重复检查
58
+ - 服务器验证:可配置后端API进行远程重复检查
59
+
60
+ 使用示例:
61
+ ----------
62
+ <ExcelForm
63
+ title="批量导入用户"
64
+ :gridCtrl="userGridCtrl"
65
+ :excelCtrl="userExcelCtrl"
66
+ :uploadParams="{
67
+ uploadUrl: '/api/users/import',
68
+ duplicateRules: ['email', 'phone'],
69
+ duplicateUrl: '/api/users/check-duplicate'
70
+ }"
71
+ :excelFieldMap="{
72
+ '姓名': 'name',
73
+ '邮箱': 'email',
74
+ '电话': 'phone'
75
+ }"
76
+ fileType="both"
77
+ />
78
+
79
+ 技术实现:
80
+ ----------
81
+ - 基于Vue 3 Composition API开发
82
+ - 使用Ant Design Vue组件库
83
+ - 集成VueOfficeExcel进行Excel预览
84
+ - 支持CSV到Excel的格式转换
85
+ - 异步验证和错误处理
86
+ - 响应式状态管理
87
+
88
+ 错误处理:
89
+ ----------
90
+ - 文件格式错误提示
91
+ - 验证失败详细说明
92
+ - 网络请求异常处理
93
+ - 用户友好的错误信息显示
94
+
95
+ 状态管理:
96
+ ----------
97
+ - 文件上传状态
98
+ - 验证进度状态
99
+ - 错误状态管理
100
+ - 加载状态控制
101
+ -->
102
+
1
103
  <script setup lang="ts">
2
104
  import { watch, ref, onMounted, computed } from 'vue';
3
- import { Button, Loading } from '../../common';
4
- import { Modal, Space, Upload, Alert } from 'ant-design-vue';
105
+ import { Button, Loading, Alert } from '../../common';
106
+ import { Modal, Space, Upload } from 'ant-design-vue';
5
107
  import {
6
108
  EditorControl,
7
109
  GridControl,
8
- validateExcel,
9
110
  getRuleTexts,
10
- checkExcelDuplicates,
11
111
  AsyncUploader,
12
112
  UploadStatus,
13
113
  doSave,
14
- doQuery,
15
114
  ExcelFileParams,
16
115
  processExcelFile,
17
116
  path,
18
117
  UploadFile,
19
118
  } from '@/index';
20
- import { csvToExcelView } from '@/utils/excel-view';
21
- import { AnyData, ResStatus, IUrlInfo, httpGet, ApiResponse } from '@skyfox2000/fapi';
119
+ import { validateExcelUnified } from '@/utils/form-excel';
120
+ import { loadPreviewFile } from '@/utils/excel-preview';
121
+ import { AnyData, ResStatus, IUrlInfo } from '@skyfox2000/fapi';
22
122
  import message from 'vue-m-message';
23
123
  //引入相关样式
24
124
  import type { UploadProps } from 'ant-design-vue';
@@ -28,63 +128,77 @@ import '@vue-office/excel/lib/index.css';
28
128
  type AlertType = 'success' | 'info' | 'warning' | 'error';
29
129
  type FileType = 'excel' | 'csv' | 'both';
30
130
 
31
- const props = defineProps<{
32
- /**
33
- * 标题
34
- */
35
- title: String;
36
- /**
37
- * 来源表格控制器
38
- */
39
- gridCtrl?: GridControl<AnyData>;
40
- /**
41
- * 当前表单控制器
42
- */
43
- excelCtrl: EditorControl<AnyData>;
44
- /**
45
- * 文件上传参数
46
- */
47
- uploadParams?: ExcelFileParams;
48
- /**
49
- * 表格字段映射
50
- * - 表头映射字段
51
- */
52
- excelFieldMap?: Record<string, string>;
53
- /**
54
- * Excel文件信息字段
55
- */
56
- fileField?: string;
57
- /**
58
- * Excel批量数据字段
59
- * - Excel数据列表转换后的上传字段
60
- */
61
- excelBatchField?: string;
62
- /**
63
- * 确认按钮文字,空字符串则不显示
64
- */
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;
78
- }>();
131
+ const props = withDefaults(
132
+ defineProps<{
133
+ /**
134
+ * 标题
135
+ */
136
+ title: String;
137
+ /**
138
+ * 来源表格控制器
139
+ */
140
+ gridCtrl?: GridControl<AnyData>;
141
+ /**
142
+ * 当前表单控制器
143
+ */
144
+ excelCtrl: EditorControl<AnyData>;
145
+ /**
146
+ * 是否启用文件上传
147
+ */
148
+ enableUpload?: boolean;
149
+ /**
150
+ * 文件上传参数
151
+ */
152
+ uploadParams?: ExcelFileParams;
153
+ /**
154
+ * 表格字段映射
155
+ * - 表头映射字段
156
+ */
157
+ excelFieldMap?: Record<string, string>;
158
+ /**
159
+ * Excel文件信息字段
160
+ */
161
+ fileField?: string;
162
+ /**
163
+ * Excel批量Record数据字段名
164
+ * - Excel数据列表转换后的上传字段
165
+ */
166
+ excelRecordsKey?: string;
167
+ /**
168
+ * Excel批量Row数据字段
169
+ */
170
+ excelRowsKey?: string;
171
+ /**
172
+ * 确认按钮文字,空字符串则不显示
173
+ */
174
+ saveText?: string;
175
+ /**
176
+ * 取消按钮文字,空字符串则不显示
177
+ */
178
+ cancelText?: string;
179
+ /**
180
+ * 外部预览地址
181
+ */
182
+ previewUrl?: IUrlInfo;
183
+ /**
184
+ * 允许的文件类型:excel(仅Excel) | csv(仅CSV) | both(都允许)
185
+ */
186
+ fileType?: FileType;
187
+ }>(),
188
+ {
189
+ enableUpload: true,
190
+ },
191
+ );
79
192
 
80
193
  const excelCtrl = props.excelCtrl;
81
194
  const open = ref<boolean>(false);
82
195
  const excelUrl = ref('');
83
196
  const fileList = ref<any[]>([]);
84
197
  const fileName = ref('');
85
- const validating = ref(true); // 表示正在验证状态
198
+
86
199
  // 是否为预览模式 - 改为计算属性
87
200
  const isPreviewMode = computed(() => !!props.previewUrl);
201
+ const enableUpload = computed(() => props.enableUpload);
88
202
 
89
203
  // 计算文件类型限制
90
204
  const fileAccept = computed(() => {
@@ -126,20 +240,12 @@ watch(
126
240
  open.value = excelCtrl.visible.value;
127
241
  // 当对话框打开时,先清空表格内容
128
242
  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;
243
+ // 使用clearAll方法初始化
244
+ clearAll();
139
245
 
140
246
  // 然后检查是否有预览地址
141
247
  if (props.previewUrl) {
142
- loadPreviewFile();
248
+ loadPreviewFileLocal();
143
249
  }
144
250
  }
145
251
  },
@@ -148,6 +254,11 @@ watch(
148
254
  () => open.value,
149
255
  () => {
150
256
  excelCtrl.visible.value = open.value;
257
+ // 当对话框关闭时,重置所有状态
258
+ if (!open.value) {
259
+ // 使用clearAll方法初始化
260
+ clearAll();
261
+ }
151
262
  },
152
263
  );
153
264
 
@@ -156,60 +267,27 @@ watch(
156
267
  () => {
157
268
  // 当预览地址变化且对话框是打开状态时,重新加载预览文件
158
269
  if (open.value && props.previewUrl) {
159
- loadPreviewFile();
270
+ loadPreviewFileLocal();
160
271
  }
161
272
  },
162
273
  { deep: true },
163
274
  );
164
275
 
165
276
  const uploadParams = ref(props.uploadParams);
166
- const uploadUrl = ref(uploadParams.value?.uploadUrl);
167
- const duplicateRules = ref(uploadParams.value?.duplicateRules);
168
- const duplicateUrl = ref(uploadParams.value?.duplicateUrl);
277
+ const uploadUrl = computed(() => uploadParams.value?.uploadUrl);
278
+ const duplicateRules = computed(() => uploadParams.value?.duplicateRules);
279
+ const duplicateUrl = computed(() => uploadParams.value?.duplicateUrl);
169
280
 
170
281
  // 加载预览文件
171
- const loadPreviewFile = async () => {
282
+ const loadPreviewFileLocal = async () => {
172
283
  if (!props.previewUrl || !excelCtrl) return;
173
284
 
174
285
  excelCtrl.isFormLoading.value = true;
175
286
  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
- }
287
+ const result = await loadPreviewFile(props.previewUrl, excelCtrl);
288
+ if (result.success) {
289
+ excelUrl.value = result.blobUrl!;
290
+ fileName.value = result.fileName!;
213
291
  }
214
292
  } catch (error: any) {
215
293
  console.error('预览文件加载错误:', error);
@@ -221,76 +299,10 @@ const loadPreviewFile = async () => {
221
299
  }
222
300
  };
223
301
 
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
- };
292
-
293
302
  const dialogUpload = async () => {
303
+ // 确保使用最新的uploadParams
304
+ uploadParams.value = props.uploadParams;
305
+
294
306
  const url = uploadUrl.value;
295
307
  if (!url) {
296
308
  message.error('未配置文件上传地址!');
@@ -333,18 +345,13 @@ const dialogUpload = async () => {
333
345
 
334
346
  // 使用上传器的 uploadFile 方法上传文件
335
347
  const abortController = new AbortController();
336
- try {
337
- await uploader.uploadFile(uploadFile, abortController.signal, (percent) => {
338
- uploadFile.percent = percent;
339
- });
340
-
348
+ const uploadRes = await uploader.uploadFile(uploadFile, abortController.signal, (percent) => {
349
+ uploadFile.percent = percent;
350
+ });
351
+ if (uploadRes.status === UploadStatus.Success) {
341
352
  // 上传成功后调用 dialogSave 方法
342
353
  message.success('文件上传成功,开始业务处理!');
343
354
  dialogSave(uploadFile);
344
- } catch (uploadError: any) {
345
- console.error('文件上传错误:', uploadError);
346
- message.error(uploadError?.message || '文件上传失败,请稍后再试!');
347
- throw uploadError; // 向外层抛出错误
348
355
  }
349
356
  } catch (error: any) {
350
357
  console.error('上传处理错误:', error);
@@ -357,11 +364,11 @@ const dialogUpload = async () => {
357
364
  const dialogSave = async (uploadFile: UploadFile) => {
358
365
  if (excelCtrl.formData.value) {
359
366
  // 使用 doSave 方法保存数据
360
- if (props.excelBatchField) {
367
+ if (props.excelRecordsKey || props.excelRowsKey) {
361
368
  // 获取Excel数据,并转换成指定数据对象结构
362
369
  const excelFileData = await processExcelFile(excelBuffer.value!);
363
370
  if (!excelFileData) return null;
364
- const { excelData } = excelFileData;
371
+ const { excelData, excelRows } = excelFileData;
365
372
  const excelDataList = excelData.map((row: Record<string, AnyData>) => {
366
373
  const result: Record<string, AnyData> = {};
367
374
  for (const key in row) {
@@ -372,29 +379,37 @@ const dialogSave = async (uploadFile: UploadFile) => {
372
379
  }
373
380
  return result;
374
381
  });
375
- excelCtrl.formData.value[props.excelBatchField] = excelDataList;
382
+ if (props.excelRecordsKey) excelCtrl.formData.value[props.excelRecordsKey] = excelDataList;
383
+ if (props.excelRowsKey) excelCtrl.formData.value[props.excelRowsKey] = excelRows;
376
384
  }
377
385
 
378
386
  const fileField = props.fileField ?? 'FileInfo';
379
387
  excelCtrl.formData.value[fileField] = uploadFile;
380
388
 
381
- if (excelCtrl.beforeSave) {
382
- excelCtrl.beforeSave();
389
+ // 构建Query对象,如果未配置primaryKey则Query为空对象
390
+ const query: Record<string, any> = {};
391
+ if (excelCtrl.primaryKey && excelCtrl.formData.value[excelCtrl.primaryKey] !== undefined) {
392
+ query[excelCtrl.primaryKey] = excelCtrl.formData.value[excelCtrl.primaryKey];
383
393
  }
384
394
 
385
- const postData = {
395
+ const saveData = {
386
396
  Data: {
387
397
  ...excelCtrl.formData.value,
388
398
  },
389
- Query: {
390
- [props.excelCtrl.primaryKey]: excelCtrl.formData.value[props.excelCtrl.primaryKey],
391
- },
399
+ Query: query,
392
400
  };
393
401
 
402
+ if (excelCtrl.beforeSave) {
403
+ const result = excelCtrl.beforeSave(saveData);
404
+ if (result === false) {
405
+ return;
406
+ }
407
+ }
408
+
394
409
  excelCtrl.isFormSaving.value = true;
395
410
  try {
396
411
  const result = await doSave(props.excelCtrl, {
397
- params: postData,
412
+ params: saveData,
398
413
  urlKey: 'save',
399
414
  url: props.excelCtrl.saveUrl,
400
415
  });
@@ -425,64 +440,57 @@ const originalFileType = ref<string>(''); // 跟踪原始文件类型
425
440
  /**
426
441
  * 执行数据验证逻辑
427
442
  * @param buffer 要验证的ArrayBuffer
428
- * @returns 验证是否通过
429
443
  */
430
- const performDataValidation = async (buffer: ArrayBuffer): Promise<boolean> => {
444
+ const performDataValidation = async (buffer: ArrayBuffer): Promise<void> => {
431
445
  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
- }
446
+ if (!gridCtrl) return;
453
447
 
454
- // 无验证错误,继续验证重复数据
455
- if (duplicateRules.value && duplicateRules.value.length > 0 && duplicateUrl.value) {
456
- try {
457
- // 检测重复数据
448
+ // 确保使用最新的uploadParams
449
+ uploadParams.value = props.uploadParams;
450
+
451
+ try {
452
+ // 配置重复检查URL
453
+ if (duplicateUrl.value) {
458
454
  if (!duplicateUrl.value.api) duplicateUrl.value.api = gridCtrl.page.api;
459
455
  if (duplicateUrl.value.authorize === undefined) duplicateUrl.value.authorize = gridCtrl.page.authorize;
456
+ }
460
457
 
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';
458
+ // 统一验证(格式验证和重复验证)
459
+ const {
460
+ hasError,
461
+ errBlob,
462
+ validationMsg: valMsg,
463
+ duplicateMsg: dupMsg,
464
+ } = await validateExcelUnified(buffer, excelCtrl.formRules.value, duplicateRules.value, duplicateUrl.value);
465
+
466
+ // 更新验证状态
467
+ validationMsg.value = valMsg;
468
+ duplicateMsg.value = dupMsg;
469
+
470
+ if (hasError) {
471
+ // 有验证错误
472
+ if (errBlob) {
473
+ excelError.value = true;
474
+ duplicateError.value = true;
475
+ validationType.value = 'error';
476
+ duplicateType.value = 'error';
477
+ const blobUrl = URL.createObjectURL(errBlob);
478
+ excelUrl.value = blobUrl;
477
479
  }
478
- } catch (error) {
479
- duplicateMsg.value = '重复检测异常';
480
- duplicateType.value = 'error';
481
- return false;
480
+ } else {
481
+ // 验证成功
482
+ excelError.value = false;
483
+ duplicateError.value = false;
484
+ validationType.value = 'success';
485
+ duplicateType.value = 'success';
486
+ // 验证成功时,excelUrl会在beforeUpload中设置
482
487
  }
488
+ } catch (error) {
489
+ validationMsg.value = '验证异常';
490
+ duplicateMsg.value = '验证异常';
491
+ validationType.value = 'error';
492
+ duplicateType.value = 'error';
483
493
  }
484
-
485
- return true; // 验证通过
486
494
  };
487
495
 
488
496
  // 上传前处理函数
@@ -493,6 +501,9 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
493
501
  return;
494
502
  }
495
503
 
504
+ // 更新uploadParams引用,确保使用最新参数
505
+ uploadParams.value = props.uploadParams;
506
+
496
507
  const isExcel =
497
508
  file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
498
509
  file.type === 'application/vnd.ms-excel';
@@ -525,19 +536,13 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
525
536
  }
526
537
 
527
538
  try {
528
- // 设置文件名和原始文件类型
539
+ // 重新选择文件后,初始化显示内容
540
+ clearAll();
541
+
542
+ // 在清理后重新设置文件相关信息
529
543
  fileName.value = file.name;
530
544
  originalFileType.value = isCsv ? 'text/csv' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
531
545
 
532
- // 清除之前的错误状态并设置为验证中
533
- excelError.value = false;
534
- duplicateError.value = false;
535
- validating.value = true;
536
- validationMsg.value = '待验证数据规则';
537
- validationType.value = 'warning';
538
- duplicateMsg.value = '待验证重复数据';
539
- duplicateType.value = 'warning';
540
-
541
546
  if (isCsv) {
542
547
  // 处理CSV文件 - 完整验证流程同Excel
543
548
  const csvBuffer = await file.arrayBuffer();
@@ -559,15 +564,14 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
559
564
  excelBuffer.value = convertedExcelBuffer;
560
565
 
561
566
  // 执行统一的验证流程
562
- const isValidationPassed = await performDataValidation(convertedExcelBuffer);
563
- if (!isValidationPassed) {
564
- return false; // 验证失败则结束
565
- }
567
+ await performDataValidation(convertedExcelBuffer);
566
568
 
567
- // 所有验证通过,使用已生成的Excel预览
568
- excelUrl.value = excelResult.blobUrl!;
569
+ // 验证完成后显示Excel预览(只有在验证成功时才显示原始文件)
570
+ if (!excelError.value && !duplicateError.value) {
571
+ excelUrl.value = excelResult.blobUrl!;
572
+ }
569
573
  fileName.value = excelResult.fileName!;
570
- validating.value = false;
574
+ excelCtrl.isFormLoading.value = false;
571
575
  return false;
572
576
  }
573
577
 
@@ -580,14 +584,13 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
580
584
  reader.onload = async (loadEvent) => {
581
585
  if (loadEvent.target) {
582
586
  // 执行统一的验证流程
583
- const isValidationPassed = await performDataValidation(buffer);
584
- if (!isValidationPassed) {
585
- return; // 验证失败则结束
586
- }
587
+ await performDataValidation(buffer);
587
588
 
588
- // 验证通过,显示原始Excel文件
589
- excelUrl.value = loadEvent.target.result as string;
590
- validating.value = false;
589
+ // 验证完成后显示Excel预览(只有在验证成功时才显示原始文件)
590
+ if (!excelError.value && !duplicateError.value) {
591
+ excelUrl.value = loadEvent.target.result as string;
592
+ }
593
+ excelCtrl.isFormLoading.value = false;
591
594
  } else {
592
595
  message.error('加载文件失败,请检查文件格式!');
593
596
  }
@@ -595,7 +598,7 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
595
598
  } catch (error) {
596
599
  console.error('文件处理错误:', error);
597
600
  message.error('文件处理失败,请检查文件格式!');
598
- validating.value = false;
601
+ excelCtrl.isFormLoading.value = false;
599
602
  validationMsg.value = '文件处理错误';
600
603
  validationType.value = 'error';
601
604
  }
@@ -609,7 +612,6 @@ const validationRules: { field: string; rules: string[] }[] = getRuleTexts(excel
609
612
  onMounted(() => {
610
613
  const pageCtrl = props.gridCtrl?.page;
611
614
  if (pageCtrl && !isPreviewMode.value) {
612
- uploadUrl.value = uploadUrl.value ?? pageCtrl.urls.upload;
613
615
  // 只有在非预览模式下才检查上传地址
614
616
  if (!uploadUrl.value) {
615
617
  message.error('未配置文件上传地址!');
@@ -628,56 +630,87 @@ onMounted(() => {
628
630
 
629
631
  // 如果在挂载时对话框已经是打开状态且是预览模式,则立即加载预览
630
632
  if (open.value && props.previewUrl) {
631
- loadPreviewFile();
633
+ loadPreviewFileLocal();
632
634
  }
633
635
  });
634
636
 
637
+ /**
638
+ * 清空文件选择和表格预览显示内容
639
+ * 可以从外部调用此方法来重置组件状态
640
+ */
641
+ const clearAll = () => {
642
+ // 清空文件相关数据
643
+ excelUrl.value = '';
644
+ fileName.value = '';
645
+ fileList.value = [];
646
+ excelBuffer.value = null;
647
+ originalFileType.value = '';
648
+
649
+ // 重置验证状态
650
+ excelCtrl.isFormLoading.value = false;
651
+ validationMsg.value = '待验证数据规则';
652
+ validationType.value = 'warning';
653
+ duplicateMsg.value = '待验证重复数据';
654
+ duplicateType.value = 'warning';
655
+ excelError.value = false;
656
+ duplicateError.value = false;
657
+
658
+ // 清空表单数据(如果需要)
659
+ if (excelCtrl.formData.value) {
660
+ const fileField = props.fileField ?? 'FileInfo';
661
+ const batchField = props.excelRecordsKey;
662
+ const rowField = props.excelRowsKey;
663
+
664
+ // 只清空文件相关字段
665
+ delete excelCtrl.formData.value[fileField];
666
+ if (batchField) delete excelCtrl.formData.value[batchField];
667
+ if (rowField) delete excelCtrl.formData.value[rowField];
668
+ }
669
+ };
670
+
635
671
  const dialogClose = () => {
636
672
  excelCtrl.visible.value = false;
637
673
  };
674
+
638
675
  const handleError = () => {
639
- console.error('渲染失败');
676
+ // console.error('渲染失败', e);
640
677
  };
678
+
679
+ // 暴露给外部的方法
680
+ defineExpose({
681
+ clearAll,
682
+ });
641
683
  </script>
642
684
  <template>
643
- <Modal
644
- :title="title ?? '文件上传'"
645
- v-model:open="open"
646
- :wrapClassName="['modal', 'mx-auto', $attrs.width ? 'w-[' + $attrs.width + ']' : ''].join(' ')"
647
- :width="940"
648
- @close="dialogClose"
649
- >
685
+ <Modal :title="title ?? '文件上传'" v-model:open="open"
686
+ :wrapClassName="['modal', 'mx-auto', $attrs.width ? 'w-[' + $attrs.width + ']' : ''].join(' ')" :width="940"
687
+ @close="dialogClose">
650
688
  <slot></slot>
651
689
  <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>
690
+ <slot name="file-before"></slot>
691
+ <Upload :file-list="fileList" :before-upload="beforeUpload" :accept="fileAccept" :showUploadList="true"
692
+ :disabled="!enableUpload">
693
+ <Button type="primary" :disabled="!enableUpload">{{ fileTypeTip }}</Button>
654
694
  </Upload>
655
695
  <div v-if="excelUrl && fileName" class="ml-3 text-gray-600">
656
- <span>{{ fileName }}</span>
696
+ <Alert :message="fileName" type="info" :show-icon="false" />
657
697
  </div>
698
+ <slot name="file-after"></slot>
658
699
  </div>
659
700
 
660
701
  <div class="flex gap-4 relative">
661
702
  <!-- 左侧Excel显示区域 -->
662
703
  <Loading size="large" v-if="excelCtrl.isFormLoading.value" />
663
- <div
664
- class="flex-shrink-0 relative border border-gray-200 rounded-md overflow-hidden"
665
- :class="[validationRules.length === 0 ? 'w-[100%]' : 'w-[80%]']"
666
- style="height: 430px"
667
- >
668
- <VueOfficeExcel
669
- :src="excelUrl"
670
- @error="handleError"
671
- style="width: 100%; height: 100%"
672
- :options="{
673
- styles: true,
674
- formatCells: true,
675
- skipStyles: false,
676
- autoStyle: true,
677
- keepOriginalFormat: true,
678
- renderingStyle: 'svg',
679
- }"
680
- />
704
+ <div class="flex-shrink-0 relative border border-gray-200 rounded-md overflow-hidden"
705
+ :class="[validationRules.length === 0 ? 'w-[100%]' : 'w-[80%]']" style="height: 430px">
706
+ <VueOfficeExcel :src="excelUrl" @error="handleError" style="width: 100%; height: 100%" :options="{
707
+ styles: true,
708
+ formatCells: true,
709
+ skipStyles: false,
710
+ autoStyle: true,
711
+ keepOriginalFormat: true,
712
+ renderingStyle: 'svg',
713
+ }" />
681
714
  </div>
682
715
 
683
716
  <!-- 右侧验证条件和错误信息 -->
@@ -720,13 +753,9 @@ const handleError = () => {
720
753
  <template #footer>
721
754
  <Space>
722
755
  <Button @click="dialogClose">{{ cancelText ?? (isPreviewMode ? '关闭' : '取消') }}</Button>
723
- <Button
724
- v-if="!isPreviewMode"
725
- @click="dialogUpload"
726
- type="primary"
756
+ <Button v-if="!isPreviewMode" @click="dialogUpload" type="primary"
727
757
  :loading="excelCtrl?.isFormSaving.value ?? false"
728
- :disabled="!excelUrl || excelError || duplicateError || validating"
729
- >
758
+ :disabled="!excelUrl || excelError || duplicateError || excelCtrl.isFormLoading.value">
730
759
  {{ saveText ?? '上传文件' }}
731
760
  </Button>
732
761
  </Space>