@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.
- package/.vscode/settings.json +1 -1
- package/lib/assets/modules/{file-upload-BBlFaIXB.js → file-upload-DhPgqGdk.js} +50 -50
- package/lib/assets/modules/index-02J2AYth.js +377 -0
- package/lib/assets/modules/{index-m5rogIyM.js → index-C31q4LHC.js} +2 -2
- package/lib/assets/modules/{index-BG1SqSVl.js → index-CCpTizF9.js} +1 -1
- package/lib/assets/modules/{menuTabs-tPIz4a89.js → menuTabs-DyhSKN9r.js} +2 -2
- package/lib/assets/modules/{toolIcon-DwWoD9TN.js → toolIcon-CqM4gBIc.js} +1 -1
- package/lib/assets/modules/{uploadList-D_Z-Y2tw.js → uploadList-DAVjJkqz.js} +511 -476
- package/lib/assets/modules/{uploadList-Da7mQUNK.js → uploadList-ZajZKqaS.js} +4 -4
- package/lib/components/common/alert/index.vue.d.ts +13 -0
- package/lib/components/common/icon/helper.vue.d.ts +1 -0
- package/lib/components/common/index.d.ts +2 -0
- package/lib/components/content/form/formItem.vue.d.ts +1 -0
- package/lib/components/content/table/index.vue.d.ts +95 -4
- package/lib/components/form/input/index.vue.d.ts +4 -1
- package/lib/components/form/select/index.vue.d.ts +2 -0
- package/lib/components/form/treeSelect/index.vue.d.ts +11 -2
- package/lib/components/index.d.ts +1 -1
- 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 +317 -274
- package/lib/es/UploadForm/index.js +4 -4
- package/lib/index.d.ts +1 -1
- package/lib/typings/form.d.ts +2 -2
- package/lib/typings/option.d.ts +2 -2
- package/lib/utils/excel-preview.d.ts +24 -0
- package/lib/utils/form-excel.d.ts +17 -4
- package/lib/utils/options.d.ts +2 -2
- package/lib/webui.css +1 -1
- package/lib/webui.es.js +759 -747
- package/package.json +1 -1
- package/src/components/common/alert/index.vue +76 -0
- package/src/components/common/icon/helper.vue +7 -1
- package/src/components/common/index.ts +4 -1
- package/src/components/common/loading/index.vue +1 -1
- package/src/components/content/dialog/excelForm.vue +343 -313
- package/src/components/content/form/formItem.vue +6 -2
- 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/components/form/input/index.vue +16 -3
- package/src/components/form/select/index.vue +5 -11
- package/src/components/form/treeSelect/index.vue +22 -17
- package/src/components/index.ts +1 -0
- package/src/index.ts +1 -0
- package/src/typings/form.d.ts +2 -2
- package/src/typings/option.d.ts +2 -2
- package/src/utils/excel-preview.ts +188 -0
- package/src/utils/file-upload.ts +0 -2
- package/src/utils/form-excel.ts +132 -126
- package/src/utils/options.ts +80 -22
- package/src/utils/table.ts +15 -2
- 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
|
|
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 {
|
|
21
|
-
import {
|
|
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 =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
+
loadPreviewFileLocal();
|
|
160
271
|
}
|
|
161
272
|
},
|
|
162
273
|
{ deep: true },
|
|
163
274
|
);
|
|
164
275
|
|
|
165
276
|
const uploadParams = ref(props.uploadParams);
|
|
166
|
-
const uploadUrl =
|
|
167
|
-
const duplicateRules =
|
|
168
|
-
const 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
|
|
282
|
+
const loadPreviewFileLocal = async () => {
|
|
172
283
|
if (!props.previewUrl || !excelCtrl) return;
|
|
173
284
|
|
|
285
|
+
excelCtrl.isFormLoading.value = true;
|
|
174
286
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
381
|
-
|
|
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
|
|
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:
|
|
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<
|
|
444
|
+
const performDataValidation = async (buffer: ArrayBuffer): Promise<void> => {
|
|
430
445
|
const gridCtrl = props.gridCtrl;
|
|
431
|
-
if (!gridCtrl) return
|
|
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
|
-
|
|
455
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
562
|
-
if (!isValidationPassed) {
|
|
563
|
-
return false; // 验证失败则结束
|
|
564
|
-
}
|
|
567
|
+
await performDataValidation(convertedExcelBuffer);
|
|
565
568
|
|
|
566
|
-
//
|
|
567
|
-
|
|
569
|
+
// 验证完成后显示Excel预览(只有在验证成功时才显示原始文件)
|
|
570
|
+
if (!excelError.value && !duplicateError.value) {
|
|
571
|
+
excelUrl.value = excelResult.blobUrl!;
|
|
572
|
+
}
|
|
568
573
|
fileName.value = excelResult.fileName!;
|
|
569
|
-
|
|
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
|
-
|
|
583
|
-
if (!isValidationPassed) {
|
|
584
|
-
return; // 验证失败则结束
|
|
585
|
-
}
|
|
587
|
+
await performDataValidation(buffer);
|
|
586
588
|
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
644
|
-
|
|
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
|
-
<
|
|
652
|
-
|
|
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
|
-
<
|
|
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="
|
|
664
|
-
:
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
:
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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 ||
|
|
728
|
-
>
|
|
758
|
+
:disabled="!excelUrl || excelError || duplicateError || excelCtrl.isFormLoading.value">
|
|
729
759
|
{{ saveText ?? '上传文件' }}
|
|
730
760
|
</Button>
|
|
731
761
|
</Space>
|