You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1414 lines
35 KiB
1414 lines
35 KiB
<!-- 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>
|