@skyfox2000/webui 1.4.19 → 1.4.22
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-BcSEYvus.js} +3 -3
- package/lib/assets/modules/{file-upload-CNBcbAAZ.js → file-upload-D11e2io7.js} +5 -5
- package/lib/assets/modules/{index-CDr74akE.js → index-CO9_YadW.js} +2 -2
- package/lib/assets/modules/{index-D14BsF7C.js → index-ClMWx3tg.js} +1 -1
- package/lib/assets/modules/{index-DOlO_4KL.js → index-voAmrZ30.js} +2 -2
- package/lib/assets/modules/{menuTabs-CqAhoF--.js → menuTabs-C9wkt-m9.js} +16 -16
- package/lib/assets/modules/{toolIcon-BnkqBipR.js → toolIcon-9zQ4jiFD.js} +1 -1
- package/lib/assets/modules/{upload-template-KI-IFzyp.js → upload-template-CM0O990W.js} +395 -378
- package/lib/assets/modules/uploadList-DhkFSkqE.js +466 -0
- package/lib/components/content/index.d.ts +4 -0
- package/lib/components/content/list/index.vue.d.ts +126 -0
- package/lib/components/content/list/listOperate.vue.d.ts +18 -0
- package/lib/components/form/index.d.ts +2 -0
- package/lib/components/form/switch/index.vue.d.ts +11 -1
- package/lib/components/form/upload/imageList.vue.d.ts +486 -0
- package/lib/components/form/upload/uploadList.vue.d.ts +449 -1
- package/lib/components/index.d.ts +2 -2
- package/lib/const/options.d.ts +4 -2
- 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 +70 -56
- package/lib/index.d.ts +1 -1
- package/lib/typings/upload.d.ts +10 -0
- package/lib/webui.css +1 -1
- package/lib/webui.es.js +1671 -1207
- package/package.json +1 -1
- package/src/components/content/dialog/uploadForm.vue +96 -13
- package/src/components/content/index.ts +5 -0
- package/src/components/content/list/index.vue +198 -0
- package/src/components/content/list/listOperate.vue +122 -0
- package/src/components/content/table/index.vue +1 -1
- package/src/components/content/table/tableOperate.vue +19 -37
- package/src/components/form/index.ts +3 -0
- package/src/components/form/switch/index.vue +27 -14
- package/src/components/form/upload/imageList.vue +386 -0
- package/src/components/form/upload/uploadList.vue +46 -3
- package/src/components/index.ts +3 -0
- package/src/const/options.ts +11 -1
- package/src/index.ts +3 -0
- package/src/typings/upload.d.ts +10 -0
- package/lib/assets/modules/uploadList-DnFXg_B3.js +0 -423
|
@@ -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>
|
|
@@ -79,6 +79,10 @@ export interface UploadListProps {
|
|
|
79
79
|
* 是否显示删除
|
|
80
80
|
*/
|
|
81
81
|
showDelete?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* 是否显示目录上传按钮
|
|
84
|
+
*/
|
|
85
|
+
showFolderUpload?: boolean;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
const props = withDefaults(defineProps<UploadListProps>(), {
|
|
@@ -93,6 +97,7 @@ const props = withDefaults(defineProps<UploadListProps>(), {
|
|
|
93
97
|
showActionText: true,
|
|
94
98
|
showOnlineSwitch: false,
|
|
95
99
|
showDelete: true,
|
|
100
|
+
showFolderUpload: false,
|
|
96
101
|
});
|
|
97
102
|
|
|
98
103
|
const inputFactory = useInputFactory();
|
|
@@ -105,6 +110,7 @@ const displayList = ref<UploadFile[]>(props.fileList);
|
|
|
105
110
|
const uploadList = ref<UploadFile[]>([]);
|
|
106
111
|
|
|
107
112
|
const fileUploader = ref();
|
|
113
|
+
const fileFolderUploader = ref();
|
|
108
114
|
const emit = defineEmits(['update:file-list']);
|
|
109
115
|
const acceptString = computed(() => (props.fileExt?.length ? props.fileExt.map((ext) => `.${ext}`).join(',') : ''));
|
|
110
116
|
|
|
@@ -137,11 +143,30 @@ const isMaxCountReached = (): boolean => {
|
|
|
137
143
|
const prepareFileInfo = (file: any): UploadFile<any> => {
|
|
138
144
|
const fileInfo = file as UploadFile<any>;
|
|
139
145
|
if (!fileInfo.params) fileInfo.params = {};
|
|
146
|
+
|
|
147
|
+
// 获取相对路径(只有目录上传时才有)
|
|
148
|
+
let relativePath = fileInfo.name;
|
|
149
|
+
if (fileInfo.webkitRelativePath) {
|
|
150
|
+
relativePath = fileInfo.webkitRelativePath;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 对于目录上传,使用相对路径作为文件名,保持目录结构
|
|
154
|
+
if (fileInfo.webkitRelativePath) {
|
|
155
|
+
fileInfo.name = fileInfo.webkitRelativePath;
|
|
156
|
+
fileInfo.fileName = fileInfo.webkitRelativePath.split('/').pop(); // 只取文件名部分
|
|
157
|
+
}
|
|
158
|
+
|
|
140
159
|
fileInfo.params.FileKey = fileInfo.name;
|
|
141
160
|
if (props.parentPath) {
|
|
142
161
|
fileInfo.params.FileKey = path.join('/', props.parentPath, fileInfo.name);
|
|
143
162
|
}
|
|
144
163
|
fileInfo.status = UploadStatus.Pending;
|
|
164
|
+
|
|
165
|
+
// 保存相对路径用于显示(只有目录上传时才使用)
|
|
166
|
+
if (fileInfo.webkitRelativePath) {
|
|
167
|
+
fileInfo.relativePath = relativePath;
|
|
168
|
+
}
|
|
169
|
+
|
|
145
170
|
return fileInfo;
|
|
146
171
|
};
|
|
147
172
|
|
|
@@ -231,6 +256,19 @@ const uploadProps = computed<UploadProps>(() => ({
|
|
|
231
256
|
},
|
|
232
257
|
}));
|
|
233
258
|
|
|
259
|
+
const folderUploadProps = computed<UploadProps>(() => ({
|
|
260
|
+
accept: acceptString.value,
|
|
261
|
+
multiple: true,
|
|
262
|
+
directory: true, // 添加标准属性
|
|
263
|
+
webkitdirectory: true, // Webkit属性
|
|
264
|
+
fileList: [] as UploadProps['fileList'],
|
|
265
|
+
'onUpdate:fileList': (newFiles) => handleFileSelect(newFiles),
|
|
266
|
+
beforeUpload: () => false,
|
|
267
|
+
listType: 'text',
|
|
268
|
+
showUploadList: false,
|
|
269
|
+
customRequest: () => {},
|
|
270
|
+
}));
|
|
271
|
+
|
|
234
272
|
watch(
|
|
235
273
|
() => props.fileList,
|
|
236
274
|
(newVal) => {
|
|
@@ -375,11 +413,15 @@ const getStatus = (status?: UploadStatus) => {
|
|
|
375
413
|
<template>
|
|
376
414
|
<div class="w-full border border-solid border-gray-100 mt-1 rounded-md py-5" :class="[errInfo?.errClass]">
|
|
377
415
|
<div class="flex items-center justify-between w-full">
|
|
378
|
-
<div class="w-35 mx-3">
|
|
416
|
+
<div class="w-35 mx-3 flex gap-2">
|
|
379
417
|
<Upload ref="fileUploader" v-bind="uploadProps"
|
|
380
418
|
v-auth="{ role: ['Super', 'Admin'], permit: ':uploadlist:upload' }">
|
|
381
419
|
<Button :class="[errInfo?.errClass + '-text']">选择文件</Button>
|
|
382
420
|
</Upload>
|
|
421
|
+
<Upload v-if="showFolderUpload" ref="fileFolderUploader" :webkitdirectory="true" :directory="true" v-bind="folderUploadProps"
|
|
422
|
+
v-auth="{ role: ['Super', 'Admin'], permit: ':uploadlist:upload' }">
|
|
423
|
+
<Button :class="[errInfo?.errClass + '-text']">选择目录</Button>
|
|
424
|
+
</Upload>
|
|
383
425
|
</div>
|
|
384
426
|
<div class="flex-1 text-sm text-gray-500" :class="[errInfo?.errClass + '-text']">
|
|
385
427
|
{{ getPlaceholder() }}
|
|
@@ -387,11 +429,12 @@ const getStatus = (status?: UploadStatus) => {
|
|
|
387
429
|
</div>
|
|
388
430
|
|
|
389
431
|
<div class="mt-4 px-3">
|
|
390
|
-
<div v-for="(file, index) in displayList" :key="index" class="mb-2 pb-1">
|
|
432
|
+
<div v-for="(file, index) in displayList" :key="(props.parentPath || '') + (file.relativePath || file.name || index)" class="mb-2 pb-1">
|
|
391
433
|
<div class="flex items-center justify-between">
|
|
392
434
|
<div class="flex items-center">
|
|
393
435
|
<span class="text-gray-700 mr-2"
|
|
394
|
-
:class="[file.status == UploadStatus.Offline ? 'line-through' : '']"
|
|
436
|
+
:class="[file.status == UploadStatus.Offline ? 'line-through' : '']"
|
|
437
|
+
:title="file.relativePath || file.fileName || file.name">{{ file.fileName ?? file.name
|
|
395
438
|
}}</span>
|
|
396
439
|
<span>
|
|
397
440
|
<Tag :color="getStatusColor(file.status)">{{ getStatus(file.status) }}</Tag>
|
package/src/components/index.ts
CHANGED
package/src/const/options.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { OptionItemProps } from '@/typings/option';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* 静态选项
|
|
3
5
|
*/
|
|
4
6
|
export class OPTIONS {
|
|
5
|
-
private static dict: Record<string, Record<string, any>[]> = {};
|
|
7
|
+
private static dict: Record<string, (Record<string, any> | OptionItemProps)[]> = {};
|
|
6
8
|
static Keys = {
|
|
7
9
|
EnableDisable: 'EnableDisable',
|
|
8
10
|
SuccessResult: 'SuccessResult',
|
|
@@ -18,6 +20,14 @@ export class OPTIONS {
|
|
|
18
20
|
static getOptions = (key: string) => {
|
|
19
21
|
return OPTIONS.dict[key];
|
|
20
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* 静态选择项列表
|
|
25
|
+
* @param key 名称
|
|
26
|
+
* @returns
|
|
27
|
+
*/
|
|
28
|
+
static getOptionItems = (key: string) => {
|
|
29
|
+
return OPTIONS.dict[key] as OptionItemProps[];
|
|
30
|
+
};
|
|
21
31
|
/**
|
|
22
32
|
* 静态选择项对象
|
|
23
33
|
* @param key 名称
|
package/src/index.ts
CHANGED
|
@@ -258,6 +258,8 @@ export {
|
|
|
258
258
|
InputIcon,
|
|
259
259
|
InputPassword,
|
|
260
260
|
InputNumber,
|
|
261
|
+
List,
|
|
262
|
+
ListOperate,
|
|
261
263
|
PropEditor,
|
|
262
264
|
type PropConfigItem,
|
|
263
265
|
Radio,
|
|
@@ -270,6 +272,7 @@ export {
|
|
|
270
272
|
Transfer,
|
|
271
273
|
TransferTable,
|
|
272
274
|
TreeSelect,
|
|
275
|
+
ImageList,
|
|
273
276
|
UploadList,
|
|
274
277
|
Breadcrumb,
|
|
275
278
|
Content,
|
package/src/typings/upload.d.ts
CHANGED
|
@@ -137,6 +137,16 @@ export interface UploadFile<T = AjaxResponse> {
|
|
|
137
137
|
* minio文件信息
|
|
138
138
|
*/
|
|
139
139
|
minioFile?: MinioFile;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 相对路径(目录上传时使用)
|
|
143
|
+
*/
|
|
144
|
+
relativePath?: string;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 文件的相对路径(浏览器原生属性)
|
|
148
|
+
*/
|
|
149
|
+
webkitRelativePath?: string;
|
|
140
150
|
}
|
|
141
151
|
|
|
142
152
|
/**
|