@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.
- package/lib/assets/modules/{file-upload-D4Pqs8h3.js → file-upload-BBlFaIXB.js} +1 -1
- package/lib/assets/modules/index-4kDAt8nS.js +333 -0
- package/lib/assets/modules/{index-V1j9haWy.js → index-BG1SqSVl.js} +1 -1
- package/lib/assets/modules/{index-CSnwbbQT.js → index-m5rogIyM.js} +2 -2
- package/lib/assets/modules/{menuTabs-e8XoJN7m.js → menuTabs-tPIz4a89.js} +2 -2
- package/lib/assets/modules/{toolIcon-BSF7eiPf.js → toolIcon-DwWoD9TN.js} +1 -1
- package/lib/assets/modules/{uploadList-DA4TRDWR.js → uploadList-D_Z-Y2tw.js} +2 -2
- package/lib/assets/modules/{uploadList-Bcf7g1bf.js → uploadList-Da7mQUNK.js} +4 -4
- package/lib/es/AceEditor/index.js +3 -3
- package/lib/es/BasicLayout/index.js +3 -3
- package/lib/es/Error403/index.js +1 -1
- package/lib/es/Error404/index.js +1 -1
- package/lib/es/ExcelForm/index.js +332 -202
- package/lib/es/UploadForm/index.js +4 -4
- package/lib/index.d.ts +3 -2
- package/lib/utils/excel-view.d.ts +25 -0
- package/lib/utils/form-csv.d.ts +18 -0
- package/lib/utils/form-excel.d.ts +2 -13
- package/lib/webui.css +1 -1
- package/lib/webui.es.js +862 -833
- package/package.json +2 -2
- package/src/components/common/loading/index.vue +1 -1
- package/src/components/content/dialog/excelForm.vue +384 -106
- package/src/components/form/autoComplete/index.vue +1 -1
- package/src/index.ts +25 -2
- package/src/utils/excel-view.ts +340 -0
- package/src/utils/form-csv.ts +55 -0
- package/src/utils/form-excel.ts +59 -192
- package/vite.config.ts +0 -1
- 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 {
|
|
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
|
|
39
|
+
gridCtrl?: GridControl<AnyData>;
|
|
37
40
|
/**
|
|
38
|
-
*
|
|
41
|
+
* 当前表单控制器
|
|
39
42
|
*/
|
|
40
43
|
excelCtrl: EditorControl<AnyData>;
|
|
41
44
|
/**
|
|
42
45
|
* 文件上传参数
|
|
43
46
|
*/
|
|
44
|
-
uploadParams
|
|
47
|
+
uploadParams?: ExcelFileParams;
|
|
45
48
|
/**
|
|
46
49
|
* 表格字段映射
|
|
47
50
|
* - 表头映射字段
|
|
48
51
|
*/
|
|
49
|
-
excelFieldMap
|
|
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
|
|
93
|
-
const duplicateRules = ref(uploadParams.value
|
|
94
|
-
const duplicateUrl = ref(uploadParams.value
|
|
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.
|
|
304
|
+
excelCtrl.isFormSaving.value = true;
|
|
109
305
|
try {
|
|
110
306
|
if (!excelBuffer.value || !fileName.value) {
|
|
111
|
-
message.warning('
|
|
112
|
-
excelCtrl.
|
|
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:
|
|
318
|
+
type: fileType,
|
|
122
319
|
});
|
|
123
320
|
|
|
124
|
-
const fileKey = path.join(uploadParams.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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
591
|
+
message.error('加载文件失败,请检查文件格式!');
|
|
317
592
|
}
|
|
318
593
|
};
|
|
319
594
|
} catch (error) {
|
|
320
|
-
console.error('
|
|
321
|
-
message.error('
|
|
595
|
+
console.error('文件处理错误:', error);
|
|
596
|
+
message.error('文件处理失败,请检查文件格式!');
|
|
322
597
|
validating.value = false;
|
|
323
|
-
validationMsg.value = '
|
|
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
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
342
|
-
|
|
618
|
+
if (!uploadUrl.value.api) uploadUrl.value.api = pageCtrl.api;
|
|
619
|
+
if (uploadUrl.value.authorize === undefined) uploadUrl.value.authorize = pageCtrl.authorize;
|
|
343
620
|
|
|
344
|
-
|
|
345
|
-
|
|
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 ?? '
|
|
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="
|
|
369
|
-
<Button type="primary"
|
|
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
|
|
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"
|
|
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
|
|
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>
|