|
|
@@ -0,0 +1,3160 @@
|
|
|
+<template>
|
|
|
+ <div class="app-container">
|
|
|
+ <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
|
|
|
+ <el-form-item label="素材名称" prop="resourceName">
|
|
|
+ <el-input
|
|
|
+ v-model="queryParams.resourceName"
|
|
|
+ placeholder="请输入素材名称"
|
|
|
+ clearable
|
|
|
+ size="small"
|
|
|
+ @keyup.enter.native="handleQuery"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="文件名称" prop="fileName">
|
|
|
+ <el-input
|
|
|
+ v-model="queryParams.fileName"
|
|
|
+ placeholder="请输入素材名称"
|
|
|
+ clearable
|
|
|
+ size="small"
|
|
|
+ @keyup.enter.native="handleQuery"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="分类" prop="typeId">
|
|
|
+ <el-select
|
|
|
+ v-model="queryParams.typeId"
|
|
|
+ clearable
|
|
|
+ placeholder="请选择或输入分类"
|
|
|
+ @change="val => changeCateType(val, 1)"
|
|
|
+ filterable
|
|
|
+ :filter-method="filterTypeOptions"
|
|
|
+ @visible-change="handleSelectVisibleChange">
|
|
|
+ <el-option
|
|
|
+ v-for="item in filteredRootTypeList"
|
|
|
+ :key="item.dictValue"
|
|
|
+ :label="item.dictLabel"
|
|
|
+ :value="item.dictValue">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="子分类" prop="typeSubId">
|
|
|
+ <el-select v-model="queryParams.typeSubId" clearable placeholder="请选择子分类">
|
|
|
+ <el-option
|
|
|
+ v-for="item in subTypeList"
|
|
|
+ :key="item.dictValue"
|
|
|
+ :label="item.dictLabel"
|
|
|
+ :value="item.dictValue">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
|
|
+ <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <el-row :gutter="10" class="mb8">
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ icon="el-icon-plus"
|
|
|
+ size="mini"
|
|
|
+ @click="handleAdd"
|
|
|
+ :disabled="hasMinimizableDialog"
|
|
|
+ v-hasPermi="['course:videoResource:add']"
|
|
|
+ >新增</el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ icon="el-icon-plus"
|
|
|
+ size="mini"
|
|
|
+ @click="handleBatchAdd"
|
|
|
+ :disabled="hasMinimizableDialog"
|
|
|
+ v-hasPermi="['course:videoResource:batchAdd']"
|
|
|
+ >批量新增</el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ icon="el-icon-plus"
|
|
|
+ size="mini"
|
|
|
+ :disabled="multiple"
|
|
|
+ @click="handleBatchUpdate"
|
|
|
+ v-hasPermi="['course:videoResource:batchUpdateClass']"
|
|
|
+ >批量修改分类</el-button>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="1.5">
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ icon="el-icon-delete"
|
|
|
+ size="mini"
|
|
|
+ :disabled="multiple"
|
|
|
+ @click="handleDelete"
|
|
|
+ v-hasPermi="['course:videoResource:remove']"
|
|
|
+ >删除</el-button>
|
|
|
+ </el-col>
|
|
|
+ <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-table v-loading="loading" :data="resourceList" @selection-change="handleSelectionChange" border>
|
|
|
+ <el-table-column type="selection" width="55" align="center" />
|
|
|
+ <el-table-column label="序号" width="55" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ scope.$index + 1 }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="素材名称" align="center" :show-overflow-tooltip="true" prop="resourceName"/>
|
|
|
+ <el-table-column label="文件名称" align="center" :show-overflow-tooltip="true" prop="fileName"/>
|
|
|
+ <el-table-column label="排序" align="center" prop="sort" />
|
|
|
+ <el-table-column label="分类" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <span v-if="scope.row.typeId">{{ getTypeName(scope.row.typeId) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="子分类" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <span v-if="scope.row.typeSubId">{{ getTypeName(scope.row.typeSubId) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="视频文件" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <a
|
|
|
+ @click="handleVideoPreview(scope.row.videoUrl)"
|
|
|
+ style="background-color: #409EFF; color: white; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; display: inline-block; text-decoration: none;">
|
|
|
+ 查看文件
|
|
|
+ </a>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="CDN" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <a
|
|
|
+ @click="copy(scope.row.videoUrl)"
|
|
|
+ style="color: #409EFF; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; display: inline-block; text-decoration: none;">
|
|
|
+ 复制链接
|
|
|
+ </a>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="关联题目" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <a
|
|
|
+ @click="handleViewProject(scope.row, 3)"
|
|
|
+ :style="scope.row.projectIds ? {
|
|
|
+ backgroundColor: '#409EFF',
|
|
|
+ color: 'white',
|
|
|
+ border: 'none',
|
|
|
+ borderRadius: '4px',
|
|
|
+ padding: '4px 12px',
|
|
|
+ cursor: 'pointer',
|
|
|
+ fontSize: '12px',
|
|
|
+ display: 'inline-block',
|
|
|
+ textDecoration: 'none'
|
|
|
+ } : {
|
|
|
+ backgroundColor: 'rgb(154 156 159)',
|
|
|
+ color: 'white',
|
|
|
+ border: 'none',
|
|
|
+ cursor: 'pointer',
|
|
|
+ borderRadius: '4px',
|
|
|
+ padding: '4px 12px',
|
|
|
+ fontSize: '12px',
|
|
|
+ display: 'inline-block',
|
|
|
+ textDecoration: 'none'
|
|
|
+ }">
|
|
|
+ {{ scope.row.projectIds ? '查看详情' : '未关联题目' }}
|
|
|
+ </a>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="视频时长" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <div style="padding: 4px 12px;background: linear-gradient(to right, rgb(196 219 255), #409EFF)">{{ formatDuration(scope.row.duration) }}</div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="视频展示类型" align="center" prop="displayType" width="110">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ scope.row.displayType === 'portrait' ? '竖屏' : '横屏' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-button
|
|
|
+ size="mini"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-edit"
|
|
|
+ @click="handleUpdate(scope.row)"
|
|
|
+ :disabled="hasMinimizableDialog"
|
|
|
+ v-hasPermi="['course:videoResource:edit']"
|
|
|
+ >修改</el-button>
|
|
|
+ <el-button
|
|
|
+ size="mini"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-delete"
|
|
|
+ @click="handleDelete(scope.row)"
|
|
|
+ v-hasPermi="['course:videoResource:remove']"
|
|
|
+ >删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <pagination
|
|
|
+ v-show="total>0"
|
|
|
+ :total="total"
|
|
|
+ :page.sync="queryParams.pageNum"
|
|
|
+ :limit.sync="queryParams.pageSize"
|
|
|
+ @pagination="getList"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 添加或修改视频素材库对话框 -->
|
|
|
+ <el-dialog :title="title" :visible.sync="open" width="700px" append-to-body :before-close="cancel"
|
|
|
+ @minimize="hasMinimizableDialog = true" @restore="hasMinimizableDialog = false">
|
|
|
+ <el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
|
|
+ <el-form-item label="素材名称" prop="resourceName" style="margin-top: 20px">
|
|
|
+ <el-input v-model="form.resourceName" placeholder="请输入" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="文件名称" prop="fileName" style="margin-top: 20px;display: none">
|
|
|
+ <el-input v-model="form.fileName" placeholder="请输入" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="分类" prop="typeId">
|
|
|
+ <el-select v-model="form.typeId" placeholder="请选择分类" style="width: 100%" @change="val => changeCateType(val, 2)">
|
|
|
+ <el-option
|
|
|
+ v-for="item in rootTypeList"
|
|
|
+ :key="item.dictValue"
|
|
|
+ :label="item.dictLabel"
|
|
|
+ :value="item.dictValue">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="子分类" prop="typeSubId">
|
|
|
+ <el-select v-model="form.typeSubId" clearable placeholder="请选择子分类" style="width: 100%">
|
|
|
+ <el-option
|
|
|
+ v-for="item in subTypeList"
|
|
|
+ :key="item.dictValue"
|
|
|
+ :label="item.dictLabel"
|
|
|
+ :value="item.dictValue">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="排序" prop="sort">
|
|
|
+ <el-input-number v-model="form.sort" :min="0" label="请输入排序"></el-input-number>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="视频展示类型" prop="displayType">
|
|
|
+ <el-radio-group v-model="form.displayType">
|
|
|
+ <el-radio label="landscape">横屏</el-radio>
|
|
|
+ <el-radio label="portrait">竖屏</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="关联题目" prop="projectIds">
|
|
|
+ <el-select
|
|
|
+ ref="customSelect"
|
|
|
+ class="custom-select-class"
|
|
|
+ v-model="form.projectIds"
|
|
|
+ multiple
|
|
|
+ placeholder="请选择关联题目"
|
|
|
+ @click.native.stop="openProjectDialog(form.projectIds, 0)"
|
|
|
+ style="width: 100%;">
|
|
|
+ <el-option
|
|
|
+ v-for="item in projectShowList"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.title"
|
|
|
+ :value="item.id">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="上传视频" prop="videoUrl" required>
|
|
|
+ <el-upload
|
|
|
+ ref="videoUpload"
|
|
|
+ action="#"
|
|
|
+ list-type="picture-card"
|
|
|
+ :http-request="videoUpload"
|
|
|
+ :file-list="fileList"
|
|
|
+ :on-change="handleFileChange"
|
|
|
+ accept=".mp4">
|
|
|
+ <i slot="default" class="el-icon-plus"></i>
|
|
|
+ <div slot="file" slot-scope="{file}">
|
|
|
+ <div
|
|
|
+ class="el-upload-list__item-thumbnail"
|
|
|
+ style="display: flex; justify-content: center; align-items: center; background-color: #f5f7fa; height: 146px;"
|
|
|
+ >
|
|
|
+ <i class="el-icon-video-camera" style="font-size: 48px;" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <span v-if="file.status === 'success'" class="el-upload-list__item-status-label">
|
|
|
+ <i class="el-icon-upload-success el-icon--check"></i>
|
|
|
+ </span>
|
|
|
+ <span v-if="file.status === 'fail'" class="el-upload-list__item-status-label">
|
|
|
+ <i class="el-icon-circle-close"></i>
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <span class="el-upload-list__item-actions">
|
|
|
+ <span
|
|
|
+ class="el-upload-list__item-preview"
|
|
|
+ @click="handleVideoPreview(form.videoUrl)"
|
|
|
+ >
|
|
|
+ <i class="el-icon-zoom-in"></i>
|
|
|
+ </span>
|
|
|
+ <span
|
|
|
+ class="el-upload-list__item-delete"
|
|
|
+ @click="handleRemove(file)"
|
|
|
+ >
|
|
|
+ <i class="el-icon-delete"></i>
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <div v-if="file.status === 'uploading'" class="el-upload-list__item-progress">
|
|
|
+ <div class="dual-upload-progress">
|
|
|
+ <div class="total-progress">
|
|
|
+ <el-progress
|
|
|
+ :percentage="currentUploadProgress.total"
|
|
|
+ :show-text="true"
|
|
|
+ :width="52"
|
|
|
+ :format="() => `${Math.round(currentUploadProgress.total)}%`"
|
|
|
+ ></el-progress>
|
|
|
+ </div>
|
|
|
+ <div class="line-progress-container">
|
|
|
+ <div class="line-progress-item">
|
|
|
+ <span class="line-label">线路1:</span>
|
|
|
+ <el-progress
|
|
|
+ :percentage="currentUploadProgress.line1"
|
|
|
+ :show-text="false"
|
|
|
+ :width="30"
|
|
|
+ :status="currentUploadProgress.line1Status === 'success' ? 'success' :
|
|
|
+ currentUploadProgress.line1Status === 'failed' ? 'exception' : ''"
|
|
|
+ ></el-progress>
|
|
|
+ <span class="line-status-text">{{ Math.round(currentUploadProgress.line1) }}%</span>
|
|
|
+ </div>
|
|
|
+ <div class="line-progress-item">
|
|
|
+ <span class="line-label">线路2:</span>
|
|
|
+ <el-progress
|
|
|
+ :percentage="currentUploadProgress.line2"
|
|
|
+ :show-text="false"
|
|
|
+ :width="30"
|
|
|
+ :status="currentUploadProgress.line2Status === 'success' ? 'success' :
|
|
|
+ currentUploadProgress.line2Status === 'failed' ? 'exception' : ''"
|
|
|
+ ></el-progress>
|
|
|
+ <span class="line-status-text">{{ Math.round(currentUploadProgress.line2) }}%</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-upload>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="时长">
|
|
|
+ <span>{{ formatDuration(form.duration) }}</span>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer" class="dialog-footer">
|
|
|
+ <el-button @click="cancel">取消</el-button>
|
|
|
+ <el-button type="primary" @click="submitForm" :loading="add" :disabled="isUploading || add">
|
|
|
+ {{ isUploading ? '上传中...' : '保存' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <el-dialog
|
|
|
+ title="视频预览"
|
|
|
+ :visible.sync="videoPreviewVisible"
|
|
|
+ append-to-body
|
|
|
+ :close-on-click-modal="true"
|
|
|
+ width="700px"
|
|
|
+ class="video-preview-dialog"
|
|
|
+ :modal-append-to-body="false"
|
|
|
+ :before-close="handleCloseVideoPreview">
|
|
|
+ <video ref="up-video" id="video" width="100%" height="400px" controls :src="videoPreviewUrl" />
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!--批量修改弹框-->
|
|
|
+ <el-dialog :title="'批量修改'" :visible.sync="batchUpdateVisible" width="700px" append-to-body :before-close="cancel"
|
|
|
+ @minimize="hasMinimizableDialog = true" @restore="hasMinimizableDialog = false">
|
|
|
+ <el-form ref="form" :model="batchUpdateForm" :rules="rules" label-width="80px">
|
|
|
+ <el-form-item label="分类" prop="typeId">
|
|
|
+ <el-select v-model="batchUpdateForm.typeId" placeholder="请选择分类" style="width: 100%" @change="val => changeCateType(val, 2)">
|
|
|
+ <el-option
|
|
|
+ v-for="item in rootTypeList"
|
|
|
+ :key="item.dictValue"
|
|
|
+ :label="item.dictLabel"
|
|
|
+ :value="item.dictValue">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="子分类" prop="typeSubId">
|
|
|
+ <el-select v-model="batchUpdateForm.typeSubId" clearable placeholder="请选择子分类" style="width: 100%">
|
|
|
+ <el-option
|
|
|
+ v-for="item in subTypeList"
|
|
|
+ :key="item.dictValue"
|
|
|
+ :label="item.dictLabel"
|
|
|
+ :value="item.dictValue">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="cancelBatch">取 消</el-button>
|
|
|
+ <el-button type="primary" @click="submitBatchUpdate">保 存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ <!-- 批量选择视频弹窗 -->
|
|
|
+ <minimizable-dialog :title="'选择视频'" :visible.sync="batchAddVisible" width="1200px" append-to-body class="batch-dialog"
|
|
|
+ :close-on-click-modal="false" :before-close="cancelBeforeBatch" @minimize="hasMinimizableDialog = true"
|
|
|
+ @restore="hasMinimizableDialog = false">
|
|
|
+ <div class="filter-container">
|
|
|
+ <el-button type="primary" icon="el-icon-plus" size="small" @click="showUploadPanel">上传视频</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table
|
|
|
+ v-loading="batchLoading"
|
|
|
+ :data="videoList"
|
|
|
+ height="350"
|
|
|
+ border>
|
|
|
+ <el-table-column label="序号" width="60" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ scope.$index + 1 }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="素材名称" align="center" prop="resourceName" min-width="120" />
|
|
|
+ <el-table-column label="文件名称" align="center" prop="fileName" min-width="120" />
|
|
|
+ <el-table-column label="分类" align="center" min-width="100">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ getTypeName(scope.row.typeId) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="子分类" align="center" min-width="100">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ getTypeName(scope.row.typeSubId) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="关联项目" align="center" min-width="100">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <a
|
|
|
+ @click="handleViewProject(scope.row, 4)"
|
|
|
+ :style="scope.row.projectIds.length > 0 ? {
|
|
|
+ backgroundColor: '#409EFF',
|
|
|
+ color: 'white',
|
|
|
+ border: 'none',
|
|
|
+ borderRadius: '4px',
|
|
|
+ padding: '4px 12px',
|
|
|
+ cursor: 'pointer',
|
|
|
+ fontSize: '12px',
|
|
|
+ display: 'inline-block',
|
|
|
+ textDecoration: 'none'
|
|
|
+ } : {
|
|
|
+ backgroundColor: 'rgb(154 156 159)',
|
|
|
+ color: 'white',
|
|
|
+ border: 'none',
|
|
|
+ cursor: 'pointer',
|
|
|
+ borderRadius: '4px',
|
|
|
+ padding: '4px 12px',
|
|
|
+ fontSize: '12px',
|
|
|
+ display: 'inline-block',
|
|
|
+ textDecoration: 'none'
|
|
|
+ }">
|
|
|
+ {{ scope.row.projectIds.length > 0 ? '查看详情' : '未关联题目' }}
|
|
|
+ </a>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="视频文件" align="center" prop="fileName" min-width="120">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <a
|
|
|
+ @click="handleVideoPreview(scope.row.videoUrl)"
|
|
|
+ style="background-color: #409EFF; color: white; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; display: inline-block; text-decoration: none;">
|
|
|
+ 查看文件
|
|
|
+ </a>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="视频时长" align="center" width="80">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ formatDuration(scope.row.duration) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="上传进度" align="center" width="200">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <div class="batch-upload-progress">
|
|
|
+ <div v-if="scope.row.uploadStatus === 'queued'" class="queue-status">
|
|
|
+ <el-tag :color="getQueueStatusColor(scope.row.uploadStatus)" size="small">
|
|
|
+ {{ getUploadStatusText(scope.row.uploadStatus, scope.row.queuePosition) }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div v-else class="total-progress-row">
|
|
|
+ <span class="progress-label">总进度:</span>
|
|
|
+ <el-progress
|
|
|
+ :percentage="scope.row.progress || 0"
|
|
|
+ :status="getProgressStatus(scope.row)"
|
|
|
+ :show-text="true"
|
|
|
+ :format="() => `${Math.round(scope.row.progress || 0)}%`"
|
|
|
+ ></el-progress>
|
|
|
+ </div>
|
|
|
+ <div v-if="scope.row.uploadDetails && scope.row.uploadStatus !== 'queued'" class="line-progress-rows">
|
|
|
+ <div class="line-progress-row">
|
|
|
+ <span class="line-label">线路1:</span>
|
|
|
+ <el-progress
|
|
|
+ :percentage="scope.row.uploadDetails.line1 || 0"
|
|
|
+ :status="scope.row.uploadDetails.line1Status === 'success' ? 'success' :
|
|
|
+ scope.row.uploadDetails.line1Status === 'failed' ? 'exception' : 'warning'"
|
|
|
+ :show-text="false"
|
|
|
+ style="width: 60px;"
|
|
|
+ ></el-progress>
|
|
|
+ <span class="line-percentage">{{ Math.round(scope.row.uploadDetails.line1 || 0) }}%</span>
|
|
|
+ </div>
|
|
|
+ <div class="line-progress-row">
|
|
|
+ <span class="line-label">线路2:</span>
|
|
|
+ <el-progress
|
|
|
+ :percentage="scope.row.uploadDetails.line2 || 0"
|
|
|
+ :status="scope.row.uploadDetails.line2Status === 'success' ? 'success' :
|
|
|
+ scope.row.uploadDetails.line2Status === 'failed' ? 'exception' : 'warning'"
|
|
|
+ :show-text="false"
|
|
|
+ style="width: 60px;"
|
|
|
+ ></el-progress>
|
|
|
+ <span class="line-percentage">{{ Math.round(scope.row.uploadDetails.line2 || 0) }}%</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" align="center" width="150">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-button
|
|
|
+ size="mini"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-edit"
|
|
|
+ :disabled="scope.row.progress !== 100"
|
|
|
+ @click="handleEditVideo(scope.row)">
|
|
|
+ 编辑
|
|
|
+ <el-tooltip
|
|
|
+ v-if="scope.row.progress !== 100"
|
|
|
+ content="上传完成后可编辑"
|
|
|
+ placement="top"
|
|
|
+ >
|
|
|
+ <i class="el-icon-question"></i>
|
|
|
+ </el-tooltip>
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="mini"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-delete"
|
|
|
+ @click="handleDeleteVideo(scope.row)"
|
|
|
+ :style="scope.row.uploadStatus === 'queued' ? 'color: #E6A23C;' : ''"
|
|
|
+ >
|
|
|
+ {{ scope.row.uploadStatus === 'queued' ? '取消' : '删除' }}
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="scope.row.progress < 100 && scope.row.uploadStatus === 'failed'"
|
|
|
+ size="mini"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-refresh"
|
|
|
+ @click="retryBatchUpload(scope.row)"
|
|
|
+ style="color: #E6A23C;"
|
|
|
+ >
|
|
|
+ 重试
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="cancelBatch">取 消</el-button>
|
|
|
+ <el-button type="primary" :loading="add" :disabled="add || isUploading" @click="submitBatchAdd">保 存</el-button>
|
|
|
+ </div>
|
|
|
+ <!-- 批量上传视频弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ title="批量上传视频"
|
|
|
+ :visible.sync="showUpload"
|
|
|
+ width="500px"
|
|
|
+ append-to-body
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ class="upload-dialog">
|
|
|
+ <el-form :model="batchUploadForm" ref="batchUploadForm" label-width="80px">
|
|
|
+ <el-form-item style="margin-top: 20px" label="分类" prop="typeId" :rules="[{ required: true, message: '请选择分类', trigger: 'blur' }]">
|
|
|
+ <el-select
|
|
|
+ v-model="batchUploadForm.typeId"
|
|
|
+ placeholder="请选择分类"
|
|
|
+ style="width: 100%"
|
|
|
+ @change="val => changeCateType(val, 3)"
|
|
|
+ filterable
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in rootTypeList"
|
|
|
+ :key="item.dictValue"
|
|
|
+ :label="item.dictLabel"
|
|
|
+ :value="item.dictValue">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="子分类" prop="typeSubId" :rules="[{ required: true, message: '请选择子分类', trigger: 'blur' }]">
|
|
|
+ <el-select
|
|
|
+ v-model="batchUploadForm.typeSubId"
|
|
|
+ clearable
|
|
|
+ placeholder="请选择子分类"
|
|
|
+ style="width: 100%"
|
|
|
+ @change="changeSubType"
|
|
|
+ filterable
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in subTypeList"
|
|
|
+ :key="item.dictValue"
|
|
|
+ :label="item.dictLabel"
|
|
|
+ :value="item.dictValue">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="视频展示类型" prop="displayType">
|
|
|
+ <el-radio-group v-model="batchUploadForm.displayType">
|
|
|
+ <el-radio label="landscape">横屏</el-radio>
|
|
|
+ <el-radio label="portrait">竖屏</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="关联题目" prop="projectIds" v-show="currentProject === 'myhk'">
|
|
|
+ <el-select
|
|
|
+ ref="customSelect"
|
|
|
+ class="custom-select-class"
|
|
|
+ v-model="batchUploadForm.projectIds"
|
|
|
+ multiple
|
|
|
+ placeholder="请选择关联题目"
|
|
|
+ @click.native.stop="openProjectDialog(batchUploadForm.projectIds, 1)"
|
|
|
+ style="width: 100%;">
|
|
|
+ <el-option
|
|
|
+ v-for="item in projectShowList"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.title"
|
|
|
+ :value="item.id">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="上传视频" prop="files">
|
|
|
+ <el-upload
|
|
|
+ ref="batchVideoUpload"
|
|
|
+ action="#"
|
|
|
+ :http-request="batchVideoUpload"
|
|
|
+ :file-list="batchFileList"
|
|
|
+ :on-change="handleBatchFileChange"
|
|
|
+ multiple
|
|
|
+ accept=".mp4">
|
|
|
+ <el-button type="primary" :disabled="batchUploadForm.typeSubId === null">选择文件</el-button>
|
|
|
+ <div slot="tip" class="el-upload__tip">选择分类后才可以上传视频</div>
|
|
|
+ </el-upload>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 添加或修改视频素材库对话框 -->
|
|
|
+ <el-dialog :title="batchEditDialog.title" :visible.sync="batchEditDialog.open" width="600px" append-to-body :before-close="batchEditCancel">
|
|
|
+ <el-form ref="batchEditDialogForm" :model="batchEditDialog.form" :rules="batchEditDialog.rules" label-width="80px">
|
|
|
+ <el-form-item label="素材名称" prop="resourceName" style="margin-top: 20px">
|
|
|
+ <el-input v-model="batchEditDialog.form.resourceName" placeholder="请输入" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="视频展示类型" prop="displayType">
|
|
|
+ <el-radio-group v-model="batchEditDialog.form.displayType">
|
|
|
+ <el-radio label="landscape">横屏</el-radio>
|
|
|
+ <el-radio label="portrait">竖屏</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="文件名称" prop="fileName" style="margin-top: 20px;display: none">
|
|
|
+ <el-input v-model="batchEditDialog.form.fileName" placeholder="请输入" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <el-form-item label="关联题目" prop="projectIds">
|
|
|
+ <el-select
|
|
|
+ ref="customSelect"
|
|
|
+ class="custom-select-class"
|
|
|
+ v-model="batchEditDialog.form.projectIds"
|
|
|
+ multiple
|
|
|
+ placeholder="请选择关联题目"
|
|
|
+ @click.native.stop="openProjectDialog(batchEditDialog.form.projectIds, 2)"
|
|
|
+ style="width: 100%;">
|
|
|
+ <el-option
|
|
|
+ v-for="item in projectShowList"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.title"
|
|
|
+ :value="item.id">
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div slot="footer" class="dialog-footer">
|
|
|
+ <el-button @click="batchEditCancel">取消</el-button>
|
|
|
+ <el-button type="primary" @click="batchEditSubmitForm">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ </minimizable-dialog>
|
|
|
+
|
|
|
+ <!-- 项目选择弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ title="选择题目"
|
|
|
+ :visible.sync="projectDialogVisible"
|
|
|
+ width="1000px"
|
|
|
+ append-to-body
|
|
|
+ @close="cancelSelectProject"
|
|
|
+ :close-on-click-modal="false">
|
|
|
+ <div class="project-container">
|
|
|
+ <!-- 左侧分类树 -->
|
|
|
+ <div class="category-tree">
|
|
|
+ <div class="tree-fixed-header">
|
|
|
+ <el-input
|
|
|
+ placeholder="请输入分类名称"
|
|
|
+ v-model="categoryFilterText"
|
|
|
+ size="small"
|
|
|
+ clearable
|
|
|
+ prefix-icon="el-icon-search">
|
|
|
+ </el-input>
|
|
|
+ </div>
|
|
|
+ <div class="tree-content">
|
|
|
+ <el-tree
|
|
|
+ ref="categoryTree"
|
|
|
+ :data="categoryTreeData"
|
|
|
+ node-key="cateId"
|
|
|
+ highlight-current
|
|
|
+ :filter-node-method="filterNode"
|
|
|
+ :props="{ label: 'cateName', children: 'children' }"
|
|
|
+ @node-click="handleCategoryClick"
|
|
|
+ :expand-on-click-node="false"
|
|
|
+ default-expand-all>
|
|
|
+ <span class="custom-tree-node" slot-scope="{ node, data }">
|
|
|
+ <span>{{ node.label }}</span>
|
|
|
+ </span>
|
|
|
+ </el-tree>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧题目列表 -->
|
|
|
+ <div class="project-list">
|
|
|
+ <div class="filter-container">
|
|
|
+ <el-form :inline="true" :model="projectQueryParams" ref="projectForm">
|
|
|
+ <el-form-item>
|
|
|
+ <el-input prefix-icon="el-icon-search" @input="searchProjects" v-model="projectQueryParams.title" placeholder="请输入题目标题" clearable size="small" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table
|
|
|
+ ref="projectTable"
|
|
|
+ height="350"
|
|
|
+ :data="projectList"
|
|
|
+ row-key="id"
|
|
|
+ v-loading="projectLoading"
|
|
|
+ border
|
|
|
+ @select="handleProjectSelect"
|
|
|
+ @select-all="handleProjectSelect">
|
|
|
+ <el-table-column type="selection" reserve-selection width="55" align="center" />
|
|
|
+ <el-table-column label="序号" width="60" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ scope.row.sort }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="题目标题" align="center" prop="title" min-width="200" show-overflow-tooltip />
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div class="table-footer">
|
|
|
+ <el-pagination style="text-align: right" v-show="projectListTotal>0" :pager-count="5" background
|
|
|
+ @size-change="handleProjectPageSizeChange" @current-change="handleProjectPageChange" :current-page="projectQueryParams.pageNum"
|
|
|
+ :page-sizes="[10, 20, 30, 50]" :page-size="projectQueryParams.pageSize" layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ :total="projectListTotal">
|
|
|
+ </el-pagination>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div slot="footer" class="dialog-footer">
|
|
|
+ <span style="float: left; color: #606266; font-size: 13px;">
|
|
|
+ 共选择 <span style="color: #409EFF; font-weight: bold;">{{ selectedProjectIds.length }}</span> 个题目
|
|
|
+ </span>
|
|
|
+ <el-button @click="projectDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="confirmSelectProject">确定</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 项目列表弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ title="题目列表"
|
|
|
+ :visible.sync="projectListDialogVisible"
|
|
|
+ width="600px"
|
|
|
+ append-to-body
|
|
|
+ :close-on-click-modal="false">
|
|
|
+ <div class="project-list-container" style="max-height: 500px; overflow-y: auto; padding: 10px;">
|
|
|
+ <!-- 题目列表 -->
|
|
|
+ <div v-for="(item, index) in projectShowList" :key="index" class="question-card">
|
|
|
+ <!-- 题目标题 -->
|
|
|
+ <div class="question-header">
|
|
|
+ <span class="question-index">{{ index + 1 }}</span>
|
|
|
+ <span class="question-title">{{ item.title }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 题目类型 -->
|
|
|
+ <div class="question-type">
|
|
|
+ <el-tag size="small" type="primary">{{ item.type === 1 ? '单选' : '多选' }}</el-tag>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 题目内容 -->
|
|
|
+ <div class="question-content" v-if="item.question && item.question.length > 0">
|
|
|
+ <div class="content-title">题目内容:</div>
|
|
|
+ <div v-for="(q, qIndex) in JSON.parse(item.question)" :key="qIndex" class="question-item"
|
|
|
+ :style=" q.isAnswer === 1 ? 'background-color: rgb(234 245 251)' : ''">
|
|
|
+ <div class="question-item-header">
|
|
|
+ <span class="item-index">{{ convertToLetter(qIndex) }}</span>
|
|
|
+ <span class="item-name">{{ q.name }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 答案 -->
|
|
|
+ <div class="question-answer" v-if="item.answer">
|
|
|
+ <span class="answer-label">答案:</span>
|
|
|
+ <span class="answer-content">{{ item.answer }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import {
|
|
|
+ addVideoResource,
|
|
|
+ deleteVideoResource,
|
|
|
+ getVideoResource,
|
|
|
+ listVideoResource,
|
|
|
+ updateVideoResource,
|
|
|
+ batchAddVideoResource,
|
|
|
+ batchUpdateVideoResource
|
|
|
+} from '@/api/course/videoResource'
|
|
|
+import {listUserCourseCategory,getCatePidList,getCateListByPid} from '@/api/course/userCourseCategory'
|
|
|
+import {getByIds, listCourseQuestionBank} from '@/api/course/courseQuestionBank'
|
|
|
+import {getThumbnail} from "@/api/course/userVideo";
|
|
|
+import {uploadObject} from "@/utils/cos.js";
|
|
|
+import {uploadToOBS} from "@/utils/obs.js";
|
|
|
+import {uploadToHSY} from "@/utils/hsy.js";
|
|
|
+import MinimizableDialog from "@/components/MinimizableDialog"
|
|
|
+import log from "@/views/monitor/job/log.vue";
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'VideoResource',
|
|
|
+ components: {
|
|
|
+ MinimizableDialog
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ /** 本页视频资源类型:0-视频资源,1-公域课视频资源(与后端 video_type 一致) */
|
|
|
+ pageVideoType: 1,
|
|
|
+ filteredRootTypeList: [], // 过滤后的分类列表
|
|
|
+ originalRootTypeList: [], // 原始分类列表
|
|
|
+ // 遮罩层
|
|
|
+ loading: true,
|
|
|
+ // 选中数组
|
|
|
+ ids: [],
|
|
|
+ // 非单个禁用
|
|
|
+ single: true,
|
|
|
+ // 非多个禁用
|
|
|
+ multiple: true,
|
|
|
+ // 显示搜索条件
|
|
|
+ showSearch: true,
|
|
|
+ // 总条数
|
|
|
+ total: 0,
|
|
|
+ // 视频素材库表格数据
|
|
|
+ resourceList: [],
|
|
|
+ // 弹出层标题
|
|
|
+ title: "",
|
|
|
+ // 是否显示弹出层
|
|
|
+ open: false,
|
|
|
+ // 查询参数
|
|
|
+ queryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ resourceName: null,
|
|
|
+ fileName: null,
|
|
|
+ typeId: null,
|
|
|
+ typeSubId: null,
|
|
|
+ videoType: 1
|
|
|
+ },
|
|
|
+ // 表单参数
|
|
|
+ form: {
|
|
|
+ id: null,
|
|
|
+ resourceName: null,
|
|
|
+ fileName: null,
|
|
|
+ thumbnail: null,
|
|
|
+ line1: null,
|
|
|
+ line2: null,
|
|
|
+ line3: null,
|
|
|
+ duration: null,
|
|
|
+ fileSize: null,
|
|
|
+ fileKey: null,
|
|
|
+ videoUrl: null,
|
|
|
+ typeId: null,
|
|
|
+ typeSubId: null,
|
|
|
+ projectIds: [],
|
|
|
+ sort: null,
|
|
|
+ displayType: 'landscape',
|
|
|
+ videoType: 1,
|
|
|
+ hsyVid:null,//火山云上传视频返回vid
|
|
|
+ hsyVodUrl:null,//火山云url
|
|
|
+ // 新增上传状态字段
|
|
|
+ uploadStatus: 'pending', // pending, uploading, success, failed
|
|
|
+ uploadProgress: {
|
|
|
+ total: 0,
|
|
|
+ line1: 0,
|
|
|
+ line2: 0,
|
|
|
+ line1Status: 'pending', // pending, uploading, success, failed
|
|
|
+ line2Status: 'pending'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 表单校验
|
|
|
+ rules: {
|
|
|
+ resourceName: [
|
|
|
+ { required: true, message: "素材名称不能为空", trigger: "blur" }
|
|
|
+ ],
|
|
|
+ fileName: [
|
|
|
+ { required: true, message: "文件名称不能为空", trigger: "blur" }
|
|
|
+ ],
|
|
|
+ typeId: [
|
|
|
+ { required: true, message: "请选择分类", trigger: "change" }
|
|
|
+ ],
|
|
|
+ typeSubId: [
|
|
|
+ { required: true, message: "请选择子分类", trigger: "change" }
|
|
|
+ ],
|
|
|
+ sort: [
|
|
|
+ { required: true, message: '排序不能为空', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ videoUrl: [
|
|
|
+ { required: true, message: "请上传视频", trigger: "change" }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ // 课程列表数据
|
|
|
+ projectList: [],
|
|
|
+ projectShowList: [],
|
|
|
+ // 分类
|
|
|
+ typeList: [],
|
|
|
+ rootTypeList: [],
|
|
|
+ subTypeList: [],
|
|
|
+ // 视频上传
|
|
|
+ videoDisabled: false,
|
|
|
+ videoPreviewVisible: false,
|
|
|
+ videoPreviewUrl: '',
|
|
|
+ fileList: [],
|
|
|
+ // 批量添加相关
|
|
|
+ batchAddVisible: false,
|
|
|
+ batchLoading: false,
|
|
|
+ videoList: [],
|
|
|
+
|
|
|
+ // 批量修改相关
|
|
|
+ batchUpdateVisible: false,
|
|
|
+ batchUpdateLoading: false,
|
|
|
+ batchUpdateForm: {
|
|
|
+ typeId: null,
|
|
|
+ typeSubId: null,
|
|
|
+ ids: [],
|
|
|
+ },
|
|
|
+
|
|
|
+ // 批量上传相关
|
|
|
+ showUpload: false,
|
|
|
+ batchUploadForm: {
|
|
|
+ typeId: null,
|
|
|
+ typeSubId: null,
|
|
|
+ projectIds: [],
|
|
|
+ files: [],
|
|
|
+ displayType: 'landscape'
|
|
|
+ },
|
|
|
+ batchFileList: [],
|
|
|
+
|
|
|
+ // 弹窗选择项目相关
|
|
|
+ projectDialogVisible: false,
|
|
|
+ projectQueryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ questionType: null,
|
|
|
+ questionSubType: null,
|
|
|
+ title: null
|
|
|
+ },
|
|
|
+ projectLoading: false,
|
|
|
+ projectListTotal: 0,
|
|
|
+ selectedProjectIds: [],
|
|
|
+ selectedType: 0,
|
|
|
+ currentRow: null,
|
|
|
+ // 分类树相关数据
|
|
|
+ categoryFilterText: '',
|
|
|
+ categoryTreeData: [],
|
|
|
+ add: false,
|
|
|
+ // 题目列表
|
|
|
+ projectListDialogVisible: false,
|
|
|
+ // 修改视频记录
|
|
|
+ batchEditDialog: {
|
|
|
+ title: '修改视频',
|
|
|
+ open: false,
|
|
|
+ form: {},
|
|
|
+ rules: {
|
|
|
+ resourceName: [
|
|
|
+ { required: true, message: "素材名称不能为空", trigger: "blur" }
|
|
|
+ ],
|
|
|
+ fileName: [
|
|
|
+ { required: true, message: "文件名称不能为空", trigger: "blur" }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ },
|
|
|
+ // 是否存在最小化窗口
|
|
|
+ hasMinimizableDialog: false,
|
|
|
+ // 新增上传相关状态
|
|
|
+ isUploading: false,
|
|
|
+ currentUploadProgress: {
|
|
|
+ total: 0,
|
|
|
+ line1: 0,
|
|
|
+ line2: 0,
|
|
|
+ line1Status: 'pending',
|
|
|
+ line2Status: 'pending'
|
|
|
+ },
|
|
|
+ // 上传任务队列
|
|
|
+ uploadQueue: [], // 上传队列
|
|
|
+ currentBatchSize: 2, // 每批上传数量
|
|
|
+ isProcessingBatch: false, // 是否正在处理批次
|
|
|
+ currentBatchIndex: 0, // 当前批次索引
|
|
|
+ uploadCancellationTokens: new Map(), // Store cancellation functions by video ID
|
|
|
+ currentProject: process.env.VUE_APP_PROJECT
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ categoryFilterText(val) {
|
|
|
+ this.$refs.categoryTree.filter(val);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ this.getTypeList();
|
|
|
+ this.getRootTypeList()
|
|
|
+ this.getList();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ /** 将数字索引转换为字母序号 (0->A, 1->B, 等) */
|
|
|
+ convertToLetter(index) {
|
|
|
+ // 确保索引是数字
|
|
|
+ let numIndex = parseInt(index, 10);
|
|
|
+ // 如果无法转换成数字,返回原始值
|
|
|
+ if (isNaN(numIndex)) {
|
|
|
+ return index;
|
|
|
+ }
|
|
|
+ // 处理负数情况
|
|
|
+ if (numIndex < 0) {
|
|
|
+ return index;
|
|
|
+ }
|
|
|
+ // 直接使用索引计算ASCII码(0对应A,1对应B,以此类推)
|
|
|
+ return String.fromCharCode(65 + numIndex); // 65 是大写字母 A 的ASCII码
|
|
|
+ },
|
|
|
+ /** 查询视频素材库列表 */
|
|
|
+ getList() {
|
|
|
+ this.loading = true;
|
|
|
+ this.queryParams.videoType = this.pageVideoType;
|
|
|
+ listVideoResource(this.queryParams).then(response => {
|
|
|
+ this.resourceList = response.rows;
|
|
|
+ this.total = response.total;
|
|
|
+ this.loading = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ // 取消按钮
|
|
|
+ cancel() {
|
|
|
+ this.open = false;
|
|
|
+ this.reset();
|
|
|
+ this.resetForm("form");
|
|
|
+ this.batchUpdateVisible = false;
|
|
|
+ this.changeCateType(this.queryParams.typeId)
|
|
|
+ this.$refs.videoUpload.clearFiles()
|
|
|
+ },
|
|
|
+ // 表单重置
|
|
|
+ reset() {
|
|
|
+ // 初始化表单对象
|
|
|
+ this.form = {
|
|
|
+ id: null,
|
|
|
+ resourceName: null,
|
|
|
+ fileName: null,
|
|
|
+ thumbnail: null,
|
|
|
+ line1: null,
|
|
|
+ line2: null,
|
|
|
+ line3: null,
|
|
|
+ duration: null,
|
|
|
+ fileSize: null,
|
|
|
+ fileKey: null,
|
|
|
+ videoUrl: null,
|
|
|
+ typeId: null,
|
|
|
+ typeSubId: null,
|
|
|
+ projectIds: [],
|
|
|
+ displayType: 'landscape',
|
|
|
+ videoType: this.pageVideoType
|
|
|
+ };
|
|
|
+ // 重置表单验证状态
|
|
|
+ this.resetForm("form");
|
|
|
+ this.add = false
|
|
|
+ },
|
|
|
+ /** 搜索按钮操作 */
|
|
|
+ handleQuery() {
|
|
|
+ this.queryParams.pageNum = 1;
|
|
|
+ this.getList();
|
|
|
+ },
|
|
|
+ /** 重置按钮操作 */
|
|
|
+ resetQuery() {
|
|
|
+ this.resetForm("queryForm");
|
|
|
+ this.queryParams.videoType = this.pageVideoType;
|
|
|
+ this.handleQuery();
|
|
|
+ },
|
|
|
+ // 多选框选中数据
|
|
|
+ handleSelectionChange(selection) {
|
|
|
+ this.ids = selection.map(item => item.id)
|
|
|
+ this.single = selection.length!==1
|
|
|
+ this.multiple = !selection.length
|
|
|
+ },
|
|
|
+ /** 新增按钮操作 */
|
|
|
+ handleAdd() {
|
|
|
+ // 先清空文件列表
|
|
|
+ this.fileList = [];
|
|
|
+ this.projectShowList = [];
|
|
|
+
|
|
|
+ // 先重置表单
|
|
|
+ this.reset();
|
|
|
+ this.subTypeList = []
|
|
|
+
|
|
|
+ // 重置上传组件
|
|
|
+ if (this.$refs.videoUpload) {
|
|
|
+ this.$refs.videoUpload.clearFiles();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 所有重置完成后再打开弹窗
|
|
|
+ this.open = true;
|
|
|
+ this.title = "添加视频素材库";
|
|
|
+ },
|
|
|
+ /** 修改按钮操作 */
|
|
|
+ handleUpdate(row) {
|
|
|
+ // 先清空文件列表
|
|
|
+ this.fileList = [];
|
|
|
+ this.projectShowList = [];
|
|
|
+
|
|
|
+ // 先重置表单
|
|
|
+ this.reset();
|
|
|
+ this.subTypeList = []
|
|
|
+
|
|
|
+ const id = row.id
|
|
|
+
|
|
|
+ // 获取数据并设置表单
|
|
|
+ getVideoResource(id).then(async response => {
|
|
|
+ this.form = response.data;
|
|
|
+ // 视频展示类型:旧数据可能为空,默认横屏
|
|
|
+ if (!this.form.displayType) {
|
|
|
+ this.form.displayType = 'landscape';
|
|
|
+ }
|
|
|
+ if (this.form.videoType === null || this.form.videoType === undefined) {
|
|
|
+ this.form.videoType = this.pageVideoType;
|
|
|
+ }
|
|
|
+ await this.changeCateType(this.form.typeId)
|
|
|
+
|
|
|
+ // 处理projectIds,确保是数组格式
|
|
|
+ if (this.form.projectIds && typeof this.form.projectIds === 'string') {
|
|
|
+ this.form.projectIds = this.form.projectIds.split(',').map(id => parseInt(id));
|
|
|
+ } else if (!this.form.projectIds) {
|
|
|
+ this.form.projectIds = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果存在关联项目,获取项目详情用于回显
|
|
|
+ if (this.form.projectIds && this.form.projectIds.length > 0) {
|
|
|
+ // 加载项目列表信息用于回显
|
|
|
+ await getByIds({ids: this.form.projectIds.join(',')}).then(reponse => {
|
|
|
+ this.projectShowList = reponse.data
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果存在视频URL,设置fileList
|
|
|
+ if (this.form.videoUrl) {
|
|
|
+ this.fileList = [{
|
|
|
+ name: this.form.fileName || '视频文件',
|
|
|
+ url: this.form.videoUrl,
|
|
|
+ thumbnail: this.form.thumbnail,
|
|
|
+ status: 'success' // 设置为成功状态
|
|
|
+ }];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 所有设置完成后再打开弹窗
|
|
|
+ this.open = true;
|
|
|
+ this.title = "修改视频素材库";
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 提交按钮 */
|
|
|
+ submitForm() {
|
|
|
+ this.$refs["form"].validate(valid => {
|
|
|
+ if (valid) {
|
|
|
+ if (this.add){
|
|
|
+ this.$message.warning("请勿重复提交")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.add = true
|
|
|
+
|
|
|
+ const params = Object.assign({}, this.form);
|
|
|
+ console.log("提交素材表单参数",this.form)
|
|
|
+ params.videoType = this.pageVideoType;
|
|
|
+ params.projectIds = this.form.projectIds.join(',');
|
|
|
+ if (this.form.id != null) {
|
|
|
+ updateVideoResource(params).then(response => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.msgSuccess("修改成功");
|
|
|
+ this.open = false;
|
|
|
+ this.getList();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ addVideoResource(params).then(response => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.msgSuccess("新增成功");
|
|
|
+ this.open = false;
|
|
|
+ this.getList();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 删除按钮操作 */
|
|
|
+ handleDelete(row) {
|
|
|
+ const ids = row.id || this.ids;
|
|
|
+ this.$confirm('是否确认删除视频素材库编号为"' + ids + '"的数据项?', "警告", {
|
|
|
+ confirmButtonText: "确定",
|
|
|
+ cancelButtonText: "取消",
|
|
|
+ type: "warning"
|
|
|
+ }).then(function() {
|
|
|
+ return deleteVideoResource(ids);
|
|
|
+ }).then(() => {
|
|
|
+ this.getList();
|
|
|
+ this.msgSuccess("删除成功");
|
|
|
+ }).catch(function() {});
|
|
|
+ },
|
|
|
+ /** 查询视频分类列表 */
|
|
|
+ getTypeList() {
|
|
|
+ listUserCourseCategory().then(response => {
|
|
|
+ this.typeList = response.data;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ getRootTypeList() {
|
|
|
+ getCatePidList().then(response => {
|
|
|
+ this.rootTypeList = response.data;
|
|
|
+ this.originalRootTypeList = [...response.data]; // 保存原始数据
|
|
|
+ this.filteredRootTypeList = [...response.data]; // 初始化过滤列表
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 过滤分类选项 */
|
|
|
+ filterTypeOptions(query) {
|
|
|
+ if (!query) {
|
|
|
+ this.filteredRootTypeList = [...this.originalRootTypeList];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.filteredRootTypeList = this.originalRootTypeList.filter(item => {
|
|
|
+ return item.dictLabel.toLowerCase().includes(query.toLowerCase()) ||
|
|
|
+ item.dictValue.toString().includes(query);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 选择后重置过滤 */
|
|
|
+ onTypeSelect(value) {
|
|
|
+ // 确保选中后恢复完整列表
|
|
|
+ this.filteredRootTypeList = [...this.originalRootTypeList];
|
|
|
+ // 触发 change 事件
|
|
|
+ this.changeCateType(value, 1);
|
|
|
+ },
|
|
|
+ /** 重置过滤 */
|
|
|
+ resetFilter() {
|
|
|
+ this.filteredRootTypeList = [...this.originalRootTypeList];
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 清除过滤 */
|
|
|
+ clearFilter() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (!this.queryParams.typeId) {
|
|
|
+ this.filteredRootTypeList = [...this.originalRootTypeList];
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 处理下拉框显示状态变化 */
|
|
|
+ handleSelectVisibleChange(visible) {
|
|
|
+ if (!visible) {
|
|
|
+ // 下拉框关闭时恢复完整列表
|
|
|
+ this.filteredRootTypeList = [...this.originalRootTypeList];
|
|
|
+ } else {
|
|
|
+ // 下拉框打开时也确保列表完整
|
|
|
+ this.filteredRootTypeList = [...this.originalRootTypeList];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async changeCateType(val, type) {
|
|
|
+ if (type === 1) {
|
|
|
+ this.queryParams.typeSubId = null
|
|
|
+ }
|
|
|
+ if (type === 2) {
|
|
|
+ this.form.typeSubId = null
|
|
|
+ }
|
|
|
+ if (type === 3) {
|
|
|
+ this.batchUploadForm.typeSubId = null
|
|
|
+ if (this.currentProject === 'myhk') {
|
|
|
+ this.batchUploadForm.projectIds = []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.subTypeList = []
|
|
|
+ if (!val) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ await getCateListByPid(val).then(response => {
|
|
|
+ this.subTypeList = response.data
|
|
|
+ })
|
|
|
+ },
|
|
|
+ changeSubType(val) {
|
|
|
+ if (this.currentProject !== 'myhk') {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.projectShowList = []
|
|
|
+ this.batchUploadForm.projectIds = []
|
|
|
+ listCourseQuestionBank({questionSubType: val}).then(async response => {
|
|
|
+ const projectIds = response.rows.map(item => item.id)
|
|
|
+
|
|
|
+ // 如果存在关联项目,获取项目详情用于回显
|
|
|
+ if (projectIds && projectIds.length > 0) {
|
|
|
+ // 加载项目列表信息用于回显
|
|
|
+ await getByIds({ids: projectIds.join(',')}).then(reponse => {
|
|
|
+ this.projectShowList = reponse.data
|
|
|
+ this.batchUploadForm.projectIds = projectIds
|
|
|
+ });
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ /** 预览视频 */
|
|
|
+ handleVideoPreview(url) {
|
|
|
+ this.videoPreviewVisible = true;
|
|
|
+ this.videoPreviewUrl = url || this.form.videoUrl;
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 格式化视频时长 */
|
|
|
+ formatDuration(seconds) {
|
|
|
+ if (!seconds) return '00:00';
|
|
|
+
|
|
|
+ const minutes = Math.floor(seconds / 60);
|
|
|
+ const remainingSeconds = seconds % 60;
|
|
|
+
|
|
|
+ return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
|
+ },
|
|
|
+ /** 移除视频 */
|
|
|
+ handleRemove(file) {
|
|
|
+ this.fileList.splice(this.fileList.indexOf(file), 1);
|
|
|
+ this.form.videoUrl = '';
|
|
|
+ this.form.thumbnail = '';
|
|
|
+ this.form.duration = 0;
|
|
|
+ this.form.fileSize = 0;
|
|
|
+ this.form.fileName = '';
|
|
|
+ this.form.line1 = '';
|
|
|
+ this.form.line_2 = '';
|
|
|
+ this.form.line_3 = '';
|
|
|
+ this.uploadType = null
|
|
|
+ this.fileSize = null
|
|
|
+ this.fileKey = null
|
|
|
+ },
|
|
|
+ //获取第一帧封面
|
|
|
+ async getFirstThumbnail(file, form){
|
|
|
+ try {
|
|
|
+ //截取小文件
|
|
|
+ // 打印原始文件名和大小
|
|
|
+ console.log("原始文件名:", file.name);
|
|
|
+ console.log("原始文件大小:", file.size, "bytes");
|
|
|
+ console.log("原始文件类型:", file.type);
|
|
|
+
|
|
|
+ // 截取小文件
|
|
|
+ const clippedBlob = await this.clipVideoFirstTwoSeconds(file);
|
|
|
+ console.log("clippedBlob:::::::::", clippedBlob);
|
|
|
+ console.log("截取后的Blob大小:", clippedBlob.size, "bytes");
|
|
|
+ console.log("截取后的Blob类型:", clippedBlob.type);
|
|
|
+
|
|
|
+ const clippedFile = new File([clippedBlob], 'clipped_video.mp4', {
|
|
|
+ type: 'video/mp4',
|
|
|
+ lastModified: Date.now()
|
|
|
+ });
|
|
|
+ // 3. 调用接口获取封面
|
|
|
+ const response = await getThumbnail(clippedFile);
|
|
|
+ console.log("获取封面请求---------------》",response)
|
|
|
+ form.thumbnail = response.url;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取封面失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ //截取大文件视频
|
|
|
+ async clipVideoFirstTwoSeconds(file) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const video = document.createElement('video');
|
|
|
+ video.src = URL.createObjectURL(file);
|
|
|
+ video.muted = true;
|
|
|
+ video.playsInline = true;
|
|
|
+
|
|
|
+ video.onloadedmetadata = () => {
|
|
|
+ video.currentTime = 0; // 定位到第一帧
|
|
|
+ video.onseeked = () => {
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ canvas.width = video.videoWidth;
|
|
|
+ canvas.height = video.videoHeight;
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
+
|
|
|
+ canvas.toBlob(
|
|
|
+ (blob) => {
|
|
|
+ URL.revokeObjectURL(video.src);
|
|
|
+ resolve(blob); // 返回 JPEG Blob
|
|
|
+ },
|
|
|
+ 'image/jpeg',
|
|
|
+ 0.8 // 质量
|
|
|
+ );
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ video.onerror = () => {
|
|
|
+ URL.revokeObjectURL(video.src);
|
|
|
+ reject(new Error('视频加载失败'));
|
|
|
+ };
|
|
|
+ });
|
|
|
+ },
|
|
|
+ //上传腾讯云Pcdn
|
|
|
+ async uploadVideoToTxPcdn(file, form, onProgress) {
|
|
|
+ try {
|
|
|
+ // 更新线路1状态为上传中
|
|
|
+ this.updateUploadProgress('line1Status', 'uploading');
|
|
|
+
|
|
|
+ const data = await uploadObject(file, (progress) => {
|
|
|
+ const progressPercent = Math.floor(progress.percent * 100);
|
|
|
+ this.updateUploadProgress('line1', progressPercent);
|
|
|
+ const progressEvent = { percent: progressPercent, loaded: progress.loaded, total: progress.total, lengthComputable: true };
|
|
|
+ onProgress(progressEvent);
|
|
|
+ }, 1, (uploadInfo) => {
|
|
|
+ if (form.tempId) {
|
|
|
+ const tokens = this.uploadCancellationTokens.get(form.tempId) || {};
|
|
|
+ tokens.cos = uploadInfo.cancel;
|
|
|
+ this.uploadCancellationTokens.set(form.tempId, tokens);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ let line_1 = `${process.env.VUE_APP_VIDEO_LINE_1}${data.urlPath}`;
|
|
|
+ form.fileKey = data.urlPath.substring(1);
|
|
|
+ form.videoUrl = line_1;
|
|
|
+ form.line1 = line_1;
|
|
|
+
|
|
|
+ // 更新线路1状态为成功
|
|
|
+ this.updateUploadProgress('line1Status', 'success');
|
|
|
+ this.updateUploadProgress('line1', 100);
|
|
|
+
|
|
|
+ this.$message.success("线路一上传成功");
|
|
|
+ return { success: true, url: line_1 };
|
|
|
+ } catch (error) {
|
|
|
+ // 更新线路1状态为失败
|
|
|
+ this.updateUploadProgress('line1Status', 'failed');
|
|
|
+ this.$message.error("线路一上传失败");
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ //上传华为云Obs
|
|
|
+ // async uploadVideoToHwObs(file, form, onProgress) {
|
|
|
+ // try {
|
|
|
+ // // 更新线路2状态为上传中
|
|
|
+ // this.updateUploadProgress('line2Status', 'uploading');
|
|
|
+ //
|
|
|
+ // const data = await uploadToOBS(file, (progress) => {
|
|
|
+ // const progressPercent = Math.floor(progress);
|
|
|
+ // this.updateUploadProgress('line2', progressPercent);
|
|
|
+ // const progressEvent = { percent: progressPercent, loaded: progress, total: progress, lengthComputable: true };
|
|
|
+ // onProgress(progressEvent);
|
|
|
+ // }, 1, (uploadInfo) => {
|
|
|
+ // if (form.tempId) {
|
|
|
+ // const tokens = this.uploadCancellationTokens.get(form.tempId) || {};
|
|
|
+ // tokens.obs = uploadInfo.cancel;
|
|
|
+ // this.uploadCancellationTokens.set(form.tempId, tokens);
|
|
|
+ // }
|
|
|
+ // });
|
|
|
+ //
|
|
|
+ // form.line2 = `${process.env.VUE_APP_VIDEO_LINE_2}/${data.urlPath}`;
|
|
|
+ //
|
|
|
+ // // 更新线路2状态为成功
|
|
|
+ // this.updateUploadProgress('line2Status', 'success');
|
|
|
+ // this.updateUploadProgress('line2', 100);
|
|
|
+ //
|
|
|
+ // this.$message.success("线路二上传成功");
|
|
|
+ // return { success: true, url: form.line2 };
|
|
|
+ // } catch (error) {
|
|
|
+ // // 更新线路2状态为失败
|
|
|
+ // this.updateUploadProgress('line2Status', 'failed');
|
|
|
+ // this.$message.error("线路二上传失败");
|
|
|
+ // return { success: false, error: error.message };
|
|
|
+ // }
|
|
|
+ // },
|
|
|
+ //上传火山云
|
|
|
+ async uploadVideoToHsy(file, form, onProgress) {
|
|
|
+ try {
|
|
|
+ this.updateUploadProgress('line2Status', 'uploading');
|
|
|
+
|
|
|
+ const data = await uploadToHSY(
|
|
|
+ file,
|
|
|
+ (progress) => {
|
|
|
+ // 火山云的进度是小数0-1
|
|
|
+ if (typeof progress.percent === 'number') {
|
|
|
+ const percent = Math.floor(progress.percent * 100);
|
|
|
+
|
|
|
+ // 更新线路2进度
|
|
|
+ this.updateUploadProgress('line2', percent);
|
|
|
+
|
|
|
+ // 对外统一 progress 事件(模拟 xhr)
|
|
|
+ onProgress?.({
|
|
|
+ percent,
|
|
|
+ loaded: percent,
|
|
|
+ total: 100,
|
|
|
+ lengthComputable: true
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 状态同步(成功 / 失败)
|
|
|
+ if (progress.status === 'success') {
|
|
|
+ this.updateUploadProgress('line2Status', 'success');
|
|
|
+ this.updateUploadProgress('line2', 100);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (progress.status === 'failed') {
|
|
|
+ this.updateUploadProgress('line2Status', 'failed');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ 1,
|
|
|
+ (uploadInfo) => {
|
|
|
+ if (form.tempId) {
|
|
|
+ const tokens = this.uploadCancellationTokens.get(form.tempId) || {};
|
|
|
+ tokens.hsy = uploadInfo.cancel;
|
|
|
+ this.uploadCancellationTokens.set(form.tempId, tokens);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ console.log("上传火山云返回参数",data)
|
|
|
+
|
|
|
+ form.line2 = `${process.env.VUE_APP_VIDEO_URL}/${data.SourceInfo.FileName}`;
|
|
|
+ this.form.hsyVid = data.Vid
|
|
|
+ this.form.hsyVodUrl = process.env.VUE_APP_VIDEO_URL+"/"+data.SourceInfo.FileName
|
|
|
+ console.log("this.form",this.form)
|
|
|
+ this.$message.success('线路二上传成功');
|
|
|
+ return { success: true, url: form.line2 };
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ this.updateUploadProgress('line2Status', 'failed');
|
|
|
+ this.$message.error('线路二上传失败');
|
|
|
+ return { success: false, error: error?.message || 'upload failed' };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 更新上传进度的辅助方法
|
|
|
+ updateUploadProgress(key, value) {
|
|
|
+ this.currentUploadProgress[key] = value;
|
|
|
+
|
|
|
+ // 计算总进度:只有两个线路都成功才算100%
|
|
|
+ if (this.currentUploadProgress.line1Status === 'success' && this.currentUploadProgress.line2Status === 'success') {
|
|
|
+ this.currentUploadProgress.total = 100;
|
|
|
+ } else if (this.currentUploadProgress.line1Status === 'failed' || this.currentUploadProgress.line2Status === 'failed') {
|
|
|
+ // 如果任一线路失败,总进度保持当前状态
|
|
|
+ this.currentUploadProgress.total = Math.min(
|
|
|
+ (this.currentUploadProgress.line1 + this.currentUploadProgress.line2) / 2,
|
|
|
+ 99 // 失败时最多99%
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ // 正常上传中,计算平均进度
|
|
|
+ this.currentUploadProgress.total = Math.min(
|
|
|
+ (this.currentUploadProgress.line1 + this.currentUploadProgress.line2) / 2,
|
|
|
+ 99 // 上传中最多99%,只有都成功才100%
|
|
|
+ );
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 上传视频
|
|
|
+ async videoUpload(options) {
|
|
|
+ this.isUploading = true;
|
|
|
+ this.form.uploadStatus = 'uploading';
|
|
|
+ this.form.tempId = Math.random().toString(36).substring(2, 15);
|
|
|
+ const file = options.file;
|
|
|
+ this.getMediaDuration(file);
|
|
|
+ this.currentUploadProgress = { total: 0, line1: 0, line2: 0, line1Status: 'pending', line2Status: 'pending' };
|
|
|
+
|
|
|
+ try {
|
|
|
+ await this.getFirstThumbnail(file, this.form);
|
|
|
+ const [line1Result, line2Result] = await Promise.allSettled([
|
|
|
+ this.uploadVideoToTxPcdn(file, this.form, options.onProgress),
|
|
|
+ //this.uploadVideoToHwObs(file, this.form, options.onProgress)
|
|
|
+ this.uploadVideoToHsy(file, this.form, options.onProgress)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const line1Success = line1Result.status === 'fulfilled' && line1Result.value.success;
|
|
|
+ const line2Success = line2Result.status === 'fulfilled' && line2Result.value.success;
|
|
|
+
|
|
|
+ if (line1Success && line2Success) {
|
|
|
+ this.form.uploadStatus = 'success';
|
|
|
+ this.form.uploadProgress = { total: 100, line1: 100, line2: 100, line1Status: 'success', line2Status: 'success' };
|
|
|
+ this.currentUploadProgress.total = 100;
|
|
|
+ this.$message.success("视频上传完成!两个线路都上传成功");
|
|
|
+ } else {
|
|
|
+ this.form.uploadStatus = 'failed';
|
|
|
+ this.form.uploadProgress = {
|
|
|
+ total: this.currentUploadProgress.total,
|
|
|
+ line1: this.currentUploadProgress.line1,
|
|
|
+ line2: this.currentUploadProgress.line2,
|
|
|
+ line1Status: this.currentUploadProgress.line1Status,
|
|
|
+ line2Status: this.currentUploadProgress.line2Status
|
|
|
+ };
|
|
|
+ const failedLines = [];
|
|
|
+ if (!line1Success) failedLines.push('线路1');
|
|
|
+ if (!line2Success) failedLines.push('线路2');
|
|
|
+ this.$message.error(`视频上传失败!${failedLines.join('、')} 上传失败,请重试`);
|
|
|
+ }
|
|
|
+ this.form.fileName = file.name;
|
|
|
+ this.form.fileSize = file.size;
|
|
|
+ } catch (error) {
|
|
|
+ this.form.uploadStatus = 'failed';
|
|
|
+ this.$message.error("视频上传过程中发生错误,请重试");
|
|
|
+ } finally {
|
|
|
+ this.isUploading = false;
|
|
|
+ if (this.form.tempId) {
|
|
|
+ this.uploadCancellationTokens.delete(this.form.tempId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 获取媒体文件时长
|
|
|
+ getMediaDuration(file) {
|
|
|
+ // 处理视频
|
|
|
+ const video = document.createElement('video');
|
|
|
+ video.preload = 'metadata';
|
|
|
+ video.onloadedmetadata = () => {
|
|
|
+ this.form.duration = Math.round(video.duration);
|
|
|
+ };
|
|
|
+ video.src = URL.createObjectURL(file);
|
|
|
+ },
|
|
|
+ handleFileChange(file, files) {
|
|
|
+ // 保留最后一个文件
|
|
|
+ this.fileList = files.slice(-1);
|
|
|
+ },
|
|
|
+ /** 批量新增 */
|
|
|
+ handleBatchAdd() {
|
|
|
+ this.batchAddVisible = true;
|
|
|
+ this.add =false
|
|
|
+ this.videoList = []; // 清空之前的视频列表
|
|
|
+ },
|
|
|
+ /** 批量修改 */
|
|
|
+ handleBatchUpdate(){
|
|
|
+ if (this.ids.length === 0) {
|
|
|
+ this.$message.warning("请至少选择一条数据");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.resetForm("form");
|
|
|
+ this.batchUpdateForm.ids = this.ids; // 将选中的ID传递给批量修改表单
|
|
|
+ this.batchUpdateVisible = true;
|
|
|
+ },
|
|
|
+ cancelBeforeBatch(done, cancel) {
|
|
|
+ if (!this.videoList || this.videoList.length === 0) {
|
|
|
+ done()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.$confirm('关闭窗口视频需要重新上传,确定关闭吗?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ done()
|
|
|
+ }).catch(() => {
|
|
|
+ cancel()
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 取消批量 */
|
|
|
+ cancelBatch() {
|
|
|
+ if (!this.videoList || this.videoList.length === 0) {
|
|
|
+ this.batchAddVisible = false
|
|
|
+ this.batchUpdateVisible = false
|
|
|
+
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.$confirm('关闭窗口视频需要重新上传,确定关闭吗?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ this.batchAddVisible = false
|
|
|
+ this.batchUpdateVisible = false
|
|
|
+ this.changeCateType(this.queryParams.typeId)
|
|
|
+ }).catch(() => { });
|
|
|
+ },
|
|
|
+ /** 批量修改 */
|
|
|
+ submitBatchUpdate() {
|
|
|
+ console.log("批量上传表单提交参数",this.form)
|
|
|
+ this.$refs["form"].validate(valid => {
|
|
|
+ if (valid) {
|
|
|
+ if (this.batchUpdateForm.ids.length === 0) {
|
|
|
+ this.$message.warning("未选择任何数据");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$confirm('是否确认修改视频素材库编号为"' + this.batchUpdateForm.ids.join(',') + '"的数据项?', "警告", {
|
|
|
+ confirmButtonText: "确定",
|
|
|
+ cancelButtonText: "取消",
|
|
|
+ type: "warning"
|
|
|
+ }).then(() => {
|
|
|
+ // 构造正确的参数格式
|
|
|
+ const params = new URLSearchParams();
|
|
|
+ params.append('typeId', this.batchUpdateForm.typeId || '');
|
|
|
+ params.append('typeSubId', this.batchUpdateForm.typeSubId || '');
|
|
|
+ params.append('ids', this.batchUpdateForm.ids.join(','));
|
|
|
+ params.append('videoType', String(this.pageVideoType));
|
|
|
+ return batchUpdateVideoResource(params);
|
|
|
+ }).then(() => {
|
|
|
+ this.getList();
|
|
|
+ this.batchUpdateVisible = false;
|
|
|
+ this.msgSuccess("修改成功");
|
|
|
+ }).catch(() => {});
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 提交批量添加 */
|
|
|
+ submitBatchAdd() {
|
|
|
+ if (this.videoList.length === 0) {
|
|
|
+ this.$message.warning("请选择视频");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否所有选中的视频都已上传完成
|
|
|
+ console.log("videoList",this.videoList)
|
|
|
+ const incompleteVideos = this.videoList.filter(item => (item.progress || 0) < 100);
|
|
|
+ if (incompleteVideos.length > 0) {
|
|
|
+ this.$message.warning('有未完成上传的视频,请先完成上传');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.add){
|
|
|
+ this.$message.warning("请勿重复提交")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.add = true
|
|
|
+
|
|
|
+ // 调用批量添加API
|
|
|
+ const videoList = JSON.parse(JSON.stringify(this.videoList));
|
|
|
+ videoList.forEach(item => {
|
|
|
+ item.projectIds = item.projectIds.join(",");
|
|
|
+ item.videoType = this.pageVideoType;
|
|
|
+ });
|
|
|
+ batchAddVideoResource(videoList).then(response => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.$message.success('批量添加成功');
|
|
|
+ this.batchAddVisible = false;
|
|
|
+ this.getList();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ /** 获取分类名称 */
|
|
|
+ getTypeName(typeId) {
|
|
|
+ const type = this.typeList.find(item => item.cateId === typeId);
|
|
|
+ return type ? type.cateName : '';
|
|
|
+ },
|
|
|
+ /** 编辑视频信息 */
|
|
|
+ handleEditVideo(row) {
|
|
|
+ if (row.progress !== 100) {
|
|
|
+ this.$message.warning('请等待上传完成后再编辑');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.batchEditDialog.form = Object.assign({}, row)
|
|
|
+ this.changeCateType(row.typeId)
|
|
|
+ this.batchEditDialog.open = true
|
|
|
+ },
|
|
|
+ batchEditCancel() {
|
|
|
+ this.resetForm('batchEditDialogForm')
|
|
|
+ this.batchEditDialog.open = false
|
|
|
+ },
|
|
|
+ batchEditSubmitForm() {
|
|
|
+ let temp = this.videoList.find(item => item.tempId === this.batchEditDialog.form.tempId)
|
|
|
+ Object.assign(temp, this.batchEditDialog.form)
|
|
|
+ this.batchEditDialog.open = false
|
|
|
+ },
|
|
|
+ handleDeleteVideo(row) {
|
|
|
+ if (row.uploadStatus === 'uploading') {
|
|
|
+ this.$confirm('该视频正在上传中,确认要取消上传并删除吗?', '取消上传', {
|
|
|
+ confirmButtonText: '确定取消',
|
|
|
+ cancelButtonText: '继续上传',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ // Cancel the upload and remove from list
|
|
|
+ this.cancelVideoUpload(row);
|
|
|
+ }).catch(() => {
|
|
|
+ // User chose to continue uploading
|
|
|
+ this.$message.info('继续上传该视频');
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // Original confirmation for non-uploading videos
|
|
|
+ this.$confirm('确认要从列表中删除该视频吗?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(() => {
|
|
|
+ this.removeVideoFromList(row);
|
|
|
+ }).catch(() => {
|
|
|
+ // 取消删除
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ cancelVideoUpload(row) {
|
|
|
+ const cancellationTokens = this.uploadCancellationTokens.get(row.tempId);
|
|
|
+ if (cancellationTokens) {
|
|
|
+ // Cancel COS upload if exists
|
|
|
+ if (cancellationTokens.cos) {
|
|
|
+ try {
|
|
|
+ cancellationTokens.cos();
|
|
|
+ console.log('COS upload cancelled for video:', row.tempId);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error cancelling COS upload:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Cancel OBS upload if exists
|
|
|
+ if (cancellationTokens.obs) {
|
|
|
+ try {
|
|
|
+ cancellationTokens.obs();
|
|
|
+ console.log('OBS upload cancelled for video:', row.tempId);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error cancelling OBS upload:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Remove cancellation tokens
|
|
|
+ this.uploadCancellationTokens.delete(row.tempId);
|
|
|
+ }
|
|
|
+
|
|
|
+ const videoIndex = this.videoList.findIndex(item => item.tempId === row.tempId);
|
|
|
+ if (videoIndex !== -1) {
|
|
|
+ this.videoList[videoIndex].uploadStatus = 'cancelled';
|
|
|
+ this.videoList.splice(videoIndex, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ const queueIndex = this.uploadQueue.findIndex(item => item.tempId === row.tempId);
|
|
|
+ if (queueIndex !== -1) {
|
|
|
+ this.uploadQueue.splice(queueIndex, 1);
|
|
|
+ this.updateQueuePositions();
|
|
|
+ }
|
|
|
+
|
|
|
+ this.$message.success('已取消视频上传并从列表中移除');
|
|
|
+ },
|
|
|
+
|
|
|
+ removeVideoFromList(row) {
|
|
|
+ const videoIndex = this.videoList.findIndex(item => item.tempId === row.tempId);
|
|
|
+ if (videoIndex !== -1) {
|
|
|
+ this.videoList.splice(videoIndex, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Remove from upload queue if it's still queued
|
|
|
+ const queueIndex = this.uploadQueue.findIndex(item => item.tempId === row.tempId);
|
|
|
+ if (queueIndex !== -1) {
|
|
|
+ this.uploadQueue.splice(queueIndex, 1);
|
|
|
+ this.updateQueuePositions();
|
|
|
+ this.$message.success('已从上传队列中移除该视频');
|
|
|
+ } else {
|
|
|
+ this.$message.success('已从列表中移除该视频');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /** 删除视频 */
|
|
|
+ // handleDeleteVideo(row) {
|
|
|
+ // this.$confirm('确认要从列表中删除该视频吗?', '提示', {
|
|
|
+ // confirmButtonText: '确定',
|
|
|
+ // cancelButtonText: '取消',
|
|
|
+ // type: 'warning'
|
|
|
+ // }).then(() => {
|
|
|
+ // const videoIndex = this.videoList.findIndex(item => item.tempId === row.tempId);
|
|
|
+ // if (videoIndex !== -1) {
|
|
|
+ // this.videoList.splice(videoIndex, 1);
|
|
|
+ // }
|
|
|
+
|
|
|
+ // // Remove from upload queue if it's still queued
|
|
|
+ // const queueIndex = this.uploadQueue.findIndex(item => item.tempId === row.tempId);
|
|
|
+ // if (queueIndex !== -1) {
|
|
|
+ // this.uploadQueue.splice(queueIndex, 1);
|
|
|
+ // this.updateQueuePositions();
|
|
|
+ // this.$message.success('已从上传队列中移除该视频');
|
|
|
+ // } else {
|
|
|
+ // this.$message.success('已从列表中移除该视频');
|
|
|
+ // }
|
|
|
+ // }).catch(() => {
|
|
|
+ // // 取消删除
|
|
|
+ // });
|
|
|
+ // },
|
|
|
+ /** 显示上传面板 */
|
|
|
+ showUploadPanel() {
|
|
|
+ this.showUpload = true;
|
|
|
+
|
|
|
+ if (this.currentProject === 'myhk') {
|
|
|
+ this.batchUploadForm = {
|
|
|
+ ...this.batchUploadForm,
|
|
|
+ files: [],
|
|
|
+ videoType: this.pageVideoType
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ this.batchUploadForm = {
|
|
|
+ typeId: null,
|
|
|
+ typeSubId: null,
|
|
|
+ projectIds: [],
|
|
|
+ files: [],
|
|
|
+ displayType: 'landscape',
|
|
|
+ videoType: this.pageVideoType
|
|
|
+ };
|
|
|
+ this.subTypeList = []
|
|
|
+ }
|
|
|
+
|
|
|
+ this.batchFileList = [];
|
|
|
+ if (this.$refs.batchVideoUpload) {
|
|
|
+ this.$refs.batchVideoUpload.clearFiles();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /** 批量文件变更 */
|
|
|
+ handleBatchFileChange(file, fileList) {
|
|
|
+ this.batchFileList = fileList;
|
|
|
+ },
|
|
|
+ /** 批量上传视频 */
|
|
|
+ async batchVideoUpload(options) {
|
|
|
+ const file = options.file;
|
|
|
+
|
|
|
+ const tempVideo = {
|
|
|
+ tempId: Math.random().toString(36).substring(2, 15),
|
|
|
+ resourceName: file.name.substring(0, file.name.lastIndexOf('.')),
|
|
|
+ fileName: file.name,
|
|
|
+ thumbnail: null,
|
|
|
+ line1: null,
|
|
|
+ line2: null,
|
|
|
+ line3: null,
|
|
|
+ duration: 0,
|
|
|
+ fileSize: file.size,
|
|
|
+ fileKey: null,
|
|
|
+ videoUrl: null,
|
|
|
+ typeId: this.batchUploadForm.typeId,
|
|
|
+ typeSubId: this.batchUploadForm.typeSubId,
|
|
|
+ projectIds: this.batchUploadForm.projectIds,
|
|
|
+ displayType: this.batchUploadForm.displayType || 'landscape',
|
|
|
+ videoType: this.pageVideoType,
|
|
|
+ progress: 0,
|
|
|
+ uploadStatus: 'queued', // Set initial status to queued
|
|
|
+ uploadDetails: {
|
|
|
+ line1: 0,
|
|
|
+ line2: 0,
|
|
|
+ line1Status: 'pending',
|
|
|
+ line2Status: 'pending'
|
|
|
+ },
|
|
|
+ file: file,
|
|
|
+ queuePosition: this.uploadQueue.length + 1 // Track queue position
|
|
|
+ };
|
|
|
+
|
|
|
+ this.uploadQueue.push(tempVideo);
|
|
|
+ this.videoList.unshift(tempVideo);
|
|
|
+
|
|
|
+ // 获取视频时长
|
|
|
+ const video = document.createElement('video');
|
|
|
+ video.preload = 'metadata';
|
|
|
+ video.onloadedmetadata = () => {
|
|
|
+ const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
|
|
|
+ if (index !== -1) {
|
|
|
+ tempVideo.duration = Math.round(video.duration);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ video.src = URL.createObjectURL(file);
|
|
|
+
|
|
|
+ // 关闭上传弹窗
|
|
|
+ this.showUpload = false;
|
|
|
+
|
|
|
+ if (!this.isProcessingBatch) {
|
|
|
+ this.processUploadQueue();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.$refs.batchVideoUpload) {
|
|
|
+ this.$refs.batchVideoUpload.clearFiles();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async processUploadQueue() {
|
|
|
+ if (this.isProcessingBatch || this.uploadQueue.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.isUploading = true;
|
|
|
+ this.isProcessingBatch = true;
|
|
|
+
|
|
|
+ while (this.uploadQueue.length > 0) {
|
|
|
+ // Get next batch (up to 5 videos)
|
|
|
+ const currentBatch = this.uploadQueue.splice(0, this.currentBatchSize);
|
|
|
+
|
|
|
+ // Update status for current batch
|
|
|
+ currentBatch.forEach(video => {
|
|
|
+ const index = this.videoList.findIndex(item => item.tempId === video.tempId);
|
|
|
+ if (index !== -1) {
|
|
|
+ this.videoList[index].uploadStatus = 'uploading';
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // Process current batch in parallel
|
|
|
+ const batchPromises = currentBatch.map(video => this.uploadSingleVideo(video));
|
|
|
+
|
|
|
+ try {
|
|
|
+ await Promise.allSettled(batchPromises);
|
|
|
+ this.$message.success(`批次上传完成,已处理 ${currentBatch.length} 个视频`);
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(`批次上传过程中发生错误: ${error.message}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update queue positions for remaining videos
|
|
|
+ this.updateQueuePositions();
|
|
|
+
|
|
|
+ // Small delay between batches
|
|
|
+ if (this.uploadQueue.length > 0) {
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.isProcessingBatch = false;
|
|
|
+ this.isUploading = false;
|
|
|
+ this.$message.success('所有视频上传队列处理完成!');
|
|
|
+ console.log("批量上传form",this.form)
|
|
|
+ },
|
|
|
+
|
|
|
+ async uploadSingleVideo(tempVideo) {
|
|
|
+ try {
|
|
|
+ // 获取封面
|
|
|
+ await this.getFirstThumbnail(tempVideo.file, tempVideo);
|
|
|
+
|
|
|
+ // 并行上传到两个服务器
|
|
|
+ const [line1Result, line2Result] = await Promise.allSettled([
|
|
|
+ this.uploadVideoToTxPcdnBatch(tempVideo.file, tempVideo),
|
|
|
+ // this.uploadVideoToHwObsBatch(tempVideo.file, tempVideo)
|
|
|
+ this.uploadVideoToHSYBatch(tempVideo.file, tempVideo),
|
|
|
+
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 检查上传结果
|
|
|
+ const line1Success = line1Result.status === 'fulfilled' && line1Result.value.success;
|
|
|
+ const line2Success = line2Result.status === 'fulfilled' && line2Result.value.success;
|
|
|
+
|
|
|
+ const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
|
|
|
+ if (index !== -1) {
|
|
|
+ if (line1Success && line2Success) {
|
|
|
+ this.videoList[index].progress = 100;
|
|
|
+ this.videoList[index].uploadStatus = 'success';
|
|
|
+ this.videoList[index].uploadDetails.line1Status = 'success';
|
|
|
+ this.videoList[index].uploadDetails.line2Status = 'success';
|
|
|
+ this.videoList[index].uploadDetails.line1 = 100;
|
|
|
+ this.videoList[index].uploadDetails.line2 = 100;
|
|
|
+ } else {
|
|
|
+ this.videoList[index].uploadStatus = 'failed';
|
|
|
+ this.videoList[index].uploadDetails.line1Status = line1Success ? 'success' : 'failed';
|
|
|
+ this.videoList[index].uploadDetails.line2Status = line2Success ? 'success' : 'failed';
|
|
|
+ this.updateBatchProgress(index);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { success: line1Success && line2Success, tempVideo };
|
|
|
+ } catch (error) {
|
|
|
+ const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
|
|
|
+ if (index !== -1) {
|
|
|
+ this.videoList[index].uploadStatus = 'failed';
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ /** 复制视频链接到剪贴板 */
|
|
|
+ copy(url) {
|
|
|
+ // 创建一个临时的input元素
|
|
|
+ const input = document.createElement('input');
|
|
|
+ // 设置input的值为要复制的视频链接
|
|
|
+ input.value = url;
|
|
|
+ // 将input添加到DOM中
|
|
|
+ document.body.appendChild(input);
|
|
|
+ // 选中input的值
|
|
|
+ input.select();
|
|
|
+ // 执行复制命令
|
|
|
+ document.execCommand('copy');
|
|
|
+ // 从DOM中移除input元素
|
|
|
+ document.body.removeChild(input);
|
|
|
+ // 提示用户复制成功
|
|
|
+ this.$message({
|
|
|
+ message: '已复制到剪贴板',
|
|
|
+ type: 'success',
|
|
|
+ duration: 1500
|
|
|
+ });
|
|
|
+ },
|
|
|
+ getProjectList() {
|
|
|
+ this.projectLoading = true
|
|
|
+ listCourseQuestionBank(this.projectQueryParams).then(response => {
|
|
|
+ this.projectList = response.rows;
|
|
|
+ this.projectListTotal = response.total;
|
|
|
+
|
|
|
+ // 如果存在已选择的项目,预选中表格中对应的行
|
|
|
+ this.$nextTick(() => {
|
|
|
+ // 获取表格组件实例
|
|
|
+ const projectTable = this.$refs.projectTable;
|
|
|
+ if (projectTable) {
|
|
|
+ if (this.selectedProjectIds.length > 0) {
|
|
|
+ // 遍历项目列表,找到匹配的ID并选中对应行
|
|
|
+ const selectedIds = this.selectedProjectIds;
|
|
|
+ this.projectList.forEach(row => {
|
|
|
+ if (selectedIds.includes(row.id)) {
|
|
|
+ projectTable.toggleRowSelection(row, true);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ this.projectLoading = false
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 打开项目选择弹窗 */
|
|
|
+ openProjectDialog(projectIds, type) {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.customSelect) {
|
|
|
+ this.$refs.customSelect.blur();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // 重置查询参数
|
|
|
+ this.projectQueryParams = {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ questionType: null,
|
|
|
+ title: null
|
|
|
+ };
|
|
|
+
|
|
|
+ this.selectedType = type
|
|
|
+
|
|
|
+ // 设置选中的项目IDs
|
|
|
+ if (projectIds) {
|
|
|
+ if (typeof projectIds === 'string') {
|
|
|
+ this.selectedProjectIds = projectIds.split(',').map(id => parseInt(id));
|
|
|
+ } else if (Array.isArray(projectIds)) {
|
|
|
+ this.selectedProjectIds = [...projectIds];
|
|
|
+ } else if (typeof projectIds === 'number') {
|
|
|
+ this.selectedProjectIds = [projectIds];
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.selectedProjectIds = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示弹窗
|
|
|
+ this.projectDialogVisible = true;
|
|
|
+
|
|
|
+ // 加载分类树数据
|
|
|
+ this.initCategoryTree();
|
|
|
+
|
|
|
+ // 加载项目列表
|
|
|
+ this.getProjectList();
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 确认选择项目 */
|
|
|
+ confirmSelectProject() {
|
|
|
+ // 更新表单中的项目ID
|
|
|
+ if (this.selectedType === 0) {
|
|
|
+ this.form.projectIds = this.selectedProjectIds;
|
|
|
+ }
|
|
|
+
|
|
|
+ else if (this.selectedType === 1) {
|
|
|
+ this.batchUploadForm.projectIds = this.selectedProjectIds;
|
|
|
+ }
|
|
|
+
|
|
|
+ else if (this.selectedType === 2) {
|
|
|
+ this.batchEditDialog.form.projectIds = this.selectedProjectIds;
|
|
|
+ }
|
|
|
+
|
|
|
+ else if (this.selectedType === 3) {
|
|
|
+ const params = {
|
|
|
+ id: this.currentRow.id,
|
|
|
+ projectIds: this.selectedProjectIds.join(","),
|
|
|
+ videoType: this.pageVideoType
|
|
|
+ }
|
|
|
+ updateVideoResource(params).then(response => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ this.msgSuccess("修改成功");
|
|
|
+ this.open = false;
|
|
|
+ this.getList();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ else if (this.selectedType === 4) {
|
|
|
+ this.currentRow.projectIds = this.selectedProjectIds
|
|
|
+ }
|
|
|
+
|
|
|
+ this.projectDialogVisible = false;
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 取消选择项目 */
|
|
|
+ cancelSelectProject() {
|
|
|
+ const projectTable = this.$refs.projectTable;
|
|
|
+ if (projectTable) {
|
|
|
+ // 清空表格数据
|
|
|
+ projectTable.clearSelection();
|
|
|
+ }
|
|
|
+ this.projectDialogVisible = false;
|
|
|
+ },
|
|
|
+ handleProjectSelect(selection) {
|
|
|
+ this.selectedProjectIds = selection.map(item => item.id);
|
|
|
+ selection.forEach(item => {
|
|
|
+ // 检查是否已存在该项目,不存在则添加
|
|
|
+ if (!this.projectShowList.some(p => p.id === item.id)) {
|
|
|
+ this.projectShowList.push(item);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ /** 项目列表页码变更 */
|
|
|
+ handleProjectPageChange(page) {
|
|
|
+ this.projectQueryParams.pageNum = page;
|
|
|
+ this.getProjectList();
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 项目列表每页条数变更 */
|
|
|
+ handleProjectPageSizeChange(size) {
|
|
|
+ this.projectQueryParams.pageNum = 1;
|
|
|
+ this.projectQueryParams.pageSize = size;
|
|
|
+ this.getProjectList();
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 处理查看项目 */
|
|
|
+ handleViewProject(row, type) {
|
|
|
+ // 保存当前选择的行,以便后续操作
|
|
|
+ this.currentRow = row;
|
|
|
+
|
|
|
+ // 设置form对象的projectIds为当前行的项目IDs
|
|
|
+ this.projectShowList = [];
|
|
|
+
|
|
|
+ if (!row.projectIds || row.projectIds.length === 0) {
|
|
|
+ this.openProjectDialog(null, type)
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let projectIds = row.projectIds
|
|
|
+ if (Array.isArray(row.projectIds)) {
|
|
|
+ projectIds = projectIds.join(',');
|
|
|
+ }
|
|
|
+
|
|
|
+ getByIds({ids: projectIds}).then(response => {
|
|
|
+ this.projectShowList = response.data;
|
|
|
+ })
|
|
|
+
|
|
|
+ // 打开弹窗展示列表
|
|
|
+ this.projectListDialogVisible = true;
|
|
|
+ },
|
|
|
+ /** 初始化分类树 */
|
|
|
+ initCategoryTree() {
|
|
|
+ // 获取分类列表
|
|
|
+ listUserCourseCategory().then(response => {
|
|
|
+ const treeDate = this.handleTree(response.data, "cateId", "pid");
|
|
|
+ this.categoryTreeData = [{
|
|
|
+ cateId: 0,
|
|
|
+ cateName: '全部',
|
|
|
+ children: treeDate
|
|
|
+ }];
|
|
|
+ });
|
|
|
+ },
|
|
|
+ filterNode(value, data) {
|
|
|
+ if (!value) return true;
|
|
|
+ return data.cateName.indexOf(value) !== -1;
|
|
|
+ },
|
|
|
+ /** 处理分类点击 */
|
|
|
+ handleCategoryClick(data, node) {
|
|
|
+ // 更新查询参数
|
|
|
+ this.projectQueryParams.pageNum = 1;
|
|
|
+
|
|
|
+ // 如果是全部分类,则清空分类过滤
|
|
|
+ if (node.level === 1) {
|
|
|
+ this.projectQueryParams.questionType = null;
|
|
|
+ this.projectQueryParams.questionSubType = null;
|
|
|
+ }
|
|
|
+ else if (node.level === 2) {
|
|
|
+ this.projectQueryParams.questionType = data.cateId;
|
|
|
+ this.projectQueryParams.questionSubType = null;
|
|
|
+ }
|
|
|
+ else if (node.level === 3) {
|
|
|
+ this.projectQueryParams.questionType = null;
|
|
|
+ this.projectQueryParams.questionSubType = data.cateId;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重新加载项目列表
|
|
|
+ this.getProjectList();
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 搜索项目 */
|
|
|
+ searchProjects() {
|
|
|
+ this.projectQueryParams.pageNum = 1;
|
|
|
+ this.getProjectList();
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 视频预览弹窗关闭前的处理函数 */
|
|
|
+ handleCloseVideoPreview(done) {
|
|
|
+ // 停止视频播放
|
|
|
+ const video = document.getElementById('video');
|
|
|
+ if (video) {
|
|
|
+ video.pause();
|
|
|
+ video.currentTime = 0;
|
|
|
+ }
|
|
|
+ // 关闭弹窗
|
|
|
+ this.videoPreviewVisible = false;
|
|
|
+ done();
|
|
|
+ },
|
|
|
+
|
|
|
+ updateQueuePositions() {
|
|
|
+ this.uploadQueue.forEach((video, index) => {
|
|
|
+ video.queuePosition = index + 1;
|
|
|
+ const videoIndex = this.videoList.findIndex(item => item.tempId === video.tempId);
|
|
|
+ if (videoIndex !== -1) {
|
|
|
+ this.videoList[videoIndex].queuePosition = index + 1;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ getQueueStatusColor(status) {
|
|
|
+ switch (status) {
|
|
|
+ case 'success':
|
|
|
+ return '#67C23A';
|
|
|
+ case 'failed':
|
|
|
+ return '#F56C6C';
|
|
|
+ case 'uploading':
|
|
|
+ return '#409EFF';
|
|
|
+ case 'queued':
|
|
|
+ return '#E6A23C';
|
|
|
+ default:
|
|
|
+ return '#909399';
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 批量上传 - 腾讯云
|
|
|
+ async uploadVideoToTxPcdnBatch(file, tempVideo) {
|
|
|
+ console.log("--------------",JSON.stringify(tempVideo))
|
|
|
+ try {
|
|
|
+ const data = await uploadObject(file, (progress) => {
|
|
|
+ const progressPercent = Math.floor(progress.percent * 100);
|
|
|
+ const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
|
|
|
+ if (index !== -1) {
|
|
|
+ this.videoList[index].uploadDetails.line1 = progressPercent;
|
|
|
+ this.videoList[index].uploadDetails.line1Status = 'uploading';
|
|
|
+ this.updateBatchProgress(index);
|
|
|
+ }
|
|
|
+ }, 1, (uploadInfo) => {
|
|
|
+ const tokens = this.uploadCancellationTokens.get(tempVideo.tempId) || {};
|
|
|
+ tokens.cos = uploadInfo.cancel;
|
|
|
+ this.uploadCancellationTokens.set(tempVideo.tempId, tokens);
|
|
|
+ });
|
|
|
+
|
|
|
+ let line_1 = `${process.env.VUE_APP_VIDEO_LINE_1}${data.urlPath}`;
|
|
|
+ tempVideo.fileKey = data.urlPath.substring(1);
|
|
|
+ tempVideo.videoUrl = line_1;
|
|
|
+ tempVideo.line1 = line_1;
|
|
|
+ return { success: true, url: line_1 };
|
|
|
+ } catch (error) {
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 批量上传 - 华为云
|
|
|
+ // async uploadVideoToHwObsBatch(file, tempVideo) {
|
|
|
+ // try {
|
|
|
+ // const data = await uploadToOBS(file, (progress) => {
|
|
|
+ // const progressPercent = Math.floor(progress);
|
|
|
+ // const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
|
|
|
+ // if (index !== -1) {
|
|
|
+ // this.videoList[index].uploadDetails.line2 = progressPercent;
|
|
|
+ // this.videoList[index].uploadDetails.line2Status = 'uploading';
|
|
|
+ // this.updateBatchProgress(index);
|
|
|
+ // }
|
|
|
+ // }, 1, (uploadInfo) => {
|
|
|
+ // const tokens = this.uploadCancellationTokens.get(tempVideo.tempId) || {};
|
|
|
+ // tokens.obs = uploadInfo.cancel;
|
|
|
+ // this.uploadCancellationTokens.set(tempVideo.tempId, tokens);
|
|
|
+ // });
|
|
|
+ //
|
|
|
+ // tempVideo.line2 = `${process.env.VUE_APP_VIDEO_LINE_2}/${data.urlPath}`;
|
|
|
+ // return { success: true, url: tempVideo.line2 };
|
|
|
+ // } catch (error) {
|
|
|
+ // return { success: false, error: error.message };
|
|
|
+ // }
|
|
|
+ // },
|
|
|
+ async uploadVideoToHSYBatch(file, tempVideo) {
|
|
|
+ try {
|
|
|
+ const data = await uploadToHSY(file, (progress) => {
|
|
|
+ const progressPercent = Math.floor(progress);
|
|
|
+ const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
|
|
|
+ if (index !== -1) {
|
|
|
+ this.videoList[index].uploadDetails.line2 = progressPercent;
|
|
|
+ this.videoList[index].uploadDetails.line2Status = 'uploading';
|
|
|
+ this.updateBatchProgress(index);
|
|
|
+ }
|
|
|
+ }, 1, (uploadInfo) => {
|
|
|
+ const tokens = this.uploadCancellationTokens.get(tempVideo.tempId) || {};
|
|
|
+ tokens.obs = uploadInfo.cancel;
|
|
|
+ this.uploadCancellationTokens.set(tempVideo.tempId, tokens);
|
|
|
+ });
|
|
|
+ console.log("批量上传返回参数",data)
|
|
|
+ tempVideo.line2 = `${process.env.VUE_APP_VIDEO_URL}/${data.SourceInfo.FileName}`;
|
|
|
+ tempVideo.hsyVid = data.Vid;
|
|
|
+
|
|
|
+ return { success: true, url: tempVideo.line2};
|
|
|
+ } catch (error) {
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 更新批量上传的总进度
|
|
|
+ updateBatchProgress(index) {
|
|
|
+ if (index >= 0 && index < this.videoList.length) {
|
|
|
+ const item = this.videoList[index];
|
|
|
+ const line1Progress = item.uploadDetails.line1 || 0;
|
|
|
+ const line2Progress = item.uploadDetails.line2 || 0;
|
|
|
+ // 只有两个线路都成功才算100%
|
|
|
+ if (item.uploadDetails.line1Status === 'success' && item.uploadDetails.line2Status === 'success') {
|
|
|
+ item.progress = 100;
|
|
|
+ } else if (item.uploadDetails.line1Status === 'failed' || item.uploadDetails.line2Status === 'failed') {
|
|
|
+ // 如果任一线路失败,总进度保持当前状态,不超过99%
|
|
|
+ item.progress = Math.min((line1Progress + line2Progress) / 2, 99);
|
|
|
+ } else {
|
|
|
+ // 正常上传中,计算平均进度,不超过99%
|
|
|
+ item.progress = Math.min((line1Progress + line2Progress) / 2, 99);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 重试批量上传
|
|
|
+ async retryBatchUpload(row) {
|
|
|
+ // const index = this.videoList.findIndex(item => item.tempId === row.tempId);
|
|
|
+ // if (index === -1) return;
|
|
|
+ // const {line1, line2,line1Status,line2Status} = this.videoList[index].uploadDetails
|
|
|
+ // // 重置状态
|
|
|
+ // this.videoList[index].uploadStatus = 'uploading';
|
|
|
+ // // this.videoList[index].progress = 0;
|
|
|
+ // this.videoList[index].uploadDetails = {
|
|
|
+ // line1: line1 == 100?line1:0,
|
|
|
+ // line2: line2 == 100?line2:0,
|
|
|
+ // line1Status: line1Status =='success'?line1Status:'pending',
|
|
|
+ // line2Status: line2Status =='success'?line2Status:'pending'
|
|
|
+ // };
|
|
|
+ // const tempVideo = this.videoList[index];
|
|
|
+ // try {
|
|
|
+ // // 重新上传
|
|
|
+ // // const [line1Result, line2Result] = await Promise.allSettled([
|
|
|
+ // // this.uploadVideoToTxPcdnBatch(tempVideo.file, tempVideo),
|
|
|
+ // // // this.uploadVideoToHwObsBatch(tempVideo.file, tempVideo)
|
|
|
+ // // ]);
|
|
|
+ // if (line1 !== 100) {
|
|
|
+ // const line1Result = await this.uploadVideoToTxPcdnBatch(tempVideo.file, tempVideo)
|
|
|
+ // }
|
|
|
+ // if (line2 !== 100) {
|
|
|
+ // const line2Result = await this.uploadVideoToHwObsBatch(tempVideo.file, tempVideo)
|
|
|
+ // }
|
|
|
+ // const line1Success = line1Result.status === 'fulfilled' && line1Result.value.success;
|
|
|
+ // const line2Success = line2Result.status === 'fulfilled' && line2Result.value.success;
|
|
|
+ // if ( line1 == 100 || line1Success) {
|
|
|
+ // this.videoList[index].uploadDetails.line1Status = 'success';
|
|
|
+ // this.videoList[index].uploadDetails.line1 = 100;
|
|
|
+ // this.$message.success(`文件 ${tempVideo.fileName} 重试上传成功`);
|
|
|
+ // } else {
|
|
|
+ // this.videoList[index].uploadDetails.line1Status = line1Success ? 'success' : 'failed';
|
|
|
+ // this.$message.error(`文件 ${tempVideo.fileName} 重试上传失败`);
|
|
|
+ // }
|
|
|
+ // if (line2 == 100 || line2Success) {
|
|
|
+ // this.videoList[index].uploadDetails.line2Status = 'success';
|
|
|
+ // this.videoList[index].uploadDetails.line2 = 100;
|
|
|
+ // this.$message.success(`文件 ${tempVideo.fileName} 重试上传成功`);
|
|
|
+ // } else {
|
|
|
+ // this.videoList[index].uploadDetails.line2Status = line2Success ? 'success' : 'failed';
|
|
|
+ // this.$message.error(`文件 ${tempVideo.fileName} 重试上传失败`);
|
|
|
+ // }
|
|
|
+ // // if (line1Success && line2Success) {
|
|
|
+ // // this.videoList[index].progress = 100;
|
|
|
+ // // this.videoList[index].uploadStatus = 'success';
|
|
|
+ // // this.videoList[index].uploadDetails.line1Status = 'success';
|
|
|
+ // // this.videoList[index].uploadDetails.line2Status = 'success';
|
|
|
+ // // this.videoList[index].uploadDetails.line1 = 100;
|
|
|
+ // // this.videoList[index].uploadDetails.line2 = 100;
|
|
|
+ // // this.$message.success(`文件 ${tempVideo.fileName} 重试上传成功`);
|
|
|
+ // // } else {
|
|
|
+ // // this.videoList[index].uploadStatus = 'failed';
|
|
|
+ // // this.videoList[index].uploadDetails.line1Status = line1Success ? 'success' : 'failed';
|
|
|
+ // // this.videoList[index].uploadDetails.line2Status = line2Success ? 'success' : 'failed';
|
|
|
+ // // this.$message.error(`文件 ${tempVideo.fileName} 重试上传失败`);
|
|
|
+ // // }
|
|
|
+ // } catch (error) {
|
|
|
+ // this.videoList[index].uploadStatus = 'failed';
|
|
|
+ // this.$message.error(`文件 ${tempVideo.fileName} 重试上传失败`);
|
|
|
+ // }
|
|
|
+ const index = this.videoList.findIndex(item => item.tempId === row.tempId);
|
|
|
+ if (index === -1) return;
|
|
|
+ const tempVideo = this.videoList[index];
|
|
|
+ const uploadDetails = tempVideo.uploadDetails || {};
|
|
|
+
|
|
|
+ // 检查哪些线路需要重试
|
|
|
+ const needRetryLine1 = uploadDetails.line1Status === 'failed' || uploadDetails.line1Status === 'pending';
|
|
|
+ const needRetryLine2 = uploadDetails.line2Status === 'failed' || uploadDetails.line2Status === 'pending';
|
|
|
+
|
|
|
+ if (!needRetryLine1 && !needRetryLine2) {
|
|
|
+ this.$message.info('所有线路都已上传成功,无需重试');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新整体状态为上传中
|
|
|
+ this.videoList[index].uploadStatus = 'uploading';
|
|
|
+
|
|
|
+ // 只重置需要重试的线路状态
|
|
|
+ if (needRetryLine1) {
|
|
|
+ this.videoList[index].uploadDetails.line1 = 0;
|
|
|
+ this.videoList[index].uploadDetails.line1Status = 'pending';
|
|
|
+ }
|
|
|
+ if (needRetryLine2) {
|
|
|
+ this.videoList[index].uploadDetails.line2 = 0;
|
|
|
+ this.videoList[index].uploadDetails.line2Status = 'pending';
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const uploadPromises = [];
|
|
|
+
|
|
|
+ // 根据需要重试的线路创建上传任务
|
|
|
+ if (needRetryLine1) {
|
|
|
+ uploadPromises.push(
|
|
|
+ this.uploadVideoToTxPcdnBatch(tempVideo.file, tempVideo)
|
|
|
+ .then(result => ({ line: 'line1', result }))
|
|
|
+ .catch(error => ({ line: 'line1', result: { success: false, error: error.message } }))
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ // 如果线路1不需要重试,创建一个已成功的Promise
|
|
|
+ uploadPromises.push(Promise.resolve({ line: 'line1', result: { success: true } }));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (needRetryLine2) {
|
|
|
+ uploadPromises.push(
|
|
|
+ this.uploadVideoToHwObsBatch(tempVideo.file, tempVideo)
|
|
|
+ .then(result => ({ line: 'line2', result }))
|
|
|
+ .catch(error => ({ line: 'line2', result: { success: false, error: error.message } }))
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ // 如果线路2不需要重试,创建一个已成功的Promise
|
|
|
+ uploadPromises.push(Promise.resolve({ line: 'line2', result: { success: true } }));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待所有上传任务完成
|
|
|
+ const results = await Promise.all(uploadPromises);
|
|
|
+
|
|
|
+ // 处理结果
|
|
|
+ let line1Success = true;
|
|
|
+ let line2Success = true;
|
|
|
+ let retryMessages = [];
|
|
|
+
|
|
|
+ results.forEach(({ line, result }) => {
|
|
|
+ if (line === 'line1') {
|
|
|
+ line1Success = result.success;
|
|
|
+ if (needRetryLine1) {
|
|
|
+ if (result.success) {
|
|
|
+ this.videoList[index].uploadDetails.line1Status = 'success';
|
|
|
+ this.videoList[index].uploadDetails.line1 = 100;
|
|
|
+ retryMessages.push('线路1重试成功');
|
|
|
+ } else {
|
|
|
+ this.videoList[index].uploadDetails.line1Status = 'failed';
|
|
|
+ retryMessages.push('线路1重试失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (line === 'line2') {
|
|
|
+ line2Success = result.success;
|
|
|
+ if (needRetryLine2) {
|
|
|
+ if (result.success) {
|
|
|
+ this.videoList[index].uploadDetails.line2Status = 'success';
|
|
|
+ this.videoList[index].uploadDetails.line2 = 100;
|
|
|
+ retryMessages.push('线路2重试成功');
|
|
|
+ } else {
|
|
|
+ this.videoList[index].uploadDetails.line2Status = 'failed';
|
|
|
+ retryMessages.push('线路2重试失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新总体状态和进度
|
|
|
+ if (line1Success && line2Success) {
|
|
|
+ this.videoList[index].progress = 100;
|
|
|
+ this.videoList[index].uploadStatus = 'success';
|
|
|
+ this.$message.success(`文件 ${tempVideo.fileName} 重试完成:${retryMessages.join(',')}`);
|
|
|
+ } else {
|
|
|
+ this.videoList[index].uploadStatus = 'failed';
|
|
|
+ // 重新计算进度
|
|
|
+ this.updateBatchProgress(index);
|
|
|
+ this.$message.error(`文件 ${tempVideo.fileName} 重试完成:${retryMessages.join(',')}`);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.videoList[index].uploadStatus = 'failed';
|
|
|
+ this.$message.error(`文件 ${tempVideo.fileName} 重试过程中发生错误:${error.message || '未知错误'}`);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 重试单个上传
|
|
|
+ async retryUpload(row) {
|
|
|
+ this.$confirm('确认要重新上传该视频吗?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }).then(async () => {
|
|
|
+ // 这里需要重新触发上传,但由于原始文件可能已经不存在
|
|
|
+ // 建议用户重新选择文件上传
|
|
|
+ this.$message.info('请重新选择文件进行上传');
|
|
|
+ this.handleUpdate(row);
|
|
|
+ }).catch(() => {});
|
|
|
+ },
|
|
|
+ // 获取上传状态图标
|
|
|
+ getUploadStatusIcon(status) {
|
|
|
+ switch (status) {
|
|
|
+ case 'success': return 'el-icon-success';
|
|
|
+ case 'failed': return 'el-icon-error';
|
|
|
+ case 'uploading': return 'el-icon-loading';
|
|
|
+ default: return 'el-icon-time';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 获取上传状态颜色
|
|
|
+ getUploadStatusColor(status) {
|
|
|
+ switch (status) {
|
|
|
+ case 'success': return '#67C23A';
|
|
|
+ case 'failed': return '#F56C6C';
|
|
|
+ case 'uploading': return '#409EFF';
|
|
|
+ default: return '#909399';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 获取上传状态文本
|
|
|
+ getUploadStatusText(status, queuePosition) {
|
|
|
+ switch (status) {
|
|
|
+ case 'success':
|
|
|
+ return '上传成功';
|
|
|
+ case 'failed':
|
|
|
+ return '上传失败';
|
|
|
+ case 'uploading':
|
|
|
+ return '上传中';
|
|
|
+ case 'queued':
|
|
|
+ return `队列中 (第${queuePosition}位)`;
|
|
|
+ default:
|
|
|
+ return '待上传';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 获取线路状态图标
|
|
|
+ getLineStatusIcon(status) {
|
|
|
+ switch (status) {
|
|
|
+ case 'success': return 'el-icon-check';
|
|
|
+ case 'failed': return 'el-icon-close';
|
|
|
+ case 'uploading': return 'el-icon-loading';
|
|
|
+ default: return 'el-icon-minus';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 获取线路状态颜色
|
|
|
+ getLineStatusColor(status) {
|
|
|
+ switch (status) {
|
|
|
+ case 'success': return '#67C23A';
|
|
|
+ case 'failed': return '#F56C6C';
|
|
|
+ case 'uploading': return '#409EFF';
|
|
|
+ default: return '#C0C4CC';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 获取进度条状态
|
|
|
+ getProgressStatus(row) {
|
|
|
+ if (row.progress === 100 && row.uploadStatus === 'success') {
|
|
|
+ return 'success';
|
|
|
+ } else if (row.uploadStatus === 'failed') {
|
|
|
+ return 'exception';
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* 自定义表格样式 */
|
|
|
+.el-table .video-cell {
|
|
|
+ padding: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 设置删除和修改按钮的间距 */
|
|
|
+.el-button + .el-button {
|
|
|
+ margin-left: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 上传状态样式 */
|
|
|
+.upload-status-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-indicator {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-text {
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.upload-details {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+ font-size: 11px;
|
|
|
+}
|
|
|
+
|
|
|
+.line-status {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.line-label {
|
|
|
+ min-width: 35px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.line-progress {
|
|
|
+ color: #909399;
|
|
|
+}
|
|
|
+
|
|
|
+/* 双线上传进度样式 */
|
|
|
+.dual-upload-progress {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.total-progress {
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.line-progress-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.line-progress-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ font-size: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.line-label {
|
|
|
+ min-width: 30px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.line-status-text {
|
|
|
+ min-width: 25px;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 批量上传进度样式 */
|
|
|
+.batch-upload-progress {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.total-progress-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ white-space: nowrap;
|
|
|
+ /* 防止换行 */
|
|
|
+ min-width: 0;
|
|
|
+ /* 允许flex项目收缩 */
|
|
|
+}
|
|
|
+
|
|
|
+.progress-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+ min-width: 40px;
|
|
|
+}
|
|
|
+
|
|
|
+.line-progress-rows {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.line-progress-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ font-size: 11px;
|
|
|
+}
|
|
|
+
|
|
|
+.line-percentage {
|
|
|
+ min-width: 30px;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 11px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .upload-icon {
|
|
|
+ font-size: 28px;
|
|
|
+ color: #8c939d;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .upload-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 表单样式 */
|
|
|
+::v-deep .el-form-item {
|
|
|
+ margin-bottom: 22px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 批量选择弹窗样式 */
|
|
|
+::v-deep .batch-dialog .el-dialog__body {
|
|
|
+ padding: 0 20px 20px;
|
|
|
+ max-height: 70vh;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-dialog .el-dialog__body {
|
|
|
+ padding: 0 20px 20px;
|
|
|
+ max-height: 70vh;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-container {
|
|
|
+ padding: 15px 0;
|
|
|
+ background-color: #fff;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ position: relative;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+.dialog-footer {
|
|
|
+ text-align: right;
|
|
|
+ margin-top: 20px;
|
|
|
+ padding-right: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .batch-dialog .el-table {
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-table__header-wrapper th {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ color: #606266;
|
|
|
+ font-weight: 500;
|
|
|
+ padding: 8px 0;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-table__header-wrapper th {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ color: #606266;
|
|
|
+ font-weight: 500;
|
|
|
+ padding: 8px 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保弹窗中表格内容垂直居中 */
|
|
|
+::v-deep .batch-dialog .el-table .cell {
|
|
|
+ line-height: 23px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-table .cell {
|
|
|
+ line-height: 23px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 自定义滚动条样式 */
|
|
|
+::v-deep .batch-dialog .el-dialog__body::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-dialog .el-dialog__body::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .batch-dialog .el-dialog__body::-webkit-scrollbar-thumb {
|
|
|
+ background: #c0c4cc;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-dialog .el-dialog__body::-webkit-scrollbar-thumb {
|
|
|
+ background: #c0c4cc;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .batch-dialog .el-dialog__body::-webkit-scrollbar-track {
|
|
|
+ background: #f5f7fa;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-dialog .el-dialog__body::-webkit-scrollbar-track {
|
|
|
+ background: #f5f7fa;
|
|
|
+}
|
|
|
+
|
|
|
+/* 批量上传视频弹窗样式 */
|
|
|
+.upload-dialog .el-dialog__body {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .upload-dialog .el-dialog__header {
|
|
|
+ padding: 15px 20px;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .upload-dialog .el-dialog__title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+/* 项目选择弹窗样式 */
|
|
|
+::v-deep .el-dialog__wrapper {
|
|
|
+ z-index: 2001 !important;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-dialog__header {
|
|
|
+ padding: 15px 20px;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-dialog__title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-pagination {
|
|
|
+ text-align: center;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 项目选择弹窗的样式 */
|
|
|
+.project-container {
|
|
|
+ display: flex;
|
|
|
+ height: 500px;
|
|
|
+}
|
|
|
+
|
|
|
+.category-tree {
|
|
|
+ width: 250px;
|
|
|
+ height: 100%;
|
|
|
+ border-right: 1px solid #ebeef5;
|
|
|
+ padding: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-fixed-header {
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #fff;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ z-index: 10;
|
|
|
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.tree-content {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 10px 10px 10px 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.project-list {
|
|
|
+ flex: 1;
|
|
|
+ padding: 10px;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ width: calc(100% - 250px);
|
|
|
+ /* 固定宽度为剩余空间 */
|
|
|
+}
|
|
|
+
|
|
|
+.project-list .filter-container {
|
|
|
+ padding: 0 0 15px 0;
|
|
|
+ text-align: left;
|
|
|
+}
|
|
|
+
|
|
|
+.project-list .table-footer {
|
|
|
+ margin-top: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-tree-node__content {
|
|
|
+ height: 36px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-bottom: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-tree-node:focus > .el-tree-node__content {
|
|
|
+ background-color: #e6f7ff;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-tree-node__content:hover {
|
|
|
+ background-color: #f0f7ff;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
|
|
|
+ background-color: #e6f7ff;
|
|
|
+ color: #1890ff;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .custom-tree-node {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 14px;
|
|
|
+ padding: 0 8px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频预览弹窗样式 */
|
|
|
+::v-deep .video-preview-dialog {
|
|
|
+ z-index: 3000 !important;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .video-preview-dialog .el-dialog {
|
|
|
+ margin-top: 10vh !important;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .video-preview-dialog .el-dialog__header {
|
|
|
+ padding: 15px 20px;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .video-preview-dialog .el-dialog__body {
|
|
|
+ padding: 15px;
|
|
|
+ background-color: #000;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .video-preview-dialog video {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 70vh;
|
|
|
+}
|
|
|
+
|
|
|
+/* 题目列表样式 */
|
|
|
+.project-list-container {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.question-card {
|
|
|
+ background-color: #ffffff;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
|
|
+ padding: 15px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.question-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ padding-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.question-index {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ background-color: #409EFF;
|
|
|
+ color: white;
|
|
|
+ border-radius: 50%;
|
|
|
+ font-size: 14px;
|
|
|
+ margin-right: 10px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.question-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #303133;
|
|
|
+ line-height: 1.5;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+
|
|
|
+.question-type {
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.content-title {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #606266;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.question-content {
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.question-item {
|
|
|
+ background-color: #f8f8f8;
|
|
|
+ border-left: 3px solid #409EFF;
|
|
|
+ padding: 10px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ border-radius: 0 4px 4px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.question-item-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.item-index {
|
|
|
+ display: inline-block;
|
|
|
+ margin-right: 10px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #409EFF;
|
|
|
+}
|
|
|
+
|
|
|
+.item-name {
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.question-answer {
|
|
|
+ background-color: #f0f9eb;
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ border-left: 3px solid #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.answer-label {
|
|
|
+ font-weight: 500;
|
|
|
+ color: #67c23a;
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.answer-content {
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .custom-select-class .el-select__tags {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ flex-wrap: nowrap;
|
|
|
+ max-height: 200px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+::v-deep .custom-select-class .el-tag {
|
|
|
+ margin-bottom: 4px;
|
|
|
+ margin-right: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* Added queue status styles */
|
|
|
+.queue-status {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.queue-status .el-tag {
|
|
|
+ font-size: 12px;
|
|
|
+ padding: 4px 8px;
|
|
|
+}
|
|
|
+</style>
|