2
0

7 Commits 242014392c ... d11d74d9d9

Autor SHA1 Mensagem Data
  wengan01 d11d74d9d9 1 há 3 semanas atrás
  wengan01 3eeaf62108 1 há 3 semanas atrás
  wengan01 83c2cf2a7c 1 há 3 semanas atrás
  wengan01 0b22f23f4c 代码功能修复 há 3 semanas atrás
  wengan01 c17914b019 Merge branch 'master' of http://115.190.200.22:3000/xpg/ui há 3 semanas atrás
  wengan01 70e731a15f 代码功能修复 há 1 mês atrás
  wengan01 1ef3bf90f6 代码功能修复 há 1 mês atrás

+ 41 - 7
src/api/system/physical/videoContent/index.ts

@@ -1,6 +1,6 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { VideoContentVO, VideoContentForm, VideoContentQuery } from '@/api/system/physical/videoContent/types';
+import { VideoContentVO, VideoContentForm, VideoContentQuery } from './types';
 
 /**
  * 查询视频内容信息列表
@@ -60,21 +60,55 @@ export const delVideoContent = (id: string | number | Array<string | number>) =>
     method: 'delete'
   });
 };
+
 /**
  * 上传视频
  * @param file 文件
+ * @param title 视频标题
+ * @param requiredPoints 所需积分
+ * @param previewDurationSeconds 试看时长
+ * @param subscriptionValidHours 订阅有效期
+ * @param categoryTagId 所属标签ID
  */
-export const uploadVideo = async (file: File) => {
+export const uploadVideo = async (file: File, title: string, requiredPoints: number, previewDurationSeconds: number, subscriptionValidHours: number, categoryTagId?: string | number) => {
   const formData = new FormData();
-  formData.append('file', file); // 后端接收的参数名是 "file"
+  formData.append('file', file);
+  formData.append('title', title);
+  formData.append('requiredPoints', String(requiredPoints));
+  formData.append('previewDurationSeconds', String(previewDurationSeconds));
+  formData.append('subscriptionValidHours', String(subscriptionValidHours));
+  if (categoryTagId !== undefined) {
+    formData.append('categoryTagId', String(categoryTagId));
+  }
+
   const res = await request({
     url: '/physical/videoContent/uploadVideo',
     method: 'POST',
     data: formData,
-    headers: {
-      'Content-Type': 'multipart/form-data'
-    }
+    headers: { 'Content-Type': 'multipart/form-data' }
   });
-
   return res;
 };
+
+/**
+ * 备用匹配接口
+ * @param videoId 视频ID
+ * @returns {*}
+ */
+export const matchFromBackup = (videoId: string | number): AxiosPromise => {
+  return request({
+    url: `/physical/videoContent/matchFromBackup/${videoId}`,
+    method: 'POST'
+  });
+};
+
+/**
+ * 获取视频时长
+ * @param id 视频ID
+ */
+export const getVideoDuration = (id: string | number): AxiosPromise<number> => {
+  return request({
+    url: `/physical/videoContent/getVideoDuration/${id}`,
+    method: 'GET'
+  });
+};

+ 28 - 4
src/api/system/physical/videoContent/types.ts

@@ -4,6 +4,11 @@ export interface VideoContentVO {
    */
   id: string | number;
 
+  /**
+   * 视频文件名
+   */
+  videoFileName: string;
+
   /**
    * 视频标题
    */
@@ -32,9 +37,9 @@ export interface VideoContentVO {
   /**
    * 视频文件在阿里云OSS上的完整访问URL
    */
-  ossVideoUrl: string | number;
+  ossVideoUrl: string;
 
-  videoTempUrl: string | number;
+  videoTempUrl: string;
 
   videoCoverUrl: string;
 
@@ -57,6 +62,10 @@ export interface VideoContentVO {
    * 视频oss_id
    */
   ossId: string | number;
+
+  videoTrueSeeCount?: number;
+
+  serviceTagName?: string;
 }
 
 export interface VideoContentForm extends BaseEntity {
@@ -65,6 +74,11 @@ export interface VideoContentForm extends BaseEntity {
    */
   id?: string | number;
 
+  /**
+   * 视频文件名
+   */
+  videoFileName?: string;
+
   /**
    * 视频标题
    */
@@ -94,7 +108,7 @@ export interface VideoContentForm extends BaseEntity {
   /**
    * 视频文件在阿里云OSS上的完整访问URL
    */
-  ossVideoUrl?: string | number;
+  ossVideoUrl?: string;
 
   /**
    * 视频状态:up=已上架(可被用户查看/购买),down=已下架(不可见)
@@ -122,6 +136,16 @@ export interface VideoContentForm extends BaseEntity {
 }
 
 export interface VideoContentQuery extends PageQuery {
+  /**
+   * 排序字段
+   */
+  orderBy?: string;
+
+  /**
+   * 排序方向(asc/desc)
+   */
+  orderDirection?: string;
+
   /**
    * 视频标题
    */
@@ -150,7 +174,7 @@ export interface VideoContentQuery extends PageQuery {
   /**
    * 视频文件在阿里云OSS上的完整访问URL
    */
-  ossVideoUrl?: string | number;
+  ossVideoUrl?: string;
 
   /**
    * 视频状态:up=已上架(可被用户查看/购买),down=已下架(不可见)

+ 9 - 8
src/views/system/business/tournaments/index.vue

@@ -1248,14 +1248,15 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  // 设置默认开始时间为今天
-  const today = new Date();
-  const yyyy = today.getFullYear();
-  const mm = String(today.getMonth() + 1).padStart(2, '0'); // 月份从0开始,所以+1
-  const dd = String(today.getDate()).padStart(2, '0');
-  queryParams.value.startTime = `${yyyy}-${mm}-${dd}`;
-  handleQuery();
+  // 将所有查询参数重置为空
+  queryParams.value.id = '';
+  queryParams.value.name = '';
+  queryParams.value.status = '';
+  queryParams.value.startTime = '';
+  // 重置页码
+  queryParams.value.pageNum = 1;
+  // 直接执行搜索
+  getList();
 };
 
 /** 多选框选中数据 */

+ 10 - 3
src/views/system/physical/leagueTournament/index.vue

@@ -257,7 +257,7 @@
 </template>
 
 <script setup name="LeagueTournament" lang="ts">
-import { getCurrentInstance, ref, reactive, toRefs, onMounted } from 'vue';
+import { getCurrentInstance, ref, reactive, toRefs, onMounted, nextTick } from 'vue';
 import type { ComponentInternalInstance } from 'vue';
 import {
   listLeagueTournament,
@@ -364,8 +364,15 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  handleQuery();
+  // 将所有查询参数重置为空
+  queryParams.value.title = '';
+  queryParams.value.startTime = '';
+  queryParams.value.endTime = '';
+  queryParams.value.location = '';
+  // 重置页码
+  queryParams.value.pageNum = 1;
+  // 直接执行搜索
+  getList();
 };
 
 /** 多选框选中数据 */

+ 28 - 24
src/views/system/physical/participants/index.vue

@@ -5,7 +5,7 @@
         <el-card shadow="hover">
           <el-form ref="queryFormRef" :model="queryParams" :inline="true">
             <el-row :gutter="10" class="mb8">
-              <el-form-item label="赛事ID" prop="userName">
+              <el-form-item label="赛事ID" prop="tournamentId">
                 <el-input
                   v-model="queryParams.tournamentId"
                   style="width: 300px; min-width: 300px"
@@ -16,7 +16,7 @@
               </el-form-item>
               <el-form-item label="报名用户" prop="userName">
                 <el-input
-                  v-model="queryParams.userName"
+                  v-model="queryParams.name"
                   style="width: 300px; min-width: 300px"
                   placeholder="请输入报名用户"
                   clearable
@@ -42,8 +42,9 @@
                   type="date"
                   value-format="YYYY-MM-DD"
                   placeholder="请选择报名时间"
-                />
+                ></el-date-picker>
               </el-form-item>
+
               <el-form-item>
                 <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                 <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -57,19 +58,6 @@
     <el-card shadow="never">
       <template #header>
         <el-row :gutter="10" class="mb8">
-          <!--          <el-col :span="1.5">
-            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['physical:participants:add']">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['physical:participants:edit']"
-              >修改</el-button
-            >
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['physical:participants:remove']"
-              >删除</el-button
-            >
-          </el-col>-->
           <el-col :span="1.5">
             <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['physical:participants:export']">导出</el-button>
           </el-col>
@@ -101,7 +89,6 @@
 
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
-    <!-- 添加或修改线下用户报名对话框 -->
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
       <el-form ref="participantsFormRef" :model="form" :rules="rules" label-width="80px">
         <el-form-item label="" prop="tournamentId">
@@ -157,6 +144,9 @@
 </template>
 
 <script setup name="Participants" lang="ts">
+import { ref, reactive, onMounted, toRefs } from 'vue';
+import { ElFormInstance } from 'element-plus';
+import { ComponentInternalInstance, getCurrentInstance } from 'vue';
 import {
   listParticipants,
   getParticipants,
@@ -167,6 +157,7 @@ import {
 } from '@/api/system/physical/participants';
 import { ParticipantsVO, ParticipantsQuery, ParticipantsForm } from '@/api/system/physical/participants/types';
 import { parseTime } from '@/utils/dateUtils';
+
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const participantsList = ref<ParticipantsVO[]>([]);
@@ -181,7 +172,7 @@ const total = ref(0);
 const queryFormRef = ref<ElFormInstance>();
 const participantsFormRef = ref<ElFormInstance>();
 
-const dialog = reactive<DialogOption>({
+const dialog = reactive({
   visible: false,
   title: ''
 });
@@ -201,20 +192,27 @@ const initFormData: ParticipantsForm = {
   finalRank: undefined,
   finalReward: undefined
 };
+
+interface PageData<T, Q> {
+  form: T;
+  queryParams: Q & { pageNum: number; pageSize: number; params?: {} };
+  rules: Record<string, unknown[]>;
+}
+
 const data = reactive<PageData<ParticipantsForm, ParticipantsQuery>>({
   form: { ...initFormData },
   queryParams: {
     pageNum: 1,
     pageSize: 10,
-    tournamentId: undefined,
+    tournamentId: '',
     playerId: undefined,
-    name: undefined,
-    mobile: undefined,
+    name: '',
+    mobile: '',
     avatar: undefined,
     currentChips: undefined,
     rebuy: undefined,
     eliminatedTime: undefined,
-    registrationTime: undefined,
+    registrationTime: '',
     status: undefined,
     finalRank: undefined,
     finalReward: undefined,
@@ -260,8 +258,12 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  handleQuery();
+  queryParams.value.tournamentId = '';
+  queryParams.value.name = '';
+  queryParams.value.mobile = '';
+  queryParams.value.registrationTime = '';
+  queryParams.value.pageNum = 1;
+  getList();
 };
 
 /** 多选框选中数据 */
@@ -287,6 +289,7 @@ const handleUpdate = async (row?: ParticipantsVO) => {
   dialog.visible = true;
   dialog.title = '修改线下用户报名';
 };
+
 //晋级
 const getNextTournament = async (row?: ParticipantsVO) => {
   try {
@@ -309,6 +312,7 @@ const getNextTournament = async (row?: ParticipantsVO) => {
     }
   }
 };
+
 /** 提交按钮 */
 const submitForm = () => {
   participantsFormRef.value?.validate(async (valid: boolean) => {

Diff do ficheiro suprimidas por serem muito extensas
+ 291 - 653
src/views/system/physical/store/index.vue


+ 23 - 25
src/views/system/physical/tag/index.vue

@@ -39,9 +39,6 @@
               >删除</el-button
             >
           </el-col>
-          <!--          <el-col :span="1.5">
-            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['physical:tag:export']">导出</el-button>
-          </el-col>-->
           <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
         </el-row>
       </template>
@@ -85,13 +82,6 @@
         <el-form-item label="服务ID" prop="serviceCode">
           <el-input v-model="form.serviceCode" placeholder="请输入服务ID" />
         </el-form-item>
-<!--        <el-form-item label="背景色" prop="colorCode">
-          <el-input v-model="form.colorCode" placeholder="请输入背景色或文字色,如 #FF6B35">
-            <template #append>
-              <el-color-picker v-model="form.colorCode" />
-            </template>
-          </el-input>
-        </el-form-item>-->
         <el-form-item label="是否启用" prop="isEnabled">
           <el-select v-model="form.isEnabled" placeholder="请选择是否启用">
             <el-option label="是" value="1" />
@@ -99,7 +89,7 @@
           </el-select>
         </el-form-item>
         <el-form-item label="排序权重" prop="sortOrder">
-          <el-input v-model="form.sortOrder" placeholder="请输入排序权重,越小越靠前" />
+          <el-input v-model.number="form.sortOrder" placeholder="请输入排序权重,越小越靠前" />
         </el-form-item>
         <el-form-item label="备注" prop="remark">
           <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
@@ -116,6 +106,9 @@
 </template>
 
 <script setup name="Tag" lang="ts">
+import { ref, reactive, onMounted, toRefs } from 'vue';
+import { ElFormInstance } from 'element-plus';
+import { ComponentInternalInstance, getCurrentInstance } from 'vue';
 import { listTag, getTag, delTag, addTag, updateTag } from '@/api/system/physical/tag';
 import { TagVO, TagQuery, TagForm } from '@/api/system/physical/tag/types';
 
@@ -141,6 +134,7 @@ const dialog = reactive<DialogOption>({
 const initFormData: TagForm = {
   id: undefined,
   serviceName: undefined,
+  serviceCode: undefined,
   iconUrl: undefined,
   colorCode: undefined,
   isEnabled: '1',
@@ -151,12 +145,14 @@ const initFormData: TagForm = {
   createdBy: undefined,
   updatedBy: undefined
 };
+
 const data = reactive<PageData<TagForm, TagQuery>>({
   form: { ...initFormData },
   queryParams: {
     pageNum: 1,
     pageSize: 10,
     serviceName: undefined,
+    serviceCode: undefined,
     iconUrl: undefined,
     colorCode: undefined,
     isEnabled: undefined,
@@ -169,8 +165,13 @@ const data = reactive<PageData<TagForm, TagQuery>>({
   },
   rules: {
     id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
-    serviceName: [{ required: true, message: '不能为空', trigger: 'blur' }],
-    serviceCode: [{ required: true, message: '不能为空', trigger: 'blur' }]
+    serviceName: [{ required: true, message: '服务名称不能为空', trigger: 'blur' }],
+    serviceCode: [{ required: true, message: '服务ID不能为空', trigger: 'blur' }],
+    sortOrder: [
+      { required: true, message: '排序权重不能为空', trigger: 'blur' },
+      { type: 'number', message: '排序权重必须为数字', trigger: 'blur' },
+      { min: 0, max: 9999, message: '排序权重必须在0-9999之间', trigger: 'blur' }
+    ]
   }
 });
 
@@ -193,7 +194,15 @@ const cancel = () => {
 
 /** 表单重置 */
 const reset = () => {
-  form.value = { ...initFormData };
+  // 逐个属性重置,保持响应式连接
+  form.value.id = undefined;
+  form.value.serviceName = undefined;
+  form.value.serviceCode = undefined;
+  form.value.iconUrl = undefined;
+  form.value.colorCode = undefined;
+  form.value.isEnabled = '1';
+  form.value.sortOrder = undefined;
+  form.value.remark = undefined;
   tagFormRef.value?.resetFields();
 };
 
@@ -260,17 +269,6 @@ const handleDelete = async (row?: TagVO) => {
   await getList();
 };
 
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download(
-    'physical/tag/export',
-    {
-      ...queryParams.value
-    },
-    `tag_${new Date().getTime()}.xlsx`
-  );
-};
-
 onMounted(() => {
   getList();
 });

+ 12 - 4
src/views/system/physical/tournaments/index.vue

@@ -518,7 +518,7 @@ import LevelsIndex from '@/views/system/physical/blindLevels/index.vue';
 import { LeagueTournamentVO } from '@/api/system/physical/leagueTournament/types';
 import { JudgeVO } from '@/api/system/physical/judge/types';
 import { ElSelect } from 'element-plus';
-import { ref } from 'vue';
+import { ref, nextTick } from 'vue';
 import { CompetitionZoneVO } from '@/api/system/physical/competitionZone/types';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { game_variant_type, tournaments_time, physical_tournaments_type, qualifier_type } = toRefs<any>(
@@ -618,7 +618,7 @@ const data = reactive<PageData<TournamentsForm, TournamentsQuery>>({
     qualifierValue: undefined,
     params: {},
     leagueTournamentId: undefined, // ← 新增字段
-    startTimeOne: parseTime(new Date(), '{y}-{m}-{d}') // 默认今天
+    startTimeOne: ''
   },
   rules: {
     id: [{ required: true, message: '不能为空', trigger: 'blur' }],
@@ -800,10 +800,18 @@ const handleQuery = () => {
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  handleQuery();
+  // 将所有查询参数重置为空
+  queryParams.value.id = '';
+  queryParams.value.name = '';
+  queryParams.value.startTimeOne = '';
+  queryParams.value.tournamentType = '';
+  // 重置页码
+  queryParams.value.pageNum = 1;
+  // 直接执行搜索
+  getList();
 };
 
+
 /** 多选框选中数据 */
 const handleSelectionChange = (selection: TournamentsVO[]) => {
   ids.value = selection.map((item) => item.id);

+ 352 - 333
src/views/system/physical/videoContent/index.vue

@@ -29,47 +29,45 @@
             <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['physical:videoContent:add']">新增</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['physical:videoContent:edit']"
-              >修改</el-button
-            >
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['physical:videoContent:edit']">修改</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['physical:videoContent:remove']"
-              >删除</el-button
-            >
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['physical:videoContent:remove']">删除</el-button>
+          </el-col>
+          <el-col :span="2">
+            <el-button type="warning" plain icon="Wrench" @click="handleFixAll" v-hasPermi="['physical:videoContent:edit']">修复所有数据</el-button>
           </el-col>
-<!--          <el-col :span="1.5">
-            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['physical:videoContent:export']">导出</el-button>
-          </el-col>-->
           <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
         </el-row>
       </template>
 
       <el-table v-loading="loading" border :data="videoContentList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="编号" align="center" prop="id" v-if="true" />
+        <el-table-column label="编号" align="center" prop="id" />
         <el-table-column label="视频标题" align="center" prop="title" />
+        <el-table-column label="文件名" align="center" prop="videoFileName" />
         <el-table-column label="封面" align="center" width="90">
           <template #default="scope">
-            <el-image
-              v-if="scope.row.videoCoverUrl"
-              :src="scope.row.videoCoverUrl"
-              style="width: 40px; height: 40px; border-radius: 4px; cursor: zoom-in"
-              :preview-src-list="[scope.row.videoCoverUrl]"
-              :preview-teleported="true"
-              fit="cover"
-            />
-            <span v-else></span>
+            <el-image v-if="scope.row.videoCoverUrl" :src="scope.row.videoCoverUrl" style="width: 40px; height: 40px; border-radius: 4px" fit="cover" />
           </template>
         </el-table-column>
         <el-table-column label="观看人数" align="center" prop="videoSeeCount" />
-        <el-table-column label="实际观看数" align="center" prop="videoTrueSeeCount" />
         <el-table-column label="所需视频点" align="center" prop="requiredPoints" />
         <el-table-column label="所属标签" align="center" prop="serviceTagName" />
-        <el-table-column label="视频时长(秒)" align="center" prop="durationSeconds" />
+        <el-table-column label="视频时长" align="center">
+          <template #default="{ row }">
+            {{ row.durationSeconds ? formatDuration(row.durationSeconds) : '待更新' }}
+          </template>
+        </el-table-column>
         <el-table-column label="试看时长(秒)" align="center" prop="previewDurationSeconds" />
         <el-table-column label="订阅时效(天)" align="center" prop="subscriptionValidHours" />
-        <!--        <el-table-column label="视频文件在阿里云OSS上的完整访问URL" align="center" prop="ossVideoUrl" />-->
+        <el-table-column label="数据状态" align="center">
+          <template #default="{ row }">
+            <el-tag :type="isVideoDataValid(row) ? 'success' : 'danger'">
+              {{ isVideoDataValid(row) ? '正常' : '异常' }}
+            </el-tag>
+          </template>
+        </el-table-column>
         <el-table-column label="视频状态" align="center">
           <template #default="{ row }">
             <el-tag :type="row.status === 'up' ? 'success' : 'info'">
@@ -78,129 +76,90 @@
           </template>
         </el-table-column>
         <el-table-column label="创建时间" align="center" prop="createdAt" width="180">
-          <template #default="scope">
-            <span>{{ parseTime(scope.row.createdAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
-          </template>
+          <template #default="scope">{{ parseTime(scope.row.createdAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</template>
         </el-table-column>
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <el-table-column label="操作" align="center" width="80">
           <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['physical:videoContent:edit']"></el-button>
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['physical:videoContent:remove']"></el-button>
-            </el-tooltip>
+            <el-dropdown trigger="click" @command="(cmd) => handleOperation(cmd, scope.row)">
+              <el-button type="text" size="small" style="color: #409eff;">操作</el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item command="edit" v-hasPermi="['physical:videoContent:edit']">修改</el-dropdown-item>
+                  <el-dropdown-item command="delete" v-hasPermi="['physical:videoContent:remove']">删除</el-dropdown-item>
+                  <el-dropdown-item command="match" v-hasPermi="['physical:videoContent:edit']">异步获取</el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
           </template>
         </el-table-column>
       </el-table>
 
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
+
     <!-- 添加或修改视频内容信息对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
       <el-form ref="videoContentFormRef" :model="form" :rules="rules" label-width="120px">
         <el-form-item label="视频标题" prop="title">
           <el-input v-model="form.title" placeholder="请输入视频标题" />
         </el-form-item>
         <el-form-item label="视频封面图" prop="videoCoverUrl">
-          <div class="upload-container">
-            <el-upload
-              class="upload-icon"
-              action="#"
-              :on-change="handleVideoCoverUrlChange"
-              :on-remove="handleVideoCoverUrlRemove"
-              :file-list="videoCoverUrlFileList"
-              :auto-upload="false"
-              :limit="1"
-              accept="image/*"
-            >
-              <template #trigger>
-                <el-button type="primary">点击选择封面图</el-button>
-              </template>
-
-              <template #default>
-                <div class="preview-area" @click="handleVideoCoverUrlPreviewClick">
-                  <img
-                    v-if="videoCoverUrlPreviewUrl || form.videoCoverUrl"
-                    :src="videoCoverUrlPreviewUrl || form.videoCoverUrl"
-                    alt="预览图"
-                    style="max-width: 100px; max-height: 100px; margin-top: 10px; cursor: pointer"
-                  />
-                  <div v-else style="margin-top: 10px; color: #999">无图片</div>
-                </div>
-              </template>
-              <template #tip>
-                <div class="el-upload__tip">
-                  <span v-if="videoCoverUrlFileList.length > 0">当前已选文件:{{ videoCoverUrlFileList[0].name }}</span>
-                </div>
-              </template>
-            </el-upload>
-          </div>
+          <el-upload
+            class="upload-icon"
+            action="#"
+            :on-change="handleVideoCoverUrlChange"
+            :on-remove="handleVideoCoverUrlRemove"
+            :file-list="videoCoverUrlFileList"
+            :auto-upload="false"
+            :limit="1"
+            accept="image/*"
+          >
+            <template #trigger><el-button type="primary">点击选择封面图</el-button></template>
+            <template #default>
+              <div v-if="videoCoverUrlPreviewUrl || form.videoCoverUrl" style="margin-top: 10px">
+                <img :src="videoCoverUrlPreviewUrl || form.videoCoverUrl" style="max-width: 100px; max-height: 100px" />
+                <el-button type="danger" size="small" icon="Delete" @click="handleVideoCoverUrlRemove" style="margin-left: 10px;">删除</el-button>
+              </div>
+              <div v-else style="margin-top: 10px; color: #999">无图片</div>
+            </template>
+          </el-upload>
         </el-form-item>
         <el-form-item label="视频文件" prop="ossVideoUrl">
-          <div class="upload-container">
-            <el-upload
-              class="upload-icon"
-              action="#"
-              :on-change="handleOssVideoUrlChange"
-              :on-remove="handleOssVideoUrlRemove"
-              :file-list="ossVideoUrlFileList"
-              :auto-upload="false"
-              :limit="1"
-              accept="video/*"
-            >
-              <template #trigger>
-                <el-button type="primary">点击选择视频</el-button>
+          <el-upload
+            class="upload-icon"
+            action="#"
+            :on-change="handleOssVideoSelect"
+            :on-remove="handleOssVideoUrlRemove"
+            :file-list="ossVideoUrlFileList"
+            :auto-upload="false"
+            :limit="1"
+            accept="video/*"
+          >
+            <template #trigger><el-button type="primary">点击上传视频</el-button></template>
+            <template #default>
+              <template v-if="uploading && uploadProgress < 100">
+                <el-progress :percentage="uploadProgress" :stroke-width="20" style="margin-top: 10px; width: 300px" />
+                <div style="font-size: 12px; color: #666; margin-top: 5px">视频上传中,请稍候...</div>
               </template>
-
-              <template #default>
-                <div class="preview-area">
-                  <video
-                    v-if="ossVideoUrlPreviewUrl || form.ossVideoUrl"
-                    :src="ossVideoUrlPreviewUrl || String(form.ossVideoUrl || '')"
-                    controls
-                    style="max-width: 300px; max-height: 200px; margin-top: 10px"
-                  />
-                  <div v-else style="margin-top: 10px; color: #999">无视频</div>
-                  <el-button
-                    v-if="ossVideoUrlPreviewUrl || form.ossVideoUrl"
-                    type="danger"
-                    size="small"
-                    icon="Delete"
-                    @click="handleOssVideoUrlRemove"
-                    style="margin-top: 10px"
-                  >
-                    删除视频
-                  </el-button>
-                </div>
+              <template v-else-if="uploading">
+                <el-progress :percentage="100" :stroke-width="20" status="success" style="margin-top: 10px; width: 300px" />
+                <div style="font-size: 12px; color: #67c23a; margin-top: 5px">视频上传完成</div>
+                <el-button v-if="form.videoFileName" type="danger" size="small" icon="Delete" @click="handleOssVideoUrlRemove" style="margin-top: 10px">删除视频</el-button>
               </template>
-
-              <template #tip>
-                <div class="el-upload__tip">
-                  <span v-if="ossVideoUrlFileList.length > 0">当前已选文件:{{ ossVideoUrlFileList[0].name }}</span>
-                </div>
+              <template v-else>
+                <div v-if="form.videoFileName" style="margin-top: 10px; color: #666">已选择: {{ form.videoFileName }}</div>
+                <div v-if="form.durationSeconds" style="font-size: 12px; color: #999">时长: {{ formatDuration(form.durationSeconds) }}</div>
+                <el-button v-if="form.videoFileName" type="danger" size="small" icon="Delete" @click="handleOssVideoUrlRemove" style="margin-top: 10px">删除视频</el-button>
               </template>
-            </el-upload>
-          </div>
-        </el-form-item>
-        <el-form-item label="观看数">
-          <el-input v-model="form.videoSeeCount" placeholder="请输入观看数" />
+            </template>
+          </el-upload>
         </el-form-item>
         <el-form-item label="所需视频点" prop="requiredPoints">
           <el-input v-model="form.requiredPoints" placeholder="请输入所需视频点" />
         </el-form-item>
         <el-form-item label="试看时长 (秒)" prop="previewDurationSeconds">
-          <div>
-            <el-input v-model="form.previewDurationSeconds" placeholder="请输入试看时长 (秒)" style="width: 100%" />
-            <div style="margin-top: 4px; font-size: 12px; color: #999">
-              <el-icon style="vertical-align: middle"><Info-Filled /></el-icon>
-              温馨提示:输入 -1 表示免费观看
-            </div>
-          </div>
+          <el-input v-model="form.previewDurationSeconds" placeholder="请输入试看时长 (秒)" />
         </el-form-item>
-<!--        <el-form-item label="试看时长(秒)" prop="previewDurationSeconds">
-          <el-input v-model="form.previewDurationSeconds" placeholder="请输入试看时长(秒)" />
-        </el-form-item>-->
         <el-form-item label="订阅时效(天)" prop="subscriptionValidHours">
           <el-input v-model="form.subscriptionValidHours" placeholder="请输入订阅时效(天)" />
         </el-form-item>
@@ -217,31 +176,29 @@
         </el-form-item>
       </el-form>
       <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
-        </div>
+        <el-button :loading="buttonLoading" :disabled="uploading || !isVideoReady" type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
       </template>
     </el-dialog>
   </div>
 </template>
 
 <script setup name="VideoContent" lang="ts">
-import {
-  listVideoContent,
-  getVideoContent,
-  delVideoContent,
-  addVideoContent,
-  updateVideoContent,
-  uploadVideo
-} from '@/api/system/physical/videoContent';
-import { VideoContentVO, VideoContentQuery, VideoContentForm } from '@/api/system/physical/videoContent/types';
-import { ServiceTabVO } from '@/api/system/physical/serviceTab/types';
+import { ref, reactive, onMounted, toRefs, computed } from 'vue';
+import { ElFormInstance, ElMessageBox } from 'element-plus';
+import request from '@/utils/request';
+import { getCurrentInstance } from 'vue';
+import type { ComponentInternalInstance } from 'vue';
+
+import { listVideoContent, getVideoContent, delVideoContent, addVideoContent, updateVideoContent, matchFromBackup } from '@/api/system/physical/videoContent';
+import type { VideoContentVO, VideoContentForm, VideoContentQuery } from '@/api/system/physical/videoContent/types';
 import { selectEnabledTabsByCategoryList } from '@/api/system/physical/serviceTab';
+import type { ServiceTabVO } from '@/api/system/physical/serviceTab/types';
 import { uploadTournament } from '@/api/system/business/tournaments';
+
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-import { InfoFilled } from '@element-plus/icons-vue';
-import { ElMessageBox } from 'element-plus';
+
+// 状态管理
 const videoContentList = ref<VideoContentVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
@@ -254,284 +211,346 @@ const total = ref(0);
 const queryFormRef = ref<ElFormInstance>();
 const videoContentFormRef = ref<ElFormInstance>();
 
-const dialog = reactive<DialogOption>({
-  visible: false,
-  title: ''
-});
+const dialog = reactive<DialogOption>({ visible: false, title: '' });
 
 const initFormData: VideoContentForm = {
-  id: undefined,
-  title: undefined,
-  requiredPoints: undefined,
-  durationSeconds: undefined,
-  previewDurationSeconds: undefined,
-  subscriptionValidHours: undefined,
-  ossVideoUrl: undefined,
-  status: 'up',
-  createdAt: undefined,
-  updatedAt: undefined,
-  ossId: undefined,
-  videoCoverUrl: undefined
+  id: undefined, videoFileName: undefined, title: undefined, requiredPoints: undefined,
+  durationSeconds: undefined, previewDurationSeconds: undefined, subscriptionValidHours: undefined,
+  ossVideoUrl: undefined, videoUrl: undefined, videoTempUrl: undefined, tempUrl: undefined,
+  status: 'up', createdAt: undefined, updatedAt: undefined, ossId: undefined,
+  videoCoverUrl: undefined, videoSeeCount: undefined, categoryTagId: undefined
 };
+
 const data = reactive<PageData<VideoContentForm, VideoContentQuery>>({
   form: { ...initFormData },
   queryParams: {
-    pageNum: 1,
-    pageSize: 10,
-    title: undefined,
-    requiredPoints: undefined,
-    durationSeconds: undefined,
-    previewDurationSeconds: undefined,
-    subscriptionValidHours: undefined,
-    status: undefined,
-    createdAt: undefined,
-    updatedAt: undefined,
-    ossId: undefined,
-    params: {}
+    pageNum: 1, pageSize: 10, title: undefined, requiredPoints: undefined,
+    durationSeconds: undefined, previewDurationSeconds: undefined, subscriptionValidHours: undefined,
+    status: undefined, createdAt: undefined, updatedAt: undefined, ossId: undefined,
+    orderBy: 'createdAt', orderDirection: 'desc', params: {}
   },
   rules: {
-    id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
     title: [{ required: true, message: '视频标题不能为空', trigger: 'blur' }],
-    requiredPoints: [{ required: true, message: '观看完整视频所需消耗的视频点数不能为空', trigger: 'blur' }],
-    durationSeconds: [{ required: true, message: '视频总时长,单位:秒不能为空', trigger: 'blur' }],
-    previewDurationSeconds: [{ required: true, message: '免费试看时长,单位:秒不能为空', trigger: 'blur' }],
-    subscriptionValidHours: [{ required: true, message: '订阅后可观看的有效期,单位:小时不能为空', trigger: 'blur' }],
-/*
-    ossVideoUrl: [{ required: true, message: '视频文件在阿里云OSS上的完整访问URL不能为空', trigger: 'blur' }],
-*/
+    requiredPoints: [{ required: true, message: '所需视频点数不能为空', trigger: 'blur' }],
+    previewDurationSeconds: [{ required: true, message: '试看时长不能为空', trigger: 'blur' }],
+    subscriptionValidHours: [{ required: true, message: '订阅时效不能为空', trigger: 'blur' }],
     videoCoverUrl: [{ required: true, message: '视频封面不能为空', trigger: 'blur' }],
-    status: [{ required: true, message: '视频状态:up=已上架不能为空', trigger: 'change' }],
-    categoryTagId: [{ required: true, message: '不能为空', trigger: 'blur' }]
+    status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
+    categoryTagId: [{ required: true, message: '所属标签不能为空', trigger: 'blur' }]
   }
 });
 
 const { queryParams, form, rules } = toRefs(data);
 
-/** 查询视频内容信息列表 */
+// 上传状态
+const uploading = ref(false);
+const uploadProgress = ref(0);
+const videoUploaded = ref(false);
+
+// 防止重复提交
+const isSubmitting = ref(false);
+
+// 文件列表
+const ossVideoUrlFileList = ref<any[]>([]);
+const videoCoverUrlFileList = ref<any[]>([]);
+const videoCoverUrlPreviewUrl = ref('');
+
+// 页签选项
+const serviceTabOptions = ref<ServiceTabVO[]>([]);
+
+// 视频是否准备好
+const isVideoReady = computed(() => {
+  // 无论新增还是编辑模式,只要有视频文件名或视频已上传完成即可
+  // 如果上传进度已完成(100%),即使还在处理中也允许保存
+  if (uploading.value && uploadProgress.value < 100) return false;
+  // 如果已经有视频文件名或视频已上传完成,则可以保存
+  return !!form.value.videoFileName || videoUploaded.value;
+});
+
+// 格式化时长
+const formatDuration = (seconds: number): string => {
+  if (!seconds || seconds < 0) return '00:00';
+  const hrs = Math.floor(seconds / 3600);
+  const mins = Math.floor((seconds % 3600) / 60);
+  const secs = Math.floor(seconds % 60);
+  if (hrs > 0) {
+    return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+  }
+  return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+};
+
+// 检查视频数据是否有效
+const isVideoDataValid = (row: VideoContentVO): boolean => {
+  return row.ossId && row.ossId > 0 && row.ossVideoUrl && String(row.ossVideoUrl).startsWith('http') && row.durationSeconds && row.durationSeconds > 0;
+};
+
+// 数据查询
 const getList = async () => {
   loading.value = true;
-  const res = await listVideoContent(queryParams.value);
-  videoContentList.value = res.rows;
-  total.value = res.total;
-  loading.value = false;
+  try {
+    const res = await listVideoContent(queryParams.value);
+    videoContentList.value = res.rows;
+    total.value = res.total;
+  } catch (error: any) {
+    proxy?.$modal.msgError('数据加载失败:' + (error.message || '未知错误'));
+  } finally {
+    loading.value = false;
+  }
 };
 
-/** 取消按钮 */
-const cancel = () => {
-  reset();
-  dialog.visible = false;
+const handleQuery = () => { queryParams.value.pageNum = 1; getList(); };
+const resetQuery = () => { queryFormRef.value?.resetFields(); handleQuery(); };
+const handleSelectionChange = (selection: VideoContentVO[]) => {
+  ids.value = selection.map(item => item.id);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
 };
 
-/** 表单重置 */
+// 表单操作
 const reset = () => {
+  // 重置表单数据
   form.value = { ...initFormData };
   videoContentFormRef.value?.resetFields();
+
+  // 清空文件列表
   ossVideoUrlFileList.value = [];
-  ossVideoUrlPreviewUrl.value = '';
   videoCoverUrlFileList.value = [];
   videoCoverUrlPreviewUrl.value = '';
-};
 
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.value.pageNum = 1;
-  getList();
-};
+  // 重置上传状态
+  uploading.value = false;
+  uploadProgress.value = 0;
+  videoUploaded.value = false;
 
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  handleQuery();
+  // 确保清除任何可能的提交状态
+  isSubmitting.value = false;
 };
 
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: VideoContentVO[]) => {
-  ids.value = selection.map((item) => item.id);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-};
+const cancel = () => { reset(); dialog.visible = false; };
 
-/** 新增按钮操作 */
 const handleAdd = () => {
   reset();
+  // 确保在新增模式下 id 为 undefined
+  form.value.id = undefined;
   dialog.visible = true;
   dialog.title = '添加';
 };
 
-/** 修改按钮操作 */
 const handleUpdate = async (row?: VideoContentVO) => {
   reset();
   const _id = row?.id || ids.value[0];
   const res = await getVideoContent(_id);
   Object.assign(form.value, res.data);
-  const videoTempUrl2 = res.data?.videoTempUrl || res.data?.ossVideoUrl || '';
-  ossVideoUrlPreviewUrl.value = String(videoTempUrl2);
-
   if (res.data.videoCoverUrl) {
-    videoCoverUrlFileList.value = [
-      {
-        name: '已上传封面图',
-        url: res.data.videoCoverUrl,
-        status: 'success'
-      }
-    ];
+    videoCoverUrlFileList.value = [{ name: '已上传封面图', url: res.data.videoCoverUrl, status: 'success' }];
     videoCoverUrlPreviewUrl.value = res.data.videoCoverUrl;
   }
-
+  // 编辑模式下,如果有视频文件则标记为已上传
+  if (res.data.videoFileName) {
+    videoUploaded.value = true;
+  }
   dialog.visible = true;
   dialog.title = '修改';
 };
 
-/** 提交按钮 */
-const submitForm = () => {
-  videoContentFormRef.value?.validate(async (valid: boolean) => {
+const submitForm = async () => {
+  if (isSubmitting.value) return;
+
+  videoContentFormRef.value?.validate(async (valid) => {
     if (valid) {
+      isSubmitting.value = true;
       buttonLoading.value = true;
-      if (form.value.id) {
-        await updateVideoContent(form.value).finally(() => (buttonLoading.value = false));
-      } else {
-        await addVideoContent(form.value).finally(() => (buttonLoading.value = false));
+
+      try {
+        // 如果视频上传进度已完成(100%),但仍在处理中,等待处理完成
+        if (uploading.value && uploadProgress.value === 100) {
+          proxy?.$modal.msgInfo('视频上传处理中,请稍候...');
+          // 等待上传完成,最多等待一定时间
+          let attempts = 0;
+          const maxAttempts = 30; // 最多等待15秒(每0.5秒检查一次)
+          while (uploading.value && attempts < maxAttempts) {
+            await new Promise(resolve => setTimeout(resolve, 500));
+            attempts++;
+          }
+
+          if (uploading.value) {
+            proxy?.$modal.msgError('视频上传处理超时,请稍后重试');
+            return;
+          }
+        }
+
+        if (form.value.id) {
+          // 修改操作
+          await updateVideoContent(form.value);
+          proxy?.$modal.msgSuccess(`视频「${form.value.title}」修改成功!`);
+        } else {
+          // 新增操作
+          await addVideoContent(form.value);
+          proxy?.$modal.msgSuccess(`视频「${form.value.title}」新增成功!`);
+        }
+
+        reset();
+        dialog.visible = false;
+        await getList();
+      } finally {
+        buttonLoading.value = false;
+        isSubmitting.value = false;
       }
-      proxy?.$modal.msgSuccess('操作成功');
-      dialog.visible = false;
-      await getList();
     }
   });
 };
 
-/** 删除按钮操作 */
 const handleDelete = async (row?: VideoContentVO) => {
   const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除视频内容信息编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
-  await delVideoContent(_ids);
-  proxy?.$modal.msgSuccess('删除成功');
-  await getList();
+  if (!_ids || (Array.isArray(_ids) && _ids.length === 0)) {
+    proxy?.$modal.msgError('请先选择要删除的数据');
+    return;
+  }
+  await proxy?.$modal.confirm(`确认删除编号为"${_ids}"的数据?`);
+  try {
+    await delVideoContent(_ids);
+    proxy?.$modal.msgSuccess('删除成功');
+    await getList();
+  } catch (error: any) {
+    proxy?.$modal.msgError('删除失败:' + (error.message || '未知错误'));
+  }
 };
 
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download(
-    'physical/videoContent/export',
-    {
-      ...queryParams.value
-    },
-    `videoContent_${new Date().getTime()}.xlsx`
-  );
+const handleOperation = async (command: string, row: VideoContentVO) => {
+  switch (command) {
+    case 'edit': await handleUpdate(row); break;
+    case 'delete': await handleDelete(row); break;
+    case 'match': await handleMatchFromBackup(row); break;
+  }
 };
 
-onMounted(() => {
-  getList();
-  loadServiceTabOptions();
-});
-const serviceTabOptions = ref<ServiceTabVO[]>([]);
-/** 加载页签选项列表 */
-const loadServiceTabOptions = async () => {
-  try {
-    const res = await selectEnabledTabsByCategoryList('video');
-    if (res.code === 200 && Array.isArray(res.data)) {
-      serviceTabOptions.value = res.data;
-    } else if (Array.isArray(res)) {
-      serviceTabOptions.value = res;
-    }
-  } catch (error) {
-    console.error('加载页签列表失败:', error);
-    proxy?.$modal.msgError('加载页签列表失败');
-  }
+const handleMatchFromBackup = async (row: VideoContentVO) => {
+  await proxy?.$modal.confirm(`确认对视频「${row.title}」执行异步获取?`);
+  const res = await matchFromBackup(row.id);
+  proxy?.$modal.msgSuccess(res.data?.message || '异步获取成功!');
+  await getList();
 };
-// 视频文件列表
-const ossVideoUrlFileList = ref<any[]>([]);
-const ossVideoUrlPreviewUrl = ref('');
 
-// 视频封面图上传相关
-const videoCoverUrlFileList = ref<any[]>([]);
-const videoCoverUrlPreviewUrl = ref('');
+const handleFixAll = async () => {
+  await proxy?.$modal.confirm('确认修复所有视频数据?');
+  const res = await request({ url: '/physical/videoContent/fixAllVideoData', method: 'POST' });
+  proxy?.$modal.msgSuccess(res.data?.message || '修复完成!');
+  await getList();
+};
 
-// ... existing code ...
-// 视频文件改变事件
-async function handleOssVideoUrlChange(file: any) {
-  // 清空文件列表,确保每次选择文件都能触发 change 事件
-  ossVideoUrlFileList.value = [];
+// 文件选择 - 选择后立即自动上传(仅新增模式)
+const handleOssVideoSelect = async (file: any) => {
   const fileObj = file.raw || file;
-  if (fileObj) {
-    try {
-      // 立即上传视频
-      const uploadRes = await uploadVideo(fileObj);
-      const videoTempUrl = uploadRes.data?.tempUrl || uploadRes.data?.ossVideoUrl || uploadRes.data;
-      const videoUrl = uploadRes.data?.url || uploadRes.data?.ossVideoUrl || uploadRes.data;
-      const ossId = uploadRes.data?.ossId;
-      const duration = uploadRes.data?.duration;
-      form.value.ossVideoUrl = videoUrl;
-      form.value.ossId = ossId;
-      form.value.durationSeconds = duration;
-      ossVideoUrlPreviewUrl.value = videoTempUrl;
-      proxy?.$modal.msgSuccess('视频上传成功');
-    } catch (error) {
-      console.error('视频上传失败:', error);
-      proxy?.$modal.msgError('视频上传失败');
-      form.value.ossVideoUrl = '';
-      form.value.ossId = undefined;
-      form.value.durationSeconds = undefined;
-    }
+  if (!fileObj) return;
+
+  form.value.videoFileName = fileObj.name;
+  ossVideoUrlFileList.value = [file];
+
+  // 只有新增模式下才自动上传
+  // 编辑模式下用户选择新视频时才上传,但保留原ID
+  if (!form.value.id) {
+    await uploadVideoAsync(fileObj);
+  } else {
+    // 编辑模式:用户选择了新视频,需要上传但要保留原ID
+    await uploadVideoAsync(fileObj, true);
   }
-}
-// 视频文件移除事件
-function handleOssVideoUrlRemove() {
-  ossVideoUrlFileList.value = [];
-  ossVideoUrlPreviewUrl.value = '';
-  form.value.ossVideoUrl = '';
-}
-
-// 视频封面图上传处理
-const handleVideoCoverUrlChange = async (file: any) => {
-  const index = videoCoverUrlFileList.value.findIndex((f) => f.uid === file.uid);
-  videoCoverUrlFileList.value = [];
+};
 
-  if (file.raw) {
-    videoCoverUrlPreviewUrl.value = URL.createObjectURL(file.raw);
+// 视频异步上传
+// isEditMode: 是否为编辑模式(编辑模式下不覆盖原ID)
+const uploadVideoAsync = async (fileObj: File, isEditMode = false) => {
+  const fileSizeMB = fileObj.size / (1024 * 1024);
+  if (fileSizeMB > 10240) {
+    proxy?.$modal.msgError('视频文件大小不能超过10GB');
+    ossVideoUrlFileList.value = [];
+    form.value.videoFileName = undefined;
+    return;
   }
 
-  if (index === -1) {
-    videoCoverUrlFileList.value.push(file);
-  }
+  uploading.value = true;
+  uploadProgress.value = 0;
+  videoUploaded.value = false;
 
   try {
-    const rawFile = file.raw;
-    const res = await uploadTournament(rawFile);
-    if (res.code === 200) {
-      videoCoverUrlFileList.value[index] = {
-        ...file,
-        status: 'success',
-        response: res.data.url
-      };
-      form.value.videoCoverUrl = videoCoverUrlFileList.value[index].response;
-      videoCoverUrlPreviewUrl.value = videoCoverUrlFileList.value[index].response;
-      proxy?.$modal.msgSuccess('封面图上传成功');
-    } else {
-      throw new Error(res.msg);
+    const formData = new FormData();
+    formData.append('file', fileObj);
+
+    const timeout = Math.max(300000, Math.floor(fileSizeMB * 3000));
+    const uploadRes = await request({
+      url: '/physical/videoContent/uploadVideo',
+      method: 'POST',
+      data: formData,
+      headers: { 'Content-Type': 'multipart/form-data' },
+      timeout,
+      onUploadProgress: (progressEvent: any) => {
+        if (progressEvent.total > 0) {
+          uploadProgress.value = Math.round((progressEvent.loaded / progressEvent.total) * 100);
+        }
+      }
+    });
+
+    uploading.value = false;
+    const video = uploadRes.data || uploadRes;
+
+    // 关键修复:编辑模式下不覆盖原ID!
+    form.value.ossVideoUrl = video.ossVideoUrl || '';
+    if (video.ossId) form.value.ossId = video.ossId;
+    if (video.videoFileName || video.fileName) form.value.videoFileName = video.videoFileName || video.fileName;
+    if (video.durationSeconds) form.value.durationSeconds = video.durationSeconds;
+
+    // 只有新增模式下才设置ID
+    if (!isEditMode && video.id) {
+      form.value.id = video.id;
     }
-  } catch (error) {
-    videoCoverUrlFileList.value[index] = {
-      ...file,
-      status: 'fail',
-      error: '上传失败'
-    };
-    proxy?.$modal.msgError('封面图上传失败,请重试');
+
+    // 无论新增还是编辑模式,上传完成后都标记为已上传
+    uploading.value = false;
+    videoUploaded.value = true;
+  } catch (error: any) {
+    uploading.value = false;
+    uploadProgress.value = 0;
+    videoUploaded.value = false;
+    ossVideoUrlFileList.value = [];
+    form.value.videoFileName = undefined;
+    proxy?.$modal.msgError('视频上传失败:' + (error.message || '未知错误'));
   }
 };
-// 视频封面图移除处理
+
+const handleOssVideoUrlRemove = () => {
+  ossVideoUrlFileList.value = [];
+  form.value.videoFileName = undefined;
+  form.value.ossVideoUrl = undefined;
+  form.value.durationSeconds = undefined;
+  videoUploaded.value = false;
+};
+
+const handleVideoCoverUrlChange = (file: any) => {
+  videoCoverUrlFileList.value = [file];
+  const reader = new FileReader();
+  reader.onload = (e) => {
+    videoCoverUrlPreviewUrl.value = e.target?.result as string;
+  };
+  reader.readAsDataURL(file.raw || file);
+};
+
 const handleVideoCoverUrlRemove = () => {
   videoCoverUrlFileList.value = [];
   videoCoverUrlPreviewUrl.value = '';
-  form.value.videoCoverUrl = '';
+  form.value.videoCoverUrl = undefined;
 };
 
-// 视频封面图预览点击
-const handleVideoCoverUrlPreviewClick = () => {
-  if (videoCoverUrlPreviewUrl.value || form.value.videoCoverUrl) {
-    ElMessageBox.alert(`<img src="${videoCoverUrlPreviewUrl.value || form.value.videoCoverUrl}" style="max-width: 100%;" />`, '视频封面图预览', {
-      dangerouslyUseHTMLString: true,
-      confirmButtonText: '关闭'
-    });
+// 获取页签选项
+const getServiceTabs = async () => {
+  try {
+    const res = await selectEnabledTabsByCategoryList('video');
+    serviceTabOptions.value = res.data;
+  } catch (error: any) {
+    proxy?.$modal.msgError('获取标签列表失败:' + (error.message || '未知错误'));
   }
 };
+
+onMounted(() => {
+  getList();
+  getServiceTabs();
+});
 </script>

+ 1 - 1
vite.config.ts

@@ -24,7 +24,7 @@ export default defineConfig(({ mode, command }) => {
       open: true,
       proxy: {
         [env.VITE_APP_BASE_API]: {
-          target: 'http://localhost:15103',
+          target: 'http://localhost:16103',
           changeOrigin: true,
           ws: true,
           rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff