|
|
|
@ -110,22 +110,41 @@ |
|
|
|
@click="showVideoDetail(video)" |
|
|
|
> |
|
|
|
<div class="cover"> |
|
|
|
<img v-if="video.coverImage" :src="video.coverImage" :alt="video.title" /> |
|
|
|
<div v-else class="no-cover"> |
|
|
|
<i class="el-icon-video-camera"></i> |
|
|
|
<!-- 封面图片 --> |
|
|
|
<img |
|
|
|
v-if="video.coverImage && video.coverImage !== 'null'" |
|
|
|
:src="getCoverImage(video)" |
|
|
|
:alt="video.title" |
|
|
|
@load="handleCoverImageLoad(video.id)" |
|
|
|
@error="handleCoverImageError(video)" |
|
|
|
style="width: 100%; height: 100%; object-fit: cover;" |
|
|
|
/> |
|
|
|
|
|
|
|
<!-- 默认封面 --> |
|
|
|
<div |
|
|
|
v-else |
|
|
|
class="default-cover" |
|
|
|
:style="{ background: getCategoryColor(video.category) }" |
|
|
|
> |
|
|
|
<div class="cover-content"> |
|
|
|
<i class="el-icon-video-camera cover-icon"></i> |
|
|
|
<span class="cover-text">{{ getCategoryText(video.category) }}</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="duration">{{ formatDuration(video.duration) }}</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="info"> |
|
|
|
<h4 class="title">{{ video.title }}</h4> |
|
|
|
<div class="title">{{ video.title }}</div> |
|
|
|
<div class="meta"> |
|
|
|
<span class="author"> |
|
|
|
<span> |
|
|
|
<i class="el-icon-user"></i> |
|
|
|
{{ video.nickName || video.userName || '未知' }} |
|
|
|
{{ video.userName || '未知' }} |
|
|
|
</span> |
|
|
|
<span class="views"> |
|
|
|
<span> |
|
|
|
<i class="el-icon-view"></i> |
|
|
|
{{ video.viewCount || 0 }}次观看 |
|
|
|
{{ video.viewCount || 0 }} |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<div class="time">{{ formatTime(video.createTime) }}</div> |
|
|
|
@ -251,9 +270,14 @@ |
|
|
|
<div class="my-video-item" v-for="video in myVideos" :key="video.id"> |
|
|
|
<div class="video-info" @click="showVideoDetail(video)"> |
|
|
|
<div class="left"> |
|
|
|
<img v-if="video.coverImage" :src="video.coverImage" class="cover-small" /> |
|
|
|
<div v-else class="cover-small-placeholder"> |
|
|
|
<i class="el-icon-video-camera"></i> |
|
|
|
<div class="cover-small-container"> |
|
|
|
<img v-if="video.coverImage && video.coverImage !== 'null'" |
|
|
|
:src="getCoverImage(video)" |
|
|
|
class="cover-small" |
|
|
|
@error="handleCoverImageError(video)" /> |
|
|
|
<div v-else class="cover-small-placeholder" :style="{ background: getCategoryColor(video.category) }"> |
|
|
|
<i class="el-icon-video-camera"></i> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="details"> |
|
|
|
<div class="title">{{ video.title }}</div> |
|
|
|
@ -262,7 +286,7 @@ |
|
|
|
{{ video.status === '1' ? '公开' : '私有' }} |
|
|
|
</el-tag> |
|
|
|
<span class="time">{{ formatTime(video.createTime) }}</span> |
|
|
|
<span class="views">{{ video.viewCount }}次观看</span> |
|
|
|
<span class="views">{{ video.viewCount || 0 }}次观看</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -287,29 +311,66 @@ |
|
|
|
:visible.sync="showDetailDialog" |
|
|
|
:title="currentVideo ? currentVideo.title : '视频详情'" |
|
|
|
width="800px" |
|
|
|
@open="handleDialogOpen" |
|
|
|
@close="handleDialogClose" |
|
|
|
> |
|
|
|
<div v-if="currentVideo" class="video-detail-dialog"> |
|
|
|
<!-- 视频播放器 --> |
|
|
|
<div class="video-player"> |
|
|
|
<video |
|
|
|
v-if="currentVideo.videoUrl" |
|
|
|
:src="currentVideo.videoUrl" |
|
|
|
controls |
|
|
|
autoplay |
|
|
|
style="width: 100%; max-height: 400px;" |
|
|
|
></video> |
|
|
|
<div v-else class="no-video"> |
|
|
|
<div v-if="!videoPlayerReady && !videoLoadFailed" class="video-loading"> |
|
|
|
<i class="el-icon-loading"></i> |
|
|
|
<p>加载视频中...</p> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-else-if="videoLoadFailed" class="video-error"> |
|
|
|
<i class="el-icon-video-camera"></i> |
|
|
|
<p>视频加载失败</p> |
|
|
|
<p class="error-tip">可能的原因:</p> |
|
|
|
<ul> |
|
|
|
<li>视频文件不存在</li> |
|
|
|
<li>网络连接问题</li> |
|
|
|
<li>视频格式不支持</li> |
|
|
|
</ul> |
|
|
|
<div class="error-actions"> |
|
|
|
<el-button type="primary" @click="retryLoadVideo" size="small"> |
|
|
|
<i class="el-icon-refresh"></i> 重试加载 |
|
|
|
</el-button> |
|
|
|
<el-button @click="openVideoInNewTab" size="small"> |
|
|
|
<i class="el-icon-link"></i> 在新标签页打开 |
|
|
|
</el-button> |
|
|
|
<el-button @click="showDetailDialog = false" size="small"> |
|
|
|
关闭 |
|
|
|
</el-button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<video |
|
|
|
v-else |
|
|
|
ref="videoPlayer" |
|
|
|
:src="currentVideo.videoUrl" |
|
|
|
controls |
|
|
|
preload="metadata" |
|
|
|
style="width: 100%; max-height: 400px; border-radius: 6px;" |
|
|
|
@error="handleVideoError" |
|
|
|
@loadeddata="handleVideoLoaded" |
|
|
|
@canplay="handleVideoCanPlay" |
|
|
|
crossorigin="anonymous" |
|
|
|
> |
|
|
|
<source :src="currentVideo.videoUrl" type="video/mp4"> |
|
|
|
<source :src="currentVideo.videoUrl" type="video/webm"> |
|
|
|
您的浏览器不支持视频播放 |
|
|
|
</video> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 视频信息 --> |
|
|
|
<div class="video-info-card"> |
|
|
|
<div class="info-row"> |
|
|
|
<span class="label">发布者:</span> |
|
|
|
<span class="value">{{ currentVideo.nickName || currentVideo.userName || '未知' }}</span> |
|
|
|
<span class="value">{{ currentVideo.userName || '未知' }}</span> |
|
|
|
</div> |
|
|
|
<div class="info-row"> |
|
|
|
<span class="label">分类:</span> |
|
|
|
<span class="value">{{ getCategoryText(currentVideo.category) }}</span> |
|
|
|
</div> |
|
|
|
<div class="info-row"> |
|
|
|
<span class="label">发布时间:</span> |
|
|
|
@ -321,7 +382,15 @@ |
|
|
|
</div> |
|
|
|
<div v-if="currentVideo.description" class="info-row"> |
|
|
|
<span class="label">视频描述:</span> |
|
|
|
<p class="value">{{ currentVideo.description }}</p> |
|
|
|
<p class="value description-text">{{ currentVideo.description }}</p> |
|
|
|
</div> |
|
|
|
<div class="info-row"> |
|
|
|
<span class="label">视频地址:</span> |
|
|
|
<p class="value"> |
|
|
|
<el-link :href="currentVideo.videoUrl" target="_blank" type="primary"> |
|
|
|
{{ currentVideo.videoUrl }} |
|
|
|
</el-link> |
|
|
|
</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -363,7 +432,14 @@ export default { |
|
|
|
|
|
|
|
// ========== 视频详情相关 ========== |
|
|
|
showDetailDialog: false, |
|
|
|
currentVideo: null |
|
|
|
currentVideo: null, |
|
|
|
videoPlayerReady: false, |
|
|
|
videoLoadFailed: false, |
|
|
|
|
|
|
|
// ========== 封面相关 ========== |
|
|
|
coverLoadingStates: new Map(), |
|
|
|
coverErrorStates: new Map(), |
|
|
|
defaultCoverCache: new Map(), |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
@ -393,14 +469,28 @@ export default { |
|
|
|
async loadPublicVideos() { |
|
|
|
try { |
|
|
|
this.loading = true |
|
|
|
console.log('🔄 加载公开视频...') |
|
|
|
|
|
|
|
const params = { |
|
|
|
title: this.searchKeyword, |
|
|
|
category: this.filterCategory |
|
|
|
category: this.filterCategory, |
|
|
|
pageNum: 1, |
|
|
|
pageSize: 100 |
|
|
|
} |
|
|
|
|
|
|
|
const res = await trainingApi.getPublicVideos(params) |
|
|
|
this.videos = res.data ? res.data.rows : [] |
|
|
|
console.log('✅ 公开视频响应:', res) |
|
|
|
|
|
|
|
if (res && res.rows) { |
|
|
|
console.log(`📊 找到 ${res.rows.length} 个视频`) |
|
|
|
this.videos = res.rows |
|
|
|
} else { |
|
|
|
console.warn('⚠️ 公开视频响应数据格式不正确') |
|
|
|
this.videos = [] |
|
|
|
} |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.error('加载视频失败:', error) |
|
|
|
console.error('❌ 加载公开视频失败:', error) |
|
|
|
this.$message.error('加载失败') |
|
|
|
} finally { |
|
|
|
this.loading = false |
|
|
|
@ -411,13 +501,27 @@ export default { |
|
|
|
async loadMyVideos() { |
|
|
|
try { |
|
|
|
this.myVideosLoading = true |
|
|
|
console.log('🔄 加载我的视频...') |
|
|
|
|
|
|
|
const params = { |
|
|
|
title: this.mySearchKeyword |
|
|
|
title: this.mySearchKeyword, |
|
|
|
pageNum: 1, |
|
|
|
pageSize: 100 |
|
|
|
} |
|
|
|
|
|
|
|
const res = await trainingApi.getMyVideos(params) |
|
|
|
this.myVideos = res.data ? res.data.rows : [] |
|
|
|
console.log('✅ 我的视频响应:', res) |
|
|
|
|
|
|
|
if (res && res.rows) { |
|
|
|
console.log(`📊 找到 ${res.rows.length} 个我的视频`) |
|
|
|
this.myVideos = res.rows |
|
|
|
} else { |
|
|
|
console.warn('⚠️ 我的视频响应数据格式不正确') |
|
|
|
this.myVideos = [] |
|
|
|
} |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.error('加载我的视频失败:', error) |
|
|
|
console.error('❌ 加载我的视频失败:', error) |
|
|
|
this.$message.error('加载失败') |
|
|
|
} finally { |
|
|
|
this.myVideosLoading = false |
|
|
|
@ -511,10 +615,141 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// ========== 视频详情相关方法 ========== |
|
|
|
|
|
|
|
// 显示视频详情 |
|
|
|
showVideoDetail(video) { |
|
|
|
this.currentVideo = video |
|
|
|
this.showDetailDialog = true |
|
|
|
console.log('🔍 查看视频详情,原始数据:', video) |
|
|
|
|
|
|
|
if (!video || !video.videoUrl) { |
|
|
|
this.$message.error('视频信息不完整') |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// 获取完整URL |
|
|
|
const fullVideoUrl = this.getVideoUrl(video.videoUrl) |
|
|
|
console.log('📹 视频URL分析:') |
|
|
|
console.log(' 原始URL:', video.videoUrl) |
|
|
|
console.log(' 完整URL:', fullVideoUrl) |
|
|
|
console.log(' 是否有效URL:', fullVideoUrl.startsWith('http') || fullVideoUrl.startsWith('/')) |
|
|
|
|
|
|
|
// 测试URL可访问性 |
|
|
|
this.testVideoUrl(fullVideoUrl).then(isAccessible => { |
|
|
|
if (!isAccessible) { |
|
|
|
console.log('⚠️ 视频URL可能无法访问') |
|
|
|
} |
|
|
|
|
|
|
|
this.currentVideo = { |
|
|
|
...video, |
|
|
|
videoUrl: fullVideoUrl, |
|
|
|
coverImage: this.getCoverImage(video) |
|
|
|
} |
|
|
|
this.showDetailDialog = true |
|
|
|
}).catch(() => { |
|
|
|
this.currentVideo = { |
|
|
|
...video, |
|
|
|
videoUrl: fullVideoUrl, |
|
|
|
coverImage: this.getCoverImage(video) |
|
|
|
} |
|
|
|
this.showDetailDialog = true |
|
|
|
}) |
|
|
|
}, |
|
|
|
|
|
|
|
// 测试视频URL是否可访问 |
|
|
|
testVideoUrl(url) { |
|
|
|
return new Promise((resolve) => { |
|
|
|
const testVideo = document.createElement('video') |
|
|
|
testVideo.preload = 'metadata' |
|
|
|
testVideo.onloadedmetadata = () => { |
|
|
|
console.log('✅ 视频URL可访问,时长:', testVideo.duration) |
|
|
|
resolve(true) |
|
|
|
} |
|
|
|
testVideo.onerror = () => { |
|
|
|
console.log('❌ 视频URL不可访问:', url) |
|
|
|
resolve(false) |
|
|
|
} |
|
|
|
|
|
|
|
// 设置超时 |
|
|
|
setTimeout(() => { |
|
|
|
console.log('⏰ 视频加载超时') |
|
|
|
resolve(false) |
|
|
|
}, 3000) |
|
|
|
|
|
|
|
testVideo.src = url |
|
|
|
}) |
|
|
|
}, |
|
|
|
|
|
|
|
// 弹窗打开时 |
|
|
|
handleDialogOpen() { |
|
|
|
this.videoPlayerReady = false |
|
|
|
this.videoLoadFailed = false |
|
|
|
|
|
|
|
// 延迟初始化视频播放器 |
|
|
|
this.$nextTick(() => { |
|
|
|
if (this.$refs.videoPlayer) { |
|
|
|
this.$refs.videoPlayer.load() |
|
|
|
} |
|
|
|
}) |
|
|
|
}, |
|
|
|
|
|
|
|
// 弹窗关闭时 |
|
|
|
handleDialogClose() { |
|
|
|
// 暂停视频 |
|
|
|
if (this.$refs.videoPlayer) { |
|
|
|
this.$refs.videoPlayer.pause() |
|
|
|
} |
|
|
|
this.currentVideo = null |
|
|
|
this.videoPlayerReady = false |
|
|
|
this.videoLoadFailed = false |
|
|
|
}, |
|
|
|
|
|
|
|
// 视频可以播放时 |
|
|
|
handleVideoCanPlay() { |
|
|
|
console.log('🎬 视频可以播放了') |
|
|
|
this.videoPlayerReady = true |
|
|
|
this.videoLoadFailed = false |
|
|
|
}, |
|
|
|
|
|
|
|
// 视频加载完成 |
|
|
|
handleVideoLoaded() { |
|
|
|
console.log('✅ 视频加载完成') |
|
|
|
}, |
|
|
|
|
|
|
|
// 视频加载错误 |
|
|
|
handleVideoError(event) { |
|
|
|
console.error('❌ 视频加载错误:', event) |
|
|
|
this.videoLoadFailed = true |
|
|
|
this.$message.error('视频加载失败,请检查视频文件') |
|
|
|
}, |
|
|
|
|
|
|
|
// 重试加载视频 |
|
|
|
retryLoadVideo() { |
|
|
|
console.log('🔄 重试加载视频') |
|
|
|
this.videoPlayerReady = false |
|
|
|
this.videoLoadFailed = false |
|
|
|
|
|
|
|
if (this.currentVideo && this.currentVideo.videoUrl) { |
|
|
|
// 添加时间戳避免缓存 |
|
|
|
const videoUrl = this.currentVideo.videoUrl + |
|
|
|
(this.currentVideo.videoUrl.includes('?') ? '&' : '?') + |
|
|
|
't=' + Date.now() |
|
|
|
|
|
|
|
this.currentVideo.videoUrl = videoUrl |
|
|
|
|
|
|
|
this.$nextTick(() => { |
|
|
|
const videoElement = this.$refs.videoPlayer |
|
|
|
if (videoElement) { |
|
|
|
videoElement.load() |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 在新标签页打开视频 |
|
|
|
openVideoInNewTab() { |
|
|
|
if (this.currentVideo && this.currentVideo.videoUrl) { |
|
|
|
window.open(this.currentVideo.videoUrl, '_blank') |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 删除我的视频 |
|
|
|
@ -528,19 +763,144 @@ export default { |
|
|
|
|
|
|
|
await trainingApi.deleteVideo(videoId) |
|
|
|
this.$message.success('删除成功') |
|
|
|
|
|
|
|
// 删除后立即刷新数据 |
|
|
|
this.loadMyVideos() |
|
|
|
|
|
|
|
// 同时刷新公开视频列表(如果删除的是公开视频) |
|
|
|
if (this.activeTab === 'list') { |
|
|
|
this.loadPublicVideos() |
|
|
|
} |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
if (error !== 'cancel') { |
|
|
|
console.error('删除失败:', error) |
|
|
|
this.$message.error('删除失败') |
|
|
|
this.$message.error('删除失败: ' + (error.message || '未知错误')) |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 弹窗关闭处理 |
|
|
|
handleDialogClose() { |
|
|
|
this.currentVideo = null |
|
|
|
// ========== 封面相关方法 ========== |
|
|
|
|
|
|
|
// 获取封面图片 |
|
|
|
getCoverImage(video) { |
|
|
|
// 如果视频对象有有效封面,返回封面URL |
|
|
|
if (video.coverImage && |
|
|
|
video.coverImage !== 'null' && |
|
|
|
video.coverImage !== 'undefined' && |
|
|
|
video.coverImage !== '') { |
|
|
|
return this.getFullUrl(video.coverImage) |
|
|
|
} |
|
|
|
|
|
|
|
// 否则返回默认封面 |
|
|
|
return this.getCategoryDefaultCover(video.category || 'other') |
|
|
|
}, |
|
|
|
|
|
|
|
// 根据分类获取默认封面 |
|
|
|
getCategoryDefaultCover(category) { |
|
|
|
// 缓存默认封面,避免重复生成 |
|
|
|
if (this.defaultCoverCache.has(category)) { |
|
|
|
return this.defaultCoverCache.get(category) |
|
|
|
} |
|
|
|
|
|
|
|
// 根据分类生成不同的默认封面SVG |
|
|
|
const color = this.getCategoryColor(category) |
|
|
|
const text = this.getCategoryText(category) |
|
|
|
|
|
|
|
// 创建SVG |
|
|
|
const svgString = `<svg width="320" height="180" xmlns="http://www.w3.org/2000/svg"> |
|
|
|
<rect width="100%" height="100%" fill="${color}" /> |
|
|
|
<text x="50%" y="50%" font-family="Arial, sans-serif" font-size="16" |
|
|
|
fill="white" text-anchor="middle" dy=".3em"> |
|
|
|
${text} |
|
|
|
</text> |
|
|
|
</svg>` |
|
|
|
|
|
|
|
// 转换为base64 |
|
|
|
const base64 = btoa(unescape(encodeURIComponent(svgString))) |
|
|
|
const dataUrl = `data:image/svg+xml;base64,${base64}` |
|
|
|
|
|
|
|
// 缓存 |
|
|
|
this.defaultCoverCache.set(category, dataUrl) |
|
|
|
|
|
|
|
return dataUrl |
|
|
|
}, |
|
|
|
|
|
|
|
// 获取分类颜色 |
|
|
|
getCategoryColor(category) { |
|
|
|
const colors = { |
|
|
|
surgery: '#667eea', // 蓝色 |
|
|
|
diagnosis: '#67cf77', // 绿色 |
|
|
|
medication: '#ff6b6b', // 红色 |
|
|
|
other: '#ffb366' // 橙色 |
|
|
|
} |
|
|
|
return colors[category] || colors.other |
|
|
|
}, |
|
|
|
|
|
|
|
// 获取分类文本 |
|
|
|
getCategoryText(category) { |
|
|
|
const texts = { |
|
|
|
surgery: '手术技巧', |
|
|
|
diagnosis: '疾病诊断', |
|
|
|
medication: '药物使用', |
|
|
|
other: '其他' |
|
|
|
} |
|
|
|
return texts[category] || '其他' |
|
|
|
}, |
|
|
|
|
|
|
|
// 处理封面图片加载成功 |
|
|
|
handleCoverImageLoad(videoId) { |
|
|
|
console.log(`✅ 封面图片加载成功: ${videoId}`) |
|
|
|
this.coverLoadingStates.set(videoId, true) |
|
|
|
this.coverErrorStates.set(videoId, false) |
|
|
|
}, |
|
|
|
|
|
|
|
// 处理封面图片加载错误 |
|
|
|
handleCoverImageError(video) { |
|
|
|
const videoId = video.id |
|
|
|
console.log(`❌ 封面图片加载失败: ${videoId}`) |
|
|
|
|
|
|
|
// 标记为错误状态 |
|
|
|
this.coverErrorStates.set(videoId, true) |
|
|
|
|
|
|
|
// 使用默认封面替换 |
|
|
|
this.$nextTick(() => { |
|
|
|
const imgElements = document.querySelectorAll(`[data-video-id="${videoId}"] img`) |
|
|
|
imgElements.forEach(img => { |
|
|
|
img.src = this.getCategoryDefaultCover(video.category || 'other') |
|
|
|
}) |
|
|
|
}) |
|
|
|
}, |
|
|
|
|
|
|
|
// ========== URL处理方法 ========== |
|
|
|
|
|
|
|
// 获取完整URL |
|
|
|
getFullUrl(url) { |
|
|
|
if (!url) return '' |
|
|
|
|
|
|
|
// 清理可能的空格 |
|
|
|
url = url.trim() |
|
|
|
|
|
|
|
// 如果是完整URL或base64,直接返回 |
|
|
|
if (url.startsWith('http://') || |
|
|
|
url.startsWith('https://') || |
|
|
|
url.startsWith('data:') || |
|
|
|
url.startsWith('blob:')) { |
|
|
|
return url |
|
|
|
} |
|
|
|
|
|
|
|
// 如果是相对路径(以/开头),直接返回 |
|
|
|
if (url.startsWith('/')) { |
|
|
|
return window.location.origin + url |
|
|
|
} |
|
|
|
|
|
|
|
// 否则添加uploads前缀 |
|
|
|
return window.location.origin + '/uploads/' + url |
|
|
|
}, |
|
|
|
|
|
|
|
// 获取视频URL |
|
|
|
getVideoUrl(videoUrl) { |
|
|
|
return this.getFullUrl(videoUrl) |
|
|
|
}, |
|
|
|
|
|
|
|
// ========== 工具函数 ========== |
|
|
|
@ -548,13 +908,20 @@ export default { |
|
|
|
if (!seconds) return '00:00' |
|
|
|
const min = Math.floor(seconds / 60) |
|
|
|
const sec = seconds % 60 |
|
|
|
return `${min}:${sec.toString().padStart(2, '0')}` |
|
|
|
return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}` |
|
|
|
}, |
|
|
|
|
|
|
|
formatTime(dateStr) { |
|
|
|
if (!dateStr) return '' |
|
|
|
const date = new Date(dateStr) |
|
|
|
return date.toLocaleDateString('zh-CN') |
|
|
|
try { |
|
|
|
const date = new Date(dateStr) |
|
|
|
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', { |
|
|
|
hour: '2-digit', |
|
|
|
minute: '2-digit' |
|
|
|
}) |
|
|
|
} catch (error) { |
|
|
|
return dateStr |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
formatFileSize(bytes) { |
|
|
|
@ -563,7 +930,7 @@ export default { |
|
|
|
const sizes = ['B', 'KB', 'MB', 'GB'] |
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)) |
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
} |
|
|
|
</script> |
|
|
|
@ -594,8 +961,6 @@ export default { |
|
|
|
gap: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.training-main { |
|
|
|
flex: 1; |
|
|
|
display: flex; |
|
|
|
@ -680,19 +1045,33 @@ export default { |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
object-fit: cover; |
|
|
|
display: block; |
|
|
|
transition: opacity 0.3s; |
|
|
|
} |
|
|
|
|
|
|
|
.no-cover { |
|
|
|
/* 默认封面样式 */ |
|
|
|
.default-cover { |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
color: #d9d9d9; |
|
|
|
} |
|
|
|
|
|
|
|
.no-cover .el-icon { |
|
|
|
font-size: 40px; |
|
|
|
.cover-content { |
|
|
|
text-align: center; |
|
|
|
color: white; |
|
|
|
} |
|
|
|
|
|
|
|
.cover-icon { |
|
|
|
font-size: 36px; |
|
|
|
margin-bottom: 8px; |
|
|
|
display: block; |
|
|
|
} |
|
|
|
|
|
|
|
.cover-text { |
|
|
|
font-size: 14px; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
|
|
|
|
.duration { |
|
|
|
@ -704,6 +1083,7 @@ export default { |
|
|
|
padding: 2px 6px; |
|
|
|
border-radius: 4px; |
|
|
|
font-size: 12px; |
|
|
|
z-index: 2; |
|
|
|
} |
|
|
|
|
|
|
|
.video-card .info { |
|
|
|
@ -837,22 +1217,28 @@ export default { |
|
|
|
flex: 1; |
|
|
|
} |
|
|
|
|
|
|
|
.cover-small { |
|
|
|
.cover-small-container { |
|
|
|
position: relative; |
|
|
|
width: 80px; |
|
|
|
height: 45px; |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.cover-small { |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
border-radius: 4px; |
|
|
|
object-fit: cover; |
|
|
|
} |
|
|
|
|
|
|
|
.cover-small-placeholder { |
|
|
|
width: 80px; |
|
|
|
height: 45px; |
|
|
|
background: #f5f5f5; |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
border-radius: 4px; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
color: #d9d9d9; |
|
|
|
color: white; |
|
|
|
} |
|
|
|
|
|
|
|
.details { |
|
|
|
@ -863,6 +1249,10 @@ export default { |
|
|
|
margin: 0 0 6px 0; |
|
|
|
font-weight: 500; |
|
|
|
color: #333; |
|
|
|
display: -webkit-box; |
|
|
|
-webkit-line-clamp: 1; |
|
|
|
-webkit-box-orient: vertical; |
|
|
|
overflow: hidden; |
|
|
|
} |
|
|
|
|
|
|
|
.details .meta { |
|
|
|
@ -878,25 +1268,79 @@ export default { |
|
|
|
padding: 10px 0; |
|
|
|
} |
|
|
|
|
|
|
|
.video-player { |
|
|
|
margin-bottom: 20px; |
|
|
|
background: #000; |
|
|
|
/* 视频加载状态 */ |
|
|
|
.video-loading { |
|
|
|
padding: 60px; |
|
|
|
text-align: center; |
|
|
|
color: #666; |
|
|
|
background: #f5f5f5; |
|
|
|
border-radius: 6px; |
|
|
|
overflow: hidden; |
|
|
|
min-height: 200px; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
} |
|
|
|
|
|
|
|
.no-video { |
|
|
|
padding: 60px; |
|
|
|
.video-loading .el-icon-loading { |
|
|
|
font-size: 36px; |
|
|
|
margin-bottom: 16px; |
|
|
|
color: #409eff; |
|
|
|
} |
|
|
|
|
|
|
|
/* 视频错误状态 */ |
|
|
|
.video-error { |
|
|
|
padding: 40px; |
|
|
|
text-align: center; |
|
|
|
color: white; |
|
|
|
background: #666; |
|
|
|
color: #f56c6c; |
|
|
|
background: #fff5f5; |
|
|
|
border-radius: 6px; |
|
|
|
min-height: 200px; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
} |
|
|
|
|
|
|
|
.no-video .el-icon { |
|
|
|
.video-error .el-icon-video-camera { |
|
|
|
font-size: 48px; |
|
|
|
margin-bottom: 16px; |
|
|
|
} |
|
|
|
|
|
|
|
.error-tip { |
|
|
|
margin: 16px 0 8px 0; |
|
|
|
color: #333; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
|
|
|
|
.error-tip ul { |
|
|
|
text-align: left; |
|
|
|
display: inline-block; |
|
|
|
margin: 8px auto; |
|
|
|
color: #666; |
|
|
|
} |
|
|
|
|
|
|
|
.error-tip li { |
|
|
|
margin: 4px 0; |
|
|
|
} |
|
|
|
|
|
|
|
.error-actions { |
|
|
|
margin-top: 20px; |
|
|
|
display: flex; |
|
|
|
justify-content: center; |
|
|
|
gap: 12px; |
|
|
|
} |
|
|
|
|
|
|
|
.video-player { |
|
|
|
margin-bottom: 20px; |
|
|
|
border-radius: 6px; |
|
|
|
overflow: hidden; |
|
|
|
min-height: 300px; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
} |
|
|
|
|
|
|
|
.video-info-card { |
|
|
|
padding: 16px; |
|
|
|
background: #fafafa; |
|
|
|
@ -904,18 +1348,28 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
.info-row { |
|
|
|
margin-bottom: 10px; |
|
|
|
margin-bottom: 12px; |
|
|
|
font-size: 14px; |
|
|
|
display: flex; |
|
|
|
} |
|
|
|
|
|
|
|
.info-row .label { |
|
|
|
color: #666; |
|
|
|
font-weight: 500; |
|
|
|
margin-right: 8px; |
|
|
|
flex-shrink: 0; |
|
|
|
width: 80px; |
|
|
|
} |
|
|
|
|
|
|
|
.info-row .value { |
|
|
|
color: #333; |
|
|
|
flex: 1; |
|
|
|
} |
|
|
|
|
|
|
|
.description-text { |
|
|
|
white-space: pre-wrap; |
|
|
|
line-height: 1.5; |
|
|
|
margin: 0; |
|
|
|
} |
|
|
|
|
|
|
|
/* 加载和空状态 */ |
|
|
|
|