Browse Source

Merge branch 'master' into 会员关联项目

# Conflicts:
#	src/views/store/user/index.vue
Long 1 week ago
parent
commit
6b1e041f59

+ 9 - 0
src/api/course/userCoursePeriod.js

@@ -181,3 +181,12 @@ export function delPeriodDay(periodId) {
     method: 'delete'
   })
 }
+
+// 根据公司批量设置红包金额
+export function batchSaveRedPacketByCompany(data) {
+  return request({
+    url: '/course/period/batchRedPacket/byCompany',
+    method: 'post',
+    data: data
+  })
+}

+ 106 - 52
src/components/VideoUpload/index.vue

@@ -2,49 +2,49 @@
   <div>
     <el-form-item label="视频上传">
       <div class="upload_video" id="upload_video">
-        <el-upload
-          class="upload-demo"
-          ref="upload"
-          action="#"
-          :http-request="uploadVideoToTxPcdn"
-          accept=".mp4"
-          :limit="1"
-          :on-remove="handleRemove"
-          :on-change="handleChange"
-          :auto-upload="false"
-          :key="uploadKey"
-        >
-          <el-button slot="trigger" size="small" type="primary" >选取视频</el-button>
-          <el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">点击上传</el-button>
-          <!-- 仅当showControl为true时显示视频库选取按钮 -->
-          <el-button v-if="showControl" style="margin-left: 10px;" size="small" type="success" @click="openVideoLibrary">视频库选取</el-button>
-          <!-- 线路一 -->
-          <div class="progress-container">
-            <span class="progress-label">线路一</span>
-            <el-progress
-              style="margin-top: 5px;"
-              class="progress"
-              :text-inside="true"
-              :stroke-width="18"
-              :percentage="txProgress"
-              status="success">
-            </el-progress>
-          </div>
+        <!--        <el-upload-->
+        <!--          class="upload-demo"-->
+        <!--          ref="upload"-->
+        <!--          action="#"-->
+        <!--          :http-request="uploadVideoToTxPcdn"-->
+        <!--          accept=".mp4"-->
+        <!--          :limit="1"-->
+        <!--          :on-remove="handleRemove"-->
+        <!--          :on-change="handleChange"-->
+        <!--          :auto-upload="false"-->
+        <!--          :key="uploadKey"-->
+        <!--        >-->
+        <!--          <el-button slot="trigger" size="small" type="primary" >选取视频</el-button>-->
+        <!--          <el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">点击上传</el-button>-->
+        <!-- 仅当showControl为true时显示视频库选取按钮 -->
+        <el-button v-if="showControl" size="small" type="success" @click="openVideoLibrary">视频库选取</el-button>
+        <!-- 线路一 -->
+        <!--          <div class="progress-container">-->
+        <!--            <span class="progress-label">线路一</span>-->
+        <!--            <el-progress-->
+        <!--              style="margin-top: 5px;"-->
+        <!--              class="progress"-->
+        <!--              :text-inside="true"-->
+        <!--              :stroke-width="18"-->
+        <!--              :percentage="txProgress"-->
+        <!--              status="success">-->
+        <!--            </el-progress>-->
+        <!--          </div>-->
 
-          <!-- 线路二 -->
-          <div class="progress-container">
-            <span class="progress-label">线路二</span>
-            <el-progress
-              style="margin-top: 5px;"
-              class="progress"
-              :text-inside="true"
-              :stroke-width="18"
-              :percentage="hwProgress"
-              status="success">
-            </el-progress>
-          </div>
-          <div slot="tip" class="el-upload__tip">只能上传mp4文件,且不超过500M</div>
-        </el-upload>
+        <!--          &lt;!&ndash; 线路二 &ndash;&gt;-->
+        <!--          <div class="progress-container">-->
+        <!--            <span class="progress-label">线路二</span>-->
+        <!--            <el-progress-->
+        <!--              style="margin-top: 5px;"-->
+        <!--              class="progress"-->
+        <!--              :text-inside="true"-->
+        <!--              :stroke-width="18"-->
+        <!--              :percentage="hwProgress"-->
+        <!--              status="success">-->
+        <!--            </el-progress>-->
+        <!--          </div>-->
+        <!--          <div slot="tip" class="el-upload__tip">只能上传mp4文件,且不超过500M</div>-->
+        <!--        </el-upload>-->
       </div>
     </el-form-item>
     <el-form-item label="视频播放">
@@ -54,13 +54,13 @@
       <div v-if="fileSize">文件大小(MB): {{ (fileSize / (1024 * 1024)).toFixed(2) }} MB</div>
     </el-form-item>
     <!-- 仅当showControl为true时显示播放线路选择器 -->
-    <el-form-item v-if="showControl" label="播放线路">
-      <el-radio-group v-model="localUploadType">
-        <el-radio :label="1" >线路一</el-radio>
-        <el-radio :label="2" >线路二</el-radio>
-        <!--        <el-radio :label="3" >线路三</el-radio>-->
-      </el-radio-group>
-    </el-form-item>
+    <!--    <el-form-item v-if="showControl" label="播放线路">-->
+    <!--      <el-radio-group v-model="localUploadType">-->
+    <!--        <el-radio :label="1" >线路一</el-radio>-->
+    <!--        <el-radio :label="2" >线路二</el-radio>-->
+    <!--        &lt;!&ndash;        <el-radio :label="3" >线路三</el-radio>&ndash;&gt;-->
+    <!--      </el-radio-group>-->
+    <!--    </el-form-item>-->
 
     <!-- 视频库选择对话框 -->
     <el-dialog title="视频库选择" :visible.sync="libraryOpen" width="900px" append-to-body>
@@ -75,6 +75,26 @@
             @keyup.enter.native="handleLibraryQuery"
           />
         </el-form-item>
+        <el-form-item label="类型">
+          <el-select v-model="libraryQueryParams.typeId" @change="changeCateType" placeholder="请选择素材类型" clearable size="small">
+            <el-option
+              v-for="item in typeOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="子类型">
+          <el-select v-model="libraryQueryParams.typeSubId" placeholder="请选择素材子类型" clearable size="small">
+            <el-option
+              v-for="item in typeSubOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
         <el-form-item>
           <el-button type="primary" icon="el-icon-search" size="mini" @click="handleLibraryQuery">搜索</el-button>
           <el-button icon="el-icon-refresh" size="mini" @click="resetLibraryQuery">重置</el-button>
@@ -129,6 +149,7 @@ import { uploadObject } from "@/utils/cos.js";
 import { uploadToOBS } from "@/utils/obs.js";
 import Pagination from "@/components/Pagination";
 import { listVideoResource } from '@/api/course/videoResource';
+import { getCateListByPid, getCatePidList } from '@/api/course/userCourseCategory'
 
 export default {
   components: {
@@ -179,6 +200,14 @@ export default {
       type: Number,
       default: 0,
     },
+    isTranscode: {
+      type: Number,
+      default: null,
+    },
+    transcodeFileKey: {
+      type: String,
+      default: "",
+    },
 
     // 使用一个变量控制显示,默认为true显示所有控制项
     showControl: {
@@ -211,10 +240,15 @@ export default {
       libraryList: [],
       selectedVideo: null,
       libraryQueryParams: {
+        isTranscode:1,
         pageNum: 1,
         pageSize: 10,
-        resourceName: null
-      }
+        resourceName: null,
+        typeId: null,
+        typeSubId: null,
+      },
+      typeOptions: [],
+      typeSubOptions: []
     };
   },
   watch: {
@@ -230,9 +264,25 @@ export default {
     // 打开视频库对话框
     openVideoLibrary() {
       this.libraryOpen = true;
+      this.getRootTypeList()
       this.selectedVideo = null;
       this.getLibraryList();
     },
+    getRootTypeList() {
+      getCatePidList().then(response => {
+        this.typeOptions = response.data
+      });
+    },
+    async changeCateType(val) {
+      this.libraryQueryParams.typeSubId = null
+      this.typeSubOptions = []
+      if (!val) {
+        return
+      }
+      await getCateListByPid(val).then(response => {
+        this.typeSubOptions = response.data
+      })
+    },
     handleChange(file, fileList) {
       this.fileList = fileList;
       this.getVideoDuration(file.raw);
@@ -351,7 +401,9 @@ export default {
       this.libraryQueryParams = {
         pageNum: 1,
         pageSize: 10,
-        resourceName: null
+        resourceName: null,
+        typeId: null,
+        typeSubId: null
       };
       this.handleLibraryQuery();
     },
@@ -385,6 +437,8 @@ export default {
       this.$emit("update:fileKey", this.selectedVideo.fileKey);
       this.$emit("update:uploadType", this.selectedVideo.uploadType);
       this.$emit("video-duration", this.selectedVideo.duration);
+      this.$emit("update:isTranscode", this.selectedVideo.isTranscode);
+      this.$emit("update:transcodeFileKey", this.selectedVideo.transcodeFileKey);
 
       // 设置预览URL
       this.$emit("update:videoUrl", this.selectedVideo.videoUrl);

+ 1 - 6
src/utils/cos.js

@@ -2,16 +2,10 @@ import COS from 'cos-js-sdk-v5';
 import { Message } from 'element-ui';
 import { getTmpSecretKey } from '@/api/common';
 
-console.log('环境变量:', process.env);
-console.log('NODE_ENV:', process.env.NODE_ENV);
-console.log('VUE_APP_COS_BUCKET:', process.env.VUE_APP_COS_BUCKET);
-console.log('VUE_APP_COS_REGION:', process.env.VUE_APP_COS_REGION);
-
 const config = {
   Bucket: process.env.VUE_APP_COS_BUCKET,
   Region: process.env.VUE_APP_COS_REGION,
 };
-console.log('COS配置:', config);
 
 // 上传到腾讯云cos
 export const uploadObject = async (file,onProgress,type,callBackUp) => {
@@ -29,6 +23,7 @@ export const uploadObject = async (file,onProgress,type,callBackUp) => {
 
         // 初始化
         const cos = new COS({
+            Timeout: 1200 * 1000,
             getAuthorization: (options, callback) => {
                 callback({
                     TmpSecretId: credentials.tmpSecretId,

+ 55 - 12
src/views/components/course/userCourseCatalogDetails.vue

@@ -74,7 +74,7 @@
     <el-table border v-loading="loading" :data="userCourseVideoList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="视频ID" align="center" prop="videoId" />
-      <el-table-column label="轮次" align="center" prop="round" />
+<!--      <el-table-column label="轮次" align="center" prop="round" />-->
       <el-table-column label="小节名称" align="center" show-overflow-tooltip prop="title" />
       <el-table-column label="视频文件名称" align="center" show-overflow-tooltip  prop="fileName" >
       </el-table-column>
@@ -145,9 +145,9 @@
         <el-form-item label="视频描述" prop="description">
           <el-input v-model="form.description" type="textarea" :rows="2"  placeholder="请输入内容" />
         </el-form-item>
-        <el-form-item label="轮次" prop="round">
-          <el-input v-model="form.round"  placeholder="请输入内容" />
-        </el-form-item>
+<!--        <el-form-item label="轮次" prop="round">-->
+<!--          <el-input v-model="form.round"  placeholder="请输入内容" />-->
+<!--        </el-form-item>-->
         <el-form-item label="课程排序" prop="courseSort">
           <el-input-number v-model="form.courseSort" :min="1" ></el-input-number>
         </el-form-item>
@@ -196,6 +196,8 @@
           :line_3.sync="form.lineThree"
           :thumbnail.sync="form.thumbnail"
           :uploadType.sync="form.uploadType"
+          :isTranscode.sync="form.isTranscode"
+          :transcodeFileKey.sync="form.transcodeFileKey"
           @video-duration="handleVideoDuration"
           @change="handleVideoChange"
           @selectProjects="handleSelectProjects"
@@ -286,6 +288,26 @@
             @keyup.enter.native="resourceList"
           />
         </el-form-item>
+        <el-form-item label="类型">
+          <el-select v-model="addBatchData.queryParams.typeId" @change="changeCateType" placeholder="请选择素材类型" clearable size="small">
+            <el-option
+              v-for="item in addBatchData.typeOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="子类型">
+          <el-select v-model="addBatchData.queryParams.typeSubId" placeholder="请选择素材子类型" clearable size="small">
+            <el-option
+              v-for="item in addBatchData.typeSubOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
         <el-form-item>
           <el-button type="primary" icon="el-icon-search" size="mini" @click="resourceList">搜索</el-button>
         </el-form-item>
@@ -379,6 +401,7 @@ import VideoUpload from "@/components/VideoUpload/index.vue";
 import { listVideoResource } from '@/api/course/videoResource';
 import {getByIds} from "@/api/course/courseQuestionBank";
 import CourseWatchComment from "./courseWatchComment.vue";
+import { getCateListByPid, getCatePidList } from '@/api/course/userCourseCategory'
 
 export default {
     name: "userCourseCatalog",
@@ -688,7 +711,9 @@ export default {
           productJson: null,
           questionBankId:null,
           questionBankList:[],
-		  redPacketMoney:0,
+          redPacketMoney:0,
+          isTranscode:0,
+          transcodeFileKey:null
         };
         this.videoURL = '';
         this.progress=0;
@@ -778,13 +803,13 @@ export default {
               });
               return
             }
-            if(this.form.uploadType==null){
-              this.$message({
-                message: '请选择播放线路!',
-                type: 'warning'
-              });
-              return
-            }
+            // if(this.form.uploadType==null){
+            //   this.$message({
+            //     message: '请选择播放线路!',
+            //     type: 'warning'
+            //   });
+            //   return
+            // }
             if (this.form.questionBankList!==null){
               this.form.questionBankId = this.form.questionBankList.map(item => item.id).join(',');
             }
@@ -847,11 +872,29 @@ export default {
       },
       openAdds(){
         this.addBatchData.open = true;
+        this.getRootTypeList();
         this.addBatchData.form = {
           courseId: this.courseId,
         };
+        this.addBatchData.queryParams.typeId = null;
+        this.addBatchData.queryParams.typeSubId = null;
         this.resourceList();
       },
+      getRootTypeList() {
+        getCatePidList().then(response => {
+          this.addBatchData.typeOptions = response.data
+        });
+      },
+      async changeCateType(val) {
+        this.addBatchData.queryParams.typeSubId = null
+        this.addBatchData.typeSubOptions = []
+        if (!val) {
+          return
+        }
+        await getCateListByPid(val).then(response => {
+          this.addBatchData.typeSubOptions = response.data
+        })
+      },
       resourceList(){
         this.addBatchData.loading = true;
         listVideoResource(this.addBatchData.queryParams).then(response => {

+ 2 - 2
src/views/course/userCoursePeriod/index.vue

@@ -1404,8 +1404,8 @@ export default {
       this.course.form = {
         periodId: this.course.queryParams.periodId,
         courseId: null,
-        timeRange: null,
-        joinTime: null,
+        timeRange: ['00:00:00', '23:59:59'],
+        joinTime: '23:59:59',
         videoIds: []
       };
       // 重置表单

+ 100 - 9
src/views/course/userCoursePeriod/redPacket.vue

@@ -1,8 +1,20 @@
 <template>
   <div>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          size="mini"
+          :disabled="this.selectCompanyIds.length === 0"
+          @click="handleChangeRedPacket"
+          v-hasPermi="['course:period:setCompanyRedPacket']"
+        >批量设置红包</el-button>
+      </el-col>
+    </el-row>
     <!-- 公司列表弹窗 -->
 <!--    <el-dialog title="设置红包" :visible.sync="companyDialogVisible" width="800px" append-to-body>-->
-      <el-table :data="companyList" border>
+      <el-table :data="companyList" border @selection-change="handleSelectionCompany">
+        <el-table-column type="selection" width="55" align="center"/>
         <el-table-column type="index" label="序号" width="60" align="center" />
         <el-table-column label="公司名称" prop="companyName" align="center" />
         <el-table-column label="操作" align="center" width="120">
@@ -31,7 +43,7 @@
           <template slot-scope="scope">
             <el-input-number
               v-model="scope.row.amount"
-              :min="0.1"
+              :min="0.0"
               :precision="2"
               :step="0.01"
               size="small"
@@ -47,11 +59,44 @@
         <el-button type="primary" @click="handleSave">保 存</el-button>
       </div>
     </el-dialog>
+
+    <!-- 课程红包设置弹窗 -->
+    <el-dialog title="设置红包金额" :visible.sync="batchRedPacketDialog.visible" width="400px" append-to-body>
+      <el-form ref="batchRedPacketForm" :model="batchRedPacketDialog.form" label-width="100px">
+        <el-form-item label="红包金额" prop="amount" :rules="{required: true, message: '红包金额不能为空', trigger: 'blur'}">
+          <div style="display: inline-flex; align-items: center">
+            <el-input-number
+              v-model="batchRedPacketDialog.form.amount"
+              :min="0.1"
+              :precision="2"
+              :step="0.01"
+              size="small"
+            >
+            </el-input-number>
+            <span style="margin-left: 10px">元</span>
+          </div>
+          <div style="color: rgba(169,88,18,0.9)">金额低于0.1元将自动置为0.1元</div>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="batchRedPacketDialog.visible = false">取 消</el-button>
+        <el-button type="primary"
+                   :loading="batchRedPacketDialog.saveLoading"
+                   :disabled="batchRedPacketDialog.saveLoading"
+                   @click="handleChange">保 存</el-button>
+      </div>
+    </el-dialog>
+
   </div>
 </template>
 
 <script>
-import { getPeriodCompanyList, batchSaveRedPacket, getPeriodRedPacketList } from "@/api/course/userCoursePeriod";
+import {
+  getPeriodCompanyList,
+  batchSaveRedPacket,
+  getPeriodRedPacketList,
+  batchSaveRedPacketByCompany
+} from "@/api/course/userCoursePeriod";
 import redPacket from "@/views/course/userCoursePeriod/redPacket.vue";
 
 export default {
@@ -86,7 +131,15 @@ export default {
       courseDialogVisible: false,
       companyList: [],
       redPacketList: [],
-      currentCompany: null
+      currentCompany: null,
+      selectCompanyIds: [],
+      batchRedPacketDialog: {
+        visible: false,
+        form: {
+          amount: 0.1
+        },
+        saveLoading: false,
+      }
     };
   },
   created() {
@@ -130,23 +183,23 @@ export default {
       }).then(response => {
         this.redPacketList = (response.data || []).map(item => ({
           ...item,
-          amount: item.amount || 0.1
+          amount:item.amount ?? 0.1
         }));
       });
     },
     // 保存红包金额
     handleSave() {
       // 筛选出有金额的项目
-      const validAmountItems = this.redPacketList.filter(item => item.amount > 0);
+      const validAmountItems = this.redPacketList.filter(item => item.amount >= 0);
       if (validAmountItems.length === 0) {
         this.$message.warning('请至少设置一个红包金额');
         return;
       }
 
       // 验证金额范围
-      const invalidItems = validAmountItems.filter(item => item.amount < 0.1);
+      const invalidItems = validAmountItems.filter(item => item.amount < 0.0);
       if (invalidItems.length > 0) {
-        this.$message.error('红包金额需要大于等于0.1元');
+        this.$message.error('红包金额需要大于等于0元');
         return;
       }
 
@@ -169,7 +222,45 @@ export default {
       }).catch(error => {
         this.$message.error("保存失败:" + error.message);
       });
-    }
+    },
+    handleSelectionCompany(selection) {
+      this.selectCompanyIds = selection.map(item => item.companyId);
+    },
+    handleChangeRedPacket() {
+      this.batchRedPacketDialog.visible = true
+      this.$refs.batchRedPacketForm.resetFields()
+    },
+    handleChange() {
+      if (!this.selectCompanyIds.length) {
+        this.$message.warning('请选择公司');
+        return;
+      }
+
+      this.$refs.batchRedPacketForm.validate((valid) => {
+        if (!valid) {
+          this.$message.warning('请填写正确的红包金额')
+          return;
+        }
+        this.batchRedPacketDialog.saveLoading = true
+
+        const saveData = {
+          periodId: this.periodId,
+          companyIds: this.selectCompanyIds,
+          redPacketMoney: this.batchRedPacketDialog.form.amount
+        }
+        batchSaveRedPacketByCompany(saveData).then(response => {
+          const {code, msg} = response
+          if (code === 200) {
+            this.$message.success('保存成功');
+            this.batchRedPacketDialog.visible = false;
+            this.$emit('success');
+          } else {
+            this.$message.error(msg || "保存失败");
+          }
+          this.batchRedPacketDialog.saveLoading = false
+        })
+      });
+    },
   }
 };
 </script>

+ 880 - 38
src/views/course/videoResource/index.vue

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

+ 88 - 28
src/views/store/user/index.vue

@@ -2,6 +2,16 @@
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
 
+      <el-form-item label="会员ID" prop="userId">
+        <el-input
+
+          v-model="queryParams.userId"
+          placeholder="请输入会员ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
       <el-form-item label="会员昵称" prop="nickname">
         <el-input
 
@@ -24,16 +34,24 @@
       </el-form-item>
       <el-form-item label="注册时间" prop="createTimeRange">
         <el-date-picker clearable size="small" style="width: 340px"
-                        v-model="dateRange"
-                        type="daterange"
-                        value-format="yyyy-MM-dd"
-                        range-separator="至"
-                        start-placeholder="开始日期"
-                        end-placeholder="结束日期"
-                        @change="handleDateRangeChange">
+          v-model="dateRange"
+          type="daterange"
+          value-format="yyyy-MM-dd"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          @change="handleDateRangeChange">
         </el-date-picker>
       </el-form-item>
-
+      <el-form-item label="所属公司" prop="companyName">
+        <el-input
+          v-model="queryParams.companyName"
+          placeholder="请输入所属公司"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
       <el-form-item label="所属销售" prop="companyUserNickName">
         <el-input
           v-model="queryParams.companyUserNickName"
@@ -71,7 +89,7 @@
           v-hasPermi="['store:user:export']"
         >导出</el-button>
       </el-col>
-      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+	  <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
     <el-table  height="500" border v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
@@ -118,14 +136,15 @@
           <el-tag prop="status" v-for="(item, index) in statusOptions"    v-if="scope.row.status==item.dictValue">{{item.dictLabel}}</el-tag>
         </template>
       </el-table-column>
-      <!--      <el-table-column label="创建时间" align="center" prop="createTime" />-->
-      <!--      <el-table-column label="累计佣金" align="center" prop="registerDate" />-->
-      <!--      <el-table-column label="可提现佣金" align="center" prop="registerCode" />-->
-      <!--      <el-table-column label="冻结佣金" align="center" prop="source" />-->
-      <!--      <el-table-column label="已提现佣金" align="center" prop="remark" />-->
+      <el-table-column label="所属公司" align="center" prop="companyName" />
+      <el-table-column label="所属销售" align="center" prop="companyUserNickName" />
+<!--      <el-table-column label="创建时间" align="center" prop="createTime" />-->
+<!--      <el-table-column label="累计佣金" align="center" prop="registerDate" />-->
+<!--      <el-table-column label="可提现佣金" align="center" prop="registerCode" />-->
+<!--      <el-table-column label="冻结佣金" align="center" prop="source" />-->
+<!--      <el-table-column label="已提现佣金" align="center" prop="remark" />-->
       <el-table-column label="看课数量" align="center" prop="watchCourseCount" />
       <el-table-column label="参与营期数" align="center" prop="partCourseCount" />
-      <el-table-column label="所属销售" align="center" prop="companyUserNickName" />
       <el-table-column label="最后看课时间" align="center" prop="lastWatchDate" width="160">
         <template slot-scope="scope">
           <span>{{ parseTime(scope.row.lastWatchDate) }}</span>
@@ -171,17 +190,35 @@
     <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
       <el-form ref="form" :model="form" :rules="rules" label-width="120px">
         <el-form-item label="会员头像" prop="avatar">
-          <el-popover
-            placement="right"
-            title=""
-            trigger="hover"
-          >
-            <img slot="reference" :src="form.avatar" width="80">
-            <img :src="form.avatar" style="max-width: 80px;">
-          </el-popover>
+          <div style="display: flex; align-items: center; gap: 16px;">
+            <el-popover
+              placement="right"
+              title=""
+              trigger="hover"
+            >
+              <img slot="reference" :src="form.avatar" width="80" style="border-radius: 8px; border: 2px solid #ddd;">
+              <img :src="form.avatar" style="max-width: 200px; border-radius: 8px;">
+            </el-popover>
+
+            <div>
+              <el-upload
+                class="avatar-uploader"
+                :action="uploadUrl"
+                :show-file-list="false"
+                :on-success="handleAvatarSuccess"
+                :before-upload="beforeAvatarUpload"
+                :http-request="handleAvatarUpload"
+              >
+                <el-button size="small" type="primary" icon="el-icon-upload">上传头像</el-button>
+              </el-upload>
+              <div style="font-size: 12px; color: #999; margin-top: 4px;">
+                支持 JPG、PNG 格式,建议尺寸 200x200
+              </div>
+            </div>
+          </div>
         </el-form-item>
         <el-form-item label="会员昵称" prop="nickname">
-          <el-input v-model="form.nickname" disabled placeholder="请输入用户昵称" />
+          <el-input v-model="form.nickname" placeholder="请输入用户昵称" />
         </el-form-item>
         <el-form-item label="手机号码" prop="phone">
           <el-input v-model="form.phone" disabled placeholder="请输入手机号码" />
@@ -197,10 +234,10 @@
         </el-form-item> -->
         <el-form-item label="进线日期" prop="registerDate">
           <el-date-picker clearable size="small"
-                          v-model="form.registerDate"
-                          type="date"
-                          value-format="yyyy-MM-dd"
-                          placeholder="选择进线日期">
+            v-model="form.registerDate"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择进线日期">
           </el-date-picker>
         </el-form-item>
         <el-form-item label="推线编码" prop="registerCode">
@@ -266,6 +303,7 @@ export default {
   name: "User",
   data() {
     return {
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS", // 上传的图片服务器地址
       userIsPromoterOptions:[],
       userLevelOptions:[],
       statusOptions:[],
@@ -325,6 +363,7 @@ export default {
         isDel: null,
         startCreateTime: null,
         endCreateTime: null,
+        companyName: null,
         companyUserNickName: null
       },
       // 表单参数
@@ -363,6 +402,26 @@ export default {
     this.getList();
   },
   methods: {
+    // 头像上传前的校验
+    beforeAvatarUpload(file) {
+      const isJPG = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg';
+      const isLt2M = file.size / 1024 / 1024 < 2;
+
+      if (!isJPG) {
+        this.$message.error('上传头像图片只能是 JPG/PNG/JPEG 格式!');
+      }
+      if (!isLt2M) {
+        this.$message.error('上传头像图片大小不能超过 2MB!');
+      }
+      return isJPG && isLt2M;
+    },
+
+
+    // 上传成功回调(如果使用服务器上传)
+    handleAvatarSuccess(response, file) {
+      this.form.avatar = response.url; // 假设服务器返回的是 {url: '图片地址'}
+      this.$message.success('头像上传成功');
+    },
     /** 查询用户列表 */
     getList() {
       this.loading = true;
@@ -421,6 +480,7 @@ export default {
     resetQuery() {
       this.dateRange = [];
       this.resetForm("queryForm");
+      this.queryParams.companyName = null;
       this.queryParams.companyUserNickName = null;
       this.handleQuery();
     },

+ 30 - 1
src/views/system/config/config.vue

@@ -456,6 +456,21 @@
              </el-tooltip>
            </el-form-item>
 
+           <el-form-item label="授权方式">
+             <el-tooltip class="item" effect="dark" content="小程序授权头像昵称方式(目前仅会员看课有效)" placement="top-end">
+               <el-radio-group v-model="form18.miniAppAuthType">
+                 <el-radio label="1">小程序原生</el-radio>
+                 <el-radio label="2">跳转H5服务号</el-radio>
+               </el-radio-group>
+             </el-tooltip>
+           </el-form-item>
+
+           <el-form-item v-if="form18.miniAppAuthType==2" label="跳转域名">
+             <el-tooltip class="item" effect="dark" content="会员看课小程序授权头像昵称,跳转H5服务号授权域名" placement="top-end">
+               <el-input style="width: 200px"  v-model="form18.userCourseAuthDomain" label="跳转域名"></el-input>
+             </el-tooltip>
+           </el-form-item>
+
            <div class="line"></div>
            <div style="float:right;margin-right:20px">
              <el-button type="primary" @click="submitForm18">提交</el-button>
@@ -582,6 +597,17 @@
               />
             </el-select>
         </el-form-item>
+        <!-- <el-form-item   label="推送erp的商品类型" v-if="form13.erpOpen == 1">
+          <el-select filterable v-model="form13.productType" placeholder="请选商品类型" multiple clearable size="small"
+              >
+              <el-option
+                  v-for="item in productTypeOptions"
+                  :key="item.dictValue"
+                  :label="item.dictLabel"
+                  :value="item.dictValue"
+              />
+            </el-select>
+        </el-form-item> -->
         <el-form-item   label="erpAppKey" v-if="form13.erpOpen == 1 && form13.erpType == 1 " prop="erpAppKey">
             <el-input   v-model="form13.erpAppKey"  label="请输入erpAppKey"></el-input>
         </el-form-item>
@@ -686,6 +712,7 @@ export default {
   },
   data() {
     return {
+      // productTypeOptions:[],
       companyOptions:[],
       uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
       videoAccept:"video/*",
@@ -904,7 +931,9 @@ export default {
              if(response.data.configValue != null) {
                this.form13 =JSON.parse(response.data.configValue);
              }
-
+            //  this.getDicts("store_product_type").then((response) => {
+            //   this.productTypeOptions = response.data;
+            // });
           }
         });
      },