|
|
@@ -8,7 +8,7 @@
|
|
|
<el-input v-model="queryParams.title" placeholder="请输入视频标题" clearable @keyup.enter="handleQuery" />
|
|
|
</el-form-item>
|
|
|
<el-form-item label="状态" prop="status">
|
|
|
- <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
|
|
|
+ <el-select v-model="queryParams.status" placeholder="请选择状态" clearable @change="handleQuery">
|
|
|
<el-option label="上架" value="up" />
|
|
|
<el-option label="下架" value="down" />
|
|
|
</el-select>
|
|
|
@@ -34,29 +34,45 @@
|
|
|
<el-col :span="1.5">
|
|
|
<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 :span="3">
|
|
|
+ <el-button type="warning" plain icon="Wrench" @click="handleFixAll" v-hasPermi="['physical:videoContent:edit']" :loading="isFixing">修复所有数据</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 v-loading="loading" border :data="videoContentList" @selection-change="handleSelectionChange" :row-height="60">
|
|
|
<el-table-column type="selection" width="55" align="center" />
|
|
|
- <el-table-column label="编号" align="center" prop="id" />
|
|
|
+ <el-table-column label="编号" align="center" prop="id" v-if="true" />
|
|
|
<el-table-column label="视频标题" align="center" prop="title" />
|
|
|
- <el-table-column label="文件名" align="center" prop="videoFileName" />
|
|
|
+ <el-table-column label="文件名" align="center" prop="videoFileName" width="180">
|
|
|
+ <template #default="scope">
|
|
|
+ <span class="file-name-ellipsis" :title="scope.row.videoFileName">{{ scope.row.videoFileName }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
<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" fit="cover" />
|
|
|
+ <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>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="观看人数" align="center" prop="videoSeeCount" />
|
|
|
<el-table-column label="所需视频点" align="center" prop="requiredPoints" />
|
|
|
<el-table-column label="所属标签" align="center" prop="serviceTagName" />
|
|
|
- <el-table-column label="视频时长" align="center">
|
|
|
+ <el-table-column label="视频时长(秒)" align="center">
|
|
|
<template #default="{ row }">
|
|
|
- {{ row.durationSeconds ? formatDuration(row.durationSeconds) : '待更新' }}
|
|
|
+ <span :class="{ 'text-danger': !isDurationValid(row.durationSeconds) && row.durationSeconds !== null && row.durationSeconds !== undefined }">
|
|
|
+ {{ row.durationSeconds ?? '待更新' }}
|
|
|
+ <span v-if="!isDurationValid(row.durationSeconds) && row.durationSeconds !== null && row.durationSeconds !== undefined" style="font-size: 12px; color: #f56c6c;">(异常)</span>
|
|
|
+ <span v-if="row.durationSeconds === null || row.durationSeconds === undefined" style="font-size: 12px; color: #909399;">(待异步更新)</span>
|
|
|
+ </span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="试看时长(秒)" align="center" prop="previewDurationSeconds" />
|
|
|
@@ -76,17 +92,21 @@
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="创建时间" align="center" prop="createdAt" width="180">
|
|
|
- <template #default="scope">{{ parseTime(scope.row.createdAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</template>
|
|
|
+ <template #default="scope">
|
|
|
+ <span>{{ parseTime(scope.row.createdAt, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
|
|
+ </template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="操作" align="center" width="80">
|
|
|
<template #default="scope">
|
|
|
- <el-dropdown trigger="click" @command="(cmd) => handleOperation(cmd, scope.row)">
|
|
|
- <el-button type="text" size="small" style="color: #409eff;">操作</el-button>
|
|
|
+ <el-dropdown trigger="click" placement="bottom">
|
|
|
+ <span style="color: #303133; cursor: pointer; font-size: 14px; font-weight: 500;">
|
|
|
+ 操作 <span style="font-size: 14px; margin-left: 2px;">▼</span>
|
|
|
+ </span>
|
|
|
<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 style="min-width: 100px; padding: 4px 0;">
|
|
|
+ <el-dropdown-item @click="handleUpdate(scope.row)" v-hasPermi="['physical:videoContent:edit']">修改</el-dropdown-item>
|
|
|
+ <el-dropdown-item @click="handleDelete(scope.row)" v-hasPermi="['physical:videoContent:remove']">删除</el-dropdown-item>
|
|
|
+ <el-dropdown-item @click="matchFromBackup(scope.row)" v-hasPermi="['physical:videoContent:edit']">异步获取</el-dropdown-item>
|
|
|
</el-dropdown-menu>
|
|
|
</template>
|
|
|
</el-dropdown>
|
|
|
@@ -98,67 +118,133 @@
|
|
|
</el-card>
|
|
|
|
|
|
<!-- 添加或修改视频内容信息对话框 -->
|
|
|
- <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
|
|
|
+ <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" 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">
|
|
|
- <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>
|
|
|
+ <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">
|
|
|
+ <template v-if="coverUploading">
|
|
|
+ <div style="margin-top: 10px; width: 200px">
|
|
|
+ <div style="display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 14px">
|
|
|
+ <span>上传中...</span>
|
|
|
+ <span>{{ coverUploadProgress }}%</span>
|
|
|
+ </div>
|
|
|
+ <el-progress :percentage="coverUploadProgress" :stroke-width="15" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div v-if="videoCoverUrlPreviewUrl || form.videoCoverUrl" style="position: relative; display: inline-block">
|
|
|
+ <img
|
|
|
+ :src="videoCoverUrlPreviewUrl || form.videoCoverUrl"
|
|
|
+ alt="预览图"
|
|
|
+ style="max-width: 100px; max-height: 100px; margin-top: 10px; cursor: pointer"
|
|
|
+ @click="handleVideoCoverUrlPreviewClick"
|
|
|
+ />
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ size="small"
|
|
|
+ icon="Delete"
|
|
|
+ @click.stop="handleVideoCoverUrlRemove"
|
|
|
+ style="position: absolute; top: 5px; right: -50px;"
|
|
|
+ >删除</el-button>
|
|
|
+ </div>
|
|
|
+ <div v-else style="margin-top: 10px; color: #999">无图片</div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-upload>
|
|
|
+ </div>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="视频文件" prop="ossVideoUrl">
|
|
|
- <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 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>
|
|
|
+ <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>
|
|
|
</template>
|
|
|
- <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 #default>
|
|
|
+ <div class="preview-area">
|
|
|
+ <template v-if="uploading && uploadProgress < 100">
|
|
|
+ <div style="margin-top: 10px; width: 300px">
|
|
|
+ <div style="display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 14px">
|
|
|
+ <span>上传中...</span>
|
|
|
+ <span>{{ uploadProgress }}%</span>
|
|
|
+ </div>
|
|
|
+ <el-progress :percentage="uploadProgress" :stroke-width="20" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else-if="uploading && uploadProgress >= 100">
|
|
|
+ <div style="margin-top: 10px; width: 300px">
|
|
|
+ <div style="display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 14px">
|
|
|
+ <span>处理中...</span>
|
|
|
+ <span>100%</span>
|
|
|
+ </div>
|
|
|
+ <el-progress :percentage="100" :stroke-width="20" status="success" />
|
|
|
+ </div>
|
|
|
+ <video v-if="ossVideoUrlPreviewUrl" :src="ossVideoUrlPreviewUrl" controls style="max-width: 300px; max-height: 200px; margin-top: 10px" />
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <!-- ✅ 使用 videoTempUrl 作为视频源 -->
|
|
|
+ <video v-if="ossVideoUrlPreviewUrl" :src="ossVideoUrlPreviewUrl" controls style="max-width: 300px; max-height: 200px; margin-top: 10px" @error="handleVideoError" />
|
|
|
+ <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>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ <!-- ✅ 数据状态显示 -->
|
|
|
+ <div v-if="dataStatus" style="margin-top: 10px;">
|
|
|
+ <span :class="['status-badge', dataStatus === '正常' ? 'status-normal' : 'status-abnormal']">
|
|
|
+ {{ dataStatus || '异常' }}
|
|
|
+ </span>
|
|
|
+ <el-button
|
|
|
+ v-if="dataStatus === '异常'"
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ @click="handleMatchFromBackup"
|
|
|
+ style="margin-left: 10px;"
|
|
|
+ >异步获取</el-button>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
- </template>
|
|
|
- </el-upload>
|
|
|
+ </el-upload>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="观看数">
|
|
|
+ <el-input v-model="form.videoSeeCount" placeholder="请输入观看数" />
|
|
|
</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">
|
|
|
- <el-input v-model="form.previewDurationSeconds" placeholder="请输入试看时长 (秒)" />
|
|
|
+ <div>
|
|
|
+ <el-input v-model="form.previewDurationSeconds" placeholder="请输入试看时长 (秒)" style="width: 100%" />
|
|
|
+ <div style="margin-top: 4px; font-size: 12px; color: #999">
|
|
|
+ 温馨提示:输入 -1 表示免费观看
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="订阅时效(天)" prop="subscriptionValidHours">
|
|
|
<el-input v-model="form.subscriptionValidHours" placeholder="请输入订阅时效(天)" />
|
|
|
@@ -174,23 +260,31 @@
|
|
|
<el-option label="下架" value="down" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
+ <el-form-item label="视频时长(秒)" prop="durationSeconds">
|
|
|
+ <el-input v-model="form.durationSeconds" placeholder="视频时长(秒)- 由系统自动获取" readonly />
|
|
|
+ <div style="margin-top: 4px; font-size: 12px; color: #909399;">
|
|
|
+ 视频时长由系统自动获取
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
- <el-button :loading="buttonLoading" :disabled="uploading || !isVideoReady" type="primary" @click="submitForm">确 定</el-button>
|
|
|
- <el-button @click="cancel">取 消</el-button>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
|
|
+ <el-button @click="cancel">取 消</el-button>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup name="VideoContent" lang="ts">
|
|
|
-import { ref, reactive, onMounted, toRefs, computed } from 'vue';
|
|
|
-import { ElFormInstance, ElMessageBox } from 'element-plus';
|
|
|
+import { ref, reactive, onMounted, getCurrentInstance, type ComponentInternalInstance, onUnmounted } from 'vue';
|
|
|
+import { ElFormInstance } from 'element-plus';
|
|
|
+import { InfoFilled, Wrench, Refresh, ChevronDown } from '@element-plus/icons-vue';
|
|
|
+import { 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 { listVideoContent, getVideoContent, delVideoContent, updateVideoContent } 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';
|
|
|
@@ -198,7 +292,7 @@ import { uploadTournament } from '@/api/system/business/tournaments';
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
|
|
|
-// 状态管理
|
|
|
+// ============ 状态管理 ============
|
|
|
const videoContentList = ref<VideoContentVO[]>([]);
|
|
|
const buttonLoading = ref(false);
|
|
|
const loading = ref(true);
|
|
|
@@ -207,6 +301,12 @@ const ids = ref<Array<string | number>>([]);
|
|
|
const single = ref(true);
|
|
|
const multiple = ref(true);
|
|
|
const total = ref(0);
|
|
|
+const isFixing = ref(false);
|
|
|
+
|
|
|
+// ✅ 轮询相关状态
|
|
|
+const pollTimer = ref<number | null>(null);
|
|
|
+const pollCount = ref(0);
|
|
|
+const dataStatus = ref(''); // 数据状态:正常/异常
|
|
|
|
|
|
const queryFormRef = ref<ElFormInstance>();
|
|
|
const videoContentFormRef = ref<ElFormInstance>();
|
|
|
@@ -214,29 +314,51 @@ const videoContentFormRef = ref<ElFormInstance>();
|
|
|
const dialog = reactive<DialogOption>({ visible: false, title: '' });
|
|
|
|
|
|
const initFormData: VideoContentForm = {
|
|
|
- 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
|
|
|
+ id: undefined,
|
|
|
+ videoFileName: undefined,
|
|
|
+ title: undefined,
|
|
|
+ requiredPoints: undefined,
|
|
|
+ durationSeconds: undefined,
|
|
|
+ previewDurationSeconds: undefined,
|
|
|
+ subscriptionValidHours: undefined,
|
|
|
+ ossVideoUrl: undefined,
|
|
|
+ status: 'up',
|
|
|
+ createdAt: undefined,
|
|
|
+ updatedAt: undefined,
|
|
|
+ ossId: undefined,
|
|
|
+ videoCoverUrl: undefined,
|
|
|
+ videoSeeCount: undefined,
|
|
|
+ categoryTagId: undefined,
|
|
|
+ serviceTagName: 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,
|
|
|
- orderBy: 'createdAt', orderDirection: 'desc', 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: {
|
|
|
title: [{ required: true, message: '视频标题不能为空', trigger: 'blur' }],
|
|
|
- requiredPoints: [{ required: true, message: '所需视频点数不能为空', trigger: 'blur' }],
|
|
|
- previewDurationSeconds: [{ required: true, message: '试看时长不能为空', trigger: 'blur' }],
|
|
|
- subscriptionValidHours: [{ required: true, message: '订阅时效不能为空', 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: '状态不能为空', trigger: 'change' }],
|
|
|
- categoryTagId: [{ required: true, message: '所属标签不能为空', trigger: 'blur' }]
|
|
|
+ status: [{ required: true, message: '视频状态:up=已上架不能为空', trigger: 'change' }],
|
|
|
+ categoryTagId: [{ required: true, message: '不能为空', trigger: 'blur' }],
|
|
|
+ durationSeconds: []
|
|
|
}
|
|
|
});
|
|
|
|
|
|
@@ -245,93 +367,138 @@ const { queryParams, form, rules } = toRefs(data);
|
|
|
// 上传状态
|
|
|
const uploading = ref(false);
|
|
|
const uploadProgress = ref(0);
|
|
|
-const videoUploaded = ref(false);
|
|
|
-
|
|
|
-// 防止重复提交
|
|
|
-const isSubmitting = ref(false);
|
|
|
+const coverUploading = ref(false);
|
|
|
+const coverUploadProgress = ref(0);
|
|
|
|
|
|
// 文件列表
|
|
|
const ossVideoUrlFileList = ref<any[]>([]);
|
|
|
+const ossVideoUrlPreviewUrl = ref('');
|
|
|
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 isDurationValid = (duration?: number): boolean => {
|
|
|
+ return duration !== undefined && duration !== null && duration >= 0;
|
|
|
+};
|
|
|
+
|
|
|
+const isVideoDataValid = (row: VideoContentVO): boolean => {
|
|
|
+ const hasValidOssId = row.ossId && row.ossId > 0;
|
|
|
+ const hasValidUrl = row.ossVideoUrl && String(row.ossVideoUrl).startsWith('http');
|
|
|
+ const hasValidDuration = isDurationValid(row.durationSeconds);
|
|
|
+ return hasValidOssId && hasValidUrl && hasValidDuration;
|
|
|
+};
|
|
|
+
|
|
|
+// ============ 轮询功能 ============
|
|
|
+// ✅ 启动轮询
|
|
|
+const startPolling = (videoId: number | string) => {
|
|
|
+ stopPolling(); // 先停止之前的轮询
|
|
|
+ pollCount.value = 0;
|
|
|
|
|
|
-// 格式化时长
|
|
|
-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')}`;
|
|
|
+ pollTimer.value = window.setInterval(async () => {
|
|
|
+ await pollVideoStatus(videoId);
|
|
|
+ }, 1000); // 每秒查询一次
|
|
|
+};
|
|
|
+
|
|
|
+// ✅ 停止轮询
|
|
|
+const stopPolling = () => {
|
|
|
+ if (pollTimer.value) {
|
|
|
+ clearInterval(pollTimer.value);
|
|
|
+ pollTimer.value = null;
|
|
|
}
|
|
|
- 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;
|
|
|
+// ✅ 轮询查询状态 - 只更新时长和状态,不更新视频URL!
|
|
|
+const pollVideoStatus = async (videoId: number | string) => {
|
|
|
+ pollCount.value++;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await request({
|
|
|
+ url: `/physical/videoContent/status/${videoId}`,
|
|
|
+ method: 'GET'
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res.data && res.data.data) {
|
|
|
+ const statusData = res.data.data;
|
|
|
+
|
|
|
+ // ✅ 更新时长
|
|
|
+ if (statusData.durationSeconds !== undefined && statusData.durationSeconds !== null) {
|
|
|
+ form.value.durationSeconds = statusData.durationSeconds;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ✅ 更新数据状态
|
|
|
+ if (statusData.dataStatus !== undefined) {
|
|
|
+ dataStatus.value = statusData.dataStatus;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ⚠️ 不要更新视频URL!只有异步获取时才更新
|
|
|
+
|
|
|
+ // 数据正常或超过30秒停止轮询
|
|
|
+ if (statusData.dataStatus === '正常' || pollCount.value >= 30) {
|
|
|
+ stopPolling();
|
|
|
+ await getList(); // 刷新列表
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('轮询查询状态失败:', error);
|
|
|
+ // 如果轮询失败超过30次也停止
|
|
|
+ if (pollCount.value >= 30) {
|
|
|
+ stopPolling();
|
|
|
+ }
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
-// 数据查询
|
|
|
+// ============ 数据查询 ============
|
|
|
const getList = async () => {
|
|
|
loading.value = true;
|
|
|
- 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 res = await listVideoContent(queryParams.value);
|
|
|
+ videoContentList.value = res.rows;
|
|
|
+ total.value = res.total;
|
|
|
+ loading.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+const handleQuery = () => {
|
|
|
+ queryParams.value.pageNum = 1;
|
|
|
+ getList();
|
|
|
+};
|
|
|
+
|
|
|
+const resetQuery = () => {
|
|
|
+ queryFormRef.value?.resetFields();
|
|
|
+ queryParams.value.status = undefined;
|
|
|
+ handleQuery();
|
|
|
};
|
|
|
|
|
|
-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 = () => {
|
|
|
- // 重置表单数据
|
|
|
+ stopPolling(); // ✅ 重置时停止轮询
|
|
|
+ dataStatus.value = ''; // 重置数据状态
|
|
|
form.value = { ...initFormData };
|
|
|
videoContentFormRef.value?.resetFields();
|
|
|
-
|
|
|
- // 清空文件列表
|
|
|
ossVideoUrlFileList.value = [];
|
|
|
+ ossVideoUrlPreviewUrl.value = '';
|
|
|
videoCoverUrlFileList.value = [];
|
|
|
videoCoverUrlPreviewUrl.value = '';
|
|
|
-
|
|
|
- // 重置上传状态
|
|
|
uploading.value = false;
|
|
|
uploadProgress.value = 0;
|
|
|
- videoUploaded.value = false;
|
|
|
-
|
|
|
- // 确保清除任何可能的提交状态
|
|
|
- isSubmitting.value = false;
|
|
|
+ coverUploading.value = false;
|
|
|
+ coverUploadProgress.value = 0;
|
|
|
};
|
|
|
|
|
|
-const cancel = () => { reset(); dialog.visible = false; };
|
|
|
+const cancel = () => {
|
|
|
+ reset();
|
|
|
+ dialog.visible = false;
|
|
|
+};
|
|
|
|
|
|
const handleAdd = () => {
|
|
|
reset();
|
|
|
- // 确保在新增模式下 id 为 undefined
|
|
|
- form.value.id = undefined;
|
|
|
dialog.visible = true;
|
|
|
dialog.title = '添加';
|
|
|
};
|
|
|
@@ -341,141 +508,217 @@ const handleUpdate = async (row?: VideoContentVO) => {
|
|
|
const _id = row?.id || ids.value[0];
|
|
|
const res = await getVideoContent(_id);
|
|
|
Object.assign(form.value, res.data);
|
|
|
+ // ✅ 核心原则1:修改页面打开时设置视频URL
|
|
|
+ ossVideoUrlPreviewUrl.value = String(res.data?.videoTempUrl || res.data?.ossVideoUrl || '');
|
|
|
+
|
|
|
if (res.data.videoCoverUrl) {
|
|
|
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 = async () => {
|
|
|
- if (isSubmitting.value) return;
|
|
|
+const submitForm = () => {
|
|
|
+ if (!form.value.ossVideoUrl && ossVideoUrlPreviewUrl.value) {
|
|
|
+ form.value.ossVideoUrl = ossVideoUrlPreviewUrl.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!form.value.ossVideoUrl) {
|
|
|
+ proxy?.$modal.msgError('请先上传视频文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
videoContentFormRef.value?.validate(async (valid) => {
|
|
|
if (valid) {
|
|
|
- isSubmitting.value = true;
|
|
|
buttonLoading.value = true;
|
|
|
-
|
|
|
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();
|
|
|
+ proxy?.$modal.msgSuccess('操作成功');
|
|
|
dialog.visible = false;
|
|
|
await getList();
|
|
|
} finally {
|
|
|
buttonLoading.value = false;
|
|
|
- isSubmitting.value = false;
|
|
|
}
|
|
|
+ } else {
|
|
|
+ proxy?.$modal.msgWarning('请检查并填写所有必填字段');
|
|
|
+ setTimeout(() => {
|
|
|
+ const errorField = document.querySelector('.el-form-item__error');
|
|
|
+ if (errorField) {
|
|
|
+ errorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
+ }
|
|
|
+ }, 100);
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
|
|
|
const handleDelete = async (row?: VideoContentVO) => {
|
|
|
const _ids = row?.id || ids.value;
|
|
|
+
|
|
|
if (!_ids || (Array.isArray(_ids) && _ids.length === 0)) {
|
|
|
proxy?.$modal.msgError('请先选择要删除的数据');
|
|
|
return;
|
|
|
}
|
|
|
- await proxy?.$modal.confirm(`确认删除编号为"${_ids}"的数据?`);
|
|
|
+
|
|
|
+ await proxy?.$modal.confirm(`是否确认删除视频内容信息编号为"${_ids}"的数据项?`).finally(() => (loading.value = false));
|
|
|
+
|
|
|
try {
|
|
|
await delVideoContent(_ids);
|
|
|
proxy?.$modal.msgSuccess('删除成功');
|
|
|
await getList();
|
|
|
} catch (error: any) {
|
|
|
+ console.error('删除失败:', error);
|
|
|
proxy?.$modal.msgError('删除失败:' + (error.message || '未知错误'));
|
|
|
+ await getList();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-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;
|
|
|
+// ✅ 编辑页面内的异步获取按钮 - 核心原则2:异步获取成功时设置视频URL
|
|
|
+const handleMatchFromBackup = async () => {
|
|
|
+ if (!form.value.id) {
|
|
|
+ proxy?.$modal.msgError('请先选择视频');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await proxy?.$modal.confirm(`确认对视频「${form.value.title}」执行异步获取?`);
|
|
|
+
|
|
|
+ const res = await request({
|
|
|
+ url: `/physical/videoContent/matchFromBackup/${form.value.id}`,
|
|
|
+ method: 'POST'
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res.data && res.data.success) {
|
|
|
+ // ✅ 核心原则2:只有异步获取成功时才能更新视频URL
|
|
|
+ if (res.data.tempUrl) {
|
|
|
+ ossVideoUrlPreviewUrl.value = res.data.tempUrl;
|
|
|
+ }
|
|
|
+ if (res.data.duration) {
|
|
|
+ form.value.durationSeconds = res.data.duration;
|
|
|
+ }
|
|
|
+ dataStatus.value = '正常';
|
|
|
+
|
|
|
+ proxy?.$modal.msgSuccess('异步获取成功!已更新视频URL和时长');
|
|
|
+ await getList();
|
|
|
+ } else {
|
|
|
+ proxy?.$modal.msgWarning(res.data?.message || '未找到可异步获取的数据');
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('异步获取失败:', error);
|
|
|
+ proxy?.$modal.msgError('异步获取失败:' + (error.message || '未知错误'));
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-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 matchFromBackup = async (row: VideoContentVO) => {
|
|
|
+ try {
|
|
|
+ await proxy?.$modal.confirm(`确认对视频「${row.title}」执行异步获取?`);
|
|
|
+
|
|
|
+ proxy?.$modal.msgSuccess(`正在执行异步获取...`);
|
|
|
+
|
|
|
+ const res = await request({
|
|
|
+ url: `/physical/videoContent/matchFromBackup/${row.id}`,
|
|
|
+ method: 'POST'
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res.data && res.data.success) {
|
|
|
+ proxy?.$modal.msgSuccess('异步获取成功!已更新视频URL和时长');
|
|
|
+ await getList();
|
|
|
+ } else {
|
|
|
+ proxy?.$modal.msgWarning(res.data?.message || '未找到可异步获取的数据');
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('异步获取失败:', error);
|
|
|
+ proxy?.$modal.msgError('异步获取失败:' + (error.message || '未知错误'));
|
|
|
+ }
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
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();
|
|
|
-};
|
|
|
+ try {
|
|
|
+ await proxy?.$modal.confirm(
|
|
|
+ '确定要批量修复所有缺失URL、时长或oss_id的视频数据吗?',
|
|
|
+ '批量修复',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }
|
|
|
+ );
|
|
|
|
|
|
-// 文件选择 - 选择后立即自动上传(仅新增模式)
|
|
|
-const handleOssVideoSelect = async (file: any) => {
|
|
|
- const fileObj = file.raw || file;
|
|
|
- if (!fileObj) return;
|
|
|
+ isFixing.value = true;
|
|
|
|
|
|
- form.value.videoFileName = fileObj.name;
|
|
|
- ossVideoUrlFileList.value = [file];
|
|
|
+ const res = await request({
|
|
|
+ url: '/physical/videoContent/fixAllVideoData',
|
|
|
+ method: 'POST'
|
|
|
+ });
|
|
|
|
|
|
- // 只有新增模式下才自动上传
|
|
|
- // 编辑模式下用户选择新视频时才上传,但保留原ID
|
|
|
- if (!form.value.id) {
|
|
|
- await uploadVideoAsync(fileObj);
|
|
|
- } else {
|
|
|
- // 编辑模式:用户选择了新视频,需要上传但要保留原ID
|
|
|
- await uploadVideoAsync(fileObj, true);
|
|
|
+ if (res.data && res.data.success) {
|
|
|
+ const data = res.data;
|
|
|
+ proxy?.$modal.msgSuccess({
|
|
|
+ message: data.message || `批量修复任务已启动!共处理 ${data.count || 0} 条数据`,
|
|
|
+ duration: 5000
|
|
|
+ });
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ getList();
|
|
|
+ }, 1500);
|
|
|
+ } else {
|
|
|
+ proxy?.$modal.msgWarning(res.data?.message || '没有需要修复的数据');
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('批量修复失败:', error);
|
|
|
+ proxy?.$modal.msgError('批量修复失败:' + (error.message || '未知错误'));
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ isFixing.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 视频异步上传
|
|
|
-// isEditMode: 是否为编辑模式(编辑模式下不覆盖原ID)
|
|
|
-const uploadVideoAsync = async (fileObj: File, isEditMode = false) => {
|
|
|
+// ============ 文件上传 ============
|
|
|
+const handleOssVideoUrlChange = async (file: any) => {
|
|
|
+ ossVideoUrlPreviewUrl.value = '';
|
|
|
+ dataStatus.value = ''; // 重置数据状态
|
|
|
+ const fileObj = file.raw || file;
|
|
|
+
|
|
|
+ if (!fileObj) return;
|
|
|
+
|
|
|
const fileSizeMB = fileObj.size / (1024 * 1024);
|
|
|
- if (fileSizeMB > 10240) {
|
|
|
- proxy?.$modal.msgError('视频文件大小不能超过10GB');
|
|
|
- ossVideoUrlFileList.value = [];
|
|
|
- form.value.videoFileName = undefined;
|
|
|
+ if (fileSizeMB > 2048) {
|
|
|
+ proxy?.$modal.msgError('视频文件大小不能超过2GB');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ form.value.ossId = undefined;
|
|
|
+ form.value.durationSeconds = undefined;
|
|
|
+
|
|
|
uploading.value = true;
|
|
|
uploadProgress.value = 0;
|
|
|
- videoUploaded.value = false;
|
|
|
+ const localPreviewUrl = URL.createObjectURL(fileObj);
|
|
|
|
|
|
try {
|
|
|
const formData = new FormData();
|
|
|
formData.append('file', fileObj);
|
|
|
|
|
|
+ if (form.value.title) formData.append('title', form.value.title);
|
|
|
+ if (form.value.requiredPoints) formData.append('requiredPoints', String(form.value.requiredPoints));
|
|
|
+ if (form.value.previewDurationSeconds) formData.append('previewDurationSeconds', String(form.value.previewDurationSeconds));
|
|
|
+ if (form.value.subscriptionValidHours) formData.append('subscriptionValidHours', String(form.value.subscriptionValidHours));
|
|
|
+ if (form.value.videoCoverUrl) formData.append('videoCoverUrl', form.value.videoCoverUrl);
|
|
|
+ if (form.value.status) formData.append('status', form.value.status);
|
|
|
+ if (form.value.categoryTagId) formData.append('categoryTagId', String(form.value.categoryTagId));
|
|
|
+ if (form.value.videoSeeCount) formData.append('videoSeeCount', String(form.value.videoSeeCount));
|
|
|
+ if (form.value.id) formData.append('videoId', String(form.value.id));
|
|
|
+
|
|
|
const timeout = Math.max(300000, Math.floor(fileSizeMB * 3000));
|
|
|
+
|
|
|
const uploadRes = await request({
|
|
|
url: '/physical/videoContent/uploadVideo',
|
|
|
method: 'POST',
|
|
|
@@ -485,72 +728,219 @@ const uploadVideoAsync = async (fileObj: File, isEditMode = false) => {
|
|
|
onUploadProgress: (progressEvent: any) => {
|
|
|
if (progressEvent.total > 0) {
|
|
|
uploadProgress.value = Math.round((progressEvent.loaded / progressEvent.total) * 100);
|
|
|
+ if (uploadProgress.value >= 100) {
|
|
|
+ ossVideoUrlPreviewUrl.value = localPreviewUrl;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
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;
|
|
|
+ // ⚠️ 根据核心原则:上传成功后不设置视频URL!
|
|
|
+ // 视频URL只在两个地方设置:1. 修改页面打开时 2. 异步获取成功时
|
|
|
+ const videoUrl = uploadRes.data?.data?.url || uploadRes.data?.data?.ossVideoUrl;
|
|
|
+ const ossId = uploadRes.data?.data?.ossId;
|
|
|
+ const duration = uploadRes.data?.data?.duration || uploadRes.data?.data?.videoDuration;
|
|
|
+ const videoFileName = uploadRes.data?.data?.videoFileName;
|
|
|
+ const serviceTagName = uploadRes.data?.data?.serviceTagName;
|
|
|
+
|
|
|
+ // ❌ 删除:不设置视频URL
|
|
|
+ // form.value.ossVideoUrl = videoUrl || localPreviewUrl;
|
|
|
+ // ossVideoUrlPreviewUrl.value = videoTempUrl;
|
|
|
+
|
|
|
+ if (ossId) form.value.ossId = ossId;
|
|
|
+ if (videoFileName) {
|
|
|
+ form.value.videoFileName = videoFileName;
|
|
|
+ } else if (fileObj?.name) {
|
|
|
+ form.value.videoFileName = fileObj.name;
|
|
|
+ }
|
|
|
+ if (serviceTagName) {
|
|
|
+ form.value.serviceTagName = serviceTagName;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (uploadRes.data?.data?.id) {
|
|
|
+ form.value.id = uploadRes.data.data.id;
|
|
|
|
|
|
- // 只有新增模式下才设置ID
|
|
|
- if (!isEditMode && video.id) {
|
|
|
- form.value.id = video.id;
|
|
|
+ // ✅ 上传成功后启动轮询(轮询只更新时长和状态,不更新视频URL)
|
|
|
+ startPolling(uploadRes.data.data.id);
|
|
|
}
|
|
|
|
|
|
- // 无论新增还是编辑模式,上传完成后都标记为已上传
|
|
|
- uploading.value = false;
|
|
|
- videoUploaded.value = true;
|
|
|
+ let finalDuration = duration && !isNaN(duration) && Number(duration) > 0 ? Math.floor(Number(duration)) : 0;
|
|
|
+
|
|
|
+ if (!isDurationValid(finalDuration) && uploadRes.data?.data?.id) {
|
|
|
+ try {
|
|
|
+ const durationRes = await request({
|
|
|
+ url: `/physical/videoContent/getVideoDuration/${uploadRes.data.data.id}`,
|
|
|
+ method: 'GET'
|
|
|
+ });
|
|
|
+ if (durationRes.data && durationRes.data.success && durationRes.data.duration > 0) {
|
|
|
+ finalDuration = durationRes.data.duration;
|
|
|
+ }
|
|
|
+ } catch (durationError) {
|
|
|
+ console.error('调用获取时长接口失败:', durationError);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (finalDuration > 0) {
|
|
|
+ form.value.durationSeconds = finalDuration;
|
|
|
+ }
|
|
|
+
|
|
|
+ proxy?.$modal.msgSuccess('视频上传成功!时长已自动获取');
|
|
|
+ await getList();
|
|
|
} catch (error: any) {
|
|
|
+ console.error('视频上传失败:', error);
|
|
|
uploading.value = false;
|
|
|
uploadProgress.value = 0;
|
|
|
- videoUploaded.value = false;
|
|
|
+ ossVideoUrlPreviewUrl.value = localPreviewUrl;
|
|
|
+ URL.revokeObjectURL(localPreviewUrl);
|
|
|
+
|
|
|
+ proxy?.$modal.msgError(error.code === 'ECONNABORTED'
|
|
|
+ ? '视频上传超时,请尝试上传更小的文件或联系管理员'
|
|
|
+ : '视频上传失败:' + (error.message || '未知错误'));
|
|
|
+
|
|
|
+ form.value.ossVideoUrl = '';
|
|
|
+ form.value.ossId = undefined;
|
|
|
+ form.value.durationSeconds = undefined;
|
|
|
+ form.value.id = undefined;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleOssVideoUrlRemove = async () => {
|
|
|
+ if (!form.value.ossVideoUrl && !ossVideoUrlPreviewUrl.value) {
|
|
|
+ ossVideoUrlFileList.value = [];
|
|
|
+ ossVideoUrlPreviewUrl.value = '';
|
|
|
+ form.value.ossVideoUrl = '';
|
|
|
+ form.value.ossId = undefined;
|
|
|
+ form.value.durationSeconds = undefined;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await proxy?.$modal.confirm('确定要删除当前视频吗?删除后需要重新上传。');
|
|
|
+
|
|
|
ossVideoUrlFileList.value = [];
|
|
|
+ ossVideoUrlPreviewUrl.value = '';
|
|
|
+ form.value.ossVideoUrl = '';
|
|
|
+ form.value.ossId = undefined;
|
|
|
+ form.value.durationSeconds = undefined;
|
|
|
form.value.videoFileName = undefined;
|
|
|
- proxy?.$modal.msgError('视频上传失败:' + (error.message || '未知错误'));
|
|
|
+ dataStatus.value = ''; // 重置数据状态
|
|
|
+
|
|
|
+ if (form.value.id) {
|
|
|
+ try {
|
|
|
+ await request({
|
|
|
+ url: `/physical/videoContent/clearVideo/${form.value.id}`,
|
|
|
+ method: 'POST'
|
|
|
+ });
|
|
|
+ proxy?.$modal.msgSuccess('视频删除成功');
|
|
|
+ await getList();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('删除视频失败:', error);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ proxy?.$modal.msgSuccess('视频已从表单中移除');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 用户取消删除
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const handleOssVideoUrlRemove = () => {
|
|
|
- ossVideoUrlFileList.value = [];
|
|
|
- form.value.videoFileName = undefined;
|
|
|
- form.value.ossVideoUrl = undefined;
|
|
|
- form.value.durationSeconds = undefined;
|
|
|
- videoUploaded.value = false;
|
|
|
-};
|
|
|
+const handleVideoCoverUrlChange = async (file: any) => {
|
|
|
+ videoCoverUrlFileList.value = [];
|
|
|
|
|
|
-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);
|
|
|
+ if (file.raw) {
|
|
|
+ videoCoverUrlPreviewUrl.value = URL.createObjectURL(file.raw);
|
|
|
+ }
|
|
|
+
|
|
|
+ videoCoverUrlFileList.value.push(file);
|
|
|
+ coverUploading.value = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await uploadTournament(file.raw);
|
|
|
+ if (res.code === 200) {
|
|
|
+ videoCoverUrlFileList.value[0] = { ...file, status: 'success', response: res.data.url };
|
|
|
+ form.value.videoCoverUrl = res.data.url;
|
|
|
+ videoCoverUrlPreviewUrl.value = res.data.url;
|
|
|
+ proxy?.$modal.msgSuccess('封面图上传成功');
|
|
|
+ } else {
|
|
|
+ throw new Error(res.msg);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ videoCoverUrlFileList.value[0] = { ...file, status: 'fail', error: '上传失败' };
|
|
|
+ proxy?.$modal.msgError('封面图上传失败,请重试');
|
|
|
+ } finally {
|
|
|
+ coverUploading.value = false;
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
const handleVideoCoverUrlRemove = () => {
|
|
|
videoCoverUrlFileList.value = [];
|
|
|
videoCoverUrlPreviewUrl.value = '';
|
|
|
- form.value.videoCoverUrl = undefined;
|
|
|
+ form.value.videoCoverUrl = '';
|
|
|
};
|
|
|
|
|
|
-// 获取页签选项
|
|
|
-const getServiceTabs = async () => {
|
|
|
- try {
|
|
|
- const res = await selectEnabledTabsByCategoryList('video');
|
|
|
- serviceTabOptions.value = res.data;
|
|
|
- } catch (error: any) {
|
|
|
- proxy?.$modal.msgError('获取标签列表失败:' + (error.message || '未知错误'));
|
|
|
+const handleVideoCoverUrlPreviewClick = () => {
|
|
|
+ const imageUrl = videoCoverUrlPreviewUrl.value || form.value.videoCoverUrl;
|
|
|
+ if (imageUrl) {
|
|
|
+ ElMessageBox.alert(`<img src="${imageUrl}" style="max-width: 100%;" />`, '视频封面图预览', {
|
|
|
+ dangerouslyUseHTMLString: true,
|
|
|
+ confirmButtonText: '关闭'
|
|
|
+ });
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+// ✅ 视频错误处理
|
|
|
+const handleVideoError = () => {
|
|
|
+ console.error('视频加载失败');
|
|
|
+};
|
|
|
+
|
|
|
+// ============ 初始化和清理 ============
|
|
|
onMounted(() => {
|
|
|
getList();
|
|
|
- getServiceTabs();
|
|
|
+ loadServiceTabOptions();
|
|
|
});
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ stopPolling(); // ✅ 组件卸载时停止轮询
|
|
|
+});
|
|
|
+
|
|
|
+const loadServiceTabOptions = async () => {
|
|
|
+ try {
|
|
|
+ const res = await selectEnabledTabsByCategoryList('video');
|
|
|
+ serviceTabOptions.value = Array.isArray(res.data) ? res.data : (Array.isArray(res) ? res : []);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载页签列表失败:', error);
|
|
|
+ proxy?.$modal.msgError('加载页签列表失败');
|
|
|
+ }
|
|
|
+};
|
|
|
</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.file-name-ellipsis {
|
|
|
+ display: block;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ max-width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+/* ✅ 数据状态样式 */
|
|
|
+.status-badge {
|
|
|
+ display: inline-block;
|
|
|
+ padding: 4px 12px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.status-normal {
|
|
|
+ background-color: #f0f9eb;
|
|
|
+ color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.status-abnormal {
|
|
|
+ background-color: #fef0f0;
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+</style>
|