reghao há 3 dias atrás
pai
commit
de2b9a4a56

+ 3 - 3
src/api/admin.js

@@ -4,7 +4,7 @@ const adminApi = {
   getAccountRegistryApi: '/api/admin/account/account_registry',
   setAccountRegistryApi: '/api/admin/account/registry',
   setAccountCodeApi: '/api/admin/account/code',
-  getUsersApi: '/api/admin/user/list',
+  getAccountApi: '/api/admin/account',
   getUserAvatarsApi: '/api/admin/user/user/avatar',
   getVipPlanApi: '/api/admin/user/vip_plan',
   fileStoreConfigApi: '/api/admin/file/file_store',
@@ -26,8 +26,8 @@ export function setAccountCode(payload) {
   return post(adminApi.setAccountCodeApi, payload)
 }
 
-export function getUserList(queryParams) {
-  return get(adminApi.getUsersApi, queryParams)
+export function getAccountList(queryParams) {
+  return get(adminApi.getAccountApi + '/list', queryParams)
 }
 
 export function getUserAvatarList() {

+ 4 - 0
src/api/vod.js

@@ -158,3 +158,7 @@ export function getAuditVideo(queryParams) {
 export function submitAuditVideo(payload) {
   return post(videoApi.vodAdminApi + '/audit', payload)
 }
+
+export function submitEditVideo(payload) {
+  return post(videoApi.vodAdminApi + '/edit', payload)
+}

+ 0 - 396
src/components/layout/NavBar.vue

@@ -1,396 +0,0 @@
-<template>
-  <div class="header-wrapper">
-    <div class="nav-bar-container">
-      <el-row type="flex" align="middle" class="nav-content">
-        <el-col :xs="3" :sm="2" :md="3" class="logo-wrapper">
-          <a href="/" class="logo-link">
-            <img src="@/assets/img/logo.png" class="logo-img" alt="logo">
-            <span class="logo-text hidden-xs-only">TNBAPP</span>
-          </a>
-        </el-col>
-
-        <el-col :sm="6" :md="7" class="hidden-xs-only">
-          <div class="nav-links">
-            <router-link to="/region" class="nav-item-link">分区</router-link>
-            <router-link to="/short" class="nav-item-link">短视频</router-link>
-            <router-link to="/playlist" class="nav-item-link">播放列表</router-link>
-          </div>
-        </el-col>
-
-        <el-col :xs="15" :sm="8" :md="6">
-          <div class="search-wrapper">
-            <el-autocomplete
-                v-model="keyword"
-                :fetch-suggestions="querySearchAsync"
-                :placeholder="searchHint"
-                prefix-icon="el-icon-search"
-                class="custom-search"
-                size="small"
-                @keyup.enter.native="onSearch"
-            />
-          </div>
-        </el-col>
-
-        <el-col :xs="6" :sm="8" :md="8" class="user-actions">
-          <div class="action-icons hidden-xs-only">
-            <div class="icon-item-wrapper" @click="goToTimeline">
-              <el-badge :value="statusCount" :max="99" :hidden="statusCount <= 0" class="badge-item">
-                <i class="el-icon-wind-power custom-icon"></i>
-              </el-badge>
-              <span class="icon-label">动态</span>
-            </div>
-
-            <div class="icon-item-wrapper" @click="goToMessage">
-              <el-badge :value="msgCount" :max="99" :hidden="msgCount <= 0" class="badge-item">
-                <i class="el-icon-message-solid custom-icon"></i>
-              </el-badge>
-              <span class="icon-label">消息</span>
-            </div>
-          </div>
-
-          <div class="user-profile-wrapper">
-            <el-dropdown v-if="user" trigger="hover" placement="bottom" class="user-dropdown-trigger">
-              <div class="avatar-wrapper">
-                <img :src="user.avatarUrl" class="user-avatar" alt="avatar">
-              </div>
-              <el-dropdown-menu slot="dropdown" class="user-dropdown-menu">
-                <div class="user-name-header">{{ user.screenName || '用户' }}</div>
-                <el-dropdown-item icon="el-icon-s-home" @click.native="goToUserHome">我的主页</el-dropdown-item>
-                <el-dropdown-item icon="el-icon-collection" @click.native="goToPlaylist">收藏列表</el-dropdown-item>
-                <el-dropdown-item icon="el-icon-collection" @click.native="goToHistory">历史记录</el-dropdown-item>
-                <el-dropdown-item icon="el-icon-collection" @click.native="goToBackground">进入后台</el-dropdown-item>
-                <el-dropdown-item icon="el-icon-switch-button" divided @click.native="goToLogout">退出登录</el-dropdown-item>
-              </el-dropdown-menu>
-            </el-dropdown>
-
-            <el-button v-else type="text" class="login-btn-text" @click="login">登录</el-button>
-
-            <el-button
-                type="primary"
-                class="upload-btn-blue hidden-xs-only"
-                @click="goToPublish"
-            >
-              <i class="el-icon-upload"></i>
-              <span>投稿</span>
-            </el-button>
-          </div>
-        </el-col>
-      </el-row>
-    </div>
-
-    <nav class="bottom-nav hidden-sm-and-up">
-      <router-link to="/" class="bottom-nav-item" exact-active-class="is-active">
-        <i class="el-icon-house"></i>
-        <span>首页</span>
-      </router-link>
-      <router-link to="/region" class="bottom-nav-item" active-class="is-active">
-        <i class="el-icon-menu"></i>
-        <span>分区</span>
-      </router-link>
-      <router-link to="/short" class="bottom-nav-item" active-class="is-active">
-        <i class="el-icon-video-camera"></i>
-        <span>短视频</span>
-      </router-link>
-      <router-link :to="user ? `/timeline` : '/login'" class="bottom-nav-item" active-class="is-active">
-        <i class="el-icon-wind-power"></i>
-        <span>动态</span>
-      </router-link>
-      <router-link :to="user ? `/user/${user.userIdStr}` : '/login'" class="bottom-nav-item" active-class="is-active">
-        <i class="el-icon-user"></i>
-        <span>我的</span>
-      </router-link>
-    </nav>
-  </div>
-</template>
-
-<script>
-import { userMixin } from 'assets/js/mixin'
-import { keywordSuggest } from '@/api/search'
-import { getAuthedUser } from '@/utils/auth'
-import { getUnreadCount } from '@/api/user'
-
-export default {
-  name: 'NavBar',
-  mixins: [userMixin],
-  data() {
-    return {
-      user: null,
-      restaurants: [],
-      searchHint: '想要搜点神马呢',
-      keyword: '',
-      statusCount: 0,
-      msgCount: 0
-    }
-  },
-  created() {
-    const userInfo = getAuthedUser()
-    if (userInfo !== null) {
-      this.user = userInfo
-      getUnreadCount().then(resp => {
-        if (resp.code === 0) {
-          this.msgCount = resp.data.total
-        }
-      })
-    }
-  },
-  methods: {
-    querySearchAsync(queryString, cb) {
-      if (!queryString) return
-      setTimeout(() => {
-        keywordSuggest(queryString).then(res => {
-          if (res.code === 0) {
-            const results = res.data.map(item => ({ value: item.keyword }))
-            cb(results)
-          }
-        })
-      }, 300)
-    },
-    onSearch() {
-      if (this.keyword.trim()) {
-        this.toSearchPage()
-      } else {
-        this.$message.warning('不能为空!')
-      }
-    },
-    toSearchPage() {
-      const query = { keyword: this.keyword, pn: 1 }
-      if (this.$route.path === '/search') {
-        this.$router.push({ path: '/search', query }).catch(() => {})
-      } else {
-        const routeUrl = this.$router.resolve({ path: '/search', query })
-        window.open(routeUrl.href, '_blank')
-      }
-    },
-    login() {
-      this.$router.push('/login').catch(() => {})
-    },
-    goToUserHome() { this.$router.push('/user/' + this.user.userIdStr) },
-    goToPlaylist() { this.$router.push('/bg/my/album') },
-    goToHistory() { this.$router.push('/bg/my/history') },
-    goToBackground() { this.$router.push('/bg') },
-    goToTimeline() { this.$router.push('/timeline') },
-    goToMessage() { this.$router.push('/bg/account/message') },
-    goToPublish() { this.$router.push('/bg/post/video') }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-/* 变量定义 */
-$primary-blue: #00a1d6;
-$hover-blue: #00b5e5;
-$text-main: #18191c;
-$text-second: #61666d;
-$bg-grey: #f1f2f3;
-
-/* 1. 顶部主容器 */
-.nav-bar-container {
-  position: fixed; /* 强制固定 */
-  top: 0;
-  left: 0;
-  right: 0;
-  z-index: 2000; /* 提高层级,确保在弹窗之下但在内容之上 */
-  height: 64px;
-  background: #fff;
-  /* 如果想要毛玻璃效果,取消下面两行的注释 */
-  /* background: rgba(255, 255, 255, 0.9); */
-  /* backdrop-filter: blur(10px); */
-  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
-  display: flex;
-  align-items: center;
-  padding: 0 20px;
-}
-
-.nav-content {
-  width: 100%;
-  max-width: 1400px;
-  margin: 0 auto;
-}
-
-/* 2. Logo 样式 */
-.logo-link {
-  display: flex;
-  align-items: center;
-  text-decoration: none;
-  .logo-img { width: 30px; height: 30px; }
-  .logo-text {
-    color: $primary-blue;
-    font-weight: bold;
-    font-size: 18px;
-    margin-left: 8px;
-  }
-}
-
-/* 3. PC端菜单链接 */
-.nav-links {
-  display: flex;
-  gap: 20px;
-  .nav-item-link {
-    text-decoration: none;
-    color: $text-main;
-    font-size: 15px;
-    transition: color 0.2s;
-    &:hover { color: $primary-blue; }
-  }
-}
-
-/* 4. 搜索框美化 */
-.custom-search {
-  width: 100%;
-  ::v-deep .el-input__inner {
-    border-radius: 20px;
-    background-color: $bg-grey;
-    border: 1px solid transparent;
-    padding-left: 35px;
-    transition: all 0.3s ease;
-    &:focus {
-      background-color: #fff;
-      border-color: $primary-blue;
-      box-shadow: 0 0 8px rgba(0, 161, 214, 0.15);
-    }
-  }
-}
-
-/* 5. 右侧操作区总布局 */
-.user-actions {
-  display: flex;
-  align-items: center;
-  justify-content: flex-end;
-  height: 100%;
-}
-
-/* 图标组 (动态/消息) */
-.action-icons {
-  display: flex;
-  align-items: center;
-  margin-right: 20px;
-
-  .icon-item-wrapper {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    margin-left: 18px;
-    cursor: pointer;
-    transition: transform 0.2s ease;
-
-    .custom-icon {
-      font-size: 20px;
-      color: $text-second;
-      transition: color 0.2s;
-    }
-    .icon-label {
-      font-size: 11px;
-      color: $text-second;
-      margin-top: 2px;
-    }
-
-    &:hover {
-      transform: translateY(-2px);
-      .custom-icon, .icon-label { color: $primary-blue; }
-    }
-  }
-}
-
-/* 头像与按钮包装 */
-.user-profile-wrapper {
-  display: flex;
-  align-items: center;
-}
-
-.avatar-wrapper {
-  display: flex;
-  align-items: center;
-  padding: 0 8px;
-  cursor: pointer;
-}
-
-.user-avatar {
-  width: 36px;
-  height: 36px;
-  border-radius: 50%;
-  object-fit: cover;
-  border: 1px solid transparent;
-  transition: all 0.3s;
-  &:hover {
-    transform: scale(1.1);
-    border-color: $primary-blue;
-  }
-}
-
-.login-btn-text {
-  color: $primary-blue;
-  font-weight: 500;
-  margin-right: 15px;
-}
-
-/* 蓝色投稿按钮 */
-.upload-btn-blue {
-  background-color: $primary-blue !important;
-  border: none !important;
-  border-radius: 8px !important;
-  height: 34px;
-  padding: 0 16px !important;
-  display: flex;
-  align-items: center;
-  margin-left: 10px !important;
-  transition: all 0.3s !important;
-
-  i { font-size: 16px; margin-right: 4px; }
-  span { font-size: 14px; }
-
-  &:hover {
-    background-color: $hover-blue !important;
-    box-shadow: 0 4px 12px rgba(0, 161, 214, 0.2);
-  }
-}
-
-/* 6. 移动端底部导航 */
-.bottom-nav {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  height: 56px;
-  background: rgba(255, 255, 255, 0.96);
-  backdrop-filter: blur(15px);
-  display: flex;
-  border-top: 1px solid #f1f2f3;
-  z-index: 2000;
-  padding-bottom: env(safe-area-inset-bottom);
-
-  .bottom-nav-item {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    text-decoration: none;
-    color: $text-second;
-    i { font-size: 20px; margin-bottom: 2px; }
-    span { font-size: 10px; }
-    &.is-active { color: $primary-blue; i { font-weight: bold; } }
-  }
-}
-
-/* 7. 移动端适配补丁 */
-@media screen and (max-width: 768px) {
-  .nav-bar-container { padding: 0 10px; height: 56px; }
-  .user-profile-wrapper { margin-right: 5px; }
-  .logo-wrapper { flex: 0 0 auto; width: 40px; }
-  .custom-search {
-    ::v-deep .el-input__inner { height: 32px; line-height: 32px; }
-  }
-}
-
-/* 下拉菜单修饰 */
-.user-dropdown-menu {
-  .user-name-header {
-    padding: 12px 20px;
-    font-weight: bold;
-    color: $text-main;
-    border-bottom: 1px solid #f1f2f3;
-    margin-bottom: 5px;
-    text-align: center;
-  }
-}
-</style>

+ 6 - 6
src/router/background_account.js

@@ -11,7 +11,7 @@ const MyContact = () => import('views/my/MyContact')
 const MyOAuth = () => import('views/my/MyOAuth')
 
 export default {
-  path: '/bg/account',
+  path: '/bg/my',
   redirect: '/bg',
   name: 'Account',
   title: '我的帐号',
@@ -20,7 +20,7 @@ export default {
   meta: { needAuth: true, roles: ['tnb_admin', 'tnb_user', 'tnb_disk'] },
   children: [
     {
-      path: '/bg/account/record',
+      path: '/bg/my/record',
       name: 'MyRecord',
       title: '登入记录',
       icon: 'el-icon-user',
@@ -28,7 +28,7 @@ export default {
       meta: { needAuth: true, roles: ['tnb_admin', 'tnb_user', 'tnb_disk'] }
     },
     {
-      path: '/bg/account/message',
+      path: '/bg/my/message',
       name: 'MyMessage',
       title: '我的消息',
       icon: 'el-icon-user',
@@ -36,7 +36,7 @@ export default {
       meta: { needAuth: true, roles: ['tnb_admin', 'tnb_user', 'tnb_disk'] }
     },
     {
-      path: '/bg/account/approval',
+      path: '/bg/my/approval',
       name: 'MyApproval',
       title: '我的审批',
       icon: 'el-icon-user',
@@ -44,7 +44,7 @@ export default {
       meta: { needAuth: true, roles: ['tnb_admin', 'tnb_user', 'tnb_disk'] }
     },
     {
-      path: '/bg/account/contact',
+      path: '/bg/my/contact',
       name: 'MyContact',
       title: '联系人',
       icon: 'el-icon-user',
@@ -52,7 +52,7 @@ export default {
       meta: { needAuth: true, roles: ['tnb_admin', 'tnb_user', 'tnb_disk'] }
     },
     {
-      path: '/bg/account/oauth',
+      path: '/bg/my/oauth',
       name: 'OAuth',
       title: 'OAuth应用',
       icon: 'el-icon-user',

+ 5 - 5
src/router/background_admin.js

@@ -4,7 +4,7 @@ const Background = () => import('views/admin/Background')
 
 // 后台管理
 const AdminApproval = () => import('views/admin/aaa/AdminApproval')
-const AdminUserList = () => import('views/admin/aaa/AdminUserList')
+const AdminAccountList = () => import('views/admin/aaa/AdminAccountList')
 const AdminVideoList = () => import('views/admin/aaa/AdminVideoList')
 
 export default {
@@ -25,11 +25,11 @@ export default {
       meta: { needAuth: true, roles: ['tnb_admin'] }
     },
     {
-      path: '/bg/admin/user',
-      name: 'AdminUserList',
-      title: '用户管理',
+      path: '/bg/admin/account',
+      name: 'AdminAccountList',
+      title: '账号管理',
       icon: 'el-icon-edit',
-      component: AdminUserList,
+      component: AdminAccountList,
       meta: { needAuth: true, roles: ['tnb_admin'] }
     },
     {

+ 7 - 0
src/router/index.js

@@ -39,6 +39,7 @@ const ChartMap = () => import('views/map/ChartMap')
 const ExamPaper = () => import('views/exam/ExamPaper')
 
 const VodAudit = () => import('views/admin/aaa/VideoAudit')
+const VodEdit = () => import('views/admin/aaa/VideoEdit')
 
 const Index = () => import('views/Index')
 const Login = () => import('views/Login')
@@ -133,6 +134,12 @@ export const constantRoutes = [
     component: VodAudit,
     meta: { needAuth: true }
   },
+  {
+    path: '/vod_edit/:id',
+    name: 'VodEdit',
+    component: VodEdit,
+    meta: { needAuth: true }
+  },
   {
     path: '/short',
     name: 'ShortVideoIndex',

+ 1 - 1
src/views/admin/Background.vue

@@ -128,7 +128,7 @@ export default {
       this.$router.push('/')
     },
     goToMessage() {
-      this.$router.push('/bg/account/message')
+      this.$router.push('/bg/my/message')
     }
   }
 }

+ 21 - 37
src/views/admin/aaa/AdminUserList.vue → src/views/admin/aaa/AdminAccountList.vue

@@ -13,6 +13,17 @@
           />
         </el-form-item>
 
+        <el-form-item label="用户 ID">
+          <el-input
+            v-model="queryParams.userId"
+            clearable
+            placeholder="输入用户 ID 搜索"
+            prefix-icon="el-icon-search"
+            class="search-input"
+            @keyup.enter.native="handleSearch"
+          />
+        </el-form-item>
+
         <el-form-item label="状态">
           <el-select
             v-model="queryParams.status"
@@ -22,8 +33,7 @@
             @change="handleSearch"
           >
             <el-option label="正常" :value="1" />
-            <el-option label="封禁中" :value="2" />
-            <el-option label="已注销" :value="3" />
+            <el-option label="禁用" :value="0" />
           </el-select>
         </el-form-item>
 
@@ -48,7 +58,6 @@
           fixed="left"
           label="No"
           type="index"
-          width="60"
           align="center"
         />
 
@@ -70,68 +79,42 @@
         <el-table-column
           prop="userId"
           label="用户 ID"
-          width="120"
           align="center"
         />
 
         <el-table-column
           prop="username"
           label="用户名"
-          min-width="120"
         />
 
         <el-table-column
           prop="screenName"
           label="显示名"
-          min-width="120"
         >
           <template slot-scope="scope">
-            <router-link :to="`/user/${scope.row.userId}`" class="user-link">
-              <span>{{ scope.row.screenName || '未设置' }}</span>
+            <router-link :to="`/user/${scope.row.userIdStr}`" class="user-link">
+              <span>{{ scope.row.screenName}}</span>
               <i class="el-icon-link link-icon" />
             </router-link>
           </template>
         </el-table-column>
 
         <el-table-column
-          prop="signature"
-          label="个性签名"
-          min-width="180"
-        >
-          <template slot-scope="scope">
-            <el-tooltip
-              v-if="scope.row.signature"
-              :content="scope.row.signature"
-              placement="top"
-              :open-delay="400"
-            >
-              <span class="ellipsis-text">{{ scope.row.signature }}</span>
-            </el-tooltip>
-            <span v-else class="text-muted">-</span>
-          </template>
-        </el-table-column>
-
-        <el-table-column
-          prop="pubDate"
+          prop="createAt"
           label="注册时间"
-          width="160"
           align="center"
         />
 
         <el-table-column
           label="帐号状态"
-          width="100"
           align="center"
         >
           <template slot-scope="scope">
             <el-tag v-if="scope.row.status === 1" type="success" size="mini" effect="light">
               正常
             </el-tag>
-            <el-tag v-else-if="scope.row.status === 2" type="danger" size="mini" effect="light">
-              封禁中
-            </el-tag>
-            <el-tag v-else type="info" size="mini" effect="light">
-              已注销
+            <el-tag v-else type="danger" size="mini" effect="light">
+              禁用
             </el-tag>
           </template>
         </el-table-column>
@@ -194,7 +177,7 @@
 </template>
 
 <script>
-import { getUserList } from '@/api/admin'
+import { getAccountList } from '@/api/admin'
 
 export default {
   name: 'AdminUserList',
@@ -203,7 +186,8 @@ export default {
       queryParams: {
         pn: 1,
         status: '', // 初始化为空字符串,以便触发 el-select 的 clearable
-        screenName: ''
+        screenName: '',
+        userId: ''
       },
       currentPage: 1,
       pageSize: 12,
@@ -225,7 +209,7 @@ export default {
       if (!params.screenName.trim()) delete params.screenName
       if (params.status === '') delete params.status
 
-      getUserList(params).then(resp => {
+      getAccountList(params).then(resp => {
         if (resp.code === 0) {
           const respData = resp.data
           this.dataList = respData.list || []

+ 15 - 1
src/views/admin/aaa/AdminVideoList.vue

@@ -218,7 +218,21 @@
                 icon="el-icon-finished"
                 class="btn-table-action btn-audit"
               >
-                去审核
+                审核
+              </el-button>
+            </router-link>
+            <router-link
+              :to="`/vod_edit/${scope.row.videoId}`"
+              target="_blank"
+              style="text-decoration: none; margin-right: 10px;"
+            >
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-edit"
+                class="btn-table-action btn-audit"
+              >
+                编辑
               </el-button>
             </router-link>
           </template>

+ 36 - 35
src/views/admin/aaa/VideoAudit.vue

@@ -187,24 +187,25 @@ export default {
       }
     }
   },
-  created() {
-    const videoId = this.$route.params.id
-    this.fetchNextVideo(videoId)
-  },
   watch: {
     // 🌟 【核心修复】当 URL 中的 videoId 发生变化时,优雅地清理并重新加载
     '$route.params.id': {
       handler(newVideoId) {
         if (newVideoId) {
           // 1. 先安全地把当前的播放器彻底断流、卸载,腾出硬件解码器空间
-          this.releaseVideoPlayer();
+          this.releaseVideoPlayer()
           // 2. 传入新的 ID,重新请求后端数据
-          this.fetchNextVideo(newVideoId);
+          this.fetchNextVideo(newVideoId)
         }
       },
       immediate: false // 首次进页面交给 created,后续切换交给 watch
     }
   },
+  created() {
+    document.title = 'VideoAudit'
+    const videoId = this.$route.params.id
+    this.fetchNextVideo(videoId)
+  },
   mounted() {
     // 全局监听键盘事件,开启极限盲审模式
     window.addEventListener('keydown', this.handleKeyboardShortcuts)
@@ -214,73 +215,73 @@ export default {
   },
   methods: {
     releaseVideoPlayer() {
-      const player = this.$refs.videoPlayer;
+      const player = this.$refs.videoPlayer
       if (player) {
         try {
-          player.pause();
-          player.src = '';                  // 切断视频网络流
-          player.removeAttribute('src');    // 彻底移除资源属性
-          player.load();                    // 强制浏览器对该 DOM 节点触发垃圾回收
+          player.pause()
+          player.src = '' // 切断视频网络流
+          player.removeAttribute('src') // 彻底移除资源属性
+          player.load() // 强制浏览器对该 DOM 节点触发垃圾回收
         } catch (e) {
-          console.warn('释放播放器时发生异常:', e);
+          console.warn('释放播放器时发生异常:', e)
         }
       }
     },
     // 获取下一条待审核视频
     // 获取下一条待审核视频
     fetchNextVideo(videoId) {
-      this.loading = true;
+      this.loading = true
       // 重置表单状态
-      this.auditForm = { decision: null, reasonTags: [], remark: '' };
-      this.currentTime = 0;
-      this.duration = 0;
+      this.auditForm = { decision: null, reasonTags: [], remark: '' }
+      this.currentTime = 0
+      this.duration = 0
 
-      const queryParams = { videoId };
+      const queryParams = { videoId }
       getAuditVideo(queryParams).then(resp => {
         if (resp.code === 0) {
-          this.currentVideo = resp.data;
-          this.auditForm.videoId = this.currentVideo.videoId;
+          this.currentVideo = resp.data
+          this.auditForm.videoId = this.currentVideo.videoId
 
           // 确保 DOM 渲染完新地址后,播放器干净利开跑
           this.$nextTick(() => {
-            const player = this.$refs.videoPlayer;
-            if (player) player.load();
-          });
+            const player = this.$refs.videoPlayer
+            if (player) player.load()
+          })
         } else {
-          this.$message.warning(resp.msg);
+          this.$message.warning(resp.msg)
         }
       }).finally(() => {
-        this.loading = false;
-      });
+        this.loading = false
+      })
     },
 
     // 提交审核决策
     submitAudit(status) {
-      this.auditForm.decision = status;
+      this.auditForm.decision = status
 
       if (status === this.auditResult.REVIEW_REJECTED && this.auditForm.reasonTags.length === 0 && !this.auditForm.remark.trim()) {
-        this.$message.error('请选择违规标签或填写不通过的具体审核意见!');
-        return;
+        this.$message.error('请选择违规标签或填写不通过的具体审核意见!')
+        return
       }
 
-      this.submitting = true;
+      this.submitting = true
       submitAuditVideo(this.auditForm).then(resp => {
         if (resp.code === 0) {
-          const nextVideoId = resp.data;
-          this.submitting = false;
+          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);
+          this.$router.push('/vod_audit/' + nextVideoId)
         }
       }).catch(() => {
-        this.submitting = false;
-      });
+        this.submitting = false
+      })
     },
 
     // 播放器时间更新

+ 540 - 0
src/views/admin/aaa/VideoEdit.vue

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

+ 2 - 0
src/views/user/UserHome.vue

@@ -246,6 +246,8 @@ export default {
           this.dataList = resp.data.list
           this.totalSize = resp.data.totalSize
           this.showEmpty = this.dataList.length === 0
+        } else {
+          this.$message.warning(resp.msg)
         }
       }).finally(() => { this.loading = false })
     },