feat: improve media preview UX with caching and loading states
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
|||||||
|
|
||||||
// 获取媒体文件URL(OSS直连或后端代理)
|
// 获取媒体文件URL(OSS直连或后端代理)
|
||||||
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;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 下载当前预览的媒体文件
|
// 下载当前预览的媒体文件
|
||||||
|
|||||||
Reference in New Issue
Block a user