@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
@@ -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,140 +267,42 @@ 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
 
285
+ excelCtrl.isFormLoading.value = true;
174
286
  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
- }
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);
216
294
  message.error('文件加载失败:' + (error?.message || '未知错误'));
217
295
  } 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('不支持的文件格式');
296
+ setTimeout(() => {
297
+ excelCtrl.isFormLoading.value = false;
298
+ }, 1000);
289
299
  }
290
300
  };
291
301
 
292
302
  const dialogUpload = async () => {
303
+ // 确保使用最新的uploadParams
304
+ uploadParams.value = props.uploadParams;
305
+
293
306
  const url = uploadUrl.value;
294
307
  if (!url) {
295
308
  message.error('未配置文件上传地址!');
@@ -332,18 +345,13 @@ const dialogUpload = async () => {
332
345
 
333
346
  // 使用上传器的 uploadFile 方法上传文件
334
347
  const abortController = new AbortController();
335
- try {
336
- await uploader.uploadFile(uploadFile, abortController.signal, (percent) => {
337
- uploadFile.percent = percent;
338
- });
339
-
348
+ const uploadRes = await uploader.uploadFile(uploadFile, abortController.signal, (percent) => {
349
+ uploadFile.percent = percent;
350
+ });
351
+ if (uploadRes.status === UploadStatus.Success) {
340
352
  // 上传成功后调用 dialogSave 方法
341
353
  message.success('文件上传成功,开始业务处理!');
342
354
  dialogSave(uploadFile);
343
- } catch (uploadError: any) {
344
- console.error('文件上传错误:', uploadError);
345
- message.error(uploadError?.message || '文件上传失败,请稍后再试!');
346
- throw uploadError; // 向外层抛出错误
347
355
  }
348
356
  } catch (error: any) {
349
357
  console.error('上传处理错误:', error);
@@ -356,11 +364,11 @@ const dialogUpload = async () => {
356
364
  const dialogSave = async (uploadFile: UploadFile) => {
357
365
  if (excelCtrl.formData.value) {
358
366
  // 使用 doSave 方法保存数据
359
- if (props.excelBatchField) {
367
+ if (props.excelRecordsKey || props.excelRowsKey) {
360
368
  // 获取Excel数据,并转换成指定数据对象结构
361
369
  const excelFileData = await processExcelFile(excelBuffer.value!);
362
370
  if (!excelFileData) return null;
363
- const { excelData } = excelFileData;
371
+ const { excelData, excelRows } = excelFileData;
364
372
  const excelDataList = excelData.map((row: Record<string, AnyData>) => {
365
373
  const result: Record<string, AnyData> = {};
366
374
  for (const key in row) {
@@ -371,29 +379,37 @@ const dialogSave = async (uploadFile: UploadFile) => {
371
379
  }
372
380
  return result;
373
381
  });
374
- 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;
375
384
  }
376
385
 
377
386
  const fileField = props.fileField ?? 'FileInfo';
378
387
  excelCtrl.formData.value[fileField] = uploadFile;
379
388
 
380
- if (excelCtrl.beforeSave) {
381
- 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];
382
393
  }
383
394
 
384
- const postData = {
395
+ const saveData = {
385
396
  Data: {
386
397
  ...excelCtrl.formData.value,
387
398
  },
388
- Query: {
389
- [props.excelCtrl.primaryKey]: excelCtrl.formData.value[props.excelCtrl.primaryKey],
390
- },
399
+ Query: query,
391
400
  };
392
401
 
402
+ if (excelCtrl.beforeSave) {
403
+ const result = excelCtrl.beforeSave(saveData);
404
+ if (result === false) {
405
+ return;
406
+ }
407
+ }
408
+
393
409
  excelCtrl.isFormSaving.value = true;
394
410
  try {
395
411
  const result = await doSave(props.excelCtrl, {
396
- params: postData,
412
+ params: saveData,
397
413
  urlKey: 'save',
398
414
  url: props.excelCtrl.saveUrl,
399
415
  });
@@ -424,64 +440,57 @@ const originalFileType = ref<string>(''); // 跟踪原始文件类型
424
440
  /**
425
441
  * 执行数据验证逻辑
426
442
  * @param buffer 要验证的ArrayBuffer
427
- * @returns 验证是否通过
428
443
  */
429
- const performDataValidation = async (buffer: ArrayBuffer): Promise<boolean> => {
444
+ const performDataValidation = async (buffer: ArrayBuffer): Promise<void> => {
430
445
  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
- }
446
+ if (!gridCtrl) return;
452
447
 
453
- // 无验证错误,继续验证重复数据
454
- if (duplicateRules.value && duplicateRules.value.length > 0 && duplicateUrl.value) {
455
- try {
456
- // 检测重复数据
448
+ // 确保使用最新的uploadParams
449
+ uploadParams.value = props.uploadParams;
450
+
451
+ try {
452
+ // 配置重复检查URL
453
+ if (duplicateUrl.value) {
457
454
  if (!duplicateUrl.value.api) duplicateUrl.value.api = gridCtrl.page.api;
458
455
  if (duplicateUrl.value.authorize === undefined) duplicateUrl.value.authorize = gridCtrl.page.authorize;
456
+ }
459
457
 
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';
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;
476
479
  }
477
- } catch (error) {
478
- duplicateMsg.value = '重复检测异常';
479
- duplicateType.value = 'error';
480
- return false;
480
+ } else {
481
+ // 验证成功
482
+ excelError.value = false;
483
+ duplicateError.value = false;
484
+ validationType.value = 'success';
485
+ duplicateType.value = 'success';
486
+ // 验证成功时,excelUrl会在beforeUpload中设置
481
487
  }
488
+ } catch (error) {
489
+ validationMsg.value = '验证异常';
490
+ duplicateMsg.value = '验证异常';
491
+ validationType.value = 'error';
492
+ duplicateType.value = 'error';
482
493
  }
483
-
484
- return true; // 验证通过
485
494
  };
486
495
 
487
496
  // 上传前处理函数
@@ -492,6 +501,9 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
492
501
  return;
493
502
  }
494
503
 
504
+ // 更新uploadParams引用,确保使用最新参数
505
+ uploadParams.value = props.uploadParams;
506
+
495
507
  const isExcel =
496
508
  file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
497
509
  file.type === 'application/vnd.ms-excel';
@@ -524,19 +536,13 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
524
536
  }
525
537
 
526
538
  try {
527
- // 设置文件名和原始文件类型
539
+ // 重新选择文件后,初始化显示内容
540
+ clearAll();
541
+
542
+ // 在清理后重新设置文件相关信息
528
543
  fileName.value = file.name;
529
544
  originalFileType.value = isCsv ? 'text/csv' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
530
545
 
531
- // 清除之前的错误状态并设置为验证中
532
- excelError.value = false;
533
- duplicateError.value = false;
534
- validating.value = true;
535
- validationMsg.value = '待验证数据规则';
536
- validationType.value = 'warning';
537
- duplicateMsg.value = '待验证重复数据';
538
- duplicateType.value = 'warning';
539
-
540
546
  if (isCsv) {
541
547
  // 处理CSV文件 - 完整验证流程同Excel
542
548
  const csvBuffer = await file.arrayBuffer();
@@ -558,15 +564,14 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
558
564
  excelBuffer.value = convertedExcelBuffer;
559
565
 
560
566
  // 执行统一的验证流程
561
- const isValidationPassed = await performDataValidation(convertedExcelBuffer);
562
- if (!isValidationPassed) {
563
- return false; // 验证失败则结束
564
- }
567
+ await performDataValidation(convertedExcelBuffer);
565
568
 
566
- // 所有验证通过,使用已生成的Excel预览
567
- excelUrl.value = excelResult.blobUrl!;
569
+ // 验证完成后显示Excel预览(只有在验证成功时才显示原始文件)
570
+ if (!excelError.value && !duplicateError.value) {
571
+ excelUrl.value = excelResult.blobUrl!;
572
+ }
568
573
  fileName.value = excelResult.fileName!;
569
- validating.value = false;
574
+ excelCtrl.isFormLoading.value = false;
570
575
  return false;
571
576
  }
572
577
 
@@ -579,14 +584,13 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
579
584
  reader.onload = async (loadEvent) => {
580
585
  if (loadEvent.target) {
581
586
  // 执行统一的验证流程
582
- const isValidationPassed = await performDataValidation(buffer);
583
- if (!isValidationPassed) {
584
- return; // 验证失败则结束
585
- }
587
+ await performDataValidation(buffer);
586
588
 
587
- // 验证通过,显示原始Excel文件
588
- excelUrl.value = loadEvent.target.result as string;
589
- 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;
590
594
  } else {
591
595
  message.error('加载文件失败,请检查文件格式!');
592
596
  }
@@ -594,7 +598,7 @@ const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
594
598
  } catch (error) {
595
599
  console.error('文件处理错误:', error);
596
600
  message.error('文件处理失败,请检查文件格式!');
597
- validating.value = false;
601
+ excelCtrl.isFormLoading.value = false;
598
602
  validationMsg.value = '文件处理错误';
599
603
  validationType.value = 'error';
600
604
  }
@@ -608,7 +612,6 @@ const validationRules: { field: string; rules: string[] }[] = getRuleTexts(excel
608
612
  onMounted(() => {
609
613
  const pageCtrl = props.gridCtrl?.page;
610
614
  if (pageCtrl && !isPreviewMode.value) {
611
- uploadUrl.value = uploadUrl.value ?? pageCtrl.urls.upload;
612
615
  // 只有在非预览模式下才检查上传地址
613
616
  if (!uploadUrl.value) {
614
617
  message.error('未配置文件上传地址!');
@@ -627,56 +630,87 @@ onMounted(() => {
627
630
 
628
631
  // 如果在挂载时对话框已经是打开状态且是预览模式,则立即加载预览
629
632
  if (open.value && props.previewUrl) {
630
- loadPreviewFile();
633
+ loadPreviewFileLocal();
631
634
  }
632
635
  });
633
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
+
634
671
  const dialogClose = () => {
635
672
  excelCtrl.visible.value = false;
636
673
  };
674
+
637
675
  const handleError = () => {
638
- console.error('渲染失败');
676
+ // console.error('渲染失败', e);
639
677
  };
678
+
679
+ // 暴露给外部的方法
680
+ defineExpose({
681
+ clearAll,
682
+ });
640
683
  </script>
641
684
  <template>
642
- <Modal
643
- :title="title ?? '文件上传'"
644
- v-model:open="open"
645
- :wrapClassName="['modal', 'mx-auto', $attrs.width ? 'w-[' + $attrs.width + ']' : ''].join(' ')"
646
- :width="940"
647
- @close="dialogClose"
648
- >
685
+ <Modal :title="title ?? '文件上传'" v-model:open="open"
686
+ :wrapClassName="['modal', 'mx-auto', $attrs.width ? 'w-[' + $attrs.width + ']' : ''].join(' ')" :width="940"
687
+ @close="dialogClose">
649
688
  <slot></slot>
650
689
  <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>
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>
653
694
  </Upload>
654
695
  <div v-if="excelUrl && fileName" class="ml-3 text-gray-600">
655
- <span>{{ fileName }}</span>
696
+ <Alert :message="fileName" type="info" :show-icon="false" />
656
697
  </div>
698
+ <slot name="file-after"></slot>
657
699
  </div>
658
700
 
659
- <div class="flex gap-4">
701
+ <div class="flex gap-4 relative">
660
702
  <!-- 左侧Excel显示区域 -->
661
- <Loading v-if="excelCtrl.isFormLoading" />
662
- <div
663
- class="flex-shrink-0 relative border border-gray-200 rounded-md overflow-hidden"
664
- :class="[validationRules.length === 0 ? 'w-[100%]' : 'w-[80%]']"
665
- style="height: 430px"
666
- >
667
- <VueOfficeExcel
668
- :src="excelUrl"
669
- @error="handleError"
670
- style="width: 100%; height: 100%"
671
- :options="{
672
- styles: true,
673
- formatCells: true,
674
- skipStyles: false,
675
- autoStyle: true,
676
- keepOriginalFormat: true,
677
- renderingStyle: 'svg',
678
- }"
679
- />
703
+ <Loading size="large" v-if="excelCtrl.isFormLoading.value" />
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
+ }" />
680
714
  </div>
681
715
 
682
716
  <!-- 右侧验证条件和错误信息 -->
@@ -719,13 +753,9 @@ const handleError = () => {
719
753
  <template #footer>
720
754
  <Space>
721
755
  <Button @click="dialogClose">{{ cancelText ?? (isPreviewMode ? '关闭' : '取消') }}</Button>
722
- <Button
723
- v-if="!isPreviewMode"
724
- @click="dialogUpload"
725
- type="primary"
756
+ <Button v-if="!isPreviewMode" @click="dialogUpload" type="primary"
726
757
  :loading="excelCtrl?.isFormSaving.value ?? false"
727
- :disabled="!excelUrl || excelError || duplicateError || validating"
728
- >
758
+ :disabled="!excelUrl || excelError || duplicateError || excelCtrl.isFormLoading.value">
729
759
  {{ saveText ?? '上传文件' }}
730
760
  </Button>
731
761
  </Space>