|
|
@@ -0,0 +1,540 @@
|
|
|
+<template>
|
|
|
+ <el-container v-loading="loading" class="audit-container">
|
|
|
+ <el-main class="audit-main">
|
|
|
+ <div class="video-card">
|
|
|
+ <div class="video-header">
|
|
|
+ <el-tag size="small" type="info" effect="plain">ID: {{ currentVideo.videoId }}</el-tag>
|
|
|
+ <h1 class="video-title">{{ currentVideo.title }}</h1>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="player-wrapper">
|
|
|
+ <video
|
|
|
+ ref="videoPlayer"
|
|
|
+ :key="currentVideo.videoId"
|
|
|
+ :src="currentVideo.videoUrl"
|
|
|
+ controls
|
|
|
+ autoplay
|
|
|
+ class="main-player"
|
|
|
+ @timeupdate="onTimeUpdate"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="timeline-fast-tracks">
|
|
|
+ <span class="track-label"><i class="el-icon-timer" /> 进度抽审:</span>
|
|
|
+ <el-button-group>
|
|
|
+ <el-button size="mini" @click="jumpTo(0.1)">10%</el-button>
|
|
|
+ <el-button size="mini" @click="jumpTo(0.3)">30%</el-button>
|
|
|
+ <el-button size="mini" @click="jumpTo(0.5)">50%</el-button>
|
|
|
+ <el-button size="mini" @click="jumpTo(0.8)">80%</el-button>
|
|
|
+ </el-button-group>
|
|
|
+ <span class="current-time-tips">当前: {{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="meta-card">
|
|
|
+ <h3 class="section-title">视频简介描述</h3>
|
|
|
+ <p class="description-text">{{ currentVideo.description || '该视频无文字简介' }}</p>
|
|
|
+
|
|
|
+ <div class="meta-grid">
|
|
|
+ <div class="meta-item">
|
|
|
+ <span class="label">画幅方向:</span>
|
|
|
+ <span class="value">{{ currentVideo.horizontal ? '横屏 (16:9)' : '竖屏 (9:16)' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="meta-item">
|
|
|
+ <span class="label">发布时间:</span>
|
|
|
+ <span class="value">{{ currentVideo.pubDate }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="meta-item">
|
|
|
+ <span class="label">可见范围:</span>
|
|
|
+ <el-tag size="mini" type="info">{{ scopeMap[currentVideo.scope] }}</el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-main>
|
|
|
+
|
|
|
+ <el-aside width="360px" class="audit-aside">
|
|
|
+ <div class="aside-card author-card">
|
|
|
+ <h3 class="section-title">创作者信息</h3>
|
|
|
+ <div class="author-info">
|
|
|
+ <el-avatar :size="46" :src="currentVideo.userCard.avatarUrl" icon="el-icon-user-solid" />
|
|
|
+ <div class="author-detail">
|
|
|
+ <div class="author-name">{{ currentVideo.userCard.screenName || '未知用户' }}</div>
|
|
|
+ <div class="author-id">UID: {{ currentVideo.userCard.userIdStr }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="author-badge-group">
|
|
|
+ <el-tag size="mini" type="success">历史通过率: 98%</el-tag>
|
|
|
+ <el-tag size="mini" type="warning">违规记录: 0次</el-tag>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="aside-card action-card">
|
|
|
+ <h3 class="section-title">审核决策面板</h3>
|
|
|
+
|
|
|
+ <el-form ref="auditForm" :model="auditForm" label-position="top" size="small">
|
|
|
+ <el-form-item label="标签" prop="videoTags">
|
|
|
+ <el-checkbox-group v-model="auditForm.videoTags">
|
|
|
+ <el-checkbox
|
|
|
+ v-for="item in tagList"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
+ </el-checkbox-group>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <div class="submit-buttons">
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ icon="el-icon-check"
|
|
|
+ class="btn-approve"
|
|
|
+ :loading="submitting"
|
|
|
+ @click="submitEdit"
|
|
|
+ >
|
|
|
+ 提交编辑
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+ </el-aside>
|
|
|
+ </el-container>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getAuditVideo, submitEditVideo } from '@/api/vod'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'VideoEdit',
|
|
|
+ // 组件激活前:摘除全局 #app 的强制滚动行为
|
|
|
+ beforeRouteEnter(to, from, next) {
|
|
|
+ next(vm => {
|
|
|
+ const appEl = document.getElementById('app')
|
|
|
+ if (appEl) appEl.style.overflowY = 'hidden'
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 组件离开前:还原还原全局 #app 的原貌,不破坏其他页面的行为
|
|
|
+ beforeRouteLeave(to, from, next) {
|
|
|
+ const appEl = document.getElementById('app')
|
|
|
+ if (appEl) appEl.style.overflowY = 'scroll'
|
|
|
+ next()
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loading: false,
|
|
|
+ submitting: false,
|
|
|
+ currentTime: 0,
|
|
|
+ duration: 0,
|
|
|
+ scopeMap: {
|
|
|
+ 1: '本人可见',
|
|
|
+ 2: '所有人可见',
|
|
|
+ 3: 'VIP 可见'
|
|
|
+ },
|
|
|
+ auditResult: {
|
|
|
+ PUBLISHED: 3,
|
|
|
+ REVIEW_REJECTED: 4
|
|
|
+ },
|
|
|
+ tagList: [
|
|
|
+ { label: '涉政违规', value: '涉政违规' },
|
|
|
+ { label: '色情低俗', value: '色情低俗' },
|
|
|
+ { label: '血腥暴力', value: '血腥暴力' },
|
|
|
+ { label: '侵权盗版', value: '侵权盗版' },
|
|
|
+ { label: '垃圾广告', value: '垃圾广告' },
|
|
|
+ { label: '画质低劣', value: '画质低劣' }
|
|
|
+ ],
|
|
|
+ // 当前待审核的视频数据模型
|
|
|
+ currentVideo: {
|
|
|
+ videoId: '',
|
|
|
+ title: '',
|
|
|
+ description: '',
|
|
|
+ videoUrl: '',
|
|
|
+ horizontal: null,
|
|
|
+ pubDate: '',
|
|
|
+ scope: 0,
|
|
|
+ userCard: {
|
|
|
+ userId: '',
|
|
|
+ screenName: '',
|
|
|
+ avatarUrl: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ auditForm: {
|
|
|
+ videoId: null,
|
|
|
+ videoTags: []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ // 🌟 【核心修复】当 URL 中的 videoId 发生变化时,优雅地清理并重新加载
|
|
|
+ '$route.params.id': {
|
|
|
+ handler(newVideoId) {
|
|
|
+ if (newVideoId) {
|
|
|
+ // 1. 先安全地把当前的播放器彻底断流、卸载,腾出硬件解码器空间
|
|
|
+ this.releaseVideoPlayer()
|
|
|
+ // 2. 传入新的 ID,重新请求后端数据
|
|
|
+ this.fetchNextVideo(newVideoId)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ immediate: false // 首次进页面交给 created,后续切换交给 watch
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ document.title = 'VideoEdit'
|
|
|
+ const videoId = this.$route.params.id
|
|
|
+ this.fetchNextVideo(videoId)
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ // 全局监听键盘事件,开启极限盲审模式
|
|
|
+ window.addEventListener('keydown', this.handleKeyboardShortcuts)
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ window.removeEventListener('keydown', this.handleKeyboardShortcuts)
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ releaseVideoPlayer() {
|
|
|
+ const player = this.$refs.videoPlayer
|
|
|
+ if (player) {
|
|
|
+ try {
|
|
|
+ player.pause()
|
|
|
+ player.src = '' // 切断视频网络流
|
|
|
+ player.removeAttribute('src') // 彻底移除资源属性
|
|
|
+ player.load() // 强制浏览器对该 DOM 节点触发垃圾回收
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('释放播放器时发生异常:', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 获取下一条待审核视频
|
|
|
+ // 获取下一条待审核视频
|
|
|
+ fetchNextVideo(videoId) {
|
|
|
+ this.loading = true
|
|
|
+ // 重置表单状态
|
|
|
+ this.auditForm = { videoTags: [] }
|
|
|
+ this.currentTime = 0
|
|
|
+ this.duration = 0
|
|
|
+
|
|
|
+ const queryParams = { videoId }
|
|
|
+ getAuditVideo(queryParams).then(resp => {
|
|
|
+ if (resp.code === 0) {
|
|
|
+ this.currentVideo = resp.data
|
|
|
+ this.auditForm.videoId = this.currentVideo.videoId
|
|
|
+
|
|
|
+ // 确保 DOM 渲染完新地址后,播放器干净利开跑
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const player = this.$refs.videoPlayer
|
|
|
+ if (player) player.load()
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ this.$message.warning(resp.msg)
|
|
|
+ }
|
|
|
+ }).finally(() => {
|
|
|
+ this.loading = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ submitEdit() {
|
|
|
+ if (this.auditForm.videoTags.length === 0) {
|
|
|
+ this.$message.error('请选择视频标签')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.submitting = true
|
|
|
+ submitEditVideo(this.auditForm).then(resp => {
|
|
|
+ if (resp.code === 0) {
|
|
|
+ const nextVideoId = resp.data
|
|
|
+ this.submitting = false
|
|
|
+ this.$notify({
|
|
|
+ title: status === this.auditResult.PUBLISHED ? '审核通过' : '已驳回',
|
|
|
+ message: `视频 [${this.currentVideo.videoId}] 处理完毕,已自动加载下一条视频 [${nextVideoId}]。`,
|
|
|
+ type: status === this.auditResult.PUBLISHED ? 'success' : 'warning',
|
|
|
+ duration: 2500
|
|
|
+ })
|
|
|
+
|
|
|
+ // 🌟 这里正常 push。URL 会发生变化,从而触发上面的 '$route.params.id' 监听器
|
|
|
+ this.$router.push('/vod_audit/' + nextVideoId)
|
|
|
+ } else {
|
|
|
+ this.$message.warning(resp.msg)
|
|
|
+ }
|
|
|
+ }).catch((error) => {
|
|
|
+ this.$message.error(error.message)
|
|
|
+ }).finally(() => {
|
|
|
+ this.submitting = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ // 播放器时间更新
|
|
|
+ onTimeUpdate(e) {
|
|
|
+ this.currentTime = e.target.currentTime
|
|
|
+ this.duration = e.target.duration || 0
|
|
|
+ },
|
|
|
+
|
|
|
+ // 时间轴快捷百分比跳跃
|
|
|
+ jumpTo(percentage) {
|
|
|
+ const player = this.$refs.videoPlayer
|
|
|
+ if (player && this.duration) {
|
|
|
+ player.currentTime = this.duration * percentage
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 工具函数:格式化时间 (00:00)
|
|
|
+ formatTime(seconds) {
|
|
|
+ if (isNaN(seconds)) return '00:00'
|
|
|
+ const min = Math.floor(seconds / 60).toString().padStart(2, '0')
|
|
|
+ const sec = Math.floor(seconds % 60).toString().padStart(2, '0')
|
|
|
+ return `${min}:${sec}`
|
|
|
+ },
|
|
|
+
|
|
|
+ // 键盘快捷键矩阵处理器
|
|
|
+ handleKeyboardShortcuts(e) {
|
|
|
+ // 如果光标正停留在文本输入框内,不触发快捷键,防止冲突
|
|
|
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
|
|
|
+
|
|
|
+ const player = this.$refs.videoPlayer
|
|
|
+
|
|
|
+ switch (e.key) {
|
|
|
+ case ' ': // 空格键:暂停/播放
|
|
|
+ e.preventDefault()
|
|
|
+ if (player) {
|
|
|
+ player.paused ? player.play() : player.pause()
|
|
|
+ }
|
|
|
+ break
|
|
|
+ case 'ArrowRight': // 方向右键:快进5秒
|
|
|
+ if (player) player.currentTime += 5
|
|
|
+ break
|
|
|
+ case 'ArrowLeft': // 方向左键:快退5秒
|
|
|
+ if (player) player.currentTime -= 5
|
|
|
+ break
|
|
|
+ case 'A': // Shift + A: 快捷通过
|
|
|
+ if (e.shiftKey) this.submitAudit(3)
|
|
|
+ break
|
|
|
+ case 'R': // Shift + R: 快捷拒绝
|
|
|
+ if (e.shiftKey) this.submitAudit(4)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* 全局页面沙箱 - 修正为直接吃满 100vh 浏览器视口 */
|
|
|
+.audit-container {
|
|
|
+ height: 100vh;
|
|
|
+ width: 100vw;
|
|
|
+ background-color: #f4f7f9;
|
|
|
+ gap: 20px;
|
|
|
+ padding: 20px; /* 给四周留出高级的视窗边距 */
|
|
|
+ box-sizing: border-box; /* 锁定边距不撑开大盘 */
|
|
|
+ overflow: hidden; /* 严禁外层容器出现滚动条 */
|
|
|
+}
|
|
|
+
|
|
|
+/* ==================== 🎬 左侧内容区 ==================== */
|
|
|
+.audit-main {
|
|
|
+ padding: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+ height: 100%; /* 锁定跟随父级高度 */
|
|
|
+ overflow-y: auto; /* 仅允许内容区独立轴向滚动 */
|
|
|
+}
|
|
|
+
|
|
|
+.video-card {
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
|
|
+}
|
|
|
+
|
|
|
+.video-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.video-title {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1e293b;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 核心播放器容器 */
|
|
|
+.player-wrapper {
|
|
|
+ background-color: #000000;
|
|
|
+ border-radius: 6px;
|
|
|
+ overflow: hidden;
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ aspect-ratio: 16 / 9; /* 保持16:9黄金比例,自适应各屏幕 */
|
|
|
+ max-height: 500px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.main-player {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: contain; /* 防止视频拉伸变形 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 抽审进度控制条 */
|
|
|
+.timeline-fast-tracks {
|
|
|
+ margin-top: 14px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ background: #f8fafc;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-radius: 6px;
|
|
|
+ border: 1px dashed #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.track-label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #64748b;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.current-time-tips {
|
|
|
+ margin-left: auto;
|
|
|
+ font-family: monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #334155;
|
|
|
+ background: #cbd5e1;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频元数据描述卡片 */
|
|
|
+.meta-card {
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #475569;
|
|
|
+ margin-top: 0;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ border-left: 3px solid #1890ff;
|
|
|
+ padding-left: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.description-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #334155;
|
|
|
+ line-height: 1.6;
|
|
|
+ background: #f8fafc;
|
|
|
+ padding: 12px;
|
|
|
+ border-radius: 6px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.meta-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.meta-item {
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.meta-item .label { color: #64748b; }
|
|
|
+.meta-item .value { color: #1e293b; font-weight: 500; }
|
|
|
+
|
|
|
+/* ==================== 🛠️ 右侧风控工作台 ==================== */
|
|
|
+.audit-aside {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+ height: 100%; /* 锁定跟随父级高度 */
|
|
|
+ overflow-y: auto; /* 防止右侧内容过多时溢出屏幕 */
|
|
|
+}
|
|
|
+
|
|
|
+.aside-card {
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
|
|
+}
|
|
|
+
|
|
|
+/* 创作者信息 */
|
|
|
+.author-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.author-name {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #1e293b;
|
|
|
+}
|
|
|
+.author-id {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #94a3b8;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+.author-badge-group {
|
|
|
+ display: flex;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 审核决策控制流 */
|
|
|
+::v-deep .el-checkbox {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ margin-right: 16px;
|
|
|
+ width: 120px; /* 两列规整排列 */
|
|
|
+}
|
|
|
+
|
|
|
+.submit-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-reject, .btn-approve {
|
|
|
+ flex: 1;
|
|
|
+ height: 40px;
|
|
|
+ font-weight: 600;
|
|
|
+ border-radius: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 盲审快捷键指南卡片 */
|
|
|
+.shortcut-card {
|
|
|
+ background: #0f172a; /* 深色极客风,与快捷键功能相呼应 */
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+.shortcut-title {
|
|
|
+ color: #f1f5f9;
|
|
|
+ margin: 0 0 12px 0;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+.shortcut-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.shortcut-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+kbd {
|
|
|
+ background-color: #334155;
|
|
|
+ color: #ffffff;
|
|
|
+ border-radius: 3px;
|
|
|
+ border: 1px solid #475569;
|
|
|
+ padding: 2px 6px;
|
|
|
+ font-family: monospace;
|
|
|
+ font-weight: bold;
|
|
|
+ box-shadow: 0 1px 0 rgba(0,0,0,0.2);
|
|
|
+}
|
|
|
+.text-success { color: #10b981; }
|
|
|
+.text-danger { color: #ef4444; }
|
|
|
+</style>
|