feat: improve media preview UX with caching and loading states

This commit is contained in:
2026-02-17 20:03:02 +08:00
parent 0885195cb5
commit c506cf83be
2 changed files with 93 additions and 6 deletions

View File

@@ -3818,7 +3818,11 @@
</div> </div>
</div> </div>
<div class="media-viewer-body"> <div class="media-viewer-body">
<img :src="currentMediaUrl" :alt="currentMediaName" class="media-viewer-image" @error="handleMediaPreviewError('image')"> <div v-if="mediaPreviewLoading" class="media-preview-loading">
<i class="fas fa-spinner fa-spin"></i>
<span>正在加载预览...</span>
</div>
<img :src="currentMediaUrl" :alt="currentMediaName" class="media-viewer-image" @load="handleMediaPreviewLoaded" @error="handleMediaPreviewError('image')">
</div> </div>
</div> </div>
</div> </div>
@@ -3838,7 +3842,11 @@
</div> </div>
</div> </div>
<div class="media-viewer-body"> <div class="media-viewer-body">
<video controls :src="currentMediaUrl" class="media-viewer-video" @error="handleMediaPreviewError('video')"> <div v-if="mediaPreviewLoading" class="media-preview-loading">
<i class="fas fa-spinner fa-spin"></i>
<span>正在缓冲视频...</span>
</div>
<video controls preload="metadata" playsinline :src="currentMediaUrl" class="media-viewer-video" @loadedmetadata="handleMediaPreviewLoaded" @canplay="handleMediaPreviewLoaded" @waiting="handleMediaPreviewWaiting" @playing="handleMediaPreviewPlaying" @error="handleMediaPreviewError('video')">
您的浏览器不支持视频播放 您的浏览器不支持视频播放
</video> </video>
</div> </div>
@@ -3863,7 +3871,11 @@
<div class="audio-player-icon"> <div class="audio-player-icon">
<i class="fas fa-music"></i> <i class="fas fa-music"></i>
</div> </div>
<audio controls :src="currentMediaUrl" class="media-viewer-audio" @error="handleMediaPreviewError('audio')"> <div v-if="mediaPreviewLoading" class="media-preview-loading">
<i class="fas fa-spinner fa-spin"></i>
<span>正在缓冲音频...</span>
</div>
<audio controls preload="metadata" :src="currentMediaUrl" class="media-viewer-audio" @loadedmetadata="handleMediaPreviewLoaded" @canplay="handleMediaPreviewLoaded" @waiting="handleMediaPreviewWaiting" @playing="handleMediaPreviewPlaying" @error="handleMediaPreviewError('audio')">
您的浏览器不支持音频播放 您的浏览器不支持音频播放
</audio> </audio>
</div> </div>
@@ -4116,6 +4128,27 @@
background: rgba(255,255,255,0.03); background: rgba(255,255,255,0.03);
flex: 1; flex: 1;
overflow: auto; overflow: auto;
position: relative;
}
.media-preview-loading {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
background: rgba(15, 23, 42, 0.45);
color: #fff;
font-size: 14px;
z-index: 2;
pointer-events: none;
backdrop-filter: blur(2px);
}
.media-preview-loading i {
font-size: 20px;
} }
.media-viewer-image { .media-viewer-image {
@@ -4184,6 +4217,14 @@
padding: 15px; padding: 15px;
} }
.media-preview-loading {
font-size: 13px;
}
.media-preview-loading i {
font-size: 18px;
}
.media-viewer-image, .media-viewer-image,
.media-viewer-video { .media-viewer-video {
max-height: 70vh; max-height: 70vh;

View File

@@ -260,6 +260,8 @@ createApp({
currentMediaUrl: '', currentMediaUrl: '',
currentMediaName: '', currentMediaName: '',
currentMediaType: '', // 'image', 'video', 'audio' currentMediaType: '', // 'image', 'video', 'audio'
mediaPreviewLoading: false,
mediaPreviewCache: {}, // { [filePath]: { url, expiresAt } }
thumbnailLoadErrors: {}, // 缩略图加载失败标记(按文件路径) thumbnailLoadErrors: {}, // 缩略图加载失败标记(按文件路径)
longPressDuration: 420, // 长按时间(毫秒) longPressDuration: 420, // 长按时间(毫秒)
// 管理员编辑用户存储权限 // 管理员编辑用户存储权限
@@ -1908,6 +1910,24 @@ handleDragLeave(e) {
// ===== 媒体预览功能 ===== // ===== 媒体预览功能 =====
getCachedMediaUrl(filePath) {
if (!filePath) return null;
const cached = this.mediaPreviewCache[filePath];
if (!cached || !cached.url || !cached.expiresAt) return null;
if (Date.now() >= cached.expiresAt) return null;
return cached.url;
},
setCachedMediaUrl(filePath, url, expiresInSeconds = 3600) {
if (!filePath || !url) return;
const ttlMs = Math.max(60 * 1000, (Number(expiresInSeconds) || 3600) * 1000);
const expiresAt = Date.now() + ttlMs - 20 * 1000;
this.mediaPreviewCache = {
...this.mediaPreviewCache,
[filePath]: { url, expiresAt }
};
},
getCurrentFilePath(file) { getCurrentFilePath(file) {
if (!file || !file.name) return ''; if (!file || !file.name) return '';
return this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`; return this.currentPath === '/' ? `/${file.name}` : `${this.currentPath}/${file.name}`;
@@ -1929,12 +1949,15 @@ handleDragLeave(e) {
// 获取媒体文件URLOSS直连或后端代理 // 获取媒体文件URLOSS直连或后端代理
async getMediaUrl(file) { async getMediaUrl(file) {
const filePath = this.currentPath === '/' const filePath = this.getCurrentFilePath(file);
? `/${file.name}`
: `${this.currentPath}/${file.name}`;
// OSS 模式优先直连,避免限流场景回退为后端中转 // OSS 模式优先直连,避免限流场景回退为后端中转
if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') { if (this.storageType === 'oss' && this.user?.oss_config_source !== 'none') {
const cachedUrl = this.getCachedMediaUrl(filePath);
if (cachedUrl) {
return cachedUrl;
}
try { try {
const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, { const { data } = await axios.get(`${this.apiBase}/api/files/download-url`, {
params: { params: {
@@ -1943,6 +1966,7 @@ handleDragLeave(e) {
} }
}); });
if (data.success) { if (data.success) {
this.setCachedMediaUrl(filePath, data.downloadUrl, data.expiresIn || 3600);
return data.downloadUrl; return data.downloadUrl;
} }
} catch (error) { } catch (error) {
@@ -1987,6 +2011,7 @@ handleDragLeave(e) {
// 打开图片预览 // 打开图片预览
async openImageViewer(file) { async openImageViewer(file) {
this.mediaPreviewLoading = true;
const url = await this.getMediaUrl(file); const url = await this.getMediaUrl(file);
if (url) { if (url) {
this.currentMediaUrl = url; this.currentMediaUrl = url;
@@ -1994,10 +2019,25 @@ handleDragLeave(e) {
this.currentMediaType = 'image'; this.currentMediaType = 'image';
this.showImageViewer = true; this.showImageViewer = true;
} else { } else {
this.mediaPreviewLoading = false;
this.showToast('error', '错误', '无法获取文件预览链接'); this.showToast('error', '错误', '无法获取文件预览链接');
} }
}, },
handleMediaPreviewLoaded() {
this.mediaPreviewLoading = false;
},
handleMediaPreviewWaiting() {
if (this.currentMediaType === 'video' || this.currentMediaType === 'audio') {
this.mediaPreviewLoading = true;
}
},
handleMediaPreviewPlaying() {
this.mediaPreviewLoading = false;
},
handleMediaPreviewError(type = 'file') { handleMediaPreviewError(type = 'file') {
const typeTextMap = { const typeTextMap = {
image: '图片', image: '图片',
@@ -2005,12 +2045,14 @@ handleDragLeave(e) {
audio: '音频' audio: '音频'
}; };
const typeText = typeTextMap[type] || '文件'; const typeText = typeTextMap[type] || '文件';
this.mediaPreviewLoading = false;
this.showToast('error', '预览失败', `${typeText}预览失败,请尝试下载后查看`); this.showToast('error', '预览失败', `${typeText}预览失败,请尝试下载后查看`);
this.closeMediaViewer(); this.closeMediaViewer();
}, },
// 打开视频播放器 // 打开视频播放器
async openVideoPlayer(file) { async openVideoPlayer(file) {
this.mediaPreviewLoading = true;
const url = await this.getMediaUrl(file); const url = await this.getMediaUrl(file);
if (url) { if (url) {
this.currentMediaUrl = url; this.currentMediaUrl = url;
@@ -2018,12 +2060,14 @@ handleDragLeave(e) {
this.currentMediaType = 'video'; this.currentMediaType = 'video';
this.showVideoPlayer = true; this.showVideoPlayer = true;
} else { } else {
this.mediaPreviewLoading = false;
this.showToast('error', '错误', '无法获取文件预览链接'); this.showToast('error', '错误', '无法获取文件预览链接');
} }
}, },
// 打开音频播放器 // 打开音频播放器
async openAudioPlayer(file) { async openAudioPlayer(file) {
this.mediaPreviewLoading = true;
const url = await this.getMediaUrl(file); const url = await this.getMediaUrl(file);
if (url) { if (url) {
this.currentMediaUrl = url; this.currentMediaUrl = url;
@@ -2031,6 +2075,7 @@ handleDragLeave(e) {
this.currentMediaType = 'audio'; this.currentMediaType = 'audio';
this.showAudioPlayer = true; this.showAudioPlayer = true;
} else { } else {
this.mediaPreviewLoading = false;
this.showToast('error', '错误', '无法获取文件预览链接'); this.showToast('error', '错误', '无法获取文件预览链接');
} }
}, },
@@ -2043,6 +2088,7 @@ handleDragLeave(e) {
this.currentMediaUrl = ''; this.currentMediaUrl = '';
this.currentMediaName = ''; this.currentMediaName = '';
this.currentMediaType = ''; this.currentMediaType = '';
this.mediaPreviewLoading = false;
}, },
// 下载当前预览的媒体文件 // 下载当前预览的媒体文件