|
|
@@ -0,0 +1,339 @@
|
|
|
+<template>
|
|
|
+ <div class="ai-audit-container">
|
|
|
+ <div v-if="!auditData" class="loading-state">
|
|
|
+ <el-skeleton :rows="10" animated />
|
|
|
+ </div>
|
|
|
+ <template v-else>
|
|
|
+ <el-page-header @back="goBack" content="视频内容审核详情"></el-page-header>
|
|
|
+
|
|
|
+ <el-card class="box-card m-t-20">
|
|
|
+ <div slot="header" class="clearfix">
|
|
|
+ <span class="card-title"><i class="el-icon-video-camera"></i> 视频元信息</span>
|
|
|
+ <el-button
|
|
|
+ style="float: right; padding: 3px 0"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-video-play"
|
|
|
+ @click="playFullVideo">播放完整视频</el-button>
|
|
|
+ </div>
|
|
|
+ <el-descriptions :column="3" border>
|
|
|
+ <el-descriptions-item label="视频路径" span="2">
|
|
|
+ <span class="path-text">{{ auditData.video_path }}</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="方向">
|
|
|
+ <el-tag size="small">{{ auditData.horizontal === 1 ? '横屏' : '竖屏' }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="场景总数">
|
|
|
+ <el-tag type="success" effect="dark" size="small">
|
|
|
+ {{ auditData.scenes ? auditData.scenes.length : 0 }} 个场景
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="时长">
|
|
|
+ {{ formatDuration(auditData.duration) }}
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="分辨率">{{ auditData.width }} x {{ auditData.height }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <el-card class="box-card m-t-20">
|
|
|
+ <div slot="header">
|
|
|
+ <span class="card-title"><i class="el-icon-location-outline"></i> 场景时间轴分布</span>
|
|
|
+ </div>
|
|
|
+ <div class="timeline-container">
|
|
|
+ <div class="timeline-track">
|
|
|
+ <el-tooltip
|
|
|
+ v-for="(frame, index) in auditData.scenes"
|
|
|
+ :key="index"
|
|
|
+ effect="dark"
|
|
|
+ :content="'场景 ' + (index + 1) + ': ' + (frame.frame_pos).toFixed(2) + 's'"
|
|
|
+ placement="top"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="timeline-marker"
|
|
|
+ :style="{ left: (frame.frame_pos / auditData.duration * 100) + '%' }"
|
|
|
+ @click="scrollToFrame(index)"
|
|
|
+ ></div>
|
|
|
+ </el-tooltip>
|
|
|
+ </div>
|
|
|
+ <div class="timeline-labels">
|
|
|
+ <span>0s</span>
|
|
|
+ <span>{{ (auditData.duration).toFixed(2) }}s</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <div class="frames-section m-t-20">
|
|
|
+ <h3 class="section-title">AI 检测结果详情</h3>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-for="(frame, index) in auditData.scenes"
|
|
|
+ :key="index"
|
|
|
+ :id="'frame-card-' + index"
|
|
|
+ class="frame-item"
|
|
|
+ >
|
|
|
+ <el-card shadow="hover" :class="{ 'active-highlight': activeFrameIndex === index }">
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :xs="24" :sm="10" :md="8" :lg="6">
|
|
|
+ <div class="frame-preview-wrapper">
|
|
|
+ <div class="frame-preview">
|
|
|
+ <el-image
|
|
|
+ :src="frame.frame_path"
|
|
|
+ fit="contain"
|
|
|
+ :preview-src-list="[frame.frame_path]"
|
|
|
+ class="preview-img"
|
|
|
+ >
|
|
|
+ <div slot="error" class="image-slot">
|
|
|
+ <i class="el-icon-picture-outline"></i>
|
|
|
+ </div>
|
|
|
+ </el-image>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="scene-progress-bar">
|
|
|
+ <div class="progress-base-labels">
|
|
|
+ <span class="label-text">0s</span>
|
|
|
+ <span class="label-text">{{ (auditData.duration).toFixed(0) }}s</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="progress-track">
|
|
|
+ <div
|
|
|
+ class="progress-dot"
|
|
|
+ :style="{ left: (frame.frame_pos / auditData.duration * 100) + '%' }"
|
|
|
+ ></div>
|
|
|
+ <div
|
|
|
+ class="current-time-label"
|
|
|
+ :style="{ left: (frame.frame_pos / auditData.duration * 100) + '%' }"
|
|
|
+ >
|
|
|
+ {{ (frame.frame_pos).toFixed(2) }}s
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="frame-controls">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ style="width: 100%"
|
|
|
+ icon="el-icon-video-play"
|
|
|
+ @click="playScene(frame.frame_pos)">播放此场景</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :xs="24" :sm="14" :md="16" :lg="18">
|
|
|
+ <el-table :data="frame.prompts" border size="small" stripe>
|
|
|
+ <el-table-column label="Prompt" prop="prompt" width="180" show-overflow-tooltip></el-table-column>
|
|
|
+ <el-table-column label="识别结果 (中文)" prop="result_zh"></el-table-column>
|
|
|
+ <el-table-column label="Result (English)" prop="result">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <div>{{ scope.row.result }}</div>
|
|
|
+ <div v-if="scope.row.chs_translation" class="translation-res">
|
|
|
+ <el-divider content-position="left">中文翻译</el-divider>
|
|
|
+ {{ scope.row.chs_translation }}
|
|
|
+ </div>
|
|
|
+ <el-button
|
|
|
+ v-else
|
|
|
+ type="text"
|
|
|
+ size="mini"
|
|
|
+ icon="el-icon-refresh"
|
|
|
+ @click="handleTranslate(scope.row)"
|
|
|
+ >点击翻译</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-dialog :title="dialogTitle" :visible.sync="videoVisible" width="900px" @closed="handleDialogClosed" destroy-on-close>
|
|
|
+ <div class="video-player-container">
|
|
|
+ <video ref="auditVideo" controls autoplay width="100%" :src="videoUrl" class="main-video"></video>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { translate } from "@/api/ai";
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'VideoAudit',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ videoVisible: false,
|
|
|
+ dialogTitle: '',
|
|
|
+ videoUrl: '',
|
|
|
+ activeFrameIndex: null,
|
|
|
+ auditData: null
|
|
|
+ };
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.fetchGpuData()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async fetchData() {
|
|
|
+ try {
|
|
|
+ // 假设这是你的获取数据逻辑
|
|
|
+ // const res = await getAuditDetail(this.id);
|
|
|
+ // this.auditData = res.data;
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error("获取详情失败");
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async handleTranslate(row) {
|
|
|
+ try {
|
|
|
+ // 开启加载状态(可选)
|
|
|
+ this.$set(row, 'result_zh', '正在翻译...');
|
|
|
+ const payload = {}
|
|
|
+ payload.text = row.result
|
|
|
+ payload.target = 'zh'
|
|
|
+ const resp = await translate(payload)
|
|
|
+ this.$set(row, 'result_zh', resp.translation);
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error('翻译失败: ' + error);
|
|
|
+ this.$set(row, 'result_zh', '');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ formatDuration(seconds) {
|
|
|
+ if (seconds === undefined || seconds === null) return '00:00';
|
|
|
+ const m = Math.floor(seconds / 60);
|
|
|
+ const s = Math.floor(seconds % 60);
|
|
|
+ return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
|
+ },
|
|
|
+ scrollToFrame(index) {
|
|
|
+ this.activeFrameIndex = index;
|
|
|
+ const el = document.getElementById(`frame-card-${index}`);
|
|
|
+ if (el) {
|
|
|
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
+ setTimeout(() => { if (this.activeFrameIndex === index) this.activeFrameIndex = null; }, 2000);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ playFullVideo() {
|
|
|
+ if (!this.auditData) return;
|
|
|
+ this.dialogTitle = '播放完整视频';
|
|
|
+ this.videoUrl = this.auditData.video_path;
|
|
|
+ this.videoVisible = true;
|
|
|
+ },
|
|
|
+ playScene(pos) {
|
|
|
+ this.dialogTitle = `场景回放 (${pos.toFixed(2)}s)`;
|
|
|
+ this.videoUrl = this.auditData.video_path;
|
|
|
+ this.videoVisible = true;
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const v = this.$refs.auditVideo;
|
|
|
+ if (v) { v.currentTime = pos; v.play(); }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ handleDialogClosed() { if (this.$refs.auditVideo) this.$refs.auditVideo.pause(); this.videoUrl = ''; },
|
|
|
+ goBack() { this.$router.back(); }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.ai-audit-container { padding: 20px; background: #f0f2f5; min-height: 100vh; }
|
|
|
+.m-t-20 { margin-top: 20px; }
|
|
|
+.section-title { border-left: 4px solid #409EFF; padding-left: 15px; margin: 30px 0 20px; margin-bottom: 30px; font-size: 18px; }
|
|
|
+.frame-item {
|
|
|
+ margin-bottom: 40px; /* 核心修改点 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 关键修复:外层容器使用 Flex 纵向排列 */
|
|
|
+.frame-preview-wrapper {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: #fff;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图片区域 */
|
|
|
+.frame-preview {
|
|
|
+ width: 100%;
|
|
|
+ height: 180px;
|
|
|
+ background: #000;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ flex-shrink: 0; /* 防止被压缩 */
|
|
|
+}
|
|
|
+.preview-img {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: block; /* 消除行内元素间隙 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 进度条区域:确保不被覆盖 */
|
|
|
+.scene-progress-bar {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ padding: 12px 20px 35px 20px;
|
|
|
+ background: #fff;
|
|
|
+ box-sizing: border-box;
|
|
|
+ z-index: 10; /* 确保在图片之上 */
|
|
|
+}
|
|
|
+
|
|
|
+.progress-base-labels {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ line-height: 1; /* 强制行高,防止偏移 */
|
|
|
+}
|
|
|
+.label-text {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #909399;
|
|
|
+ font-family: monospace;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-track {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 6px;
|
|
|
+ background-color: #f0f2f5;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-dot {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ background-color: #409EFF;
|
|
|
+ border: 2px solid #fff;
|
|
|
+ border-radius: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ z-index: 5;
|
|
|
+}
|
|
|
+
|
|
|
+.current-time-label {
|
|
|
+ position: absolute;
|
|
|
+ top: 14px;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #409EFF;
|
|
|
+ white-space: nowrap;
|
|
|
+ background: rgba(64, 158, 255, 0.1);
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.frame-controls {
|
|
|
+ padding: 5px 15px 15px 15px;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 其他辅助样式 */
|
|
|
+.timeline-container { padding: 35px 15px 15px; }
|
|
|
+.timeline-track { position: relative; width: 100%; height: 10px; background-color: #e4e7ed; border-radius: 5px; }
|
|
|
+.timeline-marker { position: absolute; top: 50%; width: 14px; height: 14px; background-color: #409EFF; border: 2px solid #fff; border-radius: 50%; transform: translate(-50%, -50%); cursor: pointer; }
|
|
|
+.active-highlight { border: 2px solid #409EFF !important; transform: scale(1.01); }
|
|
|
+.video-player-container { background: #000; line-height: 0; }
|
|
|
+
|
|
|
+.loading-state {
|
|
|
+ padding: 40px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+</style>
|