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