@skyfox2000/webui 0.1.0
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/.eslintrc.js +23 -0
- package/.prettierrc +11 -0
- package/.vscode/settings.json +25 -0
- package/README.md +104 -0
- package/env.d.ts +11 -0
- package/index.html +19 -0
- package/lib/AceEditor.d.ts +4 -0
- package/lib/BasicLayout.d.ts +4 -0
- package/lib/Error403.d.ts +4 -0
- package/lib/Error404.d.ts +4 -0
- package/lib/ExcelForm.d.ts +4 -0
- package/lib/UploadForm.d.ts +4 -0
- package/lib/assets/modules/basicLayout-YP_-EySb.js +726 -0
- package/lib/assets/modules/error403-Bi0E2twj.js +33 -0
- package/lib/assets/modules/error404-BF7vasR_.js +33 -0
- package/lib/assets/modules/excelForm-Dzndz-SG.js +109 -0
- package/lib/assets/modules/excelForm-WJVQmaDT.js +317 -0
- package/lib/assets/modules/index-FzWSvscZ.js +107 -0
- package/lib/assets/modules/index-ekkaExvB.js +49 -0
- package/lib/assets/modules/uploadForm-BahGnrAq.js +415 -0
- package/lib/assets/modules/uploadForm-DEnOjhwc.js +308 -0
- package/lib/components/common/button/index.vue.d.ts +42 -0
- package/lib/components/common/button/index.vue.d.ts.map +1 -0
- package/lib/components/common/icon/appicon.vue.d.ts +12 -0
- package/lib/components/common/icon/appicon.vue.d.ts.map +1 -0
- package/lib/components/common/icon/fullscreen.vue.d.ts +4 -0
- package/lib/components/common/icon/fullscreen.vue.d.ts.map +1 -0
- package/lib/components/common/icon/helper.vue.d.ts +23 -0
- package/lib/components/common/icon/helper.vue.d.ts.map +1 -0
- package/lib/components/common/icon/index.vue.d.ts +244 -0
- package/lib/components/common/icon/index.vue.d.ts.map +1 -0
- package/lib/components/common/icon/layoutIcon.vue.d.ts +44 -0
- package/lib/components/common/icon/layoutIcon.vue.d.ts.map +1 -0
- package/lib/components/common/icon/projectIcon.vue.d.ts +60 -0
- package/lib/components/common/icon/projectIcon.vue.d.ts.map +1 -0
- package/lib/components/common/icon/toolIcon.vue.d.ts +44 -0
- package/lib/components/common/icon/toolIcon.vue.d.ts.map +1 -0
- package/lib/components/common/index.d.ts +19 -0
- package/lib/components/common/index.d.ts.map +1 -0
- package/lib/components/common/tooltip/index.vue.d.ts +22 -0
- package/lib/components/common/tooltip/index.vue.d.ts.map +1 -0
- package/lib/components/content/dialog/excelForm.vue.d.ts +31 -0
- package/lib/components/content/dialog/excelForm.vue.d.ts.map +1 -0
- package/lib/components/content/dialog/index.vue.d.ts +35 -0
- package/lib/components/content/dialog/index.vue.d.ts.map +1 -0
- package/lib/components/content/dialog/uploadForm.vue.d.ts +25 -0
- package/lib/components/content/dialog/uploadForm.vue.d.ts.map +1 -0
- package/lib/components/content/drawer/index.vue.d.ts +27 -0
- package/lib/components/content/drawer/index.vue.d.ts.map +1 -0
- package/lib/components/content/form/formItem.vue.d.ts +26 -0
- package/lib/components/content/form/formItem.vue.d.ts.map +1 -0
- package/lib/components/content/form/index.vue.d.ts +26 -0
- package/lib/components/content/form/index.vue.d.ts.map +1 -0
- package/lib/components/content/index.d.ts +27 -0
- package/lib/components/content/index.d.ts.map +1 -0
- package/lib/components/content/search/index.vue.d.ts +30 -0
- package/lib/components/content/search/index.vue.d.ts.map +1 -0
- package/lib/components/content/search/searchItem.vue.d.ts +24 -0
- package/lib/components/content/search/searchItem.vue.d.ts.map +1 -0
- package/lib/components/content/table/index.vue.d.ts +37 -0
- package/lib/components/content/table/index.vue.d.ts.map +1 -0
- package/lib/components/content/table/tableOperate.vue.d.ts +19 -0
- package/lib/components/content/table/tableOperate.vue.d.ts.map +1 -0
- package/lib/components/content/toolbar/icontool.vue.d.ts +8 -0
- package/lib/components/content/toolbar/icontool.vue.d.ts.map +1 -0
- package/lib/components/content/toolbar/index.vue.d.ts +19 -0
- package/lib/components/content/toolbar/index.vue.d.ts.map +1 -0
- package/lib/components/content/tree/index.vue.d.ts +47 -0
- package/lib/components/content/tree/index.vue.d.ts.map +1 -0
- package/lib/components/error/error403.vue.d.ts +4 -0
- package/lib/components/error/error403.vue.d.ts.map +1 -0
- package/lib/components/error/error404.vue.d.ts +4 -0
- package/lib/components/error/error404.vue.d.ts.map +1 -0
- package/lib/components/form/aceEditor/aceConfig.d.ts +9 -0
- package/lib/components/form/aceEditor/aceConfig.d.ts.map +1 -0
- package/lib/components/form/aceEditor/index.vue.d.ts +13 -0
- package/lib/components/form/aceEditor/index.vue.d.ts.map +1 -0
- package/lib/components/form/autoComplete/index.vue.d.ts +140 -0
- package/lib/components/form/autoComplete/index.vue.d.ts.map +1 -0
- package/lib/components/form/cascader/index.vue.d.ts +110 -0
- package/lib/components/form/cascader/index.vue.d.ts.map +1 -0
- package/lib/components/form/checkbox/index.vue.d.ts +129 -0
- package/lib/components/form/checkbox/index.vue.d.ts.map +1 -0
- package/lib/components/form/datePicker/index.vue.d.ts +7 -0
- package/lib/components/form/datePicker/index.vue.d.ts.map +1 -0
- package/lib/components/form/index.d.ts +41 -0
- package/lib/components/form/index.d.ts.map +1 -0
- package/lib/components/form/input/index.vue.d.ts +27 -0
- package/lib/components/form/input/index.vue.d.ts.map +1 -0
- package/lib/components/form/input/inputIcon.vue.d.ts +11 -0
- package/lib/components/form/input/inputIcon.vue.d.ts.map +1 -0
- package/lib/components/form/input/inputNumber.vue.d.ts +4 -0
- package/lib/components/form/input/inputNumber.vue.d.ts.map +1 -0
- package/lib/components/form/input/inputPassword.vue.d.ts +4 -0
- package/lib/components/form/input/inputPassword.vue.d.ts.map +1 -0
- package/lib/components/form/propEditor/index.vue.d.ts +13 -0
- package/lib/components/form/propEditor/index.vue.d.ts.map +1 -0
- package/lib/components/form/radio/index.vue.d.ts +134 -0
- package/lib/components/form/radio/index.vue.d.ts.map +1 -0
- package/lib/components/form/radio/radioStatus.vue.d.ts +32 -0
- package/lib/components/form/radio/radioStatus.vue.d.ts.map +1 -0
- package/lib/components/form/rangePicker/index.vue.d.ts +17 -0
- package/lib/components/form/rangePicker/index.vue.d.ts.map +1 -0
- package/lib/components/form/select/index.vue.d.ts +143 -0
- package/lib/components/form/select/index.vue.d.ts.map +1 -0
- package/lib/components/form/switch/index.vue.d.ts +44 -0
- package/lib/components/form/switch/index.vue.d.ts.map +1 -0
- package/lib/components/form/textarea/index.vue.d.ts +4 -0
- package/lib/components/form/textarea/index.vue.d.ts.map +1 -0
- package/lib/components/form/transfer/index.vue.d.ts +39 -0
- package/lib/components/form/transfer/index.vue.d.ts.map +1 -0
- package/lib/components/form/transfer/transferTable.vue.d.ts +39 -0
- package/lib/components/form/transfer/transferTable.vue.d.ts.map +1 -0
- package/lib/components/form/treeSelect/index.vue.d.ts +39 -0
- package/lib/components/form/treeSelect/index.vue.d.ts.map +1 -0
- package/lib/components/form/upload/uploadList.vue.d.ts +477 -0
- package/lib/components/form/upload/uploadList.vue.d.ts.map +1 -0
- package/lib/components/index.d.ts +9 -0
- package/lib/components/index.d.ts.map +1 -0
- package/lib/components/layout/breadcrumb/index.vue.d.ts +4 -0
- package/lib/components/layout/breadcrumb/index.vue.d.ts.map +1 -0
- package/lib/components/layout/content/index.vue.d.ts +23 -0
- package/lib/components/layout/content/index.vue.d.ts.map +1 -0
- package/lib/components/layout/datetime/index.vue.d.ts +4 -0
- package/lib/components/layout/datetime/index.vue.d.ts.map +1 -0
- package/lib/components/layout/header/headerExits.vue.d.ts +4 -0
- package/lib/components/layout/header/headerExits.vue.d.ts.map +1 -0
- package/lib/components/layout/header/index.vue.d.ts +4 -0
- package/lib/components/layout/header/index.vue.d.ts.map +1 -0
- package/lib/components/layout/index.d.ts +17 -0
- package/lib/components/layout/index.d.ts.map +1 -0
- package/lib/components/layout/menu/index.vue.d.ts +7 -0
- package/lib/components/layout/menu/index.vue.d.ts.map +1 -0
- package/lib/components/layout/menu/menuTabs.vue.d.ts +4 -0
- package/lib/components/layout/menu/menuTabs.vue.d.ts.map +1 -0
- package/lib/components/layout/page/basicLayout.vue.d.ts +7 -0
- package/lib/components/layout/page/basicLayout.vue.d.ts.map +1 -0
- package/lib/es/AceEditor/index.js +168 -0
- package/lib/es/BasicLayout/index.js +4 -0
- package/lib/es/Error403/index.js +4 -0
- package/lib/es/Error404/index.js +4 -0
- package/lib/es/ExcelForm/index.js +5 -0
- package/lib/es/UploadForm/index.js +5 -0
- package/lib/index.d.ts +2 -0
- package/lib/webui.css +1 -0
- package/lib/webui.es.js +3349 -0
- package/package.json +66 -0
- package/plugins/vite-plugin-auto-generate-vue.ts +105 -0
- package/postcss.config.ts +6 -0
- package/src/assets/global.css +9 -0
- package/src/components/common/button/index.vue +126 -0
- package/src/components/common/icon/appicon.vue +28 -0
- package/src/components/common/icon/fullscreen.vue +13 -0
- package/src/components/common/icon/helper.vue +30 -0
- package/src/components/common/icon/index.vue +426 -0
- package/src/components/common/icon/layoutIcon.vue +33 -0
- package/src/components/common/icon/projectIcon.vue +41 -0
- package/src/components/common/icon/toolIcon.vue +33 -0
- package/src/components/common/index.ts +19 -0
- package/src/components/common/tooltip/index.vue +25 -0
- package/src/components/content/dialog/excelForm.vue +479 -0
- package/src/components/content/dialog/index.vue +149 -0
- package/src/components/content/dialog/uploadForm.vue +228 -0
- package/src/components/content/drawer/index.vue +93 -0
- package/src/components/content/form/formItem.vue +76 -0
- package/src/components/content/form/index.vue +48 -0
- package/src/components/content/index.ts +32 -0
- package/src/components/content/search/index.vue +135 -0
- package/src/components/content/search/searchItem.vue +52 -0
- package/src/components/content/table/index.vue +215 -0
- package/src/components/content/table/tableOperate.vue +131 -0
- package/src/components/content/toolbar/icontool.vue +151 -0
- package/src/components/content/toolbar/index.vue +107 -0
- package/src/components/content/tree/index.vue +140 -0
- package/src/components/error/error403.vue +14 -0
- package/src/components/error/error404.vue +14 -0
- package/src/components/form/aceEditor/aceConfig.ts +90 -0
- package/src/components/form/aceEditor/index.vue +175 -0
- package/src/components/form/autoComplete/index.vue +171 -0
- package/src/components/form/cascader/index.vue +110 -0
- package/src/components/form/checkbox/index.vue +108 -0
- package/src/components/form/datePicker/index.vue +29 -0
- package/src/components/form/index.ts +54 -0
- package/src/components/form/input/index.vue +70 -0
- package/src/components/form/input/inputIcon.vue +39 -0
- package/src/components/form/input/inputNumber.vue +23 -0
- package/src/components/form/input/inputPassword.vue +22 -0
- package/src/components/form/propEditor/index.vue +81 -0
- package/src/components/form/radio/index.vue +132 -0
- package/src/components/form/radio/radioStatus.vue +42 -0
- package/src/components/form/rangePicker/index.vue +64 -0
- package/src/components/form/select/index.vue +186 -0
- package/src/components/form/switch/index.vue +58 -0
- package/src/components/form/textarea/index.vue +23 -0
- package/src/components/form/transfer/index.vue +95 -0
- package/src/components/form/transfer/transferTable.vue +124 -0
- package/src/components/form/treeSelect/index.vue +108 -0
- package/src/components/form/upload/uploadList.vue +235 -0
- package/src/components/index.ts +97 -0
- package/src/components/layout/breadcrumb/index.vue +38 -0
- package/src/components/layout/content/index.vue +28 -0
- package/src/components/layout/datetime/index.vue +16 -0
- package/src/components/layout/header/headerExits.vue +28 -0
- package/src/components/layout/header/index.vue +43 -0
- package/src/components/layout/index.ts +16 -0
- package/src/components/layout/menu/index.vue +64 -0
- package/src/components/layout/menu/menuTabs.vue +56 -0
- package/src/components/layout/page/basicLayout.vue +67 -0
- package/src/vite-env.d.ts +8 -0
- package/tailwind.config.ts +11 -0
- package/tsconfig.json +53 -0
- package/vite.config.ts +117 -0
- package//344/273/243/347/240/201/350/247/204/350/214/203/345/217/212/351/243/216/346/240/274/346/214/207/345/215/227.md +116 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { watch, ref, onMounted } from 'vue';
|
|
3
|
+
import { Button } from '../../common';
|
|
4
|
+
import { Modal, Space, Upload, Alert } from 'ant-design-vue';
|
|
5
|
+
import {
|
|
6
|
+
EditorControl,
|
|
7
|
+
GridControl,
|
|
8
|
+
validateExcel,
|
|
9
|
+
getRuleTexts,
|
|
10
|
+
checkExcelDuplicates,
|
|
11
|
+
AsyncUploader,
|
|
12
|
+
UploadStatus,
|
|
13
|
+
doSave,
|
|
14
|
+
ExcelFileParams,
|
|
15
|
+
processExcelFile,
|
|
16
|
+
path,
|
|
17
|
+
} from '@skyfox2000/webbase';
|
|
18
|
+
import { AnyData, ResStatus } from '@skyfox2000/fapi';
|
|
19
|
+
import message from 'vue-m-message';
|
|
20
|
+
//引入相关样式
|
|
21
|
+
import type { UploadProps } from 'ant-design-vue';
|
|
22
|
+
import VueOfficeExcel from '@vue-office/excel';
|
|
23
|
+
import '@vue-office/excel/lib/index.css';
|
|
24
|
+
import { UploadFile } from '@skyfox2000/webbase';
|
|
25
|
+
|
|
26
|
+
type AlertType = 'success' | 'info' | 'warning' | 'error';
|
|
27
|
+
|
|
28
|
+
const props = defineProps<{
|
|
29
|
+
/**
|
|
30
|
+
* 标题
|
|
31
|
+
*/
|
|
32
|
+
title: String;
|
|
33
|
+
/**
|
|
34
|
+
* 表格控制器
|
|
35
|
+
*/
|
|
36
|
+
gridCtrl: GridControl<AnyData>;
|
|
37
|
+
/**
|
|
38
|
+
* 表格控制器
|
|
39
|
+
*/
|
|
40
|
+
excelCtrl: EditorControl<AnyData>;
|
|
41
|
+
/**
|
|
42
|
+
* 文件上传参数
|
|
43
|
+
*/
|
|
44
|
+
uploadParams: ExcelFileParams;
|
|
45
|
+
/**
|
|
46
|
+
* 表格字段映射
|
|
47
|
+
* - 表头映射字段
|
|
48
|
+
*/
|
|
49
|
+
excelFieldMap: Record<string, string>;
|
|
50
|
+
/**
|
|
51
|
+
* Excel文件信息字段
|
|
52
|
+
*/
|
|
53
|
+
fileField?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Excel批量数据字段
|
|
56
|
+
* - Excel数据列表转换后的上传字段
|
|
57
|
+
*/
|
|
58
|
+
excelBatchField?: string;
|
|
59
|
+
/**
|
|
60
|
+
* 确认按钮文字,空字符串则不显示
|
|
61
|
+
*/
|
|
62
|
+
saveText?: string;
|
|
63
|
+
}>();
|
|
64
|
+
|
|
65
|
+
const excelCtrl = props.excelCtrl;
|
|
66
|
+
const open = ref<boolean>(false);
|
|
67
|
+
const excelUrl = ref('');
|
|
68
|
+
const fileList = ref<any[]>([]);
|
|
69
|
+
const fileName = ref('');
|
|
70
|
+
const validating = ref(true); // 表示正在验证状态
|
|
71
|
+
|
|
72
|
+
// Alert状态变量
|
|
73
|
+
const validationMsg = ref('待验证数据规则');
|
|
74
|
+
const validationType = ref<AlertType>('warning');
|
|
75
|
+
const duplicateMsg = ref('待验证重复数据');
|
|
76
|
+
const duplicateType = ref<AlertType>('warning');
|
|
77
|
+
|
|
78
|
+
watch(
|
|
79
|
+
() => excelCtrl.visible.value,
|
|
80
|
+
() => {
|
|
81
|
+
open.value = excelCtrl.visible.value;
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
watch(
|
|
85
|
+
() => open.value,
|
|
86
|
+
() => {
|
|
87
|
+
excelCtrl.visible.value = open.value;
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const uploadParams = ref(props.uploadParams);
|
|
92
|
+
const uploadUrl = ref(uploadParams.value.uploadUrl);
|
|
93
|
+
const duplicateRules = ref(uploadParams.value.duplicateRules);
|
|
94
|
+
const duplicateUrl = ref(uploadParams.value.duplicateUrl);
|
|
95
|
+
|
|
96
|
+
const dialogUpload = async () => {
|
|
97
|
+
const url = uploadUrl.value;
|
|
98
|
+
if (!url) {
|
|
99
|
+
message.error('未配置文件上传地址!');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (excelError.value) {
|
|
104
|
+
message.error('表格数据存在验证错误,请修正后再上传!');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
excelCtrl.isFormLoading.value = true;
|
|
109
|
+
try {
|
|
110
|
+
if (!excelBuffer.value || !fileName.value) {
|
|
111
|
+
message.warning('请先选择Excel文件!');
|
|
112
|
+
excelCtrl.isFormLoading.value = false;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 创建文件对象和上传器
|
|
117
|
+
const uploader = new AsyncUploader(url, 1);
|
|
118
|
+
|
|
119
|
+
// 创建上传文件对象
|
|
120
|
+
const file = new File([excelBuffer.value], fileName.value, {
|
|
121
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const fileKey = path.join(uploadParams.value.basePath, fileName.value);
|
|
125
|
+
const uploadFile: UploadFile = {
|
|
126
|
+
uid: '1',
|
|
127
|
+
name: fileName.value,
|
|
128
|
+
originFileObj: file,
|
|
129
|
+
status: UploadStatus.Uploading,
|
|
130
|
+
percent: 0,
|
|
131
|
+
params: {
|
|
132
|
+
FileKey: fileKey,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// 使用上传器的 uploadFile 方法上传文件
|
|
137
|
+
const abortController = new AbortController();
|
|
138
|
+
try {
|
|
139
|
+
await uploader.uploadFile(uploadFile, abortController.signal, (percent) => {
|
|
140
|
+
uploadFile.percent = percent;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// 上传成功后调用 dialogSave 方法
|
|
144
|
+
message.success('文件上传成功,开始业务处理!');
|
|
145
|
+
dialogSave(uploadFile);
|
|
146
|
+
} catch (uploadError: any) {
|
|
147
|
+
console.error('文件上传错误:', uploadError);
|
|
148
|
+
message.error(uploadError?.message || '文件上传失败,请稍后再试!');
|
|
149
|
+
throw uploadError; // 向外层抛出错误
|
|
150
|
+
}
|
|
151
|
+
} catch (error: any) {
|
|
152
|
+
console.error('上传处理错误:', error);
|
|
153
|
+
message.error('上传处理失败:' + (error?.message || '未知错误'));
|
|
154
|
+
} finally {
|
|
155
|
+
excelCtrl.isFormLoading.value = false;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const dialogSave = async (uploadFile: UploadFile) => {
|
|
160
|
+
if (excelCtrl.formData.value) {
|
|
161
|
+
// 使用 doSave 方法保存数据
|
|
162
|
+
if (props.excelBatchField) {
|
|
163
|
+
// 获取Excel数据,并转换成指定数据对象结构
|
|
164
|
+
const excelFileData = await processExcelFile(excelBuffer.value!);
|
|
165
|
+
if (!excelFileData) return null;
|
|
166
|
+
const { excelData } = excelFileData;
|
|
167
|
+
const excelDataList = excelData.map((row: Record<string, AnyData>) => {
|
|
168
|
+
const result: Record<string, AnyData> = {};
|
|
169
|
+
for (const key in row) {
|
|
170
|
+
const field = props.excelFieldMap[key];
|
|
171
|
+
if (field) {
|
|
172
|
+
result[field] = row[key];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
});
|
|
177
|
+
excelCtrl.formData.value[props.excelBatchField] = excelDataList;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fileField = props.fileField ?? 'FileInfo';
|
|
181
|
+
excelCtrl.formData.value[fileField] = uploadFile;
|
|
182
|
+
|
|
183
|
+
if (excelCtrl.beforeSave) {
|
|
184
|
+
excelCtrl.beforeSave();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const postData = {
|
|
188
|
+
Data: {
|
|
189
|
+
...excelCtrl.formData.value,
|
|
190
|
+
},
|
|
191
|
+
Query: {
|
|
192
|
+
[props.excelCtrl.primaryKey]: excelCtrl.formData.value[props.excelCtrl.primaryKey],
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
excelCtrl.isFormLoading.value = true;
|
|
197
|
+
try {
|
|
198
|
+
const result = await doSave(props.excelCtrl, {
|
|
199
|
+
params: postData,
|
|
200
|
+
urlKey: 'save',
|
|
201
|
+
url: props.excelCtrl.saveUrl,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (result?.status === ResStatus.SUCCESS) {
|
|
205
|
+
message.success('数据保存成功!');
|
|
206
|
+
if (excelCtrl.afterSave) {
|
|
207
|
+
excelCtrl.afterSave();
|
|
208
|
+
}
|
|
209
|
+
excelCtrl.visible.value = false;
|
|
210
|
+
} else {
|
|
211
|
+
message.error('保存失败:' + (result?.msg || '未知错误'));
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error('保存错误:', error);
|
|
215
|
+
message.error('数据保存失败,请稍后再试!');
|
|
216
|
+
} finally {
|
|
217
|
+
excelCtrl.isFormLoading.value = false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const excelError = ref(false);
|
|
223
|
+
const duplicateError = ref(false);
|
|
224
|
+
const excelBuffer = ref<ArrayBuffer | null>(null);
|
|
225
|
+
|
|
226
|
+
// 上传前处理函数
|
|
227
|
+
const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
|
|
228
|
+
const isExcel =
|
|
229
|
+
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
|
230
|
+
file.type === 'application/vnd.ms-excel';
|
|
231
|
+
if (!isExcel) {
|
|
232
|
+
message.error('只能上传Excel文件!');
|
|
233
|
+
return Upload.LIST_IGNORE;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
// 设置文件名
|
|
238
|
+
fileName.value = file.name;
|
|
239
|
+
|
|
240
|
+
// 清除之前的错误状态并设置为验证中
|
|
241
|
+
excelError.value = false;
|
|
242
|
+
duplicateError.value = false;
|
|
243
|
+
validating.value = true;
|
|
244
|
+
validationMsg.value = '待验证数据规则';
|
|
245
|
+
validationType.value = 'warning';
|
|
246
|
+
duplicateMsg.value = '待验证重复数据';
|
|
247
|
+
duplicateType.value = 'warning';
|
|
248
|
+
|
|
249
|
+
// 使用ArrayBuffer读取文件
|
|
250
|
+
const buffer = await file.arrayBuffer();
|
|
251
|
+
excelBuffer.value = buffer;
|
|
252
|
+
|
|
253
|
+
const reader = new FileReader();
|
|
254
|
+
reader.readAsDataURL(file);
|
|
255
|
+
reader.onload = async (loadEvent) => {
|
|
256
|
+
if (loadEvent.target) {
|
|
257
|
+
// 读取完成后先进行普通验证
|
|
258
|
+
const { hasError, errBlob } = await validateExcel(buffer, excelCtrl.formRules.value);
|
|
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
|
+
}
|
|
270
|
+
return; // 验证失败则结束
|
|
271
|
+
} else {
|
|
272
|
+
// 验证成功
|
|
273
|
+
validationMsg.value = '数据验证成功';
|
|
274
|
+
validationType.value = 'success';
|
|
275
|
+
}
|
|
276
|
+
|
|
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
|
+
}
|
|
311
|
+
excelUrl.value = loadEvent.target.result as string;
|
|
312
|
+
|
|
313
|
+
// 验证完成
|
|
314
|
+
validating.value = false;
|
|
315
|
+
} else {
|
|
316
|
+
message.error('加载Excel文件失败,请检查文件格式!');
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error('Excel处理错误:', error);
|
|
321
|
+
message.error('Excel文件处理失败,请检查文件格式!');
|
|
322
|
+
validating.value = false;
|
|
323
|
+
validationMsg.value = 'Excel处理错误';
|
|
324
|
+
validationType.value = 'error';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return false;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// 计算验证规则显示文本
|
|
331
|
+
const validationRules: { field: string; rules: string[] }[] = getRuleTexts(excelCtrl.formRules.value);
|
|
332
|
+
|
|
333
|
+
onMounted(() => {
|
|
334
|
+
const pageCtrl = props.gridCtrl.page;
|
|
335
|
+
uploadUrl.value = uploadUrl.value ?? pageCtrl.urls.upload;
|
|
336
|
+
if (!uploadUrl.value) {
|
|
337
|
+
message.error('未配置文件上传地址!');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!uploadUrl.value.api) uploadUrl.value.api = pageCtrl.api;
|
|
342
|
+
if (uploadUrl.value.authorize === undefined) uploadUrl.value.authorize = pageCtrl.authorize;
|
|
343
|
+
|
|
344
|
+
for (const key in excelCtrl.formData.value) {
|
|
345
|
+
if (props.gridCtrl.rowData.value) excelCtrl.formData.value[key] = props.gridCtrl.rowData.value[key];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
open.value = excelCtrl.visible.value;
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const dialogClose = () => {
|
|
352
|
+
excelCtrl.visible.value = false;
|
|
353
|
+
};
|
|
354
|
+
const handleError = () => {
|
|
355
|
+
console.error('渲染失败');
|
|
356
|
+
};
|
|
357
|
+
</script>
|
|
358
|
+
<template>
|
|
359
|
+
<Modal
|
|
360
|
+
:title="title ?? 'Excel文件上传'"
|
|
361
|
+
v-model:open="open"
|
|
362
|
+
:wrapClassName="['modal', 'mx-auto', $attrs.width ? 'w-[' + $attrs.width + ']' : ''].join(' ')"
|
|
363
|
+
:width="940"
|
|
364
|
+
@close="dialogClose"
|
|
365
|
+
>
|
|
366
|
+
<slot></slot>
|
|
367
|
+
<div class="mb-4 flex items-center">
|
|
368
|
+
<Upload :file-list="fileList" :before-upload="beforeUpload" accept=".xlsx,.xls" :showUploadList="true">
|
|
369
|
+
<Button type="primary">选择Excel文件</Button>
|
|
370
|
+
</Upload>
|
|
371
|
+
<div v-if="excelUrl && fileName" class="ml-3 text-gray-600">
|
|
372
|
+
<span>{{ fileName }}</span>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div class="flex gap-4">
|
|
377
|
+
<!-- 左侧Excel显示区域 -->
|
|
378
|
+
<div
|
|
379
|
+
class="flex-shrink-0 excel-container"
|
|
380
|
+
:class="[validationRules.length === 0 ? 'w-[100%]' : 'w-[80%]']"
|
|
381
|
+
style="height: 430px"
|
|
382
|
+
>
|
|
383
|
+
<VueOfficeExcel
|
|
384
|
+
:src="excelUrl"
|
|
385
|
+
@error="handleError"
|
|
386
|
+
style="width: 100%; height: 100%"
|
|
387
|
+
:options="{
|
|
388
|
+
styles: true,
|
|
389
|
+
formatCells: true,
|
|
390
|
+
skipStyles: false,
|
|
391
|
+
autoStyle: true,
|
|
392
|
+
keepOriginalFormat: true,
|
|
393
|
+
renderingStyle: 'svg',
|
|
394
|
+
}"
|
|
395
|
+
/>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<!-- 右侧验证条件和错误信息 -->
|
|
399
|
+
<div class="w-[22%] flex-grow overflow-hidden flex flex-col" v-if="validationRules.length > 0">
|
|
400
|
+
<div class="bg-gray-50 p-2 rounded border flex-grow flex flex-col">
|
|
401
|
+
<div v-if="validationRules.length === 0" class="text-gray-500">没有设置验证规则</div>
|
|
402
|
+
<div v-else class="flex flex-col">
|
|
403
|
+
<!-- 行验证规则 -->
|
|
404
|
+
<Alert :message="validationMsg" :type="validationType" show-icon class="mb-2" />
|
|
405
|
+
<div class="overflow-y-auto">
|
|
406
|
+
<div v-for="(rule, index) in validationRules" :key="index" class="border-b pb-0">
|
|
407
|
+
<div class="text-[13px] text-gray-600">{{ rule.field }}</div>
|
|
408
|
+
<ul class="pl-5 mt-1 mb-0">
|
|
409
|
+
<li v-for="(text, i) in rule.rules" :key="i" class="text-[12px] text-gray-600">
|
|
410
|
+
{{ text }}
|
|
411
|
+
</li>
|
|
412
|
+
</ul>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<!-- 重复数据检测规则 -->
|
|
417
|
+
<div v-if="duplicateRules && duplicateRules.length > 0" class="mt-3">
|
|
418
|
+
<Alert :message="duplicateMsg" :type="duplicateType" show-icon class="mt-2 mb-2" />
|
|
419
|
+
<div class="text-[12px] text-gray-600 p-1">
|
|
420
|
+
<div class="text-[13px] text-gray-600">
|
|
421
|
+
以下{{ duplicateRules.length > 1 ? '组合' : '字段' }}必须唯一
|
|
422
|
+
</div>
|
|
423
|
+
<ul class="pl-5 mt-1 mb-0">
|
|
424
|
+
<li class="text-[12px] text-gray-600">
|
|
425
|
+
{{ duplicateRules.join(', ') }}
|
|
426
|
+
</li>
|
|
427
|
+
</ul>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<template #footer>
|
|
436
|
+
<Space>
|
|
437
|
+
<Button @click="dialogClose">取消</Button>
|
|
438
|
+
<Button
|
|
439
|
+
@click="dialogUpload"
|
|
440
|
+
type="primary"
|
|
441
|
+
:loading="excelCtrl.isFormSaving.value"
|
|
442
|
+
:disabled="!excelUrl || excelError || duplicateError || validating"
|
|
443
|
+
>
|
|
444
|
+
{{ saveText ?? '上传文件' }}
|
|
445
|
+
</Button>
|
|
446
|
+
</Space>
|
|
447
|
+
</template>
|
|
448
|
+
</Modal>
|
|
449
|
+
</template>
|
|
450
|
+
<style>
|
|
451
|
+
.modal .ant-modal-content {
|
|
452
|
+
padding: 16px;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.full-modal .ant-modal {
|
|
456
|
+
width: 100% !important;
|
|
457
|
+
max-width: 100%;
|
|
458
|
+
top: 0;
|
|
459
|
+
padding-bottom: 0;
|
|
460
|
+
margin: 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.full-modal .ant-modal-content {
|
|
464
|
+
display: flex;
|
|
465
|
+
flex-direction: column;
|
|
466
|
+
height: calc(100vh);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.full-modal .ant-modal-body {
|
|
470
|
+
flex: 1;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.excel-container {
|
|
474
|
+
position: relative;
|
|
475
|
+
border: 1px solid #f0f0f0;
|
|
476
|
+
border-radius: 4px;
|
|
477
|
+
overflow: hidden;
|
|
478
|
+
}
|
|
479
|
+
</style>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { watch, ref, onMounted, provide } from 'vue';
|
|
3
|
+
import { Button } from '../../common';
|
|
4
|
+
import { Modal, Space } from 'ant-design-vue';
|
|
5
|
+
import { onFormSave, onFormSaveAs, onFormClose, EditorControl, ProviderKeys } from '@skyfox2000/webbase';
|
|
6
|
+
import { AnyData } from '@skyfox2000/fapi';
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
/**
|
|
10
|
+
* 确认按钮文字,空字符串则不显示
|
|
11
|
+
*/
|
|
12
|
+
saveText?: string;
|
|
13
|
+
/**
|
|
14
|
+
* 另存为按钮文字,空字符串则不显示
|
|
15
|
+
*/
|
|
16
|
+
saveAsText?: string;
|
|
17
|
+
/**
|
|
18
|
+
* 取消按钮文字,空字符则不显示
|
|
19
|
+
*/
|
|
20
|
+
cancelText?: string;
|
|
21
|
+
/**
|
|
22
|
+
* 保存数据请求配置
|
|
23
|
+
*/
|
|
24
|
+
editorCtrl?: EditorControl<AnyData>;
|
|
25
|
+
/**
|
|
26
|
+
* 自定义存储方法
|
|
27
|
+
*/
|
|
28
|
+
dialogSave?: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* 宽度
|
|
31
|
+
*/
|
|
32
|
+
width?: number;
|
|
33
|
+
/**
|
|
34
|
+
* 全屏模式
|
|
35
|
+
*/
|
|
36
|
+
full?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* 是否显示
|
|
39
|
+
*/
|
|
40
|
+
open?: boolean;
|
|
41
|
+
}>();
|
|
42
|
+
|
|
43
|
+
const editorCtrl = props.editorCtrl;
|
|
44
|
+
provide(ProviderKeys.EditorControl, editorCtrl);
|
|
45
|
+
|
|
46
|
+
const open = ref<boolean>(props.open ?? false);
|
|
47
|
+
const emit = defineEmits(['update:open']);
|
|
48
|
+
const width = ref(props.width ?? 430);
|
|
49
|
+
watch(
|
|
50
|
+
() => editorCtrl?.visible.value,
|
|
51
|
+
(newVal) => {
|
|
52
|
+
open.value = newVal ?? false;
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
watch(
|
|
57
|
+
() => props.open,
|
|
58
|
+
(newVal) => {
|
|
59
|
+
open.value = newVal;
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
watch(
|
|
64
|
+
() => open.value,
|
|
65
|
+
(newVal) => {
|
|
66
|
+
emit('update:open', newVal);
|
|
67
|
+
if (!newVal) {
|
|
68
|
+
dialogClose();
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
onMounted(() => {
|
|
74
|
+
open.value = editorCtrl?.visible.value ?? false;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const dialogSave = () => {
|
|
78
|
+
if (props.dialogSave) {
|
|
79
|
+
} else if (editorCtrl) onFormSave(editorCtrl);
|
|
80
|
+
};
|
|
81
|
+
const dialogSaveAs = () => {
|
|
82
|
+
if (editorCtrl) onFormSaveAs(editorCtrl);
|
|
83
|
+
};
|
|
84
|
+
const dialogClose = () => {
|
|
85
|
+
if (editorCtrl) onFormClose(editorCtrl);
|
|
86
|
+
else open.value = false;
|
|
87
|
+
};
|
|
88
|
+
</script>
|
|
89
|
+
<template>
|
|
90
|
+
<Modal
|
|
91
|
+
v-model:open="open"
|
|
92
|
+
:wrapClassName="'modal mx-auto min-w-[430px] ' + (full ? 'full-modal w-full' : '')"
|
|
93
|
+
:width="width"
|
|
94
|
+
>
|
|
95
|
+
<div class="overflow-y-auto w-full h-full">
|
|
96
|
+
<slot></slot>
|
|
97
|
+
</div>
|
|
98
|
+
<template #footer>
|
|
99
|
+
<Space>
|
|
100
|
+
<Button @click="dialogClose" v-if="cancelText !== ''">
|
|
101
|
+
{{ cancelText ?? '取消' }}
|
|
102
|
+
</Button>
|
|
103
|
+
<Button
|
|
104
|
+
@click="dialogSaveAs"
|
|
105
|
+
v-if="saveAsText !== '' && editorCtrl?.saveAsBtnVisible !== false"
|
|
106
|
+
type="primary"
|
|
107
|
+
:loading="editorCtrl?.isFormSaving.value"
|
|
108
|
+
>
|
|
109
|
+
{{ saveAsText ?? '另存为' }}
|
|
110
|
+
</Button>
|
|
111
|
+
<Button
|
|
112
|
+
@click="dialogSave"
|
|
113
|
+
v-if="saveText !== '' && editorCtrl?.saveBtnVisible !== false"
|
|
114
|
+
type="primary"
|
|
115
|
+
:loading="editorCtrl?.isFormSaving.value"
|
|
116
|
+
>
|
|
117
|
+
{{ saveText ?? '保存' }}
|
|
118
|
+
</Button>
|
|
119
|
+
</Space>
|
|
120
|
+
</template>
|
|
121
|
+
</Modal>
|
|
122
|
+
</template>
|
|
123
|
+
<style>
|
|
124
|
+
.modal {
|
|
125
|
+
.ant-modal-content {
|
|
126
|
+
padding: 16px;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.full-modal {
|
|
131
|
+
.ant-modal {
|
|
132
|
+
width: 100% !important;
|
|
133
|
+
max-width: 100%;
|
|
134
|
+
top: 0;
|
|
135
|
+
padding-bottom: 0;
|
|
136
|
+
margin: 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.ant-modal-content {
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
height: calc(100vh);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.ant-modal-body {
|
|
146
|
+
flex: 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
</style>
|