feat: 优化上传组件,增加文件元数据获取和加载状态,更新路由和国际化支持
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import type { UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui';
|
import type { UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui';
|
||||||
import { type UploadFetchOptions, uploadToS3 } from '@/utils/aws/s3';
|
import { type UploadFetchOptions, getS3FileMetadata, uploadToS3 } from '@/utils/aws/s3';
|
||||||
|
|
||||||
defineOptions({ name: 'UploadS3' });
|
defineOptions({ name: 'UploadS3' });
|
||||||
|
|
||||||
@@ -32,15 +32,63 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const fileList = ref<UploadFileInfo[]>([]);
|
const fileList = ref<UploadFileInfo[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
// 将 modelValue 转换为文件列表显示
|
// 初始化文件列表
|
||||||
const initFileList = computed(() => {
|
async function initFileList() {
|
||||||
return props.modelValue.map((fileId, index) => ({
|
const fileIds = props.modelValue.filter(id => id && id.trim() !== '');
|
||||||
|
|
||||||
|
if (!props.modelValue || fileIds.length === 0) {
|
||||||
|
fileList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const fileInfoPromises = props.modelValue.map(async fileId => {
|
||||||
|
try {
|
||||||
|
const metadata = await getS3FileMetadata(fileId);
|
||||||
|
return {
|
||||||
id: fileId,
|
id: fileId,
|
||||||
name: `文件 ${index + 1}`,
|
name: metadata.fileName || fileId,
|
||||||
status: 'finished' as const,
|
status: 'finished' as const,
|
||||||
url: fileId
|
url: fileId,
|
||||||
}));
|
file: null as any
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
id: fileId,
|
||||||
|
name: fileId,
|
||||||
|
status: 'error' as const,
|
||||||
|
url: fileId,
|
||||||
|
file: null as any
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileList.value = await Promise.all(fileInfoPromises);
|
||||||
|
} catch (error) {
|
||||||
|
window.$message?.error('加载文件列表失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 modelValue 变化
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newVal, oldVal) => {
|
||||||
|
// 只在外部变化时重新加载(避免内部上传/删除时重复加载)
|
||||||
|
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
|
||||||
|
initFileList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 组件挂载时初始化
|
||||||
|
onMounted(() => {
|
||||||
|
initFileList();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自定义上传请求
|
// 自定义上传请求
|
||||||
@@ -76,6 +124,15 @@ async function handleCustomRequest({ file, onProgress, onFinish, onError }: Uplo
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加到文件列表
|
||||||
|
fileList.value.push({
|
||||||
|
id: fileId,
|
||||||
|
name: file.name,
|
||||||
|
status: 'finished',
|
||||||
|
url: fileId,
|
||||||
|
file: null as any
|
||||||
|
});
|
||||||
|
|
||||||
// 更新文件 ID 到 modelValue
|
// 更新文件 ID 到 modelValue
|
||||||
const newFileIds = [...props.modelValue, fileId];
|
const newFileIds = [...props.modelValue, fileId];
|
||||||
emit('update:modelValue', newFileIds);
|
emit('update:modelValue', newFileIds);
|
||||||
@@ -90,6 +147,9 @@ async function handleCustomRequest({ file, onProgress, onFinish, onError }: Uplo
|
|||||||
|
|
||||||
// 删除文件
|
// 删除文件
|
||||||
function handleRemove({ file }: { file: UploadFileInfo }) {
|
function handleRemove({ file }: { file: UploadFileInfo }) {
|
||||||
|
// 从文件列表中移除
|
||||||
|
fileList.value = fileList.value.filter(f => f.id !== file.id);
|
||||||
|
// 更新 modelValue
|
||||||
const newFileIds = props.modelValue.filter(id => id !== file.id);
|
const newFileIds = props.modelValue.filter(id => id !== file.id);
|
||||||
emit('update:modelValue', newFileIds);
|
emit('update:modelValue', newFileIds);
|
||||||
return true;
|
return true;
|
||||||
@@ -109,9 +169,9 @@ function beforeUpload({ file }: { file: UploadFileInfo }) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="upload-s3">
|
<div class="upload-s3">
|
||||||
<div v-if="label" class="mb-2 text-14px font-medium">{{ label }}</div>
|
<div v-if="label" class="mb-2 text-14px font-medium">{{ label }}</div>
|
||||||
|
<NSpin :show="loading">
|
||||||
<NUpload
|
<NUpload
|
||||||
v-model:file-list="fileList"
|
v-model:file-list="fileList"
|
||||||
:default-file-list="initFileList"
|
|
||||||
:accept="accept"
|
:accept="accept"
|
||||||
:multiple="multiple"
|
:multiple="multiple"
|
||||||
:max="maxFiles"
|
:max="maxFiles"
|
||||||
@@ -128,6 +188,7 @@ function beforeUpload({ file }: { file: UploadFileInfo }) {
|
|||||||
{{ placeholder || '选择文件' }}
|
{{ placeholder || '选择文件' }}
|
||||||
</NButton>
|
</NButton>
|
||||||
</NUpload>
|
</NUpload>
|
||||||
|
</NSpin>
|
||||||
<div class="mt-2 text-12px text-gray-400">
|
<div class="mt-2 text-12px text-gray-400">
|
||||||
<div v-if="maxSize">单个文件大小不超过 {{ maxSize }}MB</div>
|
<div v-if="maxSize">单个文件大小不超过 {{ maxSize }}MB</div>
|
||||||
<div v-if="maxFiles">最多上传 {{ maxFiles }} 个文件</div>
|
<div v-if="maxFiles">最多上传 {{ maxFiles }} 个文件</div>
|
||||||
|
|||||||
@@ -247,7 +247,8 @@ const local: App.I18n.Schema = {
|
|||||||
'tokenization_trading-pairs': 'Trading Pairs Management',
|
'tokenization_trading-pairs': 'Trading Pairs Management',
|
||||||
tokenization: 'Tokenization Management',
|
tokenization: 'Tokenization Management',
|
||||||
tokenization_product: 'Tokenization Product',
|
tokenization_product: 'Tokenization Product',
|
||||||
notification: 'Notification Management'
|
notification: 'Notification Management',
|
||||||
|
news: 'News Management'
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
login: {
|
login: {
|
||||||
|
|||||||
@@ -243,7 +243,8 @@ const local: App.I18n.Schema = {
|
|||||||
'tokenization_trading-pairs': '交易对管理',
|
'tokenization_trading-pairs': '交易对管理',
|
||||||
tokenization: '代币化管理',
|
tokenization: '代币化管理',
|
||||||
tokenization_product: '代币化产品',
|
tokenization_product: '代币化产品',
|
||||||
notification: '通知管理'
|
notification: '通知管理',
|
||||||
|
news: '新闻管理'
|
||||||
},
|
},
|
||||||
page: {
|
page: {
|
||||||
login: {
|
login: {
|
||||||
|
|||||||
@@ -116,15 +116,6 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
hideInMenu: true
|
hideInMenu: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'news',
|
|
||||||
path: '/news',
|
|
||||||
component: 'layout.base$view.news',
|
|
||||||
meta: {
|
|
||||||
title: 'news',
|
|
||||||
i18nKey: 'route.news'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'notification',
|
name: 'notification',
|
||||||
path: '/notification',
|
path: '/notification',
|
||||||
@@ -135,6 +126,16 @@ export const generatedRoutes: GeneratedRoute[] = [
|
|||||||
order: 7
|
order: 7
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'news',
|
||||||
|
path: '/news',
|
||||||
|
component: 'layout.base$view.news',
|
||||||
|
meta: {
|
||||||
|
title: 'news',
|
||||||
|
i18nKey: 'route.news',
|
||||||
|
order: 8
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'rwa',
|
name: 'rwa',
|
||||||
path: '/rwa',
|
path: '/rwa',
|
||||||
|
|||||||
2
src/typings/components.d.ts
vendored
2
src/typings/components.d.ts
vendored
@@ -81,6 +81,7 @@ declare module 'vue' {
|
|||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSelect: typeof import('naive-ui')['NSelect']
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
|
NSpin: typeof import('naive-ui')['NSpin']
|
||||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
NTab: typeof import('naive-ui')['NTab']
|
NTab: typeof import('naive-ui')['NTab']
|
||||||
@@ -178,6 +179,7 @@ declare global {
|
|||||||
const NScrollbar: typeof import('naive-ui')['NScrollbar']
|
const NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
const NSelect: typeof import('naive-ui')['NSelect']
|
const NSelect: typeof import('naive-ui')['NSelect']
|
||||||
const NSpace: typeof import('naive-ui')['NSpace']
|
const NSpace: typeof import('naive-ui')['NSpace']
|
||||||
|
const NSpin: typeof import('naive-ui')['NSpin']
|
||||||
const NStatistic: typeof import('naive-ui')['NStatistic']
|
const NStatistic: typeof import('naive-ui')['NStatistic']
|
||||||
const NSwitch: typeof import('naive-ui')['NSwitch']
|
const NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
const NTab: typeof import('naive-ui')['NTab']
|
const NTab: typeof import('naive-ui')['NTab']
|
||||||
|
|||||||
@@ -76,3 +76,23 @@ export async function uploadToS3(file: File, options: UploadOptions) {
|
|||||||
xhr.send(file);
|
xhr.send(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getS3FileUrl(fileId: string): Promise<string> {
|
||||||
|
const { data, error } = await safeClient(
|
||||||
|
client.api.file_storage.download_url.post({
|
||||||
|
fileId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (error.value || !data.value) {
|
||||||
|
throw new Error('获取文件下载 URL 失败');
|
||||||
|
}
|
||||||
|
return data.value.downloadUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getS3FileMetadata(fileId: string) {
|
||||||
|
const { data, error } = await safeClient(client.api.file_storage({ id: fileId }).get());
|
||||||
|
if (error.value || !data.value) {
|
||||||
|
throw new Error('获取文件元数据失败');
|
||||||
|
}
|
||||||
|
return data.value;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user