Переглянути джерело

添加 src/views/ai 模块, 作为 pyai 项目的前端

reghao 4 днів тому
батько
коміт
c0c4cb8ee5

+ 10 - 0
src/api/ai.js

@@ -0,0 +1,10 @@
+import { get, post, delete0 } from '@/utils/request'
+
+const aiApi = {
+  translateApi: '/api1/text/translate'
+}
+
+// *********************************************************************************************************************
+export function translate(payload) {
+  return post(aiApi.translateApi, payload)
+}

+ 49 - 0
src/router/ai.js

@@ -0,0 +1,49 @@
+const AuditIndex = () => import('views/ai/AuditIndex')
+const GpuDashboard = () => import('views/ai/GpuDashboard')
+const AudioAudit = () => import('views/ai/AudioAudit')
+const ImageAudit = () => import('views/ai/ImageAudit')
+const VideoAudit = () => import('views/ai/VideoAudit')
+const TextAudit = () => import('views/ai/TextAudit')
+
+export default {
+  path: '/ai',
+  name: 'AuditIndex',
+  component: AuditIndex,
+  meta: { needAuth: true },
+  children: [
+    {
+      path: '',
+      name: 'ai',
+      component: GpuDashboard,
+      meta: { title: 'GPU 信息', needAuth: true}
+    },
+    {
+      path: '/ai/audio',
+      hidden: true,
+      name: 'AudioAudit',
+      component: AudioAudit,
+      meta: { title: '音频识别', needAuth: true}
+    },
+    {
+      path: '/ai/image',
+      hidden: true,
+      name: 'ImageAudit',
+      component: ImageAudit,
+      meta: { title: '图像理解', needAuth: true}
+    },
+    {
+      path: '/ai/video',
+      hidden: true,
+      name: 'VideoAudit',
+      component: VideoAudit,
+      meta: { title: '视频审核', needAuth: true}
+    },
+    {
+      path: '/ai/text',
+      hidden: true,
+      name: 'TextAudit',
+      component: TextAudit,
+      meta: { title: '文本分析', needAuth: true}
+    }
+  ]
+}

+ 2 - 0
src/router/index.js

@@ -5,6 +5,7 @@ import DiskRouter from './disk'
 import UserRouter from './user'
 import BlogRouter from './blog'
 import MapRouter from './map'
+import AiRouter from './ai'
 import BackgroundDevopsRouter from './background_devops'
 import BackgroundAccountRouter from './background_account'
 import BackgroundMyRouter from './background_my'
@@ -47,6 +48,7 @@ export const constantRoutes = [
   UserRouter,
   BlogRouter,
   MapRouter,
+  AiRouter,
   {
     path: '/',
     name: 'Vod',

+ 434 - 0
src/views/ai/AudioAudit.vue

@@ -0,0 +1,434 @@
+<template>
+  <div class="audio-asr-wrapper">
+    <div class="content-header">
+      <div class="title-info">
+        <h2 class="main-title">音频 ASR 识别</h2>
+        <p class="sub-title">支持多种格式音频上传,利用 GPU 加速提取语音文本</p>
+      </div>
+    </div>
+
+    <el-row :gutter="20">
+      <el-col :xs="24" :sm="24" :md="8" :lg="7">
+        <el-card shadow="never" class="config-card">
+          <div slot="header" class="card-header">
+            <span><i class="el-icon-setting"></i> 任务配置</span>
+          </div>
+
+          <el-form label-position="top">
+            <el-form-item label="音频上传">
+              <el-upload
+                class="audio-uploader-v2"
+                drag
+                action=""
+                :auto-upload="false"
+                :show-file-list="true"
+                :limit="1"
+                accept="audio/*"
+                :on-change="handleFileChange"
+              >
+                <i class="el-icon-upload"></i>
+                <div class="el-upload__text">拖拽音频到此处 或 <em>点击上传</em></div>
+                <div class="el-upload__tip" slot="tip">建议格式:mp3, wav (不超过 50MB)</div>
+              </el-upload>
+            </el-form-item>
+
+            <el-button
+              type="primary"
+              class="submit-btn"
+              :loading="submitting"
+              :disabled="!file"
+              @click="submitAsrTask"
+            >
+              {{ submitting ? '正在上传...' : '提交识别任务' }}
+            </el-button>
+          </el-form>
+
+          <transition name="el-zoom-in-top">
+            <div v-if="taskId" class="task-monitor-box">
+              <div class="monitor-header">
+                <span class="status-dot" :class="status"></span>
+                <span class="status-label">{{ statusText }}</span>
+              </div>
+              <div class="task-details">
+                <p>任务 ID: <code>{{ taskId }}</code></p>
+                <el-progress
+                  v-if="status !== 'completed' && status !== 'error'"
+                  :percentage="status === 'processing' ? 70 : 30"
+                  :status="status === 'error' ? 'exception' : ''"
+                  :stroke-width="8"
+                  striped
+                  striped-animated
+                ></el-progress>
+              </div>
+            </div>
+          </transition>
+        </el-card>
+      </el-col>
+
+      <el-col :xs="24" :sm="24" :md="16" :lg="17">
+        <el-card shadow="never" v-if="asrResult" class="result-card">
+          <div slot="header" class="card-header-flex">
+            <div class="header-left">
+              <span class="result-title">识别结果</span>
+              <el-tag size="mini" effect="plain" class="duration-tag">时长: {{ asrResult.duration.toFixed(1) }}s</el-tag>
+              <el-button
+                type="text"
+                size="small"
+                icon="el-icon-document"
+                @click="showFullText = true"
+              >全文预览</el-button>
+            </div>
+            <div class="custom-model-tag">
+              <i class="el-icon-cpu"></i> GPU 识别加速
+            </div>
+          </div>
+
+          <div class="audio-control-section">
+            <audio ref="audioPlayer" controls :src="asrResult.audio_url" class="modern-audio-player"></audio>
+          </div>
+
+          <div class="srt-list-wrapper">
+            <div class="srt-list-header">
+              <span class="h-time">起始时间</span>
+              <span class="h-text">识别内容</span>
+            </div>
+            <div class="srt-scroll-area">
+              <div
+                v-for="(item, index) in asrResult.srt"
+                :key="index"
+                class="srt-entry"
+                @click="seekAudio(item.time)"
+              >
+                <div class="entry-time">
+                  <span class="time-badge">{{ formatTimeLabel(item.time) }}</span>
+                </div>
+                <div class="entry-content">
+                  <p class="text">{{ item.text }}</p>
+                  <el-tooltip content="复制此行" placement="top">
+                    <i class="el-icon-copy-document copy-icon" @click.stop="handleCopy(item.text)"></i>
+                  </el-tooltip>
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-card>
+
+        <div v-else class="empty-placeholder">
+          <el-empty :description="emptyDescription">
+            <template #image>
+              <i class="el-icon-service" style="font-size: 60px; color: #dcdfe6;"></i>
+            </template>
+          </el-empty>
+        </div>
+      </el-col>
+    </el-row>
+
+    <el-dialog
+      title="识别全文预览"
+      :visible.sync="showFullText"
+      width="50%"
+      custom-class="full-text-dialog"
+      append-to-body
+    >
+      <div class="full-text-body">
+        {{ asrResult ? asrResult.text : '' }}
+      </div>
+      <div slot="footer">
+        <el-button @click="showFullText = false">关闭</el-button>
+        <el-button type="primary" icon="el-icon-document-copy" @click="handleCopy(asrResult.text)">复制全文</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+
+export default {
+  name: 'AudioAsrDetail',
+  data() {
+    return {
+      submitting: false,
+      file: null,
+      taskId: '',
+      status: '',
+      asrResult: null,
+      timer: null,
+      showFullText: false
+    };
+  },
+  computed: {
+    statusText() {
+      const maps = { 'queued': '队列中', 'processing': '正在提取语音...', 'completed': '识别成功', 'error': '识别失败' };
+      return maps[this.status] || '等待操作';
+    },
+    emptyDescription() {
+      if (this.status === 'processing' || this.status === 'queued') return '正在努力识别中,请稍后...';
+      return '暂无数据,请在左侧上传音频文件';
+    }
+  },
+  beforeDestroy() {
+    this.stopPolling();
+  },
+  methods: {
+    handleFileChange(file) {
+      this.file = file.raw;
+      this.taskId = '';
+      this.status = '';
+      this.asrResult = null;
+      this.stopPolling();
+    },
+    async submitAsrTask() {
+      this.submitting = true;
+      try {
+        const formData = new FormData();
+        formData.append('file', this.file);
+        const res = await axios.post('/api1/audio/asr', formData, {
+          headers: { 'Content-Type': 'multipart/form-data' }
+        });
+        this.taskId = res.data.task_id;
+        this.status = res.data.status;
+        this.$message.success('任务已加入 GPU 队列');
+        this.startPolling();
+      } catch (error) {
+        this.$message.error('提交失败');
+      } finally {
+        this.submitting = false;
+      }
+    },
+    startPolling() {
+      this.stopPolling();
+      this.timer = setInterval(this.fetchAsrResult, 3000);
+    },
+    stopPolling() {
+      if (this.timer) clearInterval(this.timer);
+    },
+    async fetchAsrResult() {
+      try {
+        const res = await axios.get('/api1/audio/result/' + this.taskId);
+        if (res.data && (res.data.srt || res.data.text)) {
+          this.asrResult = res.data;
+          this.status = 'completed';
+          this.stopPolling();
+        }
+      } catch (e) { /* 继续轮询 */ }
+    },
+    // 提取时间戳并跳转播放
+    seekAudio(timeStr) {
+      const player = this.$refs.audioPlayer;
+      if (!player || !timeStr) return;
+      // 假设格式为 "00:00:01,500 --> ..."
+      const startPart = timeStr.split('-->')[0].trim().replace(',', '.');
+      const parts = startPart.split(':');
+      const seconds = (+parts[0]) * 3600 + (+parts[1]) * 60 + (+parts[2]);
+      player.currentTime = seconds;
+      player.play();
+    },
+    formatTimeLabel(timeStr) {
+      return timeStr.split('-->')[0].trim().split(',')[0]; // 简化显示为 HH:mm:ss
+    },
+    async handleCopy(text) {
+      try {
+        await navigator.clipboard.writeText(text);
+        this.$message.success('已复制到剪贴板');
+      } catch (e) {
+        const input = document.createElement('textarea');
+        input.value = text;
+        document.body.appendChild(input);
+        input.select();
+        document.execCommand('copy');
+        document.body.removeChild(input);
+        this.$message.success('已复制到剪贴板');
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+.audio-asr-wrapper {
+  padding: 0 4px;
+}
+
+/* 标题区 */
+.content-header {
+  margin-bottom: 24px;
+  padding-bottom: 16px;
+  border-bottom: 1px solid #ebeef5;
+}
+.main-title {
+  margin: 0;
+  font-size: 20px;
+  color: #303133;
+}
+.sub-title {
+  margin: 8px 0 0;
+  font-size: 13px;
+  color: #909399;
+}
+
+/* 卡片通用 */
+.config-card, .result-card {
+  border-radius: 8px;
+  border: 1px solid #ebeef5;
+}
+
+/* 上传区优化 */
+.audio-uploader-v2 {
+  width: 100%;
+}
+::v-deep .el-upload-dragger {
+  width: 100%;
+  height: 160px;
+}
+::v-deep .el-upload-dragger .el-icon-upload {
+  margin: 20px 0 10px;
+}
+
+.submit-btn {
+  width: 100%;
+  padding: 12px;
+  font-weight: bold;
+}
+
+/* 状态监控 */
+.task-monitor-box {
+  margin-top: 20px;
+  padding: 16px;
+  background: #f8f9fb;
+  border-radius: 6px;
+}
+.monitor-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+}
+.status-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-right: 8px;
+  background: #909399;
+}
+.status-dot.processing { background: #409EFF; box-shadow: 0 0 5px #409EFF; }
+.status-dot.completed { background: #67C23A; }
+.status-label { font-size: 14px; font-weight: 500; }
+.task-details { font-size: 12px; color: #606266; }
+.task-details code { background: #eee; padding: 2px 4px; border-radius: 3px; }
+
+/* 结果区 Header */
+.card-header-flex {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.result-title {
+  font-weight: bold;
+  font-size: 16px;
+}
+.duration-tag {
+  margin-left: 12px;
+}
+
+/* 播放器 */
+.audio-control-section {
+  background: #f0f2f5;
+  padding: 12px;
+  border-radius: 4px;
+  margin-bottom: 20px;
+}
+.modern-audio-player {
+  width: 100%;
+}
+
+/* SRT 列表 */
+.srt-list-wrapper {
+  border: 1px solid #f0f0f0;
+  border-radius: 4px;
+}
+.srt-list-header {
+  display: flex;
+  padding: 10px 16px;
+  background: #fafafa;
+  border-bottom: 1px solid #f0f0f0;
+  color: #909399;
+  font-size: 13px;
+  font-weight: 500;
+}
+.h-time { width: 120px; }
+.srt-scroll-area {
+  max-height: 480px;
+  overflow-y: auto;
+}
+.srt-entry {
+  display: flex;
+  padding: 14px 16px;
+  border-bottom: 1px solid #f9f9f9;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+.srt-entry:hover {
+  background-color: #f5f7fa;
+}
+.entry-time {
+  width: 120px;
+  flex-shrink: 0;
+}
+.time-badge {
+  font-family: monospace;
+  font-size: 12px;
+  color: #409EFF;
+  background: #ecf5ff;
+  padding: 2px 6px;
+  border-radius: 4px;
+}
+.entry-content {
+  flex: 1;
+  display: flex;
+  justify-content: space-between;
+}
+.text {
+  margin: 0;
+  font-size: 14px;
+  line-height: 1.6;
+  color: #303133;
+}
+.copy-icon {
+  margin-left: 10px;
+  color: #c0c4cc;
+  cursor: pointer;
+  visibility: hidden;
+}
+.srt-entry:hover .copy-icon {
+  visibility: visible;
+}
+.copy-icon:hover {
+  color: #409EFF;
+}
+
+/* 渐变标签 */
+.custom-model-tag {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white !important;
+  font-size: 12px;
+  padding: 4px 12px;
+  border-radius: 20px;
+  font-weight: 500;
+}
+
+.full-text-body {
+  white-space: pre-wrap;
+  line-height: 1.8;
+  background: #f8f9fb;
+  padding: 20px;
+  border-radius: 4px;
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.empty-placeholder {
+  padding: 80px 0;
+  background: #fff;
+  border-radius: 8px;
+}
+</style>

+ 207 - 0
src/views/ai/AuditIndex.vue

@@ -0,0 +1,207 @@
+<template>
+  <div class="audit-index-wrapper">
+    <header class="audit-navbar">
+      <div class="navbar-container">
+        <div class="logo-section" @click="$router.push('/ai')">
+          <div class="logo-box">
+            <i class="el-icon-cpu"></i> </div>
+          <span class="brand-name">AI</span>
+        </div>
+
+        <div class="menu-section">
+          <el-menu
+            :default-active="activePath"
+            mode="horizontal"
+            router
+            class="audit-menu"
+          >
+            <el-menu-item index="/ai/video">
+              <i class="el-icon-video-camera"></i>视频审核
+            </el-menu-item>
+            <el-menu-item index="/ai/image">
+              <i class="el-icon-picture-outline"></i>图像理解
+            </el-menu-item>
+            <el-menu-item index="/ai/audio">
+              <i class="el-icon-mic"></i>音频识别
+            </el-menu-item>
+            <el-menu-item index="/ai/text">
+              <i class="el-icon-document"></i>文本分析
+            </el-menu-item>
+          </el-menu>
+        </div>
+
+        <div class="navbar-right">
+          <el-tooltip content="刷新当前页面" placement="bottom">
+            <el-button type="text" icon="el-icon-refresh" @click="handleRefresh"></el-button>
+          </el-tooltip>
+          <el-divider direction="vertical"></el-divider>
+          <span class="user-info">Admin</span>
+        </div>
+      </div>
+    </header>
+
+    <main class="audit-content">
+      <transition name="fade-transform" mode="out-in">
+        <router-view :key="$route.fullPath"></router-view>
+      </transition>
+    </main>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'AuditIndex',
+  computed: {
+    activePath() {
+      // 如果路由是 /ai/video/detail 等,依然高亮 /ai/video
+      const pathArr = this.$route.path.split('/');
+      if (pathArr.length >= 3) {
+        return `/${pathArr[1]}/${pathArr[2]}`;
+      }
+      return this.$route.path;
+    }
+  },
+  methods: {
+    handleRefresh() {
+      location.reload();
+    }
+  }
+};
+</script>
+
+<style scoped>
+/* 容器布局 */
+.audit-index-wrapper {
+  display: flex;
+  flex-direction: column;
+  min-height: 100vh;
+  background-color: #f0f2f5; /* 略深一点的底色,衬托卡片 */
+}
+
+/* 导航栏样式 */
+.audit-navbar {
+  background-color: #ffffff;
+  height: 64px;
+  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+  position: sticky;
+  top: 0;
+  z-index: 1000;
+  padding: 0 24px;
+}
+
+.navbar-container {
+  display: flex;
+  align-items: center;
+  height: 100%;
+  max-width: 1400px;
+  margin: 0 auto;
+}
+
+/* Logo 样式 */
+.logo-section {
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  margin-right: 40px;
+  transition: opacity 0.3s;
+}
+
+.logo-section:hover {
+  opacity: 0.8;
+}
+
+.logo-box {
+  width: 32px;
+  height: 32px;
+  background: linear-gradient(135deg, #409EFF 0%, #1d42ad 100%);
+  border-radius: 6px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 12px;
+}
+
+.logo-box i {
+  color: #fff;
+  font-size: 20px;
+}
+
+.brand-name {
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+  letter-spacing: 0.5px;
+  white-space: nowrap;
+}
+
+/* 菜单区域 */
+.menu-section {
+  flex: 1;
+}
+
+.audit-menu {
+  border-bottom: none !important;
+  height: 64px;
+}
+
+/* 覆盖 Element UI 默认样式,让高度对齐 */
+.el-menu--horizontal > .el-menu-item {
+  height: 64px !important;
+  line-height: 64px !important;
+  font-size: 15px;
+  border-bottom-width: 3px !important;
+}
+
+/* 右侧工具栏 */
+.navbar-right {
+  display: flex;
+  align-items: center;
+  margin-left: 20px;
+}
+
+.navbar-right .el-button {
+  font-size: 18px;
+  color: #606266;
+}
+
+.user-info {
+  font-size: 14px;
+  color: #606266;
+  margin-left: 10px;
+  font-weight: 500;
+}
+
+/* 内容区区域 */
+.audit-content {
+  flex: 1;
+  width: 100%;
+  max-width: 1400px;
+  margin: 0 auto;
+  padding: 20px;
+  box-sizing: border-box;
+}
+
+/* 动画过渡 */
+.fade-transform-enter-active,
+.fade-transform-leave-active {
+  transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
+}
+.fade-transform-enter {
+  opacity: 0;
+  transform: translateX(-20px);
+}
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateX(20px);
+}
+
+/* 响应式适配 */
+@media (max-width: 768px) {
+  .brand-name {
+    display: none; /* 小屏隐藏文字只留 logo */
+  }
+  .navbar-container {
+    padding: 0;
+  }
+}
+</style>

+ 247 - 0
src/views/ai/GpuDashboard.vue

@@ -0,0 +1,247 @@
+<template>
+  <div class="gpu-monitor-wrapper">
+    <div class="monitor-header">
+      <div class="header-left">
+        <h3 class="page-title"><i class="el-icon-odometer"></i> 算力资源实时监控</h3>
+        <span class="refresh-tip">
+          <i class="el-icon-loading" v-if="loading"></i>
+          <i class="el-icon-time" v-else></i>
+          自动刷新中(10s/次)
+        </span>
+      </div>
+      <el-button type="primary" size="small" round icon="el-icon-refresh" :loading="loading" @click="fetchGpuData">立即同步</el-button>
+    </div>
+
+    <el-row :gutter="20">
+      <el-col :span="14">
+        <el-card shadow="never" class="info-card">
+          <div slot="header" class="card-header">
+            <span><i class="el-icon-processor"></i> 显卡硬件:{{ gpuData ? gpuData.gpu.model : '检测中...' }}</span>
+            <el-tag type="success" size="mini" effect="dark">在线</el-tag>
+          </div>
+
+          <div v-if="gpuData" class="gpu-usage-flex">
+            <div class="gauge-section">
+              <el-progress
+                type="dashboard"
+                :percentage="calculatePercentage(gpuData.gpu.mem_used, gpuData.gpu.mem_total)"
+                :stroke-width="12"
+                :width="160"
+                :color="customColors"
+              ></el-progress>
+              <div class="gauge-label">总显存占用率</div>
+            </div>
+
+            <div class="details-section">
+              <div class="detail-item">
+                <span class="label">已使用 (Used)</span>
+                <span class="value used">{{ gpuData.gpu.mem_used }}</span>
+              </div>
+              <el-progress :percentage="calculatePercentage(gpuData.gpu.mem_used, gpuData.gpu.mem_total)" :show-text="false" :stroke-width="8" :color="customColors"></el-progress>
+
+              <el-divider></el-divider>
+
+              <div class="mini-stats">
+                <div class="mini-item">
+                  <div class="m-label">空闲 (Free)</div>
+                  <div class="m-value">{{ gpuData.gpu.mem_free }}</div>
+                </div>
+                <div class="mini-item">
+                  <div class="m-label">总量 (Total)</div>
+                  <div class="m-value">{{ gpuData.gpu.mem_total }}</div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+
+      <el-col :span="10">
+        <el-card shadow="never" class="info-card">
+          <div slot="header" class="card-header">
+            <span><i class="el-icon-connection"></i> PyTorch 显存池</span>
+          </div>
+          <div v-if="gpuData" class="torch-container">
+            <div class="torch-box reserved">
+              <div class="t-label">Reserved (预留)</div>
+              <div class="t-value">{{ gpuData.torch.reserved }}</div>
+              <div class="t-desc">Torch 预先向系统申请的缓存</div>
+            </div>
+            <div class="torch-box allocated">
+              <div class="t-label">Allocated (分配)</div>
+              <div class="t-value">{{ gpuData.torch.allocated }}</div>
+              <div class="t-desc">当前 AI 模型实际占用的显存</div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-card shadow="never" class="m-t-20 table-card">
+      <div slot="header" class="card-header">
+        <span><i class="el-icon-collection"></i> Ollama 推理引擎:运行中模型</span>
+      </div>
+      <el-table :data="gpuData ? gpuData.ollama : []" size="medium" style="width: 100%">
+        <el-table-column label="模型名称" min-width="150">
+          <template slot-scope="scope">
+            <div class="model-cell">
+              <div class="model-icon"><i class="el-icon-box"></i></div>
+              <span class="model-name-text">{{ scope.row.model_name }}</span>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="size" label="磁盘大小" width="120"></el-table-column>
+        <el-table-column prop="size_vram" label="显存占用 (VRAM)" width="150">
+          <template slot-scope="scope">
+            <span class="vram-text">{{ scope.row.size_vram }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="GPU 负载">
+          <template slot-scope="scope">
+            <el-progress
+              :percentage="Number(scope.row.gpu_percentage)"
+              :color="scope.row.gpu_percentage > 90 ? '#67c23a' : '#e6a23c'"
+              :stroke-width="10"
+              stroke-linecap="round"
+            ></el-progress>
+          </template>
+        </el-table-column>
+        <el-table-column prop="stat" label="活跃状态" width="120" align="center">
+          <template slot-scope="scope">
+            <span class="status-dot" :class="{ 'is-active': scope.row.stat.includes('OK') }"></span>
+            {{ scope.row.stat }}
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+
+export default {
+  name: 'GpuDashboard',
+  data() {
+    return {
+      gpuData: null,
+      loading: false,
+      timer: null,
+      customColors: [
+        {color: '#67c23a', percentage: 40},
+        {color: '#e6a23c', percentage: 75},
+        {color: '#f56c6c', percentage: 90}
+      ]
+    };
+  },
+  mounted() {
+    this.fetchGpuData();
+    this.timer = setInterval(this.fetchGpuData, 10000);
+  },
+  beforeDestroy() {
+    if (this.timer) clearInterval(this.timer);
+  },
+  methods: {
+    async fetchGpuData() {
+      this.loading = true;
+      try {
+        const response = await axios.get('/api1/gpu/info');
+        this.gpuData = response.data;
+      } catch (error) {
+        this.$message.error('获取 GPU 监控数据失败');
+      } finally {
+        setTimeout(() => { this.loading = false; }, 500);
+      }
+    },
+    calculatePercentage(usedStr, totalStr) {
+      const used = parseFloat(usedStr) || 0;
+      const total = parseFloat(totalStr) || 1;
+      return Math.min(Math.round((used / total) * 100), 100);
+    }
+  }
+};
+</script>
+
+<style scoped>
+.gpu-monitor-wrapper { padding: 0; }
+.m-t-20 { margin-top: 20px; }
+
+/* 头部样式 */
+.monitor-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+.page-title { margin: 0; font-size: 18px; color: #303133; display: flex; align-items: center; }
+.page-title i { margin-right: 8px; color: #409EFF; }
+.refresh-tip { font-size: 12px; color: #909399; margin-left: 15px; }
+
+/* 卡片通用 */
+.info-card { border: none; border-radius: 8px; }
+.card-header { display: flex; justify-content: space-between; align-items: center; font-weight: bold; }
+
+/* GPU 显存布局 */
+.gpu-usage-flex {
+  display: flex;
+  align-items: center;
+  padding: 10px 0;
+}
+.gauge-section { width: 200px; text-align: center; border-right: 1px solid #f0f2f5; }
+.gauge-label { font-size: 13px; color: #909399; margin-top: -15px; }
+
+.details-section { flex: 1; padding-left: 30px; }
+.detail-item { display: flex; justify-content: space-between; margin-bottom: 8px; align-items: flex-end; }
+.detail-item .label { font-size: 13px; color: #606266; }
+.detail-item .value { font-size: 20px; font-weight: bold; font-family: 'PingFang SC', sans-serif; }
+.value.used { color: #f56c6c; }
+
+.mini-stats { display: flex; gap: 40px; }
+.m-label { font-size: 12px; color: #909399; margin-bottom: 4px; }
+.m-value { font-size: 14px; font-weight: 600; color: #303133; }
+
+/* PyTorch 盒子样式 */
+.torch-container { display: flex; flex-direction: column; gap: 12px; }
+.torch-box {
+  padding: 15px;
+  border-radius: 6px;
+  position: relative;
+  overflow: hidden;
+}
+.torch-box.reserved { background: #fdf6ec; border: 1px solid #faecd8; }
+.torch-box.allocated { background: #ecf5ff; border: 1px solid #d9ecff; }
+.t-label { font-size: 12px; color: #909399; }
+.t-value { font-size: 20px; font-weight: bold; margin: 5px 0; color: #303133; }
+.t-desc { font-size: 11px; color: #a8abb2; }
+
+/* 表格样式 */
+.table-card { border: none; }
+.model-cell { display: flex; align-items: center; }
+.model-icon {
+  width: 32px;
+  height: 32px;
+  background: #f0f2f5;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 10px;
+  color: #409EFF;
+}
+.model-name-text { font-weight: 600; color: #303133; }
+.vram-text { color: #409EFF; font-family: monospace; font-weight: bold; }
+
+/* 状态小点 */
+.status-dot {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  background: #909399;
+  border-radius: 50%;
+  margin-right: 6px;
+}
+.status-dot.is-active {
+  background: #67c23a;
+  box-shadow: 0 0 6px #67c23a;
+}
+</style>

+ 374 - 0
src/views/ai/ImageAudit.vue

@@ -0,0 +1,374 @@
+<template>
+  <div class="image-audit-sub-container">
+    <div class="page-top-bar">
+      <div class="title-group">
+        <h2 class="main-title">图片 AI 内容审核</h2>
+        <span class="sub-title">基于多模态大模型的图像理解与合规性检查</span>
+      </div>
+    </div>
+
+    <el-row :gutter="20">
+      <el-col :xs="24" :sm="24" :md="9" :lg="8">
+        <div class="sticky-form">
+          <el-card shadow="never" class="config-card">
+            <div slot="header" class="card-header">
+              <span><i class="el-icon-setting"></i> 任务配置</span>
+            </div>
+
+            <el-form label-position="top" size="medium">
+              <el-form-item label="待审核图片">
+                <el-upload
+                  class="image-uploader"
+                  drag
+                  action=""
+                  :auto-upload="false"
+                  :show-file-list="false"
+                  :on-change="handleFileChange"
+                >
+                  <div v-if="previewUrl" class="upload-preview">
+                    <img :src="previewUrl" class="preview-img" />
+                    <div class="upload-mask">
+                      <i class="el-icon-refresh"></i>
+                      <p>更换图片</p>
+                    </div>
+                  </div>
+                  <template v-else>
+                    <i class="el-icon-upload"></i>
+                    <div class="el-upload__text">拖拽或<em>点击上传</em></div>
+                  </template>
+                </el-upload>
+              </el-form-item>
+
+              <el-form-item label="检测指令 (Prompts)">
+                <div v-for="(item, index) in prompts" :key="index" class="prompt-input-row">
+                  <el-input
+                    v-model="prompts[index]"
+                    placeholder="例如: 图中是否有违禁品?"
+                    clearable
+                  >
+                    <el-button
+                      slot="append"
+                      icon="el-icon-minus"
+                      @click="removePrompt(index)"
+                      :disabled="prompts.length <= 1"
+                    ></el-button>
+                  </el-input>
+                </div>
+                <el-button
+                  type="text"
+                  icon="el-icon-circle-plus-outline"
+                  class="add-prompt-btn"
+                  @click="addPrompt"
+                >新增一条检测指令</el-button>
+              </el-form-item>
+
+              <el-divider></el-divider>
+
+              <el-button
+                type="primary"
+                class="submit-btn"
+                :loading="submitting"
+                @click="submitAudit"
+              >
+                {{ submitting ? 'AI 正在全力分析中...' : '提交 AI 审核' }}
+              </el-button>
+            </el-form>
+          </el-card>
+        </div>
+      </el-col>
+
+      <el-col :xs="24" :sm="24" :md="15" :lg="16">
+        <el-card v-if="auditResult" shadow="never" class="result-card">
+          <div slot="header" class="card-header-flex">
+            <div class="header-left">
+              <span class="res-title"><i class="el-icon-data-analysis"></i> 识别报告</span>
+              <span class="task-id-text">ID: {{ auditResult.task_id }}</span>
+            </div>
+            <el-tag v-if="auditResult.model_name" class="custom-model-tag">
+              <i class="el-icon-cpu"></i> {{ auditResult.model_name }}
+            </el-tag>
+          </div>
+
+          <el-row :gutter="30">
+            <el-col :span="24" :lg="10">
+              <div class="result-preview-container">
+                <el-image
+                  :src="auditResult.image_url"
+                  fit="contain"
+                  :preview-src-list="[auditResult.image_url]"
+                  class="final-image"
+                >
+                  <div slot="placeholder" class="image-loading">加载中...</div>
+                </el-image>
+                <p class="img-caption">送审图像预览</p>
+              </div>
+            </el-col>
+
+            <el-col :span="24" :lg="14">
+              <el-table :data="auditResult.results" border stripe size="small" class="res-table">
+                <el-table-column label="检测项 (Prompt)" prop="prompt" show-overflow-tooltip width="150"></el-table-column>
+                <el-table-column label="AI 分析结果">
+                  <template slot-scope="scope">
+                    <div class="result-content-box">
+                      <div class="raw-result">
+                        <span class="lang-label">EN</span> {{ scope.row.result }}
+                      </div>
+
+                      <div v-if="scope.row.result_zh || scope.row.chs_translation" class="zh-result-box">
+                        <el-divider><i class="el-icon-chat-dot-square"></i> 中文解析</el-divider>
+                        <p class="zh-text">{{ scope.row.result_zh || scope.row.chs_translation }}</p>
+                      </div>
+
+                      <el-button
+                        v-else
+                        type="text"
+                        size="mini"
+                        icon="el-icon-refresh"
+                        @click="handleTranslate(scope.row)"
+                      >翻译为中文</el-button>
+                    </div>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <el-empty v-else :image-size="200" description="暂无数据,请在左侧上传并提交审核任务"></el-empty>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+import { translate } from "@/api/ai";
+
+export default {
+  name: 'ImageAudit',
+  data() {
+    return {
+      submitting: false,
+      file: null,
+      previewUrl: '',
+      prompts: ['Analyze the overall sentiment of the image.', 'Check for any restricted objects.'],
+      auditResult: null
+    };
+  },
+  methods: {
+    handleFileChange(file) {
+      this.file = file.raw;
+      this.previewUrl = URL.createObjectURL(file.raw);
+    },
+    addPrompt() {
+      this.prompts.push('');
+    },
+    removePrompt(index) {
+      this.prompts.splice(index, 1);
+    },
+    async submitAudit() {
+      if (!this.file) return this.$message.warning('请先选择待审核图片');
+
+      this.submitting = true;
+      try {
+        const formData = new FormData();
+        formData.append('file', this.file);
+        this.prompts.filter(p => p.trim()).forEach(p => formData.append('prompts', p));
+
+        const res = await axios.post('/api1/image/analyze', formData, {
+          headers: { 'Content-Type': 'multipart/form-data' }
+        });
+
+        this.auditResult = res.data;
+        this.$message.success('审核任务处理成功');
+      } catch (error) {
+        const msg = error.response?.data?.detail || '服务繁忙,请稍后再试';
+        this.$message.error(`审核失败: ${msg}`);
+      } finally {
+        this.submitting = false;
+      }
+    },
+    async handleTranslate(row) {
+      this.$set(row, 'result_zh', '正在翻译...');
+      try {
+        const resp = await translate({ text: row.result, target: 'zh' });
+        this.$set(row, 'result_zh', resp.translation);
+      } catch (error) {
+        this.$message.error('翻译服务连接失败');
+        this.$set(row, 'result_zh', '');
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+/* 容器去掉背景,复用 AuditIndex 的背景 */
+.image-audit-sub-container {
+  padding: 0;
+}
+
+/* 页面顶部标题样式 */
+.page-top-bar {
+  margin-bottom: 25px;
+  padding-bottom: 15px;
+  border-bottom: 1px solid #e6e9f0;
+}
+.main-title {
+  margin: 0;
+  font-size: 22px;
+  color: #2c3e50;
+  font-weight: 600;
+}
+.sub-title {
+  font-size: 13px;
+  color: #7f8c8d;
+}
+
+/* 布局辅助 */
+.sticky-form {
+  position: sticky;
+  top: 80px; /* 避开 AuditIndex 的导航栏 */
+}
+
+/* 卡片美化 */
+.config-card, .result-card {
+  border-radius: 8px;
+  border: 1px solid #ebeef5;
+}
+
+.card-header {
+  font-weight: bold;
+  color: #34495e;
+}
+
+/* 上传控件自定义 */
+.image-uploader {
+  width: 100%;
+}
+::v-deep .el-upload-dragger {
+  width: 100%;
+  height: 180px;
+}
+.upload-preview {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+.preview-img {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  background: #f8f9fb;
+}
+.upload-mask {
+  position: absolute;
+  top: 0; left: 0; width: 100%; height: 100%;
+  background: rgba(0,0,0,0.4);
+  color: #fff;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  opacity: 0;
+  transition: 0.3s;
+}
+.upload-preview:hover .upload-mask {
+  opacity: 1;
+}
+
+/* 动态 Prompt 行 */
+.prompt-input-row {
+  margin-bottom: 10px;
+}
+.add-prompt-btn {
+  padding: 5px 0;
+  font-size: 13px;
+}
+
+.submit-btn {
+  width: 100%;
+  height: 40px;
+  font-weight: bold;
+}
+
+/* 结果展示区 */
+.card-header-flex {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.task-id-text {
+  font-size: 12px;
+  color: #94a3b8;
+  margin-left: 10px;
+}
+.res-title {
+  font-weight: bold;
+  color: #334155;
+}
+
+.result-preview-container {
+  text-align: center;
+  background: #f1f5f9;
+  padding: 15px;
+  border-radius: 8px;
+}
+.final-image {
+  max-width: 100%;
+  height: 280px;
+  border-radius: 4px;
+}
+.img-caption {
+  font-size: 12px;
+  color: #64748b;
+  margin-top: 10px;
+}
+
+/* 表格内识别结果美化 */
+.result-content-box {
+  padding: 5px 0;
+}
+.lang-label {
+  display: inline-block;
+  font-size: 10px;
+  background: #e2e8f0;
+  color: #475569;
+  padding: 0 4px;
+  border-radius: 3px;
+  margin-right: 5px;
+}
+.raw-result {
+  color: #1e293b;
+  line-height: 1.6;
+}
+.zh-result-box {
+  margin-top: 10px;
+  background: #f8fafc;
+  padding: 10px;
+  border-radius: 6px;
+}
+.zh-text {
+  color: #059669; /* 绿色系表示已解析结果 */
+  font-weight: 500;
+  margin: 0;
+}
+
+::v-deep .el-divider--horizontal {
+  margin: 15px 0 10px 0;
+}
+::v-deep .el-divider__text {
+  font-size: 11px;
+  color: #94a3b8;
+  background: #f8fafc;
+}
+
+/* 模型 Tag 保持科技感 */
+.custom-model-tag {
+  background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
+  color: white !important;
+  border: none;
+  border-radius: 20px;
+  padding: 0 15px;
+}
+</style>

+ 273 - 0
src/views/ai/TextAudit.vue

@@ -0,0 +1,273 @@
+<template>
+  <div class="text-audit-container">
+    <el-row :gutter="20">
+      <el-col :span="10">
+        <el-card shadow="never" class="config-card">
+          <div slot="header" class="clearfix">
+            <span class="card-title"><i class="el-icon-edit-outline"></i> 任务配置</span>
+            <el-button
+              style="float: right; padding: 3px 0"
+              type="text"
+              icon="el-icon-refresh-left"
+              @click="handleReset"
+            >清空内容</el-button>
+          </div>
+
+          <el-form label-position="top">
+            <el-form-item label="分析类型 (Select Task)">
+              <el-radio-group v-model="api" size="medium" class="full-width-radio">
+                <el-radio-button :label="summarizeApi">
+                  <i class="el-icon-document-copy"></i> 文本摘要
+                </el-radio-button>
+                <el-radio-button :label="tagApi">
+                  <i class="el-icon-collection-tag"></i> 关键词提取
+                </el-radio-button>
+              </el-radio-group>
+            </el-form-item>
+
+            <el-form-item label="分析文本 (Input Content)">
+              <el-input
+                type="textarea"
+                :rows="16"
+                placeholder="在此处粘贴或输入需要分析的长文本内容..."
+                v-model="textContent"
+                maxlength="5000"
+                show-word-limit
+                resize="none"
+              ></el-input>
+            </el-form-item>
+
+            <el-button
+              type="primary"
+              class="analyze-btn"
+              :loading="submitting"
+              :disabled="!textContent.trim() || !api"
+              @click="submitTextAnalysis"
+            >
+              {{ submitting ? '正在深度分析中...' : '开始 AI 文本分析' }}
+            </el-button>
+          </el-form>
+        </el-card>
+      </el-col>
+
+      <el-col :span="14">
+        <el-card v-if="analysisResult" shadow="never" class="result-card">
+          <div slot="header" class="card-header-flex">
+            <div class="header-left">
+              <span class="card-title"><i class="el-icon-data-analysis"></i> 分析结果</span>
+            </div>
+            <el-tag v-if="analysisResult.model_name" class="custom-model-tag">
+              <i class="el-icon-monitor"></i> {{ analysisResult.model_name }}
+            </el-tag>
+          </div>
+
+          <div class="analysis-output">
+            <div v-if="analysisResult.prompt" class="prompt-echo">
+              <i class="el-icon-guide"></i>
+              <span class="echo-text"><strong>AI 指令:</strong>{{ analysisResult.prompt }}</span>
+            </div>
+
+            <div class="result-main-box">
+              <div class="result-header">
+                <span class="title">AI 响应正文</span>
+                <el-button
+                  type="primary"
+                  size="mini"
+                  plain
+                  icon="el-icon-document-copy"
+                  @click="handleCopy(analysisResult.result)"
+                >复制结果</el-button>
+              </div>
+
+              <div class="result-content-area">
+                <div class="text-display-wrapper">
+                  <p class="text-display">{{ analysisResult.result }}</p>
+                </div>
+              </div>
+            </div>
+
+            <div class="result-footer m-t-20">
+              <el-descriptions size="mini" :column="2" border>
+                <el-descriptions-item label="字符数">{{ textContent.length }}</el-descriptions-item>
+                <el-descriptions-item label="处理耗时">{{ analysisResult.elapsed_time || '---' }}ms</el-descriptions-item>
+              </el-descriptions>
+            </div>
+          </div>
+        </el-card>
+
+        <el-empty
+          v-else
+          :image-size="200"
+          description="等待输入中..."
+        >
+          <template #description>
+            <p class="empty-tip">{{ !api ? '请先选择上方的分析任务类型' : '请在左侧输入需要分析的文本内容' }}</p>
+          </template>
+        </el-empty>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+
+export default {
+  name: 'TextAudit',
+  data() {
+    return {
+      submitting: false,
+      textContent: '',
+      analysisResult: null,
+      api: '/api1/text/summarize',
+      summarizeApi: '/api1/text/summarize',
+      tagApi: '/api1/text/tag'
+    };
+  },
+  methods: {
+    async submitTextAnalysis() {
+      this.submitting = true;
+      try {
+        const response = await axios.post(this.api, {
+          text: this.textContent
+        });
+        this.analysisResult = response.data;
+        this.$message({
+          message: '分析成功',
+          type: 'success',
+          customClass: 'message-on-top'
+        });
+      } catch (error) {
+        this.$message.error('分析失败,请检查网络或文本长度');
+      } finally {
+        this.submitting = false;
+      }
+    },
+    handleReset() {
+      this.textContent = '';
+      this.analysisResult = null;
+    },
+    async handleCopy(text) {
+      try {
+        await navigator.clipboard.writeText(text);
+        this.$message.success('已成功复制结果');
+      } catch (err) {
+        this.$message.error('浏览器拒绝访问剪贴板');
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+.text-audit-container {
+  padding: 0; /* 在 AuditIndex 已经有 padding 的情况下设为 0 */
+}
+
+/* 卡片标题统一 */
+.card-title {
+  font-weight: 600;
+  color: #303133;
+  font-size: 15px;
+}
+.card-title i {
+  margin-right: 6px;
+  color: #409EFF;
+}
+
+/* 按钮行样式 */
+.full-width-radio {
+  display: flex;
+  width: 100%;
+}
+.full-width-radio >>> .el-radio-button {
+  flex: 1;
+}
+.full-width-radio >>> .el-radio-button__inner {
+  width: 100%;
+}
+
+.analyze-btn {
+  width: 100%;
+  margin-top: 10px;
+  height: 45px;
+  font-size: 16px;
+  letter-spacing: 1px;
+}
+
+/* 结果展示区优化 */
+.card-header-flex {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.prompt-echo {
+  display: flex;
+  align-items: flex-start;
+  padding: 12px 16px;
+  background: #fdf6ec;
+  border-radius: 8px;
+  margin-bottom: 20px;
+}
+.prompt-echo i {
+  color: #e6a23c;
+  margin-top: 3px;
+  margin-right: 8px;
+}
+.echo-text {
+  font-size: 13px;
+  line-height: 1.5;
+  color: #606266;
+}
+
+.result-main-box {
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
+}
+
+.result-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 16px;
+  background: #f8f9fb;
+  border-bottom: 1px solid #e4e7ed;
+}
+
+.result-content-area {
+  padding: 24px;
+  background: #fff;
+  min-height: 300px;
+}
+
+.text-display {
+  line-height: 1.8;
+  color: #2c3e50;
+  font-size: 15px;
+  white-space: pre-wrap;
+  margin: 0;
+}
+
+/* 空状态提示文字 */
+.empty-tip {
+  color: #909399;
+  font-size: 14px;
+}
+
+/* 之前页面保持的科技感标签 */
+.custom-model-tag {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white !important;
+  border: none;
+  font-weight: bold;
+  padding: 0 12px;
+  height: 28px;
+  line-height: 26px;
+  border-radius: 14px;
+}
+
+.m-t-20 { margin-top: 20px; }
+</style>

+ 339 - 0
src/views/ai/VideoAudit.vue

@@ -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>