feat: 更新媒体上传状态显示,添加上传进度和状态文本,优化用户体验
This commit is contained in:
@@ -100,11 +100,11 @@
|
|||||||
<draggable v-model="form.covers" item-key="tempId" class="flex flex-wrap gap-4" group="covers">
|
<draggable v-model="form.covers" item-key="tempId" class="flex flex-wrap gap-4" group="covers">
|
||||||
<template #item="{ element: img, index }">
|
<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 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">
|
<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"></i>
|
<i class="pi pi-spin pi-spinner text-2xl mb-2" v-if="img.status === 'uploading'"></i>
|
||||||
<span class="text-xs font-bold">{{ img.progress }}%</span>
|
<span class="text-xs font-bold text-center px-2">{{ img.statusText }}</span>
|
||||||
</div>
|
</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)"
|
<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="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>
|
class="pi pi-times text-xs"></i></button>
|
||||||
@@ -123,18 +123,22 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-3">视频列表 (拖拽排序)</label>
|
<label class="block text-sm font-bold text-slate-700 mb-3">视频列表 (拖拽排序)</label>
|
||||||
<div class="space-y-3">
|
<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 }">
|
<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="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'}">
|
||||||
<div class="w-10 h-10 bg-blue-100 text-blue-600 rounded flex items-center justify-center"><i
|
<!-- Progress -->
|
||||||
class="pi pi-video"></i></div>
|
<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="flex-1 min-w-0">
|
||||||
<div class="font-bold text-sm text-slate-900 truncate">{{ file.name }}</div>
|
<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>
|
</div>
|
||||||
<button @click="removeMediaItem('videos', index, file)"
|
<button @click="removeMediaItem('videos', index, file)"
|
||||||
class="text-slate-400 hover:text-red-500 px-2 cursor-pointer"><i
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
@@ -149,18 +153,22 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-3">音频列表 (拖拽排序)</label>
|
<label class="block text-sm font-bold text-slate-700 mb-3">音频列表 (拖拽排序)</label>
|
||||||
<div class="space-y-3">
|
<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 }">
|
<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="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'}">
|
||||||
<div class="w-10 h-10 bg-purple-100 text-purple-600 rounded flex items-center justify-center"><i
|
<!-- Progress -->
|
||||||
class="pi pi-microphone"></i></div>
|
<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="flex-1 min-w-0">
|
||||||
<div class="font-bold text-sm text-slate-900 truncate">{{ file.name }}</div>
|
<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>
|
</div>
|
||||||
<button @click="removeMediaItem('audios', index, file)"
|
<button @click="removeMediaItem('audios', index, file)"
|
||||||
class="text-slate-400 hover:text-red-500 px-2 cursor-pointer"><i
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
@@ -175,15 +183,19 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-bold text-slate-700 mb-3">图片列表 (拖拽排序)</label>
|
<label class="block text-sm font-bold text-slate-700 mb-3">图片列表 (拖拽排序)</label>
|
||||||
<div class="flex flex-wrap gap-4">
|
<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 }">
|
<template #item="{ element: file, index }">
|
||||||
<div class="relative group w-32 aspect-square rounded-lg overflow-hidden border border-slate-200 cursor-move">
|
<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 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">
|
||||||
<div
|
<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">
|
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>
|
{{ file.name }}</div>
|
||||||
<button @click="removeMediaItem('images', index, file)"
|
<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>
|
class="pi pi-times text-xs"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -340,6 +352,7 @@ const handleFileChange = async (event) => {
|
|||||||
url: URL.createObjectURL(file), // Preview local file immediately
|
url: URL.createObjectURL(file), // Preview local file immediately
|
||||||
progress: 0,
|
progress: 0,
|
||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
|
statusText: '等待中...',
|
||||||
abort: null
|
abort: null
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -357,22 +370,35 @@ const handleFileChange = async (event) => {
|
|||||||
// Start Upload
|
// Start Upload
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
|
item.statusText = '计算中...';
|
||||||
const hash = await calculateHash(file);
|
const hash = await calculateHash(file);
|
||||||
|
|
||||||
let task;
|
let task;
|
||||||
|
|
||||||
if (type === 'video' || type === 'audio') {
|
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 {
|
} else {
|
||||||
// Check hash for images
|
// Simple upload for images
|
||||||
|
item.statusText = '校验中...';
|
||||||
let existing = null;
|
let existing = null;
|
||||||
try {
|
try {
|
||||||
existing = await commonApi.checkHash(hash);
|
existing = await commonApi.checkHash(hash);
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
item.statusText = '秒传成功';
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
task = { promise: Promise.resolve(existing), abort: () => {} };
|
task = { promise: Promise.resolve(existing), abort: () => {} };
|
||||||
} else {
|
} 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.id = res.id;
|
||||||
item.url = res.url; // Update to remote URL
|
item.url = res.url; // Update to remote URL
|
||||||
item.status = 'done';
|
item.status = 'done';
|
||||||
|
item.statusText = '上传完成';
|
||||||
item.progress = 100;
|
item.progress = 100;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === 'Aborted') {
|
if (e.message === 'Aborted') {
|
||||||
|
|||||||
Reference in New Issue
Block a user