feat: 更新媒体上传状态显示,添加上传进度和状态文本,优化用户体验

This commit is contained in:
2026-01-04 17:46:13 +08:00
parent 03773b6205
commit 5fd33745b3

View File

@@ -100,11 +100,11 @@
<draggable v-model="form.covers" item-key="tempId" class="flex flex-wrap gap-4" group="covers">
<template #item="{ element: img, index }">
<div class="relative group w-48 aspect-video rounded-lg overflow-hidden bg-slate-100 border border-slate-200 cursor-move">
<div v-if="img.status === 'uploading'" class="absolute inset-0 bg-black/60 flex flex-col items-center justify-center z-20 text-white">
<i class="pi pi-spin pi-spinner text-2xl mb-2"></i>
<span class="text-xs font-bold">{{ img.progress }}%</span>
<div v-if="img.status !== 'done'" class="absolute inset-0 bg-black/60 flex flex-col items-center justify-center z-20 text-white transition-opacity">
<i class="pi pi-spin pi-spinner text-2xl mb-2" v-if="img.status === 'uploading'"></i>
<span class="text-xs font-bold text-center px-2">{{ img.statusText }}</span>
</div>
<Image :src="img.url" preview imageClass="w-full h-full object-cover" />
<Image :src="img.url" preview imageClass="w-full h-full object-cover" :class="{'opacity-50 blur-sm': img.status !== 'done'}" />
<button @click="removeMediaItem('cover', index, img)"
class="absolute top-1 right-1 w-6 h-6 bg-black/50 hover:bg-red-500 text-white rounded-full flex items-center justify-center transition-colors cursor-pointer z-10"><i
class="pi pi-times text-xs"></i></button>
@@ -123,18 +123,22 @@
<div>
<label class="block text-sm font-bold text-slate-700 mb-3">视频列表 (拖拽排序)</label>
<div class="space-y-3">
<draggable v-model="form.videos" item-key="id" class="space-y-3" group="videos">
<draggable v-model="form.videos" item-key="tempId" class="space-y-3" group="videos">
<template #item="{ element: file, index }">
<div class="flex items-center gap-4 p-3 border border-slate-200 rounded-lg bg-slate-50 cursor-move hover:border-primary-300 transition-colors">
<div class="w-10 h-10 bg-blue-100 text-blue-600 rounded flex items-center justify-center"><i
class="pi pi-video"></i></div>
<div class="flex items-center gap-4 p-3 border border-slate-200 rounded-lg bg-slate-50 cursor-move hover:border-primary-300 transition-colors relative overflow-hidden" :class="{'opacity-60': file.status !== 'done'}">
<!-- Progress -->
<div v-if="file.status === 'uploading'" class="absolute bottom-0 left-0 h-1 bg-blue-500 transition-all duration-300" :style="{ width: file.progress + '%' }"></div>
<div class="w-10 h-10 bg-blue-100 text-blue-600 rounded flex items-center justify-center shrink-0">
<i class="pi" :class="file.status === 'uploading' ? 'pi-spin pi-spinner' : 'pi-video'"></i>
</div>
<div class="flex-1 min-w-0">
<div class="font-bold text-sm text-slate-900 truncate">{{ file.name }}</div>
<div class="text-xs text-slate-500">{{ file.size }}</div>
<div class="text-xs text-slate-500">{{ file.status !== 'done' ? file.statusText : file.size }}</div>
</div>
<button @click="removeMediaItem('videos', index, file)"
class="text-slate-400 hover:text-red-500 px-2 cursor-pointer"><i
class="pi pi-trash"></i></button>
class="pi" :class="file.status === 'uploading' ? 'pi-times-circle' : 'pi-trash'"></i></button>
</div>
</template>
</draggable>
@@ -149,18 +153,22 @@
<div>
<label class="block text-sm font-bold text-slate-700 mb-3">音频列表 (拖拽排序)</label>
<div class="space-y-3">
<draggable v-model="form.audios" item-key="id" class="space-y-3" group="audios">
<draggable v-model="form.audios" item-key="tempId" class="space-y-3" group="audios">
<template #item="{ element: file, index }">
<div class="flex items-center gap-4 p-3 border border-slate-200 rounded-lg bg-slate-50 cursor-move hover:border-purple-300 transition-colors">
<div class="w-10 h-10 bg-purple-100 text-purple-600 rounded flex items-center justify-center"><i
class="pi pi-microphone"></i></div>
<div class="flex items-center gap-4 p-3 border border-slate-200 rounded-lg bg-slate-50 cursor-move hover:border-purple-300 transition-colors relative overflow-hidden" :class="{'opacity-60': file.status !== 'done'}">
<!-- Progress -->
<div v-if="file.status === 'uploading'" class="absolute bottom-0 left-0 h-1 bg-purple-500 transition-all duration-300" :style="{ width: file.progress + '%' }"></div>
<div class="w-10 h-10 bg-purple-100 text-purple-600 rounded flex items-center justify-center shrink-0">
<i class="pi" :class="file.status === 'uploading' ? 'pi-spin pi-spinner' : 'pi-microphone'"></i>
</div>
<div class="flex-1 min-w-0">
<div class="font-bold text-sm text-slate-900 truncate">{{ file.name }}</div>
<div class="text-xs text-slate-500">{{ file.size }}</div>
<div class="text-xs text-slate-500">{{ file.status !== 'done' ? file.statusText : file.size }}</div>
</div>
<button @click="removeMediaItem('audios', index, file)"
class="text-slate-400 hover:text-red-500 px-2 cursor-pointer"><i
class="pi pi-trash"></i></button>
class="pi" :class="file.status === 'uploading' ? 'pi-times-circle' : 'pi-trash'"></i></button>
</div>
</template>
</draggable>
@@ -175,15 +183,19 @@
<div>
<label class="block text-sm font-bold text-slate-700 mb-3">图片列表 (拖拽排序)</label>
<div class="flex flex-wrap gap-4">
<draggable v-model="form.images" item-key="id" class="flex flex-wrap gap-4" group="images">
<draggable v-model="form.images" item-key="tempId" class="flex flex-wrap gap-4" group="images">
<template #item="{ element: file, index }">
<div class="relative group w-32 aspect-square rounded-lg overflow-hidden border border-slate-200 cursor-move">
<Image :src="file.url" preview imageClass="w-full h-full object-cover" />
<div
<div v-if="file.status !== 'done'" class="absolute inset-0 bg-black/60 flex flex-col items-center justify-center z-20 text-white transition-opacity">
<i class="pi pi-spin pi-spinner text-2xl mb-2" v-if="file.status === 'uploading'"></i>
<span class="text-xs font-bold text-center px-2">{{ file.statusText }}</span>
</div>
<Image :src="file.url" preview imageClass="w-full h-full object-cover" :class="{'opacity-50 blur-sm': file.status !== 'done'}" />
<div v-if="file.status === 'done'"
class="absolute bottom-0 left-0 w-full bg-black/60 text-white text-xs p-1 truncate text-center pointer-events-none">
{{ file.name }}</div>
<button @click="removeMediaItem('images', index, file)"
class="absolute top-1 right-1 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer z-10"><i
class="absolute top-1 right-1 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center transition-colors cursor-pointer z-10"><i
class="pi pi-times text-xs"></i></button>
</div>
</template>
@@ -340,6 +352,7 @@ const handleFileChange = async (event) => {
url: URL.createObjectURL(file), // Preview local file immediately
progress: 0,
status: 'uploading',
statusText: '等待中...',
abort: null
});
@@ -357,22 +370,35 @@ const handleFileChange = async (event) => {
// Start Upload
(async () => {
try {
item.statusText = '计算中...';
const hash = await calculateHash(file);
let task;
if (type === 'video' || type === 'audio') {
task = commonApi.uploadMultipart(file, hash, type, (p) => item.progress = p);
item.statusText = '准备上传...';
task = commonApi.uploadMultipart(file, hash, type, (p) => {
item.progress = Math.round(p);
item.statusText = `上传中 ${item.progress}%`;
});
} else {
// Check hash for images
// Simple upload for images
item.statusText = '校验中...';
let existing = null;
try {
existing = await commonApi.checkHash(hash);
} catch(e) {}
if (existing) {
item.statusText = '秒传成功';
await new Promise(r => setTimeout(r, 500));
task = { promise: Promise.resolve(existing), abort: () => {} };
} else {
task = commonApi.uploadWithProgress(file, type, (p) => item.progress = p);
item.statusText = '上传中...';
task = commonApi.uploadWithProgress(file, type, (p) => {
item.progress = Math.round(p);
item.statusText = `上传中 ${item.progress}%`;
});
}
}
@@ -383,6 +409,7 @@ const handleFileChange = async (event) => {
item.id = res.id;
item.url = res.url; // Update to remote URL
item.status = 'done';
item.statusText = '上传完成';
item.progress = 100;
} catch (e) {
if (e.message === 'Aborted') {