175 lines
4.5 KiB
Vue
175 lines
4.5 KiB
Vue
<script lang="ts" setup>
|
||
import { onMounted, ref, watch } from 'vue';
|
||
import type { UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui';
|
||
import { client, safeClient } from '@/service/api';
|
||
import { type UploadFetchOptions, uploadToS3 } from '@/utils/aws/s3';
|
||
|
||
defineOptions({ name: 'UploadS3' });
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
modelValue?: string[];
|
||
label?: string;
|
||
placeholder?: string;
|
||
accept?: string;
|
||
multiple?: boolean;
|
||
maxSize?: number; // MB
|
||
maxFiles?: number;
|
||
fetchOptions: Partial<UploadFetchOptions>;
|
||
}>(),
|
||
{
|
||
modelValue: () => [],
|
||
accept: '*/*',
|
||
multiple: true,
|
||
maxSize: 10,
|
||
maxFiles: 5,
|
||
placeholder: '',
|
||
label: ''
|
||
}
|
||
);
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:modelValue', value: string[]): void;
|
||
}>();
|
||
|
||
const fileList = ref<UploadFileInfo[]>([]);
|
||
const loading = ref(false);
|
||
|
||
async function initFileList() {
|
||
const fileIds = props.modelValue.filter(id => id && id.trim() !== '');
|
||
if (!props.modelValue || fileIds.length === 0) {
|
||
fileList.value = [];
|
||
return;
|
||
}
|
||
loading.value = true;
|
||
try {
|
||
const { data } = await safeClient(client.api.file_storage.access_urls.post({ fileIds }));
|
||
|
||
fileList.value =
|
||
data.value?.map(item => ({
|
||
id: item.id,
|
||
name: item.fileName || item.id,
|
||
status: 'finished' as const,
|
||
url: item.url,
|
||
file: null as any
|
||
})) || [];
|
||
} catch (error) {
|
||
window.$message?.error('加载文件列表失败');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
(newVal, oldVal) => {
|
||
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
|
||
initFileList();
|
||
}
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
onMounted(() => {
|
||
initFileList();
|
||
});
|
||
|
||
async function handleCustomRequest({ file, onProgress, onFinish, onError }: UploadCustomRequestOptions) {
|
||
try {
|
||
const fileSizeMB = file.file!.size / 1024 / 1024;
|
||
if (fileSizeMB > props.maxSize) {
|
||
window.$message?.error(`文件大小不能超过 ${props.maxSize}MB`);
|
||
onError();
|
||
return;
|
||
}
|
||
|
||
if (fileList.value.length > props.maxFiles) {
|
||
window.$message?.error(`最多只能上传 ${props.maxFiles} 个文件`);
|
||
onError();
|
||
return;
|
||
}
|
||
|
||
const options = {
|
||
fileName: file.name,
|
||
fileSize: file.file?.size,
|
||
businessType: props.fetchOptions.businessType,
|
||
...props.fetchOptions
|
||
} as UploadFetchOptions;
|
||
|
||
const fileId = await uploadToS3(file.file as File, {
|
||
fetchOptions: options,
|
||
onProgress: percent => {
|
||
onProgress({ percent });
|
||
}
|
||
});
|
||
// 直接修改 file 对象的 id,NaiveUI 会自动同步到 fileList
|
||
file.id = fileId;
|
||
file.status = 'finished';
|
||
file.url = fileId;
|
||
|
||
const newFileIds = [...props.modelValue, fileId].filter(Boolean);
|
||
emit('update:modelValue', newFileIds);
|
||
|
||
onFinish();
|
||
window.$message?.success(`文件 ${file.name} 上传成功`);
|
||
} catch (error: any) {
|
||
onError();
|
||
}
|
||
}
|
||
|
||
function handleRemove({ file }: { file: UploadFileInfo }) {
|
||
// 只删除上传成功的文件(有有效 id 的文件)
|
||
if (file.id && typeof file.id === 'string' && props.modelValue.includes(file.id)) {
|
||
const newFileIds = props.modelValue.filter(id => id !== file.id);
|
||
emit('update:modelValue', newFileIds);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function beforeUpload({ file }: { file: UploadFileInfo }) {
|
||
const fileSizeMB = file.file!.size / 1024 / 1024;
|
||
if (fileSizeMB > props.maxSize) {
|
||
window.$message?.error(`文件大小不能超过 ${props.maxSize}MB`);
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="upload-s3">
|
||
<div v-if="label" class="mb-2 text-14px font-medium">{{ label }}</div>
|
||
<NSpin :show="loading">
|
||
<NUpload
|
||
v-model:file-list="fileList"
|
||
:accept="accept"
|
||
:multiple="multiple"
|
||
:max="maxFiles"
|
||
:custom-request="handleCustomRequest"
|
||
:on-remove="handleRemove"
|
||
:on-before-upload="beforeUpload"
|
||
list-type="text"
|
||
show-download-button
|
||
>
|
||
<NButton>
|
||
<template #icon>
|
||
<icon-ic-round-upload class="text-icon" />
|
||
</template>
|
||
{{ placeholder || '选择文件' }}
|
||
</NButton>
|
||
</NUpload>
|
||
</NSpin>
|
||
<div class="mt-2 text-12px text-gray-400">
|
||
<div v-if="maxSize">单个文件大小不超过 {{ maxSize }}MB</div>
|
||
<div v-if="maxFiles">最多上传 {{ maxFiles }} 个文件</div>
|
||
<div v-if="accept !== '*/*'">支持格式:{{ accept }}</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="css" scoped>
|
||
.upload-s3 {
|
||
width: 100%;
|
||
}
|
||
</style>
|