|
|
<!-- src/views/vet/training/TrainingHome.vue --><template> <div class="training-home"> <!-- 顶部导航 --> <div class="training-header"> <h2><i class="el-icon-video-camera"></i> 兽医培训中心</h2> <div class="header-actions"> <el-button type="primary" icon="el-icon-upload" @click="activeTab = 'upload'" > 上传视频 </el-button> <el-button plain @click="activeTab = 'list'" > 返回列表 </el-button> </div> </div>
<!-- 主内容区 --> <div class="training-main"> <!-- 左侧菜单 --> <div class="sidebar"> <el-menu :default-active="activeTab" class="training-menu" @select="handleMenuSelect" > <el-menu-item index="list"> <i class="el-icon-video-camera"></i> <span slot="title">视频列表</span> </el-menu-item>
<el-menu-item index="upload"> <i class="el-icon-upload"></i> <span slot="title">上传视频</span> </el-menu-item>
<el-menu-item index="my"> <i class="el-icon-user"></i> <span slot="title">我的视频</span> </el-menu-item> </el-menu> </div>
<!-- 右侧内容区 --> <div class="content-area"> <!-- ================= 视频列表页 ================= --> <div v-if="activeTab === 'list'" class="tab-content"> <!-- 搜索栏 --> <div class="search-bar"> <el-input v-model="searchKeyword" placeholder="搜索视频标题" clearable @keyup.enter.native="loadPublicVideos" @clear="loadPublicVideos" style="width: 300px" > <i slot="prefix" class="el-icon-search"></i> </el-input>
<el-select v-model="filterCategory" placeholder="全部分类" clearable @change="loadPublicVideos" style="width: 120px" > <el-option label="手术技巧" value="surgery" /> <el-option label="疾病诊断" value="diagnosis" /> <el-option label="药物使用" value="medication" /> <el-option label="其他" value="other" /> </el-select>
<el-button type="primary" icon="el-icon-refresh" @click="loadPublicVideos" > 刷新 </el-button> </div>
<!-- 视频列表 --> <h3 style="margin: 20px 0 10px 0; color: #333;"> 培训视频 <span v-if="videos.length > 0" style="font-size: 14px; color: #999; margin-left: 10px;"> (共 {{ videos.length }} 个) </span> </h3>
<div v-if="loading" class="loading"> <el-skeleton :rows="6" animated /> </div>
<div v-else-if="videos.length === 0" class="empty"> <el-empty description="暂无视频"></el-empty> </div>
<div v-else class="video-grid"> <div v-for="video in videos" :key="video.id" class="video-card" @click="showVideoDetail(video)" > <div class="cover"> <!-- 封面图片 --> <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"> <div class="title">{{ video.title }}</div> <div class="meta"> <span> <i class="el-icon-user"></i> {{ video.userName || '未知' }} </span> <span> <i class="el-icon-view"></i> {{ video.viewCount || 0 }} </span> </div> <div class="time">{{ formatTime(video.createTime) }}</div> </div> </div> </div> </div>
<!-- ================= 上传视频页 ================= --> <div v-else-if="activeTab === 'upload'" class="tab-content"> <h3 style="margin-bottom: 20px; color: #333;">上传培训视频</h3>
<div class="upload-area" @click="handleUploadAreaClick"> <input ref="fileInput" type="file" accept="video/*" style="display: none" @change="handleFileSelect" />
<div v-if="!selectedFile" class="upload-placeholder"> <i class="el-icon-upload upload-icon"></i> <p>点击选择视频文件</p> <p class="hint">支持 mp4、avi、mov 等格式,最大 1GB</p> </div>
<div v-else class="file-info"> <i class="el-icon-video-camera file-icon"></i> <div class="file-details"> <div class="file-name">{{ selectedFile.name }}</div> <div class="file-size">{{ formatFileSize(selectedFile.size) }}</div> </div> <el-button type="text" @click.stop="removeFile"> <i class="el-icon-close"></i> </el-button> </div> </div>
<!-- 视频信息表单 --> <div class="upload-form"> <el-form :model="uploadForm" label-width="80px" style="margin-top: 20px;"> <el-form-item label="视频标题" required> <el-input v-model="uploadForm.title" placeholder="请输入视频标题" maxlength="50" show-word-limit /> </el-form-item>
<el-form-item label="分类"> <el-select v-model="uploadForm.category" placeholder="请选择分类" style="width: 100%"> <el-option label="手术技巧" value="surgery" /> <el-option label="疾病诊断" value="diagnosis" /> <el-option label="药物使用" value="medication" /> <el-option label="其他" value="other" /> </el-select> </el-form-item>
<el-form-item label="描述"> <el-input v-model="uploadForm.description" type="textarea" :rows="3" placeholder="请输入视频描述" maxlength="200" show-word-limit /> </el-form-item>
<el-form-item label="视频状态"> <el-radio-group v-model="uploadForm.status"> <el-radio label="1">公开(所有人可见)</el-radio> <el-radio label="0">私有(仅自己可见)</el-radio> </el-radio-group> </el-form-item>
<el-form-item> <el-button type="primary" :loading="uploading" :disabled="!selectedFile || !uploadForm.title" @click="handleUpload" > {{ uploading ? '上传中...' : '开始上传' }} </el-button> <el-button @click="resetUploadForm">重置</el-button> </el-form-item> </el-form> </div> </div>
<!-- ================= 我的视频页 ================= --> <div v-else-if="activeTab === 'my'" class="tab-content"> <div class="my-header"> <h3 style="margin: 0; color: #333;">我的视频</h3> <el-input v-model="mySearchKeyword" placeholder="搜索我的视频" clearable @keyup.enter.native="loadMyVideos" @clear="loadMyVideos" style="width: 200px" > <i slot="prefix" class="el-icon-search"></i> </el-input> </div>
<div v-if="myVideosLoading" class="loading"> <el-skeleton :rows="3" animated /> </div>
<div v-else-if="myVideos.length === 0" class="empty"> <el-empty description="你还没有上传过视频"> <el-button type="primary" @click="activeTab = 'upload'"> 去上传 </el-button> </el-empty> </div>
<div v-else class="my-video-list"> <div class="my-video-item" v-for="video in myVideos" :key="video.id"> <div class="video-info" @click="showVideoDetail(video)"> <div class="left"> <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> <div class="meta"> <el-tag :type="video.status === '1' ? 'success' : 'warning'" size="small"> {{ video.status === '1' ? '公开' : '私有' }} </el-tag> <span class="time">{{ formatTime(video.createTime) }}</span> <span class="views">{{ video.viewCount || 0 }}次观看</span> </div> </div> </div> <div class="right"> <el-button size="small" type="danger" @click.stop="deleteMyVideo(video.id)" > 删除 </el-button> </div> </div> </div> </div> </div> </div> </div>
<!-- ================= 视频详情弹窗 ================= --> <el-dialog :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"> <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.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> <span class="value">{{ formatTime(currentVideo.createTime) }}</span> </div> <div class="info-row"> <span class="label">观看次数:</span> <span class="value">{{ currentVideo.viewCount || 0 }}</span> </div> <div v-if="currentVideo.description" class="info-row"> <span class="label">视频描述:</span> <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> </el-dialog> </div></template>
<script>import trainingApi from '@/api/vet/training'
export default { name: 'TrainingHome', data() { return { // 当前激活的标签页
activeTab: 'list',
// ========== 视频列表相关 ==========
searchKeyword: '', filterCategory: '', videos: [], loading: false,
// ========== 上传视频相关 ==========
fileInput: null, selectedFile: null, uploading: false, uploadForm: { title: '', category: '', description: '', status: '1' },
// ========== 我的视频相关 ==========
mySearchKeyword: '', myVideos: [], myVideosLoading: false,
// ========== 视频详情相关 ==========
showDetailDialog: false, currentVideo: null, videoPlayerReady: false, videoLoadFailed: false,
// ========== 封面相关 ==========
coverLoadingStates: new Map(), coverErrorStates: new Map(), defaultCoverCache: new Map(), } },
mounted() { this.loadPublicVideos() },
methods: { // 菜单选择
handleMenuSelect(index) { this.activeTab = index if (index === 'list') { this.loadPublicVideos() } else if (index === 'my') { this.loadMyVideos() } },
// 上传区域点击事件
handleUploadAreaClick() { if (this.$refs.fileInput) { this.$refs.fileInput.click() } },
// 加载公开视频
async loadPublicVideos() { try { this.loading = true console.log('🔄 加载公开视频...')
const params = { title: this.searchKeyword, category: this.filterCategory, pageNum: 1, pageSize: 100 }
const res = await trainingApi.getPublicVideos(params) 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) this.$message.error('加载失败') } finally { this.loading = false } },
// 加载我的视频
async loadMyVideos() { try { this.myVideosLoading = true console.log('🔄 加载我的视频...')
const params = { title: this.mySearchKeyword, pageNum: 1, pageSize: 100 }
const res = await trainingApi.getMyVideos(params) 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) this.$message.error('加载失败') } finally { this.myVideosLoading = false } },
// 文件选择
handleFileSelect(event) { const input = event.target if (!input) return
const file = input.files[0] if (!file) return
// 检查文件大小(1GB)
if (file.size > 1024 * 1024 * 1024) { this.$message.error('文件大小不能超过 1GB') return }
// 检查文件类型
if (!file.type.startsWith('video/')) { this.$message.error('请选择视频文件') return }
this.selectedFile = file },
// 移除文件
removeFile() { this.selectedFile = null if (this.$refs.fileInput) { this.$refs.fileInput.value = '' } },
// 上传视频
async handleUpload() { if (!this.selectedFile) { this.$message.error('请选择视频文件') return }
if (!this.uploadForm.title.trim()) { this.$message.error('请输入视频标题') return }
try { this.uploading = true
const formData = new FormData() formData.append('title', this.uploadForm.title) formData.append('videoFile', this.selectedFile) formData.append('status', this.uploadForm.status)
if (this.uploadForm.category) { formData.append('category', this.uploadForm.category) }
if (this.uploadForm.description) { formData.append('description', this.uploadForm.description) }
await trainingApi.uploadVideo(formData)
this.$message.success('上传成功!') this.resetUploadForm() this.activeTab = 'my' this.loadMyVideos()
} catch (error) { console.error('上传失败:', error) this.$message.error(error.message || '上传失败,请重试') } finally { this.uploading = false } },
// 重置上传表单
resetUploadForm() { this.selectedFile = null this.uploadForm.title = '' this.uploadForm.category = '' this.uploadForm.description = '' this.uploadForm.status = '1'
if (this.$refs.fileInput) { this.$refs.fileInput.value = '' } },
// ========== 视频详情相关方法 ==========
// 显示视频详情
showVideoDetail(video) { 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') } },
// 删除我的视频
async deleteMyVideo(videoId) { try { await this.$confirm('确定要删除这个视频吗?删除后无法恢复。', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
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('删除失败: ' + (error.message || '未知错误')) } } },
// ========== 封面相关方法 ==========
// 获取封面图片
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) },
// ========== 工具函数 ==========
formatDuration(seconds) { if (!seconds) return '00:00' const min = Math.floor(seconds / 60) const sec = seconds % 60 return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}` },
formatTime(dateStr) { if (!dateStr) return '' 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) { if (bytes === 0) return '0 B' const k = 1024 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>
<style scoped>.training-home { height: calc(100vh - 50px); display: flex; flex-direction: column;}
.training-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; background: white; border-bottom: 1px solid #e8e8e8; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);}
.training-header h2 { margin: 0; color: #1890ff; font-size: 18px; display: flex; align-items: center; gap: 8px;}
.training-main { flex: 1; display: flex; background: #f0f2f5; overflow: hidden;}
.sidebar { width: 180px; background: white; border-right: 1px solid #e8e8e8;}
.training-menu { border-right: none;}
.training-menu .el-menu-item { height: 50px; line-height: 50px; font-size: 14px;}
.content-area { flex: 1; padding: 20px; overflow-y: auto;}
.tab-content { background: white; border-radius: 8px; padding: 20px; min-height: 500px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);}
/* 搜索栏样式 */.search-bar { display: flex; gap: 12px; margin-bottom: 20px; align-items: center;}
.my-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;}
/* 视频列表样式 */.video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px;}
.video-card { border: 1px solid #e8e8e8; border-radius: 8px; overflow: hidden; cursor: pointer; transition: all 0.3s; background: white;}
.video-card:hover { transform: translateY(-4px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);}
.video-card .cover { position: relative; height: 140px; background: #f5f5f5; overflow: hidden;}
.video-card .cover img { width: 100%; height: 100%; object-fit: cover; display: block; transition: opacity 0.3s;}
/* 默认封面样式 */.default-cover { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;}
.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 { position: absolute; bottom: 8px; right: 8px; background: rgba(0, 0, 0, 0.7); color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; z-index: 2;}
.video-card .info { padding: 12px;}
.video-card .title { margin: 0 0 8px 0; font-size: 14px; font-weight: 500; line-height: 1.4; color: #333; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;}
.video-card .meta { display: flex; justify-content: space-between; font-size: 12px; color: #8c8c8c; margin-bottom: 4px;}
.video-card .meta span { display: flex; align-items: center; gap: 4px;}
.video-card .time { font-size: 12px; color: #bfbfbf;}
/* 上传区域样式 */.upload-area { border: 2px dashed #dcdfe6; border-radius: 8px; padding: 40px 20px; text-align: center; cursor: pointer; transition: border-color 0.3s; background: #fafafa; margin-bottom: 20px;}
.upload-area:hover { border-color: #409eff;}
.upload-placeholder { color: #909399;}
.upload-icon { font-size: 48px; margin-bottom: 16px; color: #409eff;}
.hint { font-size: 12px; color: #c0c4cc; margin-top: 8px;}
.file-info { display: flex; align-items: center; justify-content: center; gap: 16px;}
.file-icon { font-size: 36px; color: #409eff;}
.file-details { flex: 1; text-align: left;}
.file-name { font-weight: 500; margin-bottom: 4px; word-break: break-all;}
.file-size { font-size: 12px; color: #909399;}
.upload-form { max-width: 600px;}
/* 我的视频列表样式 */.my-video-list { margin-top: 20px;}
.my-video-item { border: 1px solid #e8e8e8; border-radius: 8px; margin-bottom: 12px; background: white; transition: border-color 0.3s;}
.my-video-item:hover { border-color: #1890ff;}
.video-info { display: flex; justify-content: space-between; align-items: center; padding: 12px; cursor: pointer;}
.video-info .left { display: flex; align-items: center; gap: 12px; flex: 1;}
.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: 100%; height: 100%; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: white;}
.details { flex: 1;}
.details .title { 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 { display: flex; align-items: center; gap: 12px; font-size: 12px; color: #8c8c8c;}
/* 视频详情弹窗样式 */.video-detail-dialog { padding: 10px 0;}
/* 视频加载状态 */.video-loading { padding: 60px; text-align: center; color: #666; background: #f5f5f5; border-radius: 6px; min-height: 200px; display: flex; flex-direction: column; align-items: center; justify-content: center;}
.video-loading .el-icon-loading { font-size: 36px; margin-bottom: 16px; color: #409eff;}
/* 视频错误状态 */.video-error { padding: 40px; text-align: center; color: #f56c6c; background: #fff5f5; border-radius: 6px; min-height: 200px; display: flex; flex-direction: column; align-items: center; justify-content: center;}
.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; border-radius: 6px;}
.info-row { 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;}
/* 加载和空状态 */.loading, .empty { padding: 60px 0; text-align: center; color: #8c8c8c;}
/* 响应式设计 */@media (max-width: 768px) { .training-main { flex-direction: column; }
.sidebar { width: 100%; border-right: none; border-bottom: 1px solid #e8e8e8; }
.video-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
.search-bar { flex-direction: column; align-items: stretch; }
.search-bar .el-input { width: 100% !important; }
.my-header { flex-direction: column; align-items: stretch; gap: 12px; }}</style>
|