@skyfox2000/webui 1.4.19 → 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/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 +1227 -1014
- package/package.json +1 -1
- package/src/components/form/index.ts +3 -0
- 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 };
|
|
@@ -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