@skyfox2000/webui 1.4.18 → 1.4.21
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/{baseLayout-D3_NxEzk.js → baseLayout-Da4Ox7Lj.js} +3 -3
- package/lib/assets/modules/{file-upload-CNBcbAAZ.js → file-upload-Bu6FkNjZ.js} +5 -5
- package/lib/assets/modules/{index-CDr74akE.js → index-BYVerdEw.js} +2 -2
- package/lib/assets/modules/{index-DOlO_4KL.js → index-BysCt107.js} +2 -2
- package/lib/assets/modules/{index-D14BsF7C.js → index-Ch3meKe4.js} +1 -1
- package/lib/assets/modules/{menuTabs-CqAhoF--.js → menuTabs-BqLT-YbD.js} +16 -16
- package/lib/assets/modules/{toolIcon-BnkqBipR.js → toolIcon-Dd58W0UM.js} +1 -1
- package/lib/assets/modules/{upload-template-KI-IFzyp.js → upload-template-Csccple9.js} +28 -28
- package/lib/assets/modules/{uploadList-DnFXg_B3.js → uploadList-D8scq04c.js} +4 -4
- package/lib/components/form/index.d.ts +2 -0
- package/lib/components/form/propEditor/index.vue.d.ts +1 -0
- package/lib/components/form/upload/imageList.vue.d.ts +486 -0
- package/lib/components/form/upload/uploadList.vue.d.ts +1 -1
- package/lib/components/index.d.ts +1 -1
- package/lib/es/AceEditor/index.js +3 -3
- package/lib/es/BasicLayout/index.js +2 -2
- package/lib/es/Error403/index.js +1 -1
- package/lib/es/Error404/index.js +1 -1
- package/lib/es/ExcelForm/index.js +5 -5
- package/lib/es/MenuLayout/index.js +2 -2
- package/lib/es/TemplateFile/index.js +4 -4
- package/lib/es/UploadForm/index.js +4 -4
- package/lib/index.d.ts +1 -1
- package/lib/webui.css +1 -1
- package/lib/webui.es.js +1275 -1059
- package/package.json +1 -1
- package/src/components/form/index.ts +3 -0
- package/src/components/form/propEditor/index.vue +30 -28
- package/src/components/form/upload/imageList.vue +386 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -54,5 +54,8 @@ export { TransferTable };
|
|
|
54
54
|
import TreeSelect from './treeSelect/index.vue';
|
|
55
55
|
export { TreeSelect };
|
|
56
56
|
|
|
57
|
+
import ImageList from './upload/imageList.vue';
|
|
58
|
+
export { ImageList };
|
|
59
|
+
|
|
57
60
|
import UploadList from './upload/uploadList.vue';
|
|
58
61
|
export { UploadList };
|
|
@@ -12,6 +12,10 @@ const props = defineProps<{
|
|
|
12
12
|
* 配置项
|
|
13
13
|
*/
|
|
14
14
|
selectList?: PropConfigItem[];
|
|
15
|
+
/**
|
|
16
|
+
* 忽略项
|
|
17
|
+
*/
|
|
18
|
+
ignoreList?: string[];
|
|
15
19
|
/**
|
|
16
20
|
* 配置名宽度%
|
|
17
21
|
*/
|
|
@@ -56,11 +60,13 @@ const initConfigList = () => {
|
|
|
56
60
|
} else {
|
|
57
61
|
// 没有selectList的情况:基于value动态生成配置项
|
|
58
62
|
if (props.value && Object.keys(props.value).length > 0) {
|
|
59
|
-
configList.value = Object.entries(props.value)
|
|
60
|
-
|
|
61
|
-
field,
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
configList.value = Object.entries(props.value)
|
|
64
|
+
.filter(([field]) => !props.ignoreList || !props.ignoreList.includes(field))
|
|
65
|
+
.map(([field, value]) => ({
|
|
66
|
+
id: Date.now() + Math.random(),
|
|
67
|
+
field,
|
|
68
|
+
value,
|
|
69
|
+
}));
|
|
64
70
|
} else {
|
|
65
71
|
// value为空时,初始化为空数组
|
|
66
72
|
configList.value = [];
|
|
@@ -81,7 +87,7 @@ watch(
|
|
|
81
87
|
|
|
82
88
|
const updateConfig = () => {
|
|
83
89
|
let newConfig: Record<string, string>;
|
|
84
|
-
|
|
90
|
+
|
|
85
91
|
if (props.selectList && props.selectList.length > 0) {
|
|
86
92
|
// 有selectList时,只更新selectList中定义的字段,保留其他字段不变
|
|
87
93
|
newConfig = { ...props.value };
|
|
@@ -91,7 +97,7 @@ const updateConfig = () => {
|
|
|
91
97
|
}
|
|
92
98
|
});
|
|
93
99
|
} else {
|
|
94
|
-
// 没有selectList
|
|
100
|
+
// 没有selectList时,更新所有字段,包括ignoreList中的字段
|
|
95
101
|
newConfig = configList.value.reduce(
|
|
96
102
|
(acc: Record<string, string>, item: PropConfigItem) => {
|
|
97
103
|
if (item.field) {
|
|
@@ -101,8 +107,17 @@ const updateConfig = () => {
|
|
|
101
107
|
},
|
|
102
108
|
{} as Record<string, string>,
|
|
103
109
|
);
|
|
110
|
+
|
|
111
|
+
// 保留ignoreList中的字段
|
|
112
|
+
if (props.ignoreList && props.ignoreList.length > 0) {
|
|
113
|
+
props.ignoreList.forEach((field) => {
|
|
114
|
+
if (props.value[field] !== undefined) {
|
|
115
|
+
newConfig[field] = props.value[field];
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
104
119
|
}
|
|
105
|
-
|
|
120
|
+
|
|
106
121
|
isInnerChange = true;
|
|
107
122
|
emit('update:value', newConfig);
|
|
108
123
|
};
|
|
@@ -124,15 +139,9 @@ const handleInputChange = () => {
|
|
|
124
139
|
<div class="flex flex-col gap-2">
|
|
125
140
|
<div v-for="item in configList" :key="item.id" class="flex items-center gap-2">
|
|
126
141
|
<div :class="[fieldWidth ? `w-[${fieldWidth}%]` : 'w-[33%]']">
|
|
127
|
-
<Input
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
:title="item.text || item.field"
|
|
131
|
-
class="w-full"
|
|
132
|
-
:placeholder="item.text || labelHolder || '配置名'"
|
|
133
|
-
@input="handleInputChange"
|
|
134
|
-
:disabled="!addMore"
|
|
135
|
-
/>
|
|
142
|
+
<Input v-if="!selectList || selectList.length === 0" v-model:value="item.field"
|
|
143
|
+
:title="item.text || item.field" class="w-full" :placeholder="item.text || labelHolder || '配置名'"
|
|
144
|
+
@input="handleInputChange" :disabled="!addMore" />
|
|
136
145
|
<div v-else>
|
|
137
146
|
<Input v-model:value="item.text" :title="item.text" :disabled="true" class="w-[100%]" />
|
|
138
147
|
<Input type="hidden" v-model:value="item.field" />
|
|
@@ -140,20 +149,13 @@ const handleInputChange = () => {
|
|
|
140
149
|
</div>
|
|
141
150
|
<div class="w-[3%]">=</div>
|
|
142
151
|
<div :class="[fieldWidth ? `w-[${97 - fieldWidth}%]` : 'w-[64%]']">
|
|
143
|
-
<Input
|
|
144
|
-
|
|
145
|
-
:placeholder="valueHolder || '请输入' + item.text || '请输入配置值'"
|
|
146
|
-
@input="handleInputChange"
|
|
147
|
-
:title="item.value"
|
|
148
|
-
/>
|
|
152
|
+
<Input v-model:value="item.value" :placeholder="valueHolder || '请输入' + item.text || '请输入配置值'"
|
|
153
|
+
@input="handleInputChange" :title="item.value" />
|
|
149
154
|
</div>
|
|
150
155
|
</div>
|
|
151
|
-
<Button
|
|
152
|
-
v-if="addMore"
|
|
153
|
-
@click="addNewLine"
|
|
156
|
+
<Button v-if="addMore" @click="addNewLine"
|
|
154
157
|
class="mt-1 w-[80px] !text-[12px] text-[#666] bg-[#e6f7ff] border-[#b3e0ff] hover:bg-[#b3e0ff] hover:border-[#8abeff]"
|
|
155
|
-
size="small"
|
|
156
|
-
>
|
|
158
|
+
size="small">
|
|
157
159
|
新增配置行
|
|
158
160
|
</Button>
|
|
159
161
|
</div>
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ToolIcon } from '../../common';
|
|
3
|
+
import { computed, ref, watch } from 'vue';
|
|
4
|
+
import message from 'vue-m-message';
|
|
5
|
+
import type { UploadProps } from 'ant-design-vue';
|
|
6
|
+
import { Upload, Image } from 'ant-design-vue';
|
|
7
|
+
import { UploadFile, path } from '@/index';
|
|
8
|
+
import { useInputFactory } from '@/utils/form-validate';
|
|
9
|
+
import { ApiResponse, httpPost, IUrlInfo, ResStatus } from '@skyfox2000/fapi';
|
|
10
|
+
import { fastUpload } from '@/utils/file-upload';
|
|
11
|
+
import { MicroOpenApis } from '@/utils/micro-openapis';
|
|
12
|
+
import { isMicroApp } from '@skyfox2000/microbase';
|
|
13
|
+
import { useUserInfo } from '@/stores/userInfo';
|
|
14
|
+
|
|
15
|
+
export interface ImageListProps {
|
|
16
|
+
/**
|
|
17
|
+
* 是否自动上传
|
|
18
|
+
*/
|
|
19
|
+
autoUpload?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* 上传Url
|
|
22
|
+
*/
|
|
23
|
+
uploadUrl: IUrlInfo;
|
|
24
|
+
/**
|
|
25
|
+
* 预览Url
|
|
26
|
+
*/
|
|
27
|
+
previewUrl?: IUrlInfo;
|
|
28
|
+
/**
|
|
29
|
+
* 删除Url
|
|
30
|
+
*/
|
|
31
|
+
deleteUrl?: IUrlInfo;
|
|
32
|
+
/**
|
|
33
|
+
* 文件列表
|
|
34
|
+
*/
|
|
35
|
+
fileList: UploadFile[];
|
|
36
|
+
/**
|
|
37
|
+
* 提示文字
|
|
38
|
+
*/
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
/**
|
|
41
|
+
* 文件后缀列表
|
|
42
|
+
*/
|
|
43
|
+
fileExt?: string[];
|
|
44
|
+
/**
|
|
45
|
+
* 最大文件大小
|
|
46
|
+
*/
|
|
47
|
+
maxFileSize?: number;
|
|
48
|
+
/**
|
|
49
|
+
* 最大数量
|
|
50
|
+
*/
|
|
51
|
+
maxCount?: number;
|
|
52
|
+
/**
|
|
53
|
+
* 最大数量提示
|
|
54
|
+
*/
|
|
55
|
+
maxCountTip?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* 文件大小提示
|
|
58
|
+
*/
|
|
59
|
+
maxFileSizeTip?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* 文件类型提示
|
|
62
|
+
*/
|
|
63
|
+
fileExtTip?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* 文件路径
|
|
66
|
+
*/
|
|
67
|
+
parentPath?: string;
|
|
68
|
+
/**
|
|
69
|
+
* 是否显示删除
|
|
70
|
+
*/
|
|
71
|
+
showDelete?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const props = withDefaults(defineProps<ImageListProps>(), {
|
|
75
|
+
autoUpload: false,
|
|
76
|
+
fileList: () => [],
|
|
77
|
+
placeholder: '',
|
|
78
|
+
maxFileSize: 20,
|
|
79
|
+
maxCount: 5,
|
|
80
|
+
maxCountTip: false,
|
|
81
|
+
maxFileSizeTip: true,
|
|
82
|
+
fileExtTip: true,
|
|
83
|
+
showDelete: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const inputFactory = useInputFactory();
|
|
87
|
+
const { errInfo } = inputFactory;
|
|
88
|
+
|
|
89
|
+
// Upload 组件的文件列表更新
|
|
90
|
+
const displayList = ref<UploadFile[]>(props.fileList);
|
|
91
|
+
// Upload 组件的内部文件列表
|
|
92
|
+
const uploadList = ref<UploadFile[]>([]);
|
|
93
|
+
|
|
94
|
+
const fileUploader = ref();
|
|
95
|
+
const emit = defineEmits(['update:file-list']);
|
|
96
|
+
const acceptString = computed(() => (props.fileExt?.length ? props.fileExt.map((ext) => `.${ext}`).join(',') : ''));
|
|
97
|
+
|
|
98
|
+
// 统一的文件验证函数
|
|
99
|
+
const validateFile = (file: File): boolean => {
|
|
100
|
+
// 文件类型验证
|
|
101
|
+
if (props.fileExt && props.fileExt.length > 0) {
|
|
102
|
+
const extension = file.name.split('.').pop()?.toLowerCase() || '';
|
|
103
|
+
if (!props.fileExt.includes(extension)) {
|
|
104
|
+
message.error('文件类型不支持');
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 文件大小验证
|
|
110
|
+
if (file.size / 1024 / 1024 > props.maxFileSize) {
|
|
111
|
+
message.error(`文件大小超过 ${props.maxFileSize}MB 限制`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return true;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// 检查是否达到最大数量限制
|
|
119
|
+
const isMaxCountReached = (): boolean => {
|
|
120
|
+
return props.maxCount > 1 && displayList.value.length >= props.maxCount;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// 准备文件信息
|
|
124
|
+
const prepareFileInfo = (file: any): UploadFile<any> => {
|
|
125
|
+
const fileInfo = file as UploadFile<any>;
|
|
126
|
+
if (!fileInfo.params) fileInfo.params = {};
|
|
127
|
+
fileInfo.params.FileKey = fileInfo.name;
|
|
128
|
+
if (props.parentPath) {
|
|
129
|
+
fileInfo.params.FileKey = path.join('/', props.parentPath, fileInfo.name);
|
|
130
|
+
}
|
|
131
|
+
return fileInfo;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// 简化的 beforeUpload - 只做基本验证,不处理业务逻辑
|
|
135
|
+
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
|
136
|
+
return validateFile(file) && props.autoUpload;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// 处理文件选择(核心逻辑)
|
|
140
|
+
const handleFileSelect = async (newFiles: any[]) => {
|
|
141
|
+
if (newFiles.length === 0) return;
|
|
142
|
+
|
|
143
|
+
const tempFiles = [...displayList.value];
|
|
144
|
+
let hasError = false;
|
|
145
|
+
|
|
146
|
+
for (const file of newFiles) {
|
|
147
|
+
// 验证文件
|
|
148
|
+
if (!validateFile(file)) {
|
|
149
|
+
hasError = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const fileInfo = prepareFileInfo(file);
|
|
154
|
+
|
|
155
|
+
// 如果maxCount=1,直接替换
|
|
156
|
+
if (props.maxCount === 1) {
|
|
157
|
+
tempFiles.length = 0;
|
|
158
|
+
tempFiles.push(fileInfo);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 检查是否达到最大数量限制
|
|
163
|
+
if (isMaxCountReached()) {
|
|
164
|
+
message.error(`最多上传 ${props.maxCount} 个文件`);
|
|
165
|
+
hasError = true;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 检查是否为同名文件(严格判断)
|
|
170
|
+
const existingIndex = tempFiles.findIndex((f) => f.name === fileInfo.name);
|
|
171
|
+
|
|
172
|
+
if (existingIndex > -1) {
|
|
173
|
+
// 同名文件:替换
|
|
174
|
+
tempFiles[existingIndex] = fileInfo;
|
|
175
|
+
} else {
|
|
176
|
+
// 新文件:添加到列表
|
|
177
|
+
tempFiles.push(fileInfo);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 更新显示列表
|
|
182
|
+
if (!hasError || tempFiles.length > 0) {
|
|
183
|
+
displayList.value = tempFiles;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 清空 Upload 组件的内部列表
|
|
187
|
+
uploadList.value = [];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Upload 组件的文件列表更新
|
|
191
|
+
const updateUploadList: UploadProps['onUpdate:fileList'] = (newFiles) => {
|
|
192
|
+
// 只更新 Upload 内部列表
|
|
193
|
+
uploadList.value = newFiles as unknown as UploadFile[];
|
|
194
|
+
|
|
195
|
+
// 处理新选择的文件
|
|
196
|
+
handleFileSelect(newFiles);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const uploadProps = computed<UploadProps>(() => ({
|
|
200
|
+
accept: acceptString.value,
|
|
201
|
+
multiple: props.maxCount !== 1,
|
|
202
|
+
fileList: uploadList.value as UploadProps['fileList'],
|
|
203
|
+
'onUpdate:fileList': updateUploadList,
|
|
204
|
+
beforeUpload: beforeUpload,
|
|
205
|
+
listType: 'text',
|
|
206
|
+
showUploadList: false,
|
|
207
|
+
customRequest: async () => {
|
|
208
|
+
if (props.autoUpload && props.uploadUrl) {
|
|
209
|
+
for (const file of displayList.value) {
|
|
210
|
+
if (file.percent === 0) {
|
|
211
|
+
await fastUpload(props.uploadUrl, file);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
watch(
|
|
219
|
+
() => props.fileList,
|
|
220
|
+
(newVal) => {
|
|
221
|
+
displayList.value = newVal;
|
|
222
|
+
},
|
|
223
|
+
{ deep: true, immediate: true },
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
watch(
|
|
227
|
+
() => displayList.value,
|
|
228
|
+
(newVal) => {
|
|
229
|
+
emit('update:file-list', newVal);
|
|
230
|
+
},
|
|
231
|
+
{ deep: true },
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
watch(
|
|
235
|
+
() => props.parentPath,
|
|
236
|
+
(newVal) => {
|
|
237
|
+
if (newVal) {
|
|
238
|
+
displayList.value.forEach((file) => {
|
|
239
|
+
file.params.FileKey = path.join('/', newVal, file.fileName);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const previewVisible = ref(false);
|
|
246
|
+
const previewImage = ref('');
|
|
247
|
+
|
|
248
|
+
const previewFile = (index: number) => {
|
|
249
|
+
const file = displayList.value[index];
|
|
250
|
+
previewImage.value = getImageUrl(file);
|
|
251
|
+
previewVisible.value = true;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const removeFile = (index: number) => {
|
|
255
|
+
const file = displayList.value[index];
|
|
256
|
+
if (props.deleteUrl && file.minioFile && file.minioFile.Key) {
|
|
257
|
+
httpPost<ApiResponse>(props.deleteUrl, {
|
|
258
|
+
Query: {
|
|
259
|
+
FileKey: file.minioFile!.Key,
|
|
260
|
+
},
|
|
261
|
+
}).then((res) => {
|
|
262
|
+
if (res && res.status === ResStatus.SUCCESS) {
|
|
263
|
+
message.success('删除文件成功!');
|
|
264
|
+
displayList.value.splice(index, 1);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
displayList.value.splice(index, 1);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const imageUrlCache = ref<Map<string, string>>(new Map());
|
|
273
|
+
|
|
274
|
+
const getImageUrl = (file: UploadFile) => {
|
|
275
|
+
// 新选择的文件,使用本地预览
|
|
276
|
+
if (file.originFileObj) {
|
|
277
|
+
return URL.createObjectURL(file.originFileObj);
|
|
278
|
+
}
|
|
279
|
+
// 已上传的文件,使用缓存的 Blob URL
|
|
280
|
+
if (file.minioFile) {
|
|
281
|
+
const cacheKey = file.minioFile.Key;
|
|
282
|
+
if (imageUrlCache.value.has(cacheKey)) {
|
|
283
|
+
return imageUrlCache.value.get(cacheKey)!;
|
|
284
|
+
}
|
|
285
|
+
// 异步加载图片
|
|
286
|
+
loadImageFromMinio(file);
|
|
287
|
+
return '';
|
|
288
|
+
}
|
|
289
|
+
return '';
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const loadImageFromMinio = async (file: UploadFile) => {
|
|
293
|
+
if (!file.minioFile || !props.previewUrl) return;
|
|
294
|
+
|
|
295
|
+
const cacheKey = file.minioFile.Key;
|
|
296
|
+
try {
|
|
297
|
+
// 构建GET请求URL
|
|
298
|
+
const url = `${props.previewUrl.url}?FileKey=${encodeURIComponent(file.minioFile.Key)}`;
|
|
299
|
+
|
|
300
|
+
// 获取token
|
|
301
|
+
const token = isMicroApp() ? await MicroOpenApis.getToken() : useUserInfo().getToken();
|
|
302
|
+
|
|
303
|
+
// 使用fetch发起带token的GET请求
|
|
304
|
+
const response = await fetch(url, {
|
|
305
|
+
method: 'GET',
|
|
306
|
+
headers: {
|
|
307
|
+
Authorization: `Bearer ${token}`,
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (response.ok) {
|
|
312
|
+
const blob = await response.blob();
|
|
313
|
+
// 创建 Blob URL 并缓存
|
|
314
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
315
|
+
imageUrlCache.value.set(cacheKey, blobUrl);
|
|
316
|
+
} else {
|
|
317
|
+
console.error('加载图片失败:', response.status);
|
|
318
|
+
}
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error('加载图片失败:', error);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const showUploadButton = computed(() => {
|
|
325
|
+
return displayList.value.length < props.maxCount;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const hoveredIndex = ref(-1);
|
|
329
|
+
</script>
|
|
330
|
+
|
|
331
|
+
<template>
|
|
332
|
+
<div class="w-full mt-1">
|
|
333
|
+
<div class="flex flex-wrap gap-2">
|
|
334
|
+
<div v-for="(file, index) in displayList" :key="index" class="relative image-item"
|
|
335
|
+
@mouseenter="hoveredIndex = index" @mouseleave="hoveredIndex = -1">
|
|
336
|
+
<div class="w-16 h-16 border border-solid border-gray-200 rounded overflow-hidden">
|
|
337
|
+
<img :src="getImageUrl(file)" class="w-full h-full object-cover" />
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<div v-if="hoveredIndex === index"
|
|
341
|
+
class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center gap-4 rounded">
|
|
342
|
+
<div v-if="previewUrl" class="flex items-center text-white cursor-pointer hover:text-blue-400"
|
|
343
|
+
@click="previewFile(index)">
|
|
344
|
+
<ToolIcon icon="icon-eye" clickable />
|
|
345
|
+
<span class="text-sm ml-1">预览</span>
|
|
346
|
+
</div>
|
|
347
|
+
<div v-if="showDelete !== false" class="flex items-center text-white cursor-pointer hover:text-red-400"
|
|
348
|
+
@click="removeFile(index)">
|
|
349
|
+
<ToolIcon icon="icon-new" :angle="45" clickable />
|
|
350
|
+
<span class="text-sm ml-1">删除</span>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<div v-if="showUploadButton" class="w-16 h-16">
|
|
356
|
+
<Upload ref="fileUploader" v-bind="uploadProps"
|
|
357
|
+
v-auth="{ role: ['Super', 'Admin'], permit: ':imagelist:upload' }">
|
|
358
|
+
<div
|
|
359
|
+
class="w-16 h-16 border border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-blue-500 transition-colors"
|
|
360
|
+
:class="[errInfo?.errClass]">
|
|
361
|
+
<ToolIcon icon="icon-new" class="text-gray-400 w-10 h-10" />
|
|
362
|
+
</div>
|
|
363
|
+
</Upload>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<!-- 图片预览模态窗口 -->
|
|
368
|
+
<Image :preview="{ visible: previewVisible, onVisibleChange: (vis) => (previewVisible = vis) }"
|
|
369
|
+
:src="previewImage" style="display: none" />
|
|
370
|
+
</div>
|
|
371
|
+
</template>
|
|
372
|
+
|
|
373
|
+
<style scoped>
|
|
374
|
+
.error {
|
|
375
|
+
border-color: #ff4d4f80;
|
|
376
|
+
box-shadow: 0 0 3px 0 #ff4d4f;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.error-text {
|
|
380
|
+
color: #ff4d4f !important;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.image-item {
|
|
384
|
+
transition: all 0.3s;
|
|
385
|
+
}
|
|
386
|
+
</style>
|
package/src/components/index.ts
CHANGED