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