|
@@ -270,13 +270,47 @@
|
|
|
<i class="el-icon-delete"></i>
|
|
|
</span>
|
|
|
</span>
|
|
|
-
|
|
|
+ <!-- 改进的上传进度显示 -->
|
|
|
<div v-if="file.status === 'uploading'" class="el-upload-list__item-progress">
|
|
|
- <el-progress
|
|
|
+ <!-- <el-progress
|
|
|
:percentage="file.percentage"
|
|
|
:show-text="false"
|
|
|
:width="52"
|
|
|
- ></el-progress>
|
|
|
+ ></el-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>
|
|
@@ -287,8 +321,12 @@
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<div slot="footer" class="dialog-footer">
|
|
|
+ <!-- <el-button @click="cancel">取消</el-button>
|
|
|
+ <el-button type="primary" @click="submitForm">保存</el-button> -->
|
|
|
<el-button @click="cancel">取消</el-button>
|
|
|
- <el-button type="primary" @click="submitForm">保存</el-button>
|
|
|
+ <el-button type="primary" @click="submitForm" :disabled="isUploading">
|
|
|
+ {{ isUploading ? '上传中...' : '保存' }}
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
</minimizable-dialog>
|
|
|
|
|
@@ -371,10 +409,50 @@
|
|
|
{{ formatDuration(scope.row.duration) }}
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column label="上传进度" align="center" width="120">
|
|
|
+ <!-- 改进的上传进度列 -->
|
|
|
+ <!-- <el-table-column label="上传进度" align="center" width="120">
|
|
|
<template slot-scope="scope">
|
|
|
<el-progress :percentage="scope.row.progress" :status="scope.row.progress === 100 ? 'success' : undefined"></el-progress>
|
|
|
</template>
|
|
|
+ </el-table-column> -->
|
|
|
+ <el-table-column label="上传进度" align="center" width="200">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <div class="batch-upload-progress">
|
|
|
+ <div 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" 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">
|
|
@@ -388,6 +466,13 @@
|
|
|
type="text"
|
|
|
icon="el-icon-delete"
|
|
|
@click="handleDeleteVideo(scope.row)">删除</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>
|
|
@@ -700,7 +785,16 @@ export default {
|
|
|
videoUrl: null,
|
|
|
typeId: null,
|
|
|
typeSubId: null,
|
|
|
- projectIds: []
|
|
|
+ projectIds: [],
|
|
|
+ // 新增上传状态字段
|
|
|
+ uploadStatus: 'pending', // pending, uploading, success, failed
|
|
|
+ uploadProgress: {
|
|
|
+ total: 0,
|
|
|
+ line1: 0,
|
|
|
+ line2: 0,
|
|
|
+ line1Status: 'pending', // pending, uploading, success, failed
|
|
|
+ line2Status: 'pending'
|
|
|
+ }
|
|
|
},
|
|
|
// 表单校验
|
|
|
rules: {
|
|
@@ -783,6 +877,17 @@ export default {
|
|
|
},
|
|
|
// 是否存在最小化窗口
|
|
|
hasMinimizableDialog: false,
|
|
|
+ // 新增上传相关状态
|
|
|
+ isUploading: false,
|
|
|
+ currentUploadProgress: {
|
|
|
+ total: 0,
|
|
|
+ line1: 0,
|
|
|
+ line2: 0,
|
|
|
+ line1Status: 'pending',
|
|
|
+ line2Status: 'pending'
|
|
|
+ },
|
|
|
+ // 上传任务队列
|
|
|
+ uploadQueue: []
|
|
|
}
|
|
|
},
|
|
|
watch: {
|
|
@@ -1010,6 +1115,7 @@ export default {
|
|
|
handleVideoPreview(url) {
|
|
|
this.videoPreviewVisible = true;
|
|
|
this.videoPreviewUrl = url || this.form.videoUrl;
|
|
|
+ console.log("--------------------",this.videoPreviewUrl)
|
|
|
},
|
|
|
/** 格式化视频时长 */
|
|
|
formatDuration(seconds) {
|
|
@@ -1037,72 +1143,301 @@ export default {
|
|
|
},
|
|
|
//获取第一帧封面
|
|
|
async getFirstThumbnail(file, form){
|
|
|
- getThumbnail(file).then(response => {
|
|
|
+ try {
|
|
|
+ //截取小文件
|
|
|
+ const clippedBlob = await this.clipVideoFirstTwoSeconds(file);
|
|
|
+
|
|
|
+ const clippedFile = new File([clippedBlob], 'clipped_video.mp4', {
|
|
|
+ type: 'video/mp4',
|
|
|
+ lastModified: Date.now()
|
|
|
+ });
|
|
|
+ console.log("调用请请求---------------》",response)
|
|
|
+ // 3. 调用接口获取封面
|
|
|
+ const response = await getThumbnail(clippedFile);
|
|
|
+ console.log("获取封面请求---------------》",response)
|
|
|
form.thumbnail = response.url;
|
|
|
- })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取封面失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ //截取大文件视频
|
|
|
+ clipVideoFirstTwoSeconds(file) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ // 创建视频元素用于处理
|
|
|
+ const video = document.createElement('video');
|
|
|
+ video.src = URL.createObjectURL(file);
|
|
|
+ video.crossOrigin = 'anonymous';
|
|
|
+ video.preload = 'metadata';
|
|
|
+
|
|
|
+ // 视频元数据加载完成后开始处理
|
|
|
+ video.onloadedmetadata = async () => {
|
|
|
+ try {
|
|
|
+ // 计算截取时长
|
|
|
+ const duration = Math.min(2, video.duration);
|
|
|
+
|
|
|
+ // 直接从视频元素捕获流
|
|
|
+ const stream = video.captureStream();
|
|
|
+
|
|
|
+ // 创建 MediaRecorder 录制截取的片段
|
|
|
+ const mediaRecorder = new MediaRecorder(stream);
|
|
|
+ const chunks = [];
|
|
|
+
|
|
|
+ // 收集录制的视频数据
|
|
|
+ mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
|
|
|
+
|
|
|
+ // 录制结束后处理结果
|
|
|
+ mediaRecorder.onstop = () => {
|
|
|
+ // 合并数据为 Blob(MP4 格式)
|
|
|
+ const blob = new Blob(chunks, { type: 'video/mp4' });
|
|
|
+ resolve(blob);
|
|
|
+
|
|
|
+ // 清理资源
|
|
|
+ URL.revokeObjectURL(video.src);
|
|
|
+ stream.getTracks().forEach(track => track.stop());
|
|
|
+ };
|
|
|
+
|
|
|
+ // 开始录制
|
|
|
+ mediaRecorder.start();
|
|
|
+
|
|
|
+ // 播放视频并在指定时间后停止录制
|
|
|
+ video.currentTime = 0; // 从开头开始
|
|
|
+ video.play();
|
|
|
+
|
|
|
+ // 到达截取时长后停止录制
|
|
|
+ setTimeout(() => {
|
|
|
+ video.pause();
|
|
|
+ mediaRecorder.stop();
|
|
|
+ }, duration * 1000); // 转换为毫秒
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ reject(new Error('视频截取失败: ' + error.message));
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 视频加载错误处理
|
|
|
+ video.onerror = () => {
|
|
|
+ reject(new Error('视频加载失败,请检查文件格式'));
|
|
|
+ };
|
|
|
+ });
|
|
|
},
|
|
|
//上传腾讯云Pcdn
|
|
|
async uploadVideoToTxPcdn(file, form, onProgress) {
|
|
|
+ // try {
|
|
|
+ // const data = await uploadObject(file, (progress) => {
|
|
|
+ // const progressEvent = {
|
|
|
+ // percent: Math.floor(progress.percent * 100 / 2), // COS SDK 百分比是 0-1,el-upload 需要 0-100
|
|
|
+ // loaded: progress.loaded,
|
|
|
+ // total: progress.total,
|
|
|
+ // lengthComputable: true // 文件上传通常总大小可知
|
|
|
+ // };
|
|
|
+ // onProgress(progressEvent);
|
|
|
+ // }, 1);
|
|
|
+
|
|
|
+ // 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;
|
|
|
+
|
|
|
+ // this.$message.success("线路一上传成功");
|
|
|
+ // } catch (error) {
|
|
|
+ // this.$message.error("线路一上传失败");
|
|
|
+ // }
|
|
|
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: Math.floor(progress.percent * 100 / 2), // COS SDK 百分比是 0-1,el-upload 需要 0-100
|
|
|
+ percent: progressPercent,
|
|
|
loaded: progress.loaded,
|
|
|
total: progress.total,
|
|
|
- lengthComputable: true // 文件上传通常总大小可知
|
|
|
+ lengthComputable: true
|
|
|
};
|
|
|
onProgress(progressEvent);
|
|
|
}, 1);
|
|
|
-
|
|
|
+
|
|
|
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 {
|
|
|
+ // const data = await uploadToOBS(file, (progress) => {
|
|
|
+ // const progressEvent = {
|
|
|
+ // percent: Math.floor(progress / 2) + 50,
|
|
|
+ // loaded: progress,
|
|
|
+ // total: progress,
|
|
|
+ // lengthComputable: true // 文件上传通常总大小可知
|
|
|
+ // };
|
|
|
+ // onProgress(progressEvent);
|
|
|
+ // }, 1);
|
|
|
+
|
|
|
+ // form.line2 = `${process.env.VUE_APP_VIDEO_LINE_2}/${data.urlPath}`;
|
|
|
+ // this.$message.success("线路二上传成功");
|
|
|
+ // } catch (error) {
|
|
|
+ // this.$message.error("线路二上传失败");
|
|
|
+ // }
|
|
|
try {
|
|
|
+ // 更新线路2状态为上传中
|
|
|
+ this.updateUploadProgress('line2Status', 'uploading');
|
|
|
+
|
|
|
const data = await uploadToOBS(file, (progress) => {
|
|
|
+ const progressPercent = Math.floor(progress);
|
|
|
+ this.updateUploadProgress('line2', progressPercent);
|
|
|
+
|
|
|
const progressEvent = {
|
|
|
- percent: Math.floor(progress / 2) + 50,
|
|
|
+ percent: progressPercent,
|
|
|
loaded: progress,
|
|
|
total: progress,
|
|
|
- lengthComputable: true // 文件上传通常总大小可知
|
|
|
+ lengthComputable: true
|
|
|
};
|
|
|
onProgress(progressEvent);
|
|
|
}, 1);
|
|
|
-
|
|
|
+
|
|
|
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 };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 更新上传进度的辅助方法
|
|
|
+ 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.add = true
|
|
|
- const file = options.file;
|
|
|
- this.getMediaDuration(file)
|
|
|
+ // this.add = true
|
|
|
+ // const file = options.file;
|
|
|
+ // this.getMediaDuration(file)
|
|
|
|
|
|
- // 获取第一帧图片
|
|
|
- await this.getFirstThumbnail(file, this.form);
|
|
|
+ // // 获取第一帧图片
|
|
|
+ // await this.getFirstThumbnail(file, this.form);
|
|
|
|
|
|
- // 上传腾讯云pcdn
|
|
|
- await this.uploadVideoToTxPcdn(file, this.form, options.onProgress);
|
|
|
+ // // 上传腾讯云pcdn
|
|
|
+ // await this.uploadVideoToTxPcdn(file, this.form, options.onProgress);
|
|
|
|
|
|
- // 上传华为obs
|
|
|
- await this.uploadVideoToHwObs(file, this.form, options.onProgress);
|
|
|
+ // // 上传华为obs
|
|
|
+ // await this.uploadVideoToHwObs(file, this.form, options.onProgress);
|
|
|
|
|
|
- this.form.fileName = file.name;
|
|
|
- this.form.fileSize = file.size;
|
|
|
+ // this.form.fileName = file.name;
|
|
|
+ // this.form.fileSize = file.size;
|
|
|
|
|
|
- this.add = false
|
|
|
+ // this.add = false
|
|
|
+ this.isUploading = true;
|
|
|
+ this.form.uploadStatus = 'uploading';
|
|
|
+
|
|
|
+ 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)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 检查上传结果
|
|
|
+ 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;
|
|
|
+ }
|
|
|
},
|
|
|
// 获取媒体文件时长
|
|
|
getMediaDuration(file) {
|
|
@@ -1264,7 +1599,14 @@ export default {
|
|
|
typeId: this.batchUploadForm.typeId, // 使用选择的分类
|
|
|
typeSubId: this.batchUploadForm.typeSubId, // 使用选择的子分类
|
|
|
projectIds: this.batchUploadForm.projectIds, // 使用选择的项目
|
|
|
- progress: 0, // 上传进度
|
|
|
+ progress: 0, // 总进度
|
|
|
+ uploadStatus: 'uploading',
|
|
|
+ uploadDetails: {
|
|
|
+ line1: 0,
|
|
|
+ line2: 0,
|
|
|
+ line1Status: 'pending',
|
|
|
+ line2Status: 'pending'
|
|
|
+ },
|
|
|
file: file // 保存文件引用
|
|
|
};
|
|
|
|
|
@@ -1289,15 +1631,53 @@ export default {
|
|
|
// 获取封面
|
|
|
await this.getFirstThumbnail(file, tempVideo);
|
|
|
|
|
|
- // 上传到第一个服务器
|
|
|
- await this.uploadVideoToTxPcdn(file, tempVideo, (event) => {
|
|
|
- tempVideo.progress = event.percent
|
|
|
- });
|
|
|
-
|
|
|
- // 上传到第二个服务器
|
|
|
- await this.uploadVideoToHwObs(file, tempVideo, (event) => {
|
|
|
- tempVideo.progress = event.percent
|
|
|
- });
|
|
|
+ // // 上传到第一个服务器
|
|
|
+ // await this.uploadVideoToTxPcdn(file, tempVideo, (event) => {
|
|
|
+ // tempVideo.progress = event.percent
|
|
|
+ // });
|
|
|
+
|
|
|
+ // // 上传到第二个服务器
|
|
|
+ // await this.uploadVideoToHwObs(file, tempVideo, (event) => {
|
|
|
+ // tempVideo.progress = event.percent
|
|
|
+ // });
|
|
|
+ // 并行上传到两个服务器
|
|
|
+ const [line1Result, line2Result] = await Promise.allSettled([
|
|
|
+ this.uploadVideoToTxPcdnBatch(file, tempVideo),
|
|
|
+ this.uploadVideoToHwObsBatch(file, tempVideo)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 检查上传结果
|
|
|
+ const line1Success = line1Result.status === 'fulfilled' && line1Result.value.success;
|
|
|
+ const line2Success = line2Result.status === 'fulfilled' && line2Result.value.success;
|
|
|
+ if (line1Success && line2Success) {
|
|
|
+ // 两个都成功,更新进度为100%
|
|
|
+ const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
|
|
|
+ if (index !== -1) {
|
|
|
+ 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(`文件 ${file.name} 上传成功`);
|
|
|
+ } else {
|
|
|
+ // 至少有一个失败
|
|
|
+ const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
|
|
|
+ if (index !== -1) {
|
|
|
+ this.videoList[index].uploadStatus = 'failed';
|
|
|
+ this.videoList[index].uploadDetails.line1Status = line1Success ? 'success' : 'failed';
|
|
|
+ this.videoList[index].uploadDetails.line2Status = line2Success ? 'success' : 'failed';
|
|
|
+ // 失败时保持当前进度,不设为100%
|
|
|
+ this.updateBatchProgress(index);
|
|
|
+ }
|
|
|
+
|
|
|
+ const failedLines = [];
|
|
|
+ if (!line1Success) failedLines.push('线路1');
|
|
|
+ if (!line2Success) failedLines.push('线路2');
|
|
|
+
|
|
|
+ this.$message.error(`文件 ${file.name} 在 ${failedLines.join('、')} 上传失败`);
|
|
|
+ }
|
|
|
|
|
|
} catch (error) {
|
|
|
this.$message.error(`文件 ${file.name} 上传失败: ${error.message || '未知错误'}`);
|
|
@@ -1542,6 +1922,350 @@ export default {
|
|
|
this.videoPreviewVisible = false;
|
|
|
done();
|
|
|
},
|
|
|
+ // 批量上传 - 腾讯云
|
|
|
+ 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);
|
|
|
+
|
|
|
+ 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);
|
|
|
+
|
|
|
+ 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 };
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 更新批量上传的总进度
|
|
|
+ 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) {
|
|
|
+ switch (status) {
|
|
|
+ case 'success':
|
|
|
+ return '上传成功';
|
|
|
+ case 'failed':
|
|
|
+ return '上传失败';
|
|
|
+ case 'uploading':
|
|
|
+ return '上传中';
|
|
|
+ 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>
|
|
@@ -1557,6 +2281,124 @@ export default {
|
|
|
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;
|