Quellcode durchsuchen

update 租户后台 课程管理新增目录,视频上传以及问题设置

ct vor 1 Woche
Ursprung
Commit
c00702c46d

+ 34 - 0
src/api/course/userCourseComment.js

@@ -50,4 +50,38 @@ export function exportUserCourseComment(query) {
     method: 'get',
     params: query
   })
+}
+
+// 查询课节精选留言(历史上传)
+export function listFeaturedComments(courseId, videoId) {
+  return request({
+    url: '/course/userCourseComment/featuredList',
+    method: 'get',
+    params: { courseId, videoId }
+  })
+}
+
+// 下载精选留言导入模板
+export function downloadCommentImportTemplate() {
+  return request({
+    url: '/course/userCourseComment/importTemplate',
+    method: 'get',
+    responseType: 'blob'
+  })
+}
+
+// 导入精选留言
+export function importComments(courseId, videoId, file) {
+  const formData = new FormData()
+  formData.append('file', file)
+  formData.append('courseId', courseId)
+  formData.append('videoId', videoId)
+  return request({
+    url: '/course/userCourseComment/importData',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
 }

+ 62 - 0
src/api/his/courserCoupon.js

@@ -0,0 +1,62 @@
+import request from '@/utils/request'
+
+// 查询课程优惠券列表
+export function listCourserCoupon(query) {
+  return request({
+    url: '/his/courserCoupon/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询课程优惠券详细
+export function getCourserCoupon(id) {
+  return request({
+    url: '/his/courserCoupon/' + id,
+    method: 'get'
+  })
+}
+
+// 新增课程优惠券
+export function addCourserCoupon(data) {
+  return request({
+    url: '/his/courserCoupon',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改课程优惠券
+export function updateCourserCoupon(data) {
+  return request({
+    url: '/his/courserCoupon',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除课程优惠券
+export function delCourserCoupon(id) {
+  return request({
+    url: '/his/courserCoupon/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出课程优惠券
+export function exportCourserCoupon(query) {
+  return request({
+    url: '/his/courserCoupon/export',
+    method: 'get',
+    params: query
+  })
+}
+
+
+// 课程优惠券选项列表
+export function options() {
+  return request({
+    url: '/his/courserCoupon/options',
+    method: 'get'
+  })
+}

+ 26 - 33
src/components/VideoUpload/index.vue

@@ -122,12 +122,12 @@
 </template>
 
 <script>
-import {getSignature, uploadHuaWeiObs, uploadHuaWeiVod} from "@/api/common";
 import {getThumbnail} from "@/api/course/userVideo";
 import { uploadObject } from "@/utils/cos.js";
-import { uploadToOBS } from "@/utils/obs.js";
+import { uploadToHSY } from "@/utils/hsy.js";
 import Pagination from "@/components/Pagination";
 import { listVideoResource } from '@/api/course/videoResource';
+import Vue from 'vue';
 
 export default {
   components: {
@@ -193,8 +193,8 @@ export default {
       duration: 0,
       fileId: "",
       uploadTypeOptions: [
-        { dictLabel: "线路一", dictValue: 1 }, // 腾讯pcdn
-        { dictLabel: "线路二", dictValue: 2 }, // 华为云obs
+        { dictLabel: "线路一", dictValue: 1 }, // 腾讯云 COS
+        { dictLabel: "线路二", dictValue: 2 }, // 火山云点播
         // { dictLabel: "华为云VOD", dictValue: 4 },
         // { dictLabel: "腾讯云VOD", dictValue: 5 },
       ],
@@ -256,7 +256,7 @@ export default {
       await this.getFirstThumbnail();
       //同时上传个线路
       await this.uploadVideoToTxPcdn();
-      await this.uploadVideoToHwObs();
+      await this.uploadVideoToHsy();
       this.$emit("update:fileName", this.fileList[0].name);
     },
     //获取第一帧封面
@@ -267,9 +267,13 @@ export default {
         this.$emit("update:thumbnail", response.url);
       })
     },
-    //更新华为线路进度条
+    // 更新火山云线路进度条
     updateHwProgress(progress) {
-      this.hwProgress = progress;
+      if (typeof progress === 'number') {
+        this.hwProgress = progress
+      } else if (progress && typeof progress.percent === 'number') {
+        this.hwProgress = Math.floor(progress.percent * 100)
+      }
     },
     //更新腾讯线路进度条
     updateTxProgress(progressData) {
@@ -280,45 +284,34 @@ export default {
       try {
         const file = this.fileList[0].raw;
         const data = await uploadObject(file, this.updateTxProgress,this.type);
-        console.log("腾讯COS返回========>",data);
-        console.log("isPrivate=======>",this.isPrivate)
-        let line_1='' ;
-        if (this.isPrivate===0){
-          line_1 = `${process.env.VUE_APP_VIDEO_LINE_1}${data.urlPath}`;
-        }else {
-          line_1 = `${process.env.VUE_APP_VIDEO_LINE_1}${data.urlPath}`;
-        }
-
-        let urlPathWithoutFirstSlash = data.urlPath.substring(1);
+        const lineBase = Vue.prototype.$runtimeConfig.VUE_APP_VIDEO_LINE_1 || process.env.VUE_APP_VIDEO_LINE_1 || '';
+        const line_1 = `${lineBase}${data.urlPath}`;
+        const urlPathWithoutFirstSlash = data.urlPath.startsWith('/') ? data.urlPath.substring(1) : data.urlPath;
         this.$emit("update:fileKey", urlPathWithoutFirstSlash);
-
-        console.log("文件key",urlPathWithoutFirstSlash);
-        console.log("组装URL========>",line_1);
         this.$emit("update:videoUrl", line_1);
         this.$emit("update:line_1", line_1);
-        // this.$emit("update:line_2", line_2);
         this.$message.success("线路一上传成功");
       } catch (error) {
-        this.$message.error("线路一上传失败");
+        console.error('线路一上传失败', error);
+        this.$message.error(error?.message || "线路一上传失败");
       }
     },
-    //上传华为云Obs
-    async uploadVideoToHwObs() {
+    // 上传火山云点播(线路二)
+    async uploadVideoToHsy() {
       try {
         const file = this.fileList[0].raw;
-        const data = await uploadToOBS(file, this.updateHwProgress,this.type);
-        console.log("华为OBS返回========>",data);
-        let line_2='' ;
-        if (this.isPrivate===0){
-          line_2 = `${process.env.VUE_APP_VIDEO_LINE_2}/${data.urlPath}`;
-        }else {
-          line_2 = `${process.env.VUE_APP_VIDEO_LINE_2}/${data.urlPath}`;
+        const data = await uploadToHSY(file, this.updateHwProgress, this.type);
+        const videoBase = Vue.prototype.$runtimeConfig.VUE_APP_VIDEO_URL || process.env.VUE_APP_VIDEO_URL || '';
+        if (!videoBase) {
+          throw new Error('未配置火山云视频地址,请在「系统参数 → 前端配置」中填写')
         }
-        // this.$emit("update:videoUrl", data);
+        const line_2 = `${videoBase}/${data.SourceInfo.FileName}`;
         this.$emit("update:line_2", line_2);
         this.$message.success("线路二上传成功");
       } catch (error) {
-        this.$message.error("线路二上传失败");
+        console.error('线路二上传失败', error);
+        const msg = error?.message || error?.extra?.message || "线路二上传失败";
+        this.$message.error(msg);
       }
     },
     handleRemove(file, fileList) {

+ 49 - 69
src/utils/obs.js

@@ -1,78 +1,58 @@
-import ObsClient from "esdk-obs-browserjs/src/obs"
-import Vue from "vue";
+import request from '@/utils/request'
+
+/**
+ * 通过后端 presigned URL 上传到华为 OBS(AK/SK 仅在后端)
+ * @param file 文件
+ * @param progressCallback 进度 0-100
+ * @param type 1=课程视频, 2=用户视频
+ * @param cancelCallback { cancel: fn }
+ */
 export const uploadToOBS = async (file, progressCallback, type, cancelCallback) => {
-  try {
-    const obsClient = new ObsClient({
-      access_key_id:  Vue.prototype.$runtimeConfig.VUE_APP_OBS_ACCESS_KEY_ID,
-      secret_access_key:  Vue.prototype.$runtimeConfig.VUE_APP_OBS_SECRET_ACCESS_KEY,
-      server:  Vue.prototype.$runtimeConfig.VUE_APP_OBS_SERVER,
-      timeout: 1200,
-    })
+  const suffix = (file.name || 'mp4').split('.').pop() || 'mp4'
+  const presignRes = await request({
+    url: '/common/obs/presignedUploadUrl',
+    method: 'get',
+    params: { type: type || 2, suffix }
+  })
 
-    const fileName = file.name || ""
-    const upload_file_name = new Date().getTime() + "." + fileName.split(".")[fileName.split(".").length - 1]
-    const date = new Date()
-    const year = date.getFullYear()
-    const month = date.getMonth() + 1
-    const strDate = date.getDate()
-    const uploadDay = `${year}${month}${strDate}`
-    const videoKey = `userVideo/${uploadDay}/${upload_file_name}`
-    const courseKey = `course/${uploadDay}/${upload_file_name}`
-    const key = type === 1 ? courseKey : videoKey
+  if (!presignRes || !presignRes.url) {
+    throw new Error(presignRes?.msg || '获取 OBS 签名 URL 失败,请检查租户 OSS 是否配置为华为云')
+  }
 
-    var callback = (transferredAmount, totalAmount, totalSeconds) => {
-      const progress = Number.parseInt((transferredAmount * 100.0) / totalAmount)
-      if (progressCallback) {
-        progressCallback(progress)
-      }
+  const presignedUrl = presignRes.url
+  const objKey = presignRes.objKey
+
+  return new Promise((resolve, reject) => {
+    const xhr = new XMLHttpRequest()
+
+    if (cancelCallback) {
+      cancelCallback({
+        cancel: () => {
+          xhr.abort()
+          reject(new Error('Upload cancelled by user'))
+        }
+      })
     }
 
-    return new Promise((resolve, reject) => {
-      let isCancelled = false
+    xhr.upload.onprogress = (event) => {
+      if (event.lengthComputable && progressCallback) {
+        progressCallback(Math.round((event.loaded / event.total) * 100))
+      }
+    }
 
-      if (cancelCallback) {
-        cancelCallback({
-          cancel: () => {
-            console.log("Cancelling OBS upload")
-            isCancelled = true
-            reject(new Error("Upload cancelled by user"))
-          },
-        })
+    xhr.onload = () => {
+      if (xhr.status >= 200 && xhr.status < 300) {
+        resolve({ urlPath: objKey })
+      } else {
+        reject(new Error('OBS 上传失败: HTTP ' + xhr.status))
       }
-      //四福堂专属配置
-   /*   resolve({
-        "RequestId": "",
-        "urlPath": ""
-      })*/
-      obsClient.putObject(
-        {
-          Bucket: Vue.prototype.$runtimeConfig.VUE_APP_OBS_BUCKET,
-          Key: key,
-          Body: file,
-          ProgressCallback: callback,
-        },
-        (err, result) => {
-          if (isCancelled) {
-            return // Don't process result if cancelled
-          }
+    }
 
-          if (err) {
-            reject(err)
-            console.error("Error-->" + err)
-          } else {
-            const a = {
-              ...result,
-              urlPath: key,
-            }
-            console.log("上传成功", a)
-            resolve(a)
-          }
-        },
-      )
-      //注释到这里【四福堂】
-    })
-  } catch (error) {
-    console.error("Error during upload:", error)
-    throw error
-  }
+    xhr.onerror = () => reject(new Error('OBS 上传网络错误'))
+    xhr.onabort = () => reject(new Error('上传已取消'))
+
+    xhr.open('PUT', presignedUrl, true)
+    xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream')
+    xhr.send(file)
+  })
 }

+ 488 - 0
src/views/components/VideoUpload/index.vue

@@ -0,0 +1,488 @@
+<template>
+  <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" 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>-->
+
+<!--          &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="视频播放">
+      <video v-if="videoUrl" ref="myvideo" :src="videoUrl" id="video" width="100%" height="300px" controls></video>
+      <div v-if="fileName">视频文件名: {{ fileName }}</div>
+      <div v-if="fileKey">文件Key: {{ fileKey }}</div>
+      <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>-->
+<!--        &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>
+      <!-- 搜索条件 -->
+      <el-form :inline="true" :model="libraryQueryParams" class="library-search">
+        <el-form-item label="素材名称">
+          <el-input
+            v-model="libraryQueryParams.resourceName"
+            placeholder="请输入素材名称"
+            clearable
+            size="small"
+            @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>
+        </el-form-item>
+      </el-form>
+
+      <!-- 视频列表 -->
+      <el-table v-loading="libraryLoading" :data="libraryList" @row-click="handleLibrarySelect" highlight-current-row border>
+        <el-table-column label="素材名称" align="center" prop="resourceName" />
+        <el-table-column label="文件名称" align="center" prop="fileName" />
+        <el-table-column label="缩略图" align="center">
+          <template slot-scope="scope">
+            <el-popover
+              placement="right"
+              title=""
+              trigger="hover"
+            >
+              <img alt="" slot="reference" :src="scope.row.thumbnail" style="width: 80px; height: 50px" />
+              <img alt="" :src="scope.row.thumbnail" style="max-width: 150px;" />
+            </el-popover>
+          </template>
+        </el-table-column>
+        <el-table-column label="视频时长" align="center">
+          <template slot-scope="scope">
+            <span>{{ formatDuration(scope.row.duration) }}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <pagination
+        v-show="libraryTotal>0"
+        :total="libraryTotal"
+        :page.sync="libraryQueryParams.pageNum"
+        :limit.sync="libraryQueryParams.pageSize"
+        @pagination="getLibraryList"
+      />
+
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="confirmVideoSelection">确 定</el-button>
+        <el-button @click="cancelVideoSelection">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {getSignature, uploadHuaWeiObs, uploadHuaWeiVod} from "@/api/common";
+import {getThumbnail} from "@/api/course/userVideo";
+import TcVod from "vod-js-sdk-v6";
+import { uploadObject } from "@/utils/cos.js";
+import { uploadToOBS } from "@/utils/obs.js";
+import Pagination from "@/components/Pagination";
+import { listVideoResource, listPublicVideoResource } from '@/api/course/videoResource';
+import { getCateListByPid, getCatePidList, getPublicCateListByPid, getPublicCatePidList } from '@/api/course/userCourseCategory'
+
+export default {
+  components: {
+    Pagination
+  },
+  props: {
+    videoUrl: {
+      type: String,
+      default: "",
+    },
+    fileKey: {
+      type: String,
+      default: "",
+    },
+    fileSize: {
+      type: Number,
+      default: null,
+    },
+    fileName: {
+      type: String,
+      default: "",
+    },
+    line_1: {
+      type: String,
+      default: "",
+    },
+    line_2: {
+      type: String,
+      default: "",
+    },
+    line_3: {
+      type: String,
+      default: "",
+    },
+    thumbnail: {
+      type: String,
+      default: "",
+    },
+    uploadType: {
+      type: Number,
+      default: null,
+    },
+    type: {
+      type: Number,
+      default: null,
+    },
+    isPrivate: {
+      type: Number,
+      default: 0,
+    },
+    isTranscode: {
+      type: Number,
+      default: null,
+    },
+    transcodeFileKey: {
+      type: String,
+      default: "",
+    },
+
+    // 使用一个变量控制显示,默认为true显示所有控制项
+    showControl: {
+      type: Boolean,
+      default: true,
+    },
+    /** true:视频库选取走公域分类 + /course/videoResource/publicList;false:原接口 */
+    usePublicVideoLibrary: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      videoName:'',
+      isHidden : false,
+      localUploadType: this.uploadType,
+      duration: 0,
+      fileId: "",
+      uploadTypeOptions: [
+        { dictLabel: "线路一", dictValue: 1 }, // 腾讯pcdn
+        { dictLabel: "线路二", dictValue: 2 }, // 华为云obs
+        // { dictLabel: "华为云VOD", dictValue: 4 },
+        // { dictLabel: "腾讯云VOD", dictValue: 5 },
+      ],
+      fileList: [],
+      txProgress: 0,
+      hwProgress: 0,
+      uploadLoading: false,
+      uploadKey: 0,
+      // 视频库选择相关数据
+      libraryOpen: false,
+      libraryLoading: false,
+      libraryTotal: 0,
+      libraryList: [],
+      selectedVideo: null,
+      libraryQueryParams: {
+        isTranscode:1,
+        pageNum: 1,
+        pageSize: 10,
+        resourceName: null,
+        typeId: null,
+        typeSubId: null,
+      },
+      typeOptions: [],
+      typeSubOptions: []
+    };
+  },
+  watch: {
+    localUploadType(newType) {
+      this.$emit("update:uploadType", newType);
+      this.$emit("change");
+    },
+    uploadType(newType) {
+      this.localUploadType = newType;
+    },
+  },
+  methods: {
+    // 打开视频库对话框
+    openVideoLibrary() {
+      this.libraryOpen = true;
+      this.getRootTypeList()
+      this.selectedVideo = null;
+      this.getLibraryList();
+    },
+    getRootTypeList() {
+      const req = this.usePublicVideoLibrary ? getPublicCatePidList() : getCatePidList()
+      req.then(response => {
+        this.typeOptions = response.data
+      });
+    },
+    async changeCateType(val) {
+      this.libraryQueryParams.typeSubId = null
+      this.typeSubOptions = []
+      if (!val) {
+        return
+      }
+      const subReq = this.usePublicVideoLibrary ? getPublicCateListByPid(val) : getCateListByPid(val)
+      await subReq.then(response => {
+        this.typeSubOptions = response.data
+      })
+    },
+    handleChange(file, fileList) {
+      this.fileList = fileList;
+      this.getVideoDuration(file.raw);
+    },
+    getVideoDuration(file) {
+      const video = document.createElement("video");
+      video.preload = "metadata";
+      video.onloadedmetadata = () => {
+        window.URL.revokeObjectURL(video.src);
+        this.duration = parseInt(video.duration.toFixed(2));
+        console.log("视频时长=========>",this.duration);
+        this.$emit("video-duration", this.duration);
+        console.log("文件大小=====>",file.size);
+        this.$emit("update:fileSize", file.size);
+      };
+      video.src = URL.createObjectURL(file);
+    },
+    async submitUpload() {
+      if (this.fileList.length < 1) {
+        return this.$message.error("请先选取视频,再进行上传");
+      }
+      await this.getFirstThumbnail();
+      //同时上传个线路
+      await this.uploadVideoToTxPcdn();
+      await this.uploadVideoToHwObs();
+      this.$emit("update:fileName", this.fileList[0].name);
+    },
+    //获取第一帧封面
+    async getFirstThumbnail(){
+      const file = this.fileList[0].raw;
+      getThumbnail(file).then(response => {
+        console.log("获取到第一帧为封面======>",response.url)
+        this.$emit("update:thumbnail", response.url);
+      })
+    },
+    //更新华为线路进度条
+    updateHwProgress(progress) {
+      this.hwProgress = progress;
+    },
+    //更新腾讯线路进度条
+    updateTxProgress(progressData) {
+      this.txProgress = Math.round(progressData.percent * 100);
+    },
+    //上传腾讯云Pcdn
+    async uploadVideoToTxPcdn() {
+      try {
+        const file = this.fileList[0].raw;
+        const data = await uploadObject(file, this.updateTxProgress,this.type);
+        console.log("腾讯COS返回========>",data);
+        console.log("isPrivate=======>",this.isPrivate)
+        let line_1='' ;
+        if (this.isPrivate===0){
+          line_1 = `${process.env.VUE_APP_VIDEO_LINE_1}${data.urlPath}`;
+        }else {
+          line_1 = `${process.env.VUE_APP_VIDEO_LINE_1}${data.urlPath}`;
+        }
+
+        let urlPathWithoutFirstSlash = data.urlPath.substring(1);
+        this.$emit("update:fileKey", urlPathWithoutFirstSlash);
+
+        console.log("文件key",urlPathWithoutFirstSlash);
+        console.log("组装URL========>",line_1);
+        this.$emit("update:videoUrl", line_1);
+        this.$emit("update:line_1", line_1);
+        // this.$emit("update:line_2", line_2);
+        this.$message.success("线路一上传成功");
+      } catch (error) {
+        this.$message.error("线路一上传失败");
+      }
+    },
+    //上传华为云Obs
+    async uploadVideoToHwObs() {
+      try {
+        const file = this.fileList[0].raw;
+        const data = await uploadToOBS(file, this.updateHwProgress,this.type);
+        console.log("华为OBS返回========>",data);
+        let line_2='' ;
+        if (this.isPrivate===0){
+          line_2 = `${process.env.VUE_APP_VIDEO_LINE_2}/${data.urlPath}`;
+        }else {
+          line_2 = `${process.env.VUE_APP_VIDEO_LINE_2}/${data.urlPath}`;
+        }
+        // this.$emit("update:videoUrl", data);
+        this.$emit("update:line_2", line_2);
+        this.$message.success("线路二上传成功");
+      } catch (error) {
+        this.$message.error("线路二上传失败");
+      }
+    },
+    handleRemove(file, fileList) {
+      console.log(file, fileList.length);
+    },
+    resetUpload() {
+      // 重置内部状态
+      this.txProgress = 0;
+      this.hwProgress = 0;
+      this.fileList = [];
+      this.uploadKey++;
+    },
+    /** 查询视频库列表 */
+    getLibraryList() {
+      this.libraryLoading = true;
+      const api = this.usePublicVideoLibrary ? listPublicVideoResource : listVideoResource
+      api(this.libraryQueryParams).then(response => {
+        this.libraryList = response.rows;
+        this.libraryTotal = response.total;
+        this.libraryLoading = false;
+      });
+    },
+    /** 搜索视频库按钮操作 */
+    handleLibraryQuery() {
+      this.libraryQueryParams.pageNum = 1;
+      this.getLibraryList();
+    },
+    /** 重置视频库查询按钮操作 */
+    resetLibraryQuery() {
+      this.libraryQueryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        resourceName: null,
+        typeId: null,
+        typeSubId: null
+      };
+      this.handleLibraryQuery();
+    },
+    /** 视频库选择行点击 */
+    handleLibrarySelect(row) {
+      this.selectedVideo = row;
+    },
+    /** 格式化视频时长 */
+    formatDuration(seconds) {
+      if (!seconds) return '00:00';
+
+      const minutes = Math.floor(seconds / 60);
+      const remainingSeconds = seconds % 60;
+
+      return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
+    },
+    /** 确认选择视频 */
+    confirmVideoSelection() {
+      if (!this.selectedVideo) {
+        this.$message.warning("请选择一个视频");
+        return;
+      }
+
+      // 更新组件内部数据
+      this.$emit("update:fileName", this.selectedVideo.fileName);
+      this.$emit("update:thumbnail", this.selectedVideo.thumbnail);
+      this.$emit("update:line_1", this.selectedVideo.line1);
+      this.$emit("update:line_2", this.selectedVideo.line2);
+      this.$emit("update:line_3", this.selectedVideo.line3);
+      this.$emit("update:fileSize", this.selectedVideo.fileSize);
+      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);
+
+      // 题目
+      this.$emit("selectProjects", this.selectedVideo.projectIds)
+
+      this.libraryOpen = false;
+    },
+    /** 取消视频选择 */
+    cancelVideoSelection() {
+      this.libraryOpen = false;
+      this.selectedVideo = null;
+    }
+  }
+};
+</script>
+
+<style>
+.progress-container {
+  margin-bottom: 5px; /* 进度条之间的间距 */
+}
+
+.progress-label {
+  display: block;
+  font-weight: bold;
+  font-size: 13px;
+  color: #303331; /* 标签颜色,可以根据需要调整 */
+}
+
+/* 视频库选择对话框样式 */
+.library-search {
+  margin-bottom: 15px;
+}
+
+.el-table .el-table__row:hover {
+  cursor: pointer;
+}
+</style>

+ 1823 - 451
src/views/components/course/userCourseCatalogDetails.vue

@@ -1,38 +1,118 @@
 <template>
   <div class="app-container">
     <div style="padding-bottom: 20px">
-        <span v-if="courseName!=null">{{ courseName }}</span>
+      <span v-if="courseName != null">{{ courseName }}</span>
     </div>
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
       <el-form-item label="小节名称" prop="title">
-        <el-input
-          v-model="queryParams.title"
-          placeholder="请输入小节名称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
+        <el-input v-model="queryParams.title" placeholder="请输入小节名称" clearable size="small"
+                  @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="小节id" prop="videoId">
+        <el-input v-model="queryParams.videoId" placeholder="请输入小节id" clearable size="small"
+                  @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item>
         <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+                   v-hasPermi="['course:userCourseVideo:add']">新增目录
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="primary" plain :disabled="!ids || ids.length <= 0" size="mini" @click="openUpdates"
+                   v-hasPermi="['course:userCourseVideo:updateTime']">修改时间
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="primary" plain size="mini" @click="openAdds"
+                   v-hasPermi="['course:userCourseVideo:batchAdd']">批量添加
+        </el-button>
+      </el-col>
+      <el-col v-if="usePublicVideoLibrary" :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          :disabled="!ids || ids.length <= 0"
+          v-hasPermi="['course:userCourseVideo:edit']"
+          @click="openBatchWatchIntegralDialog"
+        >批量修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="primary" plain size="mini" @click="updateRedPageckeOpen"
+                   v-hasPermi="['course:userCourseVideo:updateRed']">修改红包
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete"
+                   v-hasPermi="['course:userCourseVideo:remove']">删除
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-edit" size="mini" @click="handleCourseSort"
+                   v-hasPermi="['course:userCourseVideo:sort']">修改课节排序
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-delete" size="mini" @click="handleSync"
+                   v-hasPermi="['course:userCourseVideo:sync']">同步模板数据
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-edit" size="mini" :disabled="multiple" @click="handleDown"
+                   v-hasPermi="['course:userCourseVideo:batchDown']">批量下架
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-edit" size="mini" :disabled="multiple" @click="handleUp"
+                   v-hasPermi="['course:userCourseVideo:batchUp']">批量上架</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-edit" size="mini" :disabled="multiple" @click="handleEditCover"
+                   v-hasPermi="['course:userCourseVideo:batchEditCover']">批量修改封面图
+        </el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
     <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" show-overflow-tooltip prop="title" />
-      <el-table-column label="视频文件名称" align="center" show-overflow-tooltip  prop="fileName" >
+      <el-table-column type="selection" width="55" align="center"/>
+      <el-table-column label="视频ID" align="center" prop="videoId"/>
+      <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>
       <el-table-column label="视频时长" align="center" prop="duration">
-          <template slot-scope="{ row }">
-              {{ formatDuration(row.duration) }}
-          </template>
+        <template slot-scope="{ row }">
+          {{ formatDuration(row.duration) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="看课开始时间" align="center" prop="duration">
+        <template slot-scope="{ row }">
+          <el-tag v-if="row.viewStartTime">{{ row.viewStartTime }}</el-tag>
+          <el-tag type="danger" v-if="!row.viewStartTime">无</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="看课结束时间" align="center" prop="duration">
+        <template slot-scope="{ row }">
+          <el-tag v-if="row.viewEndTime">{{ row.viewEndTime }}</el-tag>
+          <el-tag type="danger" v-if="!row.viewEndTime">无</el-tag>
+        </template>
       </el-table-column>
-      <el-table-column label="排序" align="center" prop="courseSort" />
-      <el-table-column label="上传时间" align="center" prop="createTime" />
-      <el-table-column label="默认红包" align="center" prop="redPacketMoney" />
-      <el-table-column label="公司红包" align="center" prop="companyRedPacketMoney" />
+      <el-table-column label="领取红包时间" align="center" prop="duration">
+        <template slot-scope="{ row }">
+          <el-tag v-if="row.lastJoinTime">{{ row.lastJoinTime }}</el-tag>
+          <el-tag type="danger" v-if="!row.lastJoinTime">无</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="红包金额" align="center" prop="redPacketMoney"/>
+      <el-table-column label="排序" align="center" prop="courseSort"/>
+      <el-table-column v-if="usePublicVideoLibrary" label="观看时长(分钟)" align="center" width="120" prop="watchDurationMinutes"/>
+      <el-table-column v-if="usePublicVideoLibrary" label="积分奖励" align="center" width="100" prop="integralReward"/>
+      <el-table-column label="上传时间" align="center" prop="createTime"/>
       <el-table-column label="是否上架" align="center" prop="isOnPut">
         <template slot-scope="{ row }">
           <el-tag v-if="row.isOnPut == 0">是</el-tag>
@@ -41,508 +121,1800 @@
       </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
-          <el-button
-            size="mini"
-            type="text"
-            @click="handleDetails(scope.row)"
-          >查看</el-button>
-
-          <el-button
-            size="mini"
-            type="text"
-            v-hasPermi="['course:userCourseVideo:edit']"
-            @click="updateMoney(scope.row)"
-          >设置红包金额</el-button>
-
-          <el-button size="mini"
-                     type="text" @click="openTagDialog(scope.row)">
-            绑定看课标签
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+                     v-hasPermi="['course:userCourseVideo:edit']">修改
+          </el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleComment(scope.row)"
+                     v-hasPermi="['course:courseWatchComment:list']">查看评论
+          </el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['course:userCourseVideo:remove']">删除
           </el-button>
           <el-button size="mini"
-                     type="text" v-if="projectFrom === 'hzyy'" @click="openDialog(scope.row)">
-            复制看课链接
+                     type="text" v-if="projectFrom === 'myhk'" @click="openXsyDialog(scope.row)">
+            复制参数
           </el-button>
         </template>
       </el-table-column>
     </el-table>
 
-    <pagination
-      v-show="total>0"
-      :total="total"
-      :page.sync="queryParams.pageNum"
-      :limit.sync="queryParams.pageSize"
-      @pagination="getList"
-    />
-    <el-drawer
-      :with-header="false"
-      size="75%"
-      :visible.sync="open" append-to-body>
-      <userCourseVideoDetails  ref="userCourseVideoDetails" />
-    </el-drawer>
-
-    <el-dialog title="设置红包金额" :visible.sync="moneyOpen" width="600px" append-to-body>
-      <el-form ref="form"  label-width="100px">
-        <el-form-item label="红包金额" prop="corpId">
-          <el-input-number v-model="redPacketMoneyForm.redPacketMoney" :min="0.1" :max="200" :step="0.1" ></el-input-number>
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+    <el-dialog :title="title" :visible.sync="open" width="1000px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="110px" v-loading="uploadLoading">
+        <el-form-item label="视频标题" prop="title">
+          <el-input v-model="form.title" placeholder="请输入内容"/>
+        </el-form-item>
+        <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="courseSort">
+          <el-input-number v-model="form.courseSort" :min="1"></el-input-number>
         </el-form-item>
-      </el-form>
-        <div slot="footer" class="dialog-footer">
-          <el-button type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
 
-    <el-dialog
-      title="生成链接"
-      :visible.sync="dialogVisible"
-      width="400px"
-      @close="resetRoomForm"
-      append-to-body>
-      <el-form :model="roomLinkForm" label-width="120px">
-        <!-- 新增下拉框 -->
-        <el-form-item label="销售企微选择">
-          <el-select
-            v-model="roomLinkForm.qwUserId"
-            placeholder="请选择销售企微"
-            style="width: 100%">
+
+        <el-form-item label="视频缩略图" prop="thumbnail">
+          <el-upload v-model="form.thumbnail" class="avatar-uploader" :action="uploadUrl" :show-file-list="false"
+                     :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
+            <img v-if="form.thumbnail" :src="form.thumbnail" class="avatar" width="300px">
+            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+          </el-upload>
+        </el-form-item>
+        <video-upload :type="1" :isPrivate="isPrivate" :use-public-video-library="usePublicVideoLibrary" :fileKey.sync="form.fileKey" :fileSize.sync="form.fileSize"
+                      :videoUrl.sync="videoUrl" :fileName.sync="form.fileName" :line_1.sync="form.lineOne"
+                      :line_2.sync="form.lineTwo" :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" ref="videoUpload"
+                      append-to-body/>
+
+<!--        <el-form-item label="关联疗法" >-->
+<!--&lt;!&ndash;          <el-button size="small" type="primary" @click="choosePackage">选取疗法</el-button>&ndash;&gt;-->
+<!--          <el-table border width="100%" style="margin-top:5px;"  :data="packageList">-->
+<!--            <el-table-column label="疗法名称" align="center" prop="packageName"/>-->
+<!--            <el-table-column label="疗法图片" align="center" prop="imgUrl">-->
+<!--              <template slot-scope="scope">-->
+<!--                <img :src="scope.row.imgUrl" style="height: 80px;">-->
+<!--              </template>-->
+<!--            </el-table-column>-->
+<!--            <el-table-column label="疗法别名" align="center" prop="secondName"/>-->
+<!--            <el-table-column label="总金额" align="center" prop="totalPrice"/>-->
+<!--            &lt;!&ndash; 根据课程类型控制是否显示弹出时间列:0是公域(显示),1是私域(不显示) &ndash;&gt;-->
+<!--            <el-table-column label="弹出时间" align="center" width="250px" v-if="isPrivate == 0">-->
+<!--              <template slot-scope="scope">-->
+<!--                <div>-->
+<!--                  <el-time-select-->
+<!--                    v-model="scope.row.duration"-->
+<!--                    size="mini"-->
+<!--                    placeholder="选择时间"-->
+<!--                    :picker-options="getPickerOptions()"-->
+<!--                    @change="handleTimeChange(scope.$index, scope.row)"-->
+<!--                  ></el-time-select>-->
+<!--                </div>-->
+<!--              </template>-->
+<!--            </el-table-column>-->
+<!--            <el-table-column label="操作" align="center" width="100px" fixed="right">-->
+<!--              <template slot-scope="scope">-->
+<!--                <el-button-->
+<!--                  size="mini"-->
+<!--                  type="text"-->
+<!--                  icon="el-icon-delete"-->
+<!--                  @click="handlePackageDelete(scope.row)"-->
+<!--                >删除</el-button>-->
+<!--              </template>-->
+<!--            </el-table-column>-->
+<!--          </el-table>-->
+<!--        </el-form-item >-->
+
+        <el-form-item label="课题选择" prop="questionBankId">
+          <el-button size="small" type="primary" @click="chooseQuestionBank">选取课题</el-button>
+          <el-table border width="100%" style="margin-top:5px;" :data="form.questionBankList">
+            <el-table-column label="问题" align="center" prop="title">
+              <template slot-scope="scope">
+                <el-tooltip class="item" effect="dark" :content="scope.row.title" placement="top">
+                  <div
+                    style="display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; text-overflow: ellipsis;">
+                    <span>{{ scope.row.title }}</span>
+                  </div>
+                </el-tooltip>
+              </template>
+            </el-table-column>
+            <el-table-column label="类别" align="center" prop="type">
+              <template slot-scope="scope">
+                <dict-tag :options="typeOptions" :value="scope.row.type"/>
+              </template>
+            </el-table-column>
+            <el-table-column label="答案" align="center" prop="answer"/>
+            <el-table-column label="操作" align="center" width="100px" fixed="right">
+              <template slot-scope="scope">
+                <el-button size="mini" type="text" icon="el-icon-delete"
+                           @click="handleQuestionBankDelete(scope.row)">删除
+                </el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-form-item>
+        <el-form-item label="红包金额" prop="redPacketMoney">
+          <el-input-number v-model="form.redPacketMoney" :min="0.1" :max="200" :step="0.1"></el-input-number>
+        </el-form-item>
+         <!-- v-if="!!form.randomRedPacketRulesArr" -->
+        <el-form-item v-if="!!enableRandomRedPacket" label="随机红包金额"  >
+          <template >
+          <div v-for="(rule, index) in form.randomRedPacketRulesArr" :key="index" class="form-row">
+           <el-form-item
+            label="随机红包金额区间"
+            :prop="`randomRedPacketRulesArr.${index}.minAmount`"
+            :rules="[
+              { required: true, message: '请输入最小金额', trigger: 'blur' },
+              { validator: validateMinAmount, trigger: 'blur', index: index }
+            ]"
+            class="form-item-amount"
+          >
+            <el-input
+              v-model.number="rule.minAmount"
+              type="number"
+              :min="0.01"
+              :precision="2"
+              :step="0.01"
+              placeholder="最小金额"
+              size="small"
+              class="amount-input"
+              @input="handleAmountInput(rule, 'minAmount')"
+            ></el-input>
+            <span class="separator">-</span>
+            <el-input
+              v-model.number="rule.maxAmount"
+              type="number"
+              :min="rule.minAmount || 0.01"
+              :precision="2"
+              :step="0.01"
+              placeholder="最大金额"
+              size="small"
+              class="amount-input"
+              @input="handleAmountInput(rule, 'maxAmount')"
+            ></el-input>
+            <span class="suffix">元</span>
+          </el-form-item>
+              <el-form-item
+                label="随机权重"
+                :prop="`randomRedPacketRulesArr.${index}.weight`"
+                :rules="[
+                  { required: true, message: '请输入权重', trigger: 'blur' },
+                  { type: 'integer', message: '权重必须为整数', trigger: 'blur' },
+                ]"
+                class="form-item-weight"
+              >
+                <el-input
+                  v-model.number="rule.weight"
+                  type="number"
+                  :min="1"
+                  placeholder="权重"
+                  size="small"
+                ></el-input>
+              </el-form-item>
+              <el-tooltip class="item" effect="dark" content="权重越高,被随机到的概率越大" placement="top">
+                <i class="el-icon-question"></i>
+              </el-tooltip>
+              <div class="action-buttons">
+                <el-button
+                  icon="el-icon-plus"
+                  size="mini"
+                  type="text"
+                  @click="addRule(index)"
+                  class="add-btn"
+                >
+                  新增
+                </el-button>
+                <el-button
+                  icon="el-icon-delete"
+                  size="mini"
+                  type="text"
+                  @click="deleteRule(index)"
+                  :disabled="form.randomRedPacketRulesArr.length <= 1"
+                  class="delete-btn"
+                >
+                  删除
+                </el-button>
+              </div>
+          </div>
+        </template>
+
+        </el-form-item>
+        <el-form-item label="是否关联商品" prop="isProduct">
+          <el-radio-group v-model="form.isProduct">
+            <el-radio :label="1">是</el-radio>
+            <el-radio :label="0">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="是否先导课" prop="isFirst">
+          <el-radio-group v-model="form.isFirst">
+            <el-radio :label="1">是</el-radio>
+            <el-radio :label="0">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="是否启用倍速" prop="isSpeed">
+          <el-radio-group v-model="form.isSpeed">
+            <el-radio :label="1">是</el-radio>
+            <el-radio :label="0">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="是否上架" prop="isOnPut">
+          <el-radio-group v-model="form.isOnPut">
+            <el-radio :label="0">上架</el-radio>
+            <el-radio :label="1">下架</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="课程优惠券" prop="courseCouponId">
+          <el-select v-model="form.courseCouponId" clearable placeholder="请选择课程优惠券">
             <el-option
-              v-for="item in qwUserList"
+              v-for="item in courseCouponList"
               :key="item.id"
-              :label="formatOptionLabel(item)"
+              :label="item.title"
               :value="item.id">
             </el-option>
           </el-select>
         </el-form-item>
+        <el-form-item label="商品选择" v-if="form.isProduct === 1">
+          <el-button size="small" type="primary" @click="chooseCourseProduct">选取商品</el-button>
+          <el-table border width="100%" style="margin-top:5px;" :data="form.courseProducts">
+            <el-table-column label="商品名称" align="center" prop="productName"/>
+            <el-table-column label="产品条码" align="center" prop="barCode"/>
+            <el-table-column label="商品价格" align="center" prop="productPrice"/>
+            <el-table-column label="库存" align="center" prop="stock"/>
+            <el-table-column label="操作" align="center" width="100px" fixed="right">
+              <template slot-scope="scope">
+                <el-button size="mini" type="text" icon="el-icon-delete"
+                           @click="handleCourseProductDelete(scope.row)">删除
+                </el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-form-item>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item v-if="form.isProduct === 1" label="商品售卖时间" prop="listingStartTime">
+              <el-input-number v-model="form.listingStartTime" :min="0" label="商品售卖时间"></el-input-number>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item v-if="form.isProduct === 1" label="结束售卖时间" prop="listingStartTime">
+              <el-input-number v-model="form.listingEndTime" :min="0" label="结束售卖时间"></el-input-number>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item v-if="usePublicVideoLibrary" label="观看时长" prop="watchDurationMinutes">
+          <el-input-number
+            v-model="form.watchDurationMinutes"
+            :min="0"
+            :precision="0"
+            :step="1"
+            controls-position="right"
+            placeholder="请输入"
+            class="input-num-fixed"
+            style="width: 200px"
+          />
+          <span class="form-unit">分钟</span>
+          <span class="form-hint">注释:用户在页面的停留时间</span>
+        </el-form-item>
+        <el-form-item v-if="usePublicVideoLibrary" label="积分奖励" prop="integralReward">
+          <el-input-number
+            v-model="form.integralReward"
+            :min="0"
+            :precision="0"
+            :step="1"
+            controls-position="right"
+            placeholder="请输入"
+            class="input-num-fixed"
+            style="width: 200px"
+          />
+        </el-form-item>
+        <el-form-item label="课程介绍图" prop="courseIntroImg">
+          <el-upload
+            class="avatar-uploader"
+            :action="uploadUrl"
+            :show-file-list="false"
+            :on-success="handleCourseIntroSuccess"
+            :before-upload="beforeCourseIntroUpload">
+            <img v-if="form.courseIntroImg" :src="form.courseIntroImg" class="avatar" style="max-width: 300px; max-height: 200px;">
+            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+          </el-upload>
+          <div style="margin-top:5px;">
+            <el-button v-if="form.courseIntroImg" size="mini" type="danger" @click="form.courseIntroImg = null">删除图片</el-button>
+          </div>
+        </el-form-item>
+<!--        <el-form-item label="精选留言">-->
+<!--          <el-button size="small" type="primary" @click="openCommentImportDialog">导入留言</el-button>-->
+<!--          <el-button size="small" style="margin-left: 8px;" @click="openUserCommentDialog">用户留言</el-button>-->
+<!--        </el-form-item>-->
       </el-form>
       <div slot="footer" class="dialog-footer">
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="confirm">确认</el-button>
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+    <el-dialog title="导入精选留言" :visible.sync="commentImportDialog.visible" width="600px" append-to-body>
+      <el-upload
+        drag
+        :auto-upload="false"
+        :limit="1"
+        accept=".xls,.xlsx"
+        :on-change="handleCommentFileChange"
+        :file-list="commentImportDialog.fileList">
+        <i class="el-icon-upload"></i>
+        <div class="el-upload__text">将Excel文件拖到此处,或<em>点击上传</em></div>
+        <div class="el-upload__tip" slot="tip">仅支持.xls/.xlsx格式</div>
+      </el-upload>
+      <div style="margin-top: 10px;">
+        <el-button size="small" type="text" @click="handleDownloadTemplate">下载Excel导入模板</el-button>
+      </div>
+      <div v-if="commentImportDialog.historyList.length > 0" style="margin-top: 15px;">
+        <el-divider content-position="left">历史上传留言</el-divider>
+        <el-table :data="commentImportDialog.historyList" border size="small" max-height="300">
+          <el-table-column label="昵称" prop="nickName" width="100"/>
+          <el-table-column label="评论内容" prop="content" show-overflow-tooltip/>
+          <el-table-column label="上传时间" prop="createTime" width="160"/>
+          <el-table-column label="操作" width="70" align="center">
+            <template slot-scope="scope">
+              <el-button type="text" size="mini" style="color:#F56C6C" @click="handleDeleteComment(scope.row)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" :loading="commentImportDialog.loading" @click="handleImportComments">确认导入</el-button>
+        <el-button @click="commentImportDialog.visible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+    <el-dialog :title="title" :visible.sync="updateBatchData.open" width="1000px" append-to-body>
+      <el-form ref="form" :model="updateBatchData.form" label-width="110px">
+        <el-form-item label="看课时间" prop="timeRange">
+          <el-time-picker is-range v-model="updateBatchData.form.timeRange" range-separator="至"
+                          start-placeholder="开始时间"
+                          value-format="HH:mm:ss" end-placeholder="结束时间" placeholder="选择时间范围">
+          </el-time-picker>
+        </el-form-item>
+        <el-form-item label="领取红包时间" prop="lastJoinTime">
+          <el-time-picker v-model="updateBatchData.form.lastJoinTime" :selectableRange="updateBatchData.form.timeRange"
+                          value-format="HH:mm:ss" placeholder="选择时间范围">
+          </el-time-picker>
+          <p style="color: red;margin: 0;font-size: 12px">超过领取红包时间,只允许看课,不允许领取红包</p>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="updateBatch">确 定</el-button>
+        <el-button @click="updateBatchData.open = false">取 消</el-button>
       </div>
     </el-dialog>
 
-    <AutoTagDialog
-      :visible.sync="tagDialogVisible"
-      :title="tagDialogTitle"
-      :videoId="currentVideoId"
-      append-to-body
-    />
+    <el-dialog title="批量修改观看时长与积分" :visible.sync="batchWatchIntegral.open" width="520px" append-to-body>
+      <p class="batch-watch-tip">已选 <strong>{{ ids.length }}</strong> 条课节,将统一更新为下方数值(仅本课程下已选中的记录)。</p>
+      <el-form label-width="120px">
+        <el-form-item label="观看时长">
+          <el-input-number
+            v-model="batchWatchIntegral.form.watchDurationMinutes"
+            :min="0"
+            :precision="0"
+            :step="1"
+            controls-position="right"
+            style="width: 200px"
+          />
+          <span class="form-unit">分钟</span>
+        </el-form-item>
+        <el-form-item label="积分奖励">
+          <el-input-number
+            v-model="batchWatchIntegral.form.integralReward"
+            :min="0"
+            :precision="0"
+            :step="1"
+            controls-position="right"
+            style="width: 200px"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="batchWatchIntegral.open = false">取 消</el-button>
+        <el-button type="primary" @click="submitBatchWatchIntegral">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog :title="questionBank.title" :visible.sync="questionBank.open" width="1000px" append-to-body>
+      <question-bank ref="questionBank" @questionBankResult="questionBankResult"></question-bank>
+    </el-dialog>
+    <el-dialog :title="courseProduct.title" :visible.sync="courseProduct.open" width="1000px" append-to-body>
+      <course-product ref="courseProduct" @courseProductResult="courseProductResult"></course-product>
+    </el-dialog>
+    <el-dialog title="视频库选择" :visible.sync="addBatchData.open" width="900px" append-to-body>
+      <!-- 搜索条件 -->
+      <el-form :inline="true" :model="addBatchData.queryParams" class="library-search">
+        <el-form-item label="素材名称">
+          <el-input v-model="addBatchData.queryParams.resourceName" placeholder="请输入素材名称" clearable size="small"
+                    @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>
+      </el-form>
+
+      <!-- 视频列表 -->
+      <el-table v-loading="addBatchData.loading" :data="addBatchData.list"
+                @selection-change="handVideoleSelectionChange" height="400px">
+        <el-table-column type="selection" width="55" align="center"/>
+        <el-table-column label="素材名称" align="center" prop="resourceName"/>
+        <el-table-column label="文件名称" align="center" prop="fileName"/>
+        <el-table-column label="排序" align="center" prop="sort"/>
+        <el-table-column label="缩略图" align="center">
+          <template slot-scope="scope">
+            <el-popover placement="right" title="" trigger="hover">
+              <img alt="" slot="reference" :src="scope.row.thumbnail" style="width: 80px; height: 50px"/>
+              <img alt="" :src="scope.row.thumbnail" style="max-width: 150px;"/>
+            </el-popover>
+          </template>
+        </el-table-column>
+        <el-table-column label="视频时长" align="center">
+          <template slot-scope="scope">
+            <span>{{ formatDuration(scope.row.duration) }}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <pagination v-show="addBatchData.total > 0" :total="addBatchData.total"
+                  :page.sync="addBatchData.queryParams.pageNum" :limit.sync="addBatchData.queryParams.pageSize"
+                  @pagination="resourceList"/>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="batchVideoSave">确 定</el-button>
+      </div>
+    </el-dialog>
+    <el-dialog title="章节红包" :visible.sync="redData.open" :close-on-click-modal="false" width="900px" append-to-body>
+      <el-table border v-loading="redData.loading" :data="redData.list" height="600px">
+        <el-table-column label="小节名称" align="center" show-overflow-tooltip prop="title">
+          <template slot-scope="scope">
+            <el-input class="el-input" v-model="scope.row.title"/>
+          </template>
+        </el-table-column>
+        <el-table-column label="视频文件名称" align="center" show-overflow-tooltip prop="fileName">
+        </el-table-column>
+        <el-table-column label="视频时长" align="center" prop="duration">
+          <template slot-scope="{ row }">
+            {{ formatDuration(row.duration) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="红包金额" align="center" prop="redPacketMoney">
+          <template slot-scope="scope">
+            <el-input class="el-input" v-model="scope.row.redPacketMoney"/>
+          </template>
+        </el-table-column>
+        <el-table-column label="排序" align="center" prop="courseSort"/>
+        <el-table-column label="上传时间" align="center" prop="createTime"/>
+      </el-table>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="batchRedSave">确 定</el-button>
+      </div>
+    </el-dialog>
+    <el-dialog :title="commentDialog.title" :visible.sync="commentDialog.open" width="1000px" append-to-body
+               :close-on-click-modal="false">
+      <course-watch-comment ref="courseWatchComment" :courseId="commentDialog.courseId" :videoId="commentDialog.videoId"
+                            v-if="commentDialog.open">
+      </course-watch-comment>
+    </el-dialog>
+    <el-dialog :title="userCommentDialog.title" :visible.sync="userCommentDialog.open" width="1000px" append-to-body
+               :close-on-click-modal="false" @opened="onUserCommentDialogOpened">
+      <user-course-section-comment
+        v-if="userCommentDialog.open"
+        ref="userCourseSectionComment"
+        :course-id="userCommentDialog.courseId"
+        :video-id="userCommentDialog.videoId"
+      />
+    </el-dialog>
+
 
+    <el-dialog title="修改课节排序" :visible.sync="openVideoSort" style="width: 1600px;" append-to-body>
+      <draggable v-model="userCourseVideoSortList" @end="onDragEndDay" style="padding: 10px">
+        <el-button style="margin: 8px 4px;" v-for="(item, index) in userCourseVideoSortList"
+                   :class="item.newCourseSort != item.courseSort ? 'red':''">第{{
+            item.newCourseSort
+          }}序(原排序第{{ item.courseSort }})
+        </el-button>
+      </draggable>
+      <div style="float: right;margin-top: -20px">
+        <el-button type="primary" @click="saveSorts">保存</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 批量修改封面 -->
+    <el-dialog :title="batchEditCoverDialog.title" :visible.sync="batchEditCoverDialog.visible" width="500px" append-to-body>
+      <el-form ref="batchEditCoverDialogForm"
+               :model="batchEditCoverDialog.form"
+               :rules="batchEditCoverDialog.rules"
+               v-loading="batchEditCoverDialog.uploadLoading">
+        <el-form-item label="视频封面" prop="thumbnail">
+          <el-upload v-model="batchEditCoverDialog.form.thumbnail"
+                     class="avatar-uploader"
+                     :action="uploadUrl"
+                     :show-file-list="false"
+                     :on-success="handleCoverSuccess"
+                     :before-upload="beforeAvatarUpload">
+            <img v-if="batchEditCoverDialog.form.thumbnail" :src="batchEditCoverDialog.form.thumbnail" class="avatar" width="300px">
+            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitEditCoverForm">确 定</el-button>
+        <el-button @click="cancelEditCoverForm">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 销售易参数弹窗 -->
+    <el-dialog
+      title="复制销售易参数"
+      :visible.sync="xsyDialogVisible"
+      width="500px"
+      append-to-body
+    >
+      <div style="margin-bottom: 20px;">
+        <el-input
+          v-model="xsyParams"
+          type="textarea"
+          :rows="3"
+          readonly
+          placeholder="参数将显示在这里"
+          class="xsy-textarea"
+        />
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="xsyDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="copyXsyParams">复制参数</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import {getVideoListByCourseId, updatePacketMoney, updateUserCourseVideoUpdate} from "@/api/course/userCourseVideo";
-import userCourseVideoDetails from '../../components/course/userCourseVideoDetails.vue';
-import {createLinkUrl, createRoomLinkUrl, queryQwIds} from "@/api/course/sopCourseLink";
-import AutoTagDialog from "@/views/components/tag/AutoTagDialog.vue";
-import {addTag, updateTag} from "@/api/tag/api";
+import {
+  addUserCourseVideo,
+  batchSaveVideo,
+  batchUpdateRed,
+  delUserCourseVideo,
+  getSort,
+  getUserCourseVideo,
+  getVideoListByCourseId,
+  getPublicVideoListByCourseId,
+  getVideoListByCourseIdAll,
+  sortCourseVideo,
+  updates,
+  updateUserCourseVideo,
+  batchUpdateWatchIntegral,
+  syncTemplate, batchDownUserCourseVideo, batchEditCover, batchUpUserCourseVideo, getPublicUserCourseVideo
+} from '@/api/course/userCourseVideo'
+// import {syncTemplate} from '@/api/course/userCourse'
+import QuestionBank from "@/views/course/courseQuestionBank/QuestionBank.vue";
+import CourseProduct from "@/views/course/fsCourseProduct/CourseProduct.vue";
+import VideoUpload from "@/components/VideoUpload/index.vue";
+import {listVideoResource, listPublicVideoResource} from '@/api/course/videoResource';
+import {getByIds} from '@/api/course/courseQuestionBank'
+import CourseWatchComment from "./courseWatchComment.vue";
+import UserCourseSectionComment from "./userCourseSectionComment.vue";
+import {getCateListByPid, getCatePidList, getPublicCateListByPid, getPublicCatePidList} from '@/api/course/userCourseCategory'
+import {downloadCommentImportTemplate, importComments, listFeaturedComments, delUserCourseComment} from '@/api/course/userCourseComment'
+import draggable from 'vuedraggable'
+import { getConfigByKey } from '@/api/system/config'
+import { options as courserCouponOptions} from "@/api/his/courserCoupon";
 
 export default {
-    name: "userCourseCatalog",
-    components: {
-      userCourseVideoDetails,
-      AutoTagDialog
-    },
-    props: {
-      video: {
-        type: Object,
-        required: true,
+  name: "userCourseCatalog",
+  components: {VideoUpload, QuestionBank, CourseWatchComment, UserCourseSectionComment, CourseProduct, draggable},
+  props: {
+    /** 为 true 时「视频库选择」使用公域分类 + 公域素材接口;默认 false 走原 list / 原分类下拉 */
+    usePublicVideoLibrary: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch:{
+    // 深度监听 rules 数组的变化,以更新总权重
+    "form.randomRedPacketRulesArr": {
+      handler(val) {
+        // this.calculateTotalWeight();
+        this.validateRules();
       },
+      deep: true,
     },
-    data() {
-      return {
-        currentRow: null,
-        // 假设这里有当前课程小节的数据传入,里面含id等
-        tagGroups: [],
-        tagsInGroup: [],
-        tagDialogVisible: false,
-        tagDialogTitle: "",
-        tagDialogFormData: null,
-        currentVideoId:null,
-        projectFrom:process.env.VUE_APP_PROJECT_FROM,
-        tagDialog: {
-          videoId: null,
-          groupId: null,
-          watchTagId: null,
-          finishTagId: null,
-          tgId: null,
-          watchingTgId: null,
-          watchedTgId: null
-        },
-        linkForm:{
-          days:null,
-          courseId:null,
-          videoId:null
-        },
-        roomLinkForm:{
-          courseId:null,
-          videoId:null,
-          qwUserId:null,
-          qwUserName:null,
-          corpId:null,
-          title:null,
-        },
-        dialogVisible: false, // 控制弹框显示
-        //短链
-        sortLink:'',
-        //课题
-        questionBank:{
-          title:'',
-          open:false,
-        },
-		    moneyOpen:false,
-        videoUrl: "",
-        uploadTypeOptions: [
-          { dictLabel: "线路一", dictValue: 2 },
-
-          { dictLabel: "线路二", dictValue: 1 },
-          { dictLabel: "线路三", dictValue: 3 },
-        ],
-        uploadLoading:false,
-        courseId:null,
-        videoName:'',
-        title: "",
-        redPacketMoneyForm:{
-          redPacketMoney:null,
-          voidId:null
+  },
+  data() {
+    return {
+      courseCouponList: [],
+      duration: null,
+      packageList: [],
+      //课题
+      package: {
+        title: '',
+        open: false,
+      },
+      //课题
+      questionBank: {
+        title: '',
+        open: false,
+      },
+      //拍商品
+      courseProduct: {
+        title: '',
+        open: false,
+      },
+      isPrivate: null,
+      videoUrl: "",
+      uploadTypeOptions: [
+        {dictLabel: "线路一", dictValue: 2},
+        {dictLabel: "线路二", dictValue: 3},
+      ],
+      uploadLoading: false,
+      courseId: null,
+      videoName: '',
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      uploadUrl: process.env.VUE_APP_BASE_API + "/common/uploadOSS",
+      baseUrl: process.env.VUE_APP_BASE_API,
+      projectFrom:process.env.VUE_APP_PROJECT,
+      typeOptions: [],
+      files: [],
+      fileList: [],
+      // 上传成功后的地址
+      videoURL: '',
+      // 进度条百分比
+      progress: 0,
+      // 上传视频获取成功后拿到的fileID【备用】
+      fileId: '',
+      courseName: null,
+      userCourseVideoList: [],
+      userCourseVideoSortList: [],
+      total: 0,
+      redData: {
+        queryParams: {
+          pageNum: 1,
+          pageSize: 99999,
+          courseId: null,
         },
-        // 是否显示弹出层
+        list: [],
         open: false,
-        uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadHuaWeiObs",
-        baseUrl: process.env.VUE_APP_BASE_API,
-        typeOptions:[],
-        files:[],
-        fileList: [],
-        // 上传成功后的地址
-        videoURL: '',
-        // 进度条百分比
-        progress: 0,
-        // 上传视频获取成功后拿到的fileID【备用】
-        fileId: '',
-        courseName:null,
-        userCourseVideoList:[],
-
-        fetchingQwIds: false,
-        qwUserList:[],
+        loading: true,
+        form: {}
+      },
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        courseId: null,
+        videoId: null,
+        title: null
+      },
+      addBatchData: {
+        open: false,
+        loading: true,
+        form: {},
+        select: [], // 按用户选择顺序存储视频ID
         total: 0,
         queryParams: {
           pageNum: 1,
           pageSize: 10,
-          courseId:null,
-          title:null
+          resourceName: null,
+          typeId: null,
+          typeSubId: null
         },
-        // 显示搜索条件
-        showSearch: true,
-        // 遮罩层
-        loading: true,
-        // 导出遮罩层
-        exportLoading: false,
-        // 选中数组
-        ids: [],
-        // 非单个禁用
-        single: true,
-        // 非多个禁用
-        multiple: true,
-          // 表单参数
-        form: {},
-        // 表单校验
-        rules: {
-          title: [
-            { required: true, message: "小节名称不能为空", trigger: "change" }
-          ],
-          courseSort: [
-            { required: true, message: "排序不能为空", trigger: "change" }
-          ],
-        }
-      }
-    },
-    created() {
-      this.getDicts("sys_course_temp_type").then(response => {
-        this.typeOptions = response.data;
-      });
-    },
-    methods: {
-      formatOptionLabel(item) {
-        return item.corpName ? `${item.qwUserName} (${item.corpName})`  : item.qwUserName;
+        typeOptions: [],
+        typeSubOptions: []
       },
-      closeTagDialog(){
-        this.tagDialogVisible = false;
+      // 显示搜索条件
+      showSearch: true,
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      loading3: false,
+      openVideoSort: false,
+      // 表单参数
+      form: {
+        isOnPut: 0,
+        courseProducts: [],
+        courseIntroImg: null,
+        randomRedPacketRules:null,
+        randomRedPacketRulesArr:[
+           {
+            minAmount: 0.01,
+            maxAmount: 0.01,
+            weight: 100,
+          }
+        ]
       },
-      openTagDialog(row) {
-        this.currentRow = row;
-
+      updateBatchData: {
+        open: false,
+        form: {}
+      },
+      batchWatchIntegral: {
+        open: false,
+        form: {
+          watchDurationMinutes: 0,
+          integralReward: 0
+        }
+      },
+      // 表单校验
+      rules: {
+        title: [
+          {required: true, message: "小节名称不能为空", trigger: "change"}
+        ],
+        courseSort: [
+          {required: true, message: "排序不能为空", trigger: "change"}
+        ],
 
-        this.tagDialogVisible = true;
-        this.currentVideoId = row.videoId;
       },
-      updateTagsInGroup(groupId) {
-        let tagGroup = this.tagGroups.find(e=> e.groupId === groupId);
-
-        this.tagsInGroup = tagGroup.tag || [];
-        // 切换组时清空标签选择
-        this.tagDialog.watchTagId = null;
-        this.tagDialog.finishTagId = null;
-        this.tagDialog.tgId = tagGroup.id;
+      // 评论弹窗数据
+      commentDialog: {
+        open: false,
+        courseId: null,
+        videoId: null,
+        title: ""
       },
-      resetDialog() {
-        this.tagDialog = {
-          videoId: null,
-          groupId: null,
-          watchTagId: null,
-          finishTagId: null,
-        };
-        this.tagGroups = [];
-        this.tagsInGroup = [];
+      userCommentDialog: {
+        open: false,
+        courseId: null,
+        videoId: null,
+        title: ""
       },
-      // 打开弹框
-      openDialog(row) {
-        if (!this.fetchingQwIds) {
-          this.fetchingQwIds = true;
-          queryQwIds().then(response => {
-            if (response.code === 200){
-              this.qwUserList = response.list;
-            }
-          }).finally(() => {
-            this.fetchingQwIds = false;
-            // 在请求完成后再显示弹框
-            this.showDialog(row);
-          });
-        } else {
-          // 如果已经在请求中,直接显示弹框
-          this.showDialog(row);
+      enableRandomRedPacket:false,
+      // 批量修改封面
+      batchEditCoverDialog: {
+        title: '修改视频封面',
+        visible: false,
+        uploadLoading: false,
+        form: {
+          thumbnail: null,
+        },
+        rules: {
+          thumbnail: [
+            {required: true, message: "视频封面不能为空", trigger: "change"}
+          ],
         }
       },
-      // 新增方法:显示弹框
-      showDialog(row) {
-        this.roomLinkForm.courseId = row.courseId;
-        this.roomLinkForm.videoId = row.videoId;
-        this.roomLinkForm.title = row.title;
-
-        this.dialogVisible = true;
+      // 销售易弹窗
+      xsyDialogVisible: false,
+      xsyParams: '',
+      // 精选留言导入弹窗
+      commentImportDialog: {
+        visible: false,
+        loading: false,
+        fileList: [],
+        file: null,
+        historyList: [],
+        courseId: null,
+        videoId: null
       },
-      // 确认按钮操作
-      confirm() {
-        if (!this.roomLinkForm.qwUserId) {
-          this.$message.error("请选择销售企微!");
+    }
+  },
+  created() {
+    this.getDicts("sys_course_temp_type").then(response => {
+      this.typeOptions = response.data;
+    });
+        getConfigByKey('randomRedpacket:config').then(res=>{
+        let configData = res.data;
+        if(!!configData && !!configData.configValue){
+           let configValue = JSON.parse(configData.configValue);
+           if(!!configValue.enableRandomRedpacket){
+            this.enableRandomRedPacket = configValue.enableRandomRedpacket;
+            console.log("this.enableRandomRedPacket ::" + this.enableRandomRedPacket)
+           }
+        }
+    }).catch(res=>{
+
+    });
+    courserCouponOptions().then(res=>{
+      this.courseCouponList = res.data;
+    }).catch(res=>{
+
+    });
+
+  },
+  methods: {
+    getPickerOptions() {
+      const durationInMinutes = Math.floor(this.form.duration / 60); // 将秒转换为分钟
+      const endHour = Math.floor(durationInMinutes / 60); // 起始小时
+      const endMinute = durationInMinutes % 60; // 起始分钟
+      return {
+        start: "00:00", // 固定开始时间
+        step: "00:01", // 时间间隔
+        end: `${endHour.toString().padStart(2, "0")}:${endMinute
+          .toString()
+          .padStart(2, "0")}`, // 动态结束时间
+      };
+    },
+
+    // 处理时间选择框值变化
+    handleTimeChange(index, row) {
+      // 确保 packageList 中的数据被正确更新
+      this.$set(this.packageList, index, row);
+      // 同步更新 form.packageJson 字段
+      this.$nextTick(() => {
+        // 确保每个疗法包都有 duration 字段
+        this.packageList.forEach(item => {
+          if (item.duration === undefined || item.duration === null) {
+            item.duration = ''; // 空值应初始化为空字符串而不是null,避免显示"null"
+          }
+        });
+        this.form.packageJson = JSON.stringify(this.packageList);
+      });
+    },
+    handlePackageDelete(row) {
+      this.packageList.splice(this.packageList.findIndex(item => item.packageId === row.packageId), 1)
+    },
+    choosePackage() {
+      this.package.open = true;
+      this.package.title = '疗法选择';
+    },
+    /**
+     * 选择课题
+     */
+    chooseQuestionBank() {
+      this.questionBank.open = true;
+      this.questionBank.title = '课题选择';
+    },
+
+    /**
+     * 选择拍商品
+     */
+    chooseCourseProduct() {
+      this.courseProduct.open = true;
+      this.courseProduct.title = '拍商品选择';
+    },
+
+
+    //选择疗法
+    selectPackage(row) {
+      const drug = {};
+      for (var i = 0; i < this.packageList.length; i++) {
+        if (this.packageList[i].packageId == row.packageId) {
+          this.$message.warning("疗法已存在!")
           return;
         }
-        // 获取选中的用户信息
-        const selectedUser = this.qwUserList.find(user => user.id === this.roomLinkForm.qwUserId);
-        if (selectedUser) {
-          // 将用户信息填充到表单中
-          this.roomLinkForm.qwUserName = selectedUser.qwUserName;
-          this.roomLinkForm.corpId = selectedUser.corpId;
-          console.log(this.roomLinkForm);
-          // 调用创建链接的API
-          createRoomLinkUrl(this.roomLinkForm).then(response => {
-            if (response.code === 200){
-              this.msgSuccess("创建成功");
-              this.copyLink(response.link);
-              console.log(response.link);
+      }
+      drug.packageId = row.packageId;
+      drug.packageName = row.packageName;
+      drug.secondName = row.secondName;
+      drug.totalPrice = row.totalPrice;
+      drug.imgUrl = row.imgUrl;
+      this.packageList.push(drug);
+      this.$message({
+        message: '添加成功',
+        type: 'success'
+      });
+
+    },
+
+    courseProductResult(val) {
+      this.form.courseProducts = this.form.courseProducts || [];
+      for (var i = 0; i < this.form.courseProducts.length; i++) {
+        if (this.form.courseProducts[i].id == val.id) {
+          return this.$message.error("当前商品已选择")
+        }
+      }
+
+      //先置空 只选择一件商品
+      this.form.courseProducts = [];
+      this.form.courseProducts.push(val);
+      this.$message({
+        message: '添加成功',
+        type: 'success'
+      });
+    },
+
+    //选择结果
+    questionBankResult(val) {
+
+      // 确保 questionBankList 是数组
+      this.form.questionBankList = this.form.questionBankList || [];
+
+      for (var i = 0; i < this.form.questionBankList.length; i++) {
+        if (this.form.questionBankList[i].id == val.id) {
+          return this.$message.error("当前课题已选择")
+        }
+      }
+
+      this.form.questionBankList.push(val);
+      this.$message({
+        message: '添加成功',
+        type: 'success'
+      });
+    },
+
+    //删除课题
+    handleQuestionBankDelete(row) {
+      this.form.questionBankList.splice(this.form.questionBankList.findIndex(item => item.id === row.id), 1)
+    },
+
+    //删除商品
+    handleCourseProductDelete(row) {
+      this.form.courseProducts.splice(this.form.courseProducts.findIndex(item => item.id === row.id), 1)
+    },
+    handleVideoChange() {
+      if (this.form.uploadType == 1) {
+        this.videoUrl = this.form.lineOne;
+      } else if (this.form.uploadType == 2) {
+        this.videoUrl = this.form.lineTwo;
+      } else if (this.form.uploadType == 3) {
+        this.videoUrl = this.form.lineThree;
+      }
+    },
+    // 视频库课题
+    handleSelectProjects(projectIds) {
+      this.form.questionBankList = []
+      if (!projectIds || projectIds.length === 0 || this.isPrivate === 0) {
+        return
+      }
+
+      const params = {ids: projectIds}
+      getByIds(params).then(response => {
+        if (response.code === 200) {
+          response.data.forEach(item => {
+            let isExist = this.form.questionBankList.some(q => q.id === item.id)
+            if (!isExist) {
+              this.form.questionBankList.push(item)
             }
           });
         }
+      })
+    },
+    handleVideoDuration(duration) {
+      this.form.duration = duration;
+    },
+    formatDuration(seconds) {
+      if (seconds === null || seconds === undefined) {
+        return '未上传视频';
+      }
+      const hours = Math.floor(seconds / 3600);
+      const minutes = Math.floor((seconds % 3600) / 60);
+      const remainingSeconds = seconds % 60;
 
-        // 关闭弹框
-        this.dialogVisible = false;
-      },
-      // 重置表单
-      resetForm() {
-        this.linkForm={
-            days:null,
-            courseId:null,
-            videoId:null
+      const formattedHours = hours > 0 ? hours.toString() + ':' : '';
+      const formattedMinutes = minutes.toString().padStart(2, '0');
+      const formattedSeconds = remainingSeconds.toString().padStart(2, '0');
+
+      return `${formattedHours}${formattedMinutes}:${formattedSeconds}`;
+    },
+
+    handleAvatarSuccess(res, file) {
+      if (res.code == 200) {
+        this.form.thumbnail = res.url;
+        this.$forceUpdate()
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUpload(file) {
+      const isLt1M = file.size / 1024 / 1024 < 5;
+      if (!isLt1M) {
+        this.$message.error('上传图片大小不能超过 5MB!');
+      }
+      return isLt1M;
+    },
+    getDetails(courseId, courseName, isPrivate) {
+      this.isPrivate = isPrivate
+      this.courseName = courseName
+      this.courseId = courseId;
+      this.queryParams.courseId = courseId;
+      this.getList();
+    },
+    getList() {
+      this.loading = true;
+      const api = this.usePublicVideoLibrary ? getPublicVideoListByCourseId : getVideoListByCourseId;
+      api(this.queryParams).then(response => {
+        this.userCourseVideoList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        videoId: null,
+        title: null,
+        description: null,
+        url: null,
+        thumbnail: null,
+        duration: null,
+        createTime: null,
+        uploadType: null,
+        lineOne: null,
+        lineTwo: null,
+        lineThree: null,
+        fileName: null,
+        userId: null,
+        cateId: null,
+        courseId: null,
+        likes: null,
+        views: null,
+        comments: null,
+        status: 0,
+        courseSort: 1,
+        isHot: null,
+        isShow: null,
+        isAudit: null,
+        auditBy: null,
+        auditTime: null,
+        updateTime: null,
+        source: null,
+        isDel: null,
+        shares: null,
+        tags: null,
+        productId: null,
+        productJson: null,
+        questionBankId: null,
+        questionBankList: [],
+        redPacketMoney: 0,
+        isTranscode: 0,
+        transcodeFileKey: null,
+        isProduct: 0,
+        isFirst: 0,
+        isSpeed: 0,
+        isOnPut: 0,
+        listingStartTime: null,
+        listingEndTime: null,
+        watchDurationMinutes: null,
+        integralReward: null,
+        randomRedPacketRules:null,
+        randomRedPacketRulesArr:[
+          {
+            minAmount: 0.01,
+            maxAmount: 0.01,
+            weight: 100,
+          }
+        ],
+        courseIntroImg: null
+      };
+      this.videoURL = '';
+      this.progress = 0;
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.videoId)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    // 视频库多选框选中数据(按用户点击顺序记录)
+    handVideoleSelectionChange(selection) {
+      // 提取当前选中的所有ID
+      const selectedIds = selection.map(item => item.id);
+      // 处理新增选中项:保留用户点击顺序
+      selectedIds.forEach(id => {
+        if (!this.addBatchData.select.includes(id)) {
+          this.addBatchData.select.push(id);
         }
-      },
+      });
+      // 处理取消选中项:移除已取消的ID
+      this.addBatchData.select = this.addBatchData.select.filter(id => selectedIds.includes(id));
+    },
+    handleAdd() {
+      this.reset();
+      this.form.courseId = this.courseId;
+      this.open = true;
+      this.title = "添加课堂视频";
+      this.videoUrl = '';
+      this.packageList = [];
+      getSort(this.courseId).then(response => {
+        this.form.courseSort = Number(response.data);
+      })
+      setTimeout(() => {
+        this.$refs.videoUpload.resetUpload();
+      }, 500);
 
-      resetRoomForm() {
-        this.roomLinkForm={
-          courseId:null,
-          videoId:null,
-          qwUserId:null,
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      this.form.isOnPut=row.isOnPut
+      this.packageList = [];
+      const videoId = row.videoId || this.ids
+      const api = this.usePublicVideoLibrary ? getPublicUserCourseVideo : getUserCourseVideo;
+      api(videoId).then(response => {
+        console.log(response);
+        this.form = response.data;
+        this.$set(this.form, 'courseIntroImg', response.data.courseIntroImg || null);
+        this.$set(this.form, 'isOnPut', response.data.isOnPut !== undefined ? response.data.isOnPut : 0);
+        if(!!this.form.randomRedPacketRules){
+           this.$set(this.form, 'randomRedPacketRulesArr', JSON.parse(this.form.randomRedPacketRules)) ;
+          // this.form.randomRedPacketRulesArr = JSON.parse(this.form.randomRedPacketRules);
+        }else{
+          //处理初始值
+         this.form.randomRedPacketRulesArr = [{
+            minAmount: 0.01,
+            maxAmount: 0.01,
+            weight: 100,
+          }]
         }
-      },
-      handleCreateLink(){
-        createLinkUrl(this.linkForm).then(response => {
-          if (response.code === 200){
-            this.copyLink(response.url);
+        if (response.data.videoUrl != null && response.data.videoUrl !== '') {
+          this.videoUrl = response.data.videoUrl;
+        }
+        if (this.form.packageJson != null) {
+          this.packageList = JSON.parse(this.form.packageJson);
+        }
+
+        if (response.data.viewStartTime != null && response.data.viewEndTime != null) {
+          this.form.timeRange = [response.data.viewStartTime, response.data.viewEndTime]
+        }
+        setTimeout(() => {
+          this.$refs.videoUpload.resetUpload();
+        }, 500);
+        this.open = true;
+        this.title = "修改课堂视频";
+      });
+
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+
+        if (valid) {
+          this.form.videoUrl = this.videoUrl;
+          if (this.form.videoUrl == null || this.form.videoUrl === '') {
+            this.$message({
+              message: '请上传视频!',
+              type: 'warning'
+            });
+            return
           }
-        });
-      },
-      copyLink(url) {
-        const link = url;
-        // navigator clipboard 需要https等安全上下文
-        if (navigator.clipboard && window.isSecureContext) {
-          // navigator clipboard 向剪贴板写文本
-          navigator.clipboard.writeText(link).then(() => {
-            this.$message.success('链接已复制到剪贴板');
-          });
-        } else {
-          // document.execCommand('copy') 向剪贴板写文本
-          let input = document.createElement('input')
-          input.style.position = 'fixed'
-          input.style.top = '-10000px'
-          input.style.zIndex = '-999'
-          document.body.appendChild(input)
-          input.value = link
-          input.focus()
-          input.select()
-          try {
-            let result = document.execCommand('copy')
-            document.body.removeChild(input)
-            if (!result || result === 'unsuccessful') {
-              this.$message.error('复制失败');
-              console.log('复制失败')
-            } else {
-              this.$message.success('链接已复制到剪贴板');
-              console.log('复制成功')
-            }
-          } catch (e) {
-            document.body.removeChild(input)
-            alert('当前浏览器不支持复制功能,请检查更新或更换其他浏览器操作')
+
+          if (this.form.timeRange && this.form.timeRange.length === 2) {
+            this.form.viewStartTime = this.form.timeRange[0];
+            this.form.viewEndTime = this.form.timeRange[1];
+          }
+
+          if (this.form.duration == null) {
+            this.$message({
+              message: '未识别到视频时长请稍等。。。',
+              type: 'warning'
+            });
+            return
+          }
+          if (this.form.isProduct != null && this.form.isProduct == 1 && (this.form.courseProducts == null || this.form.courseProducts.length < 1)) {
+            this.$message({
+              message: '请选择关联商品',
+              type: 'warning'
+            });
+            return
+          }
+          if (this.form.questionBankList !== null) {
+            this.form.questionBankId = this.form.questionBankList.map(item => item.id).join(',');
+          }
+          if (this.packageList.length > 0) {
+            this.form.packageJson = JSON.stringify(this.packageList);
+          }
+          if (this.form.courseProducts != null) {
+            this.form.productId = this.form.courseProducts.map(item => item.id).join(',');
+          }
+          if(!!this.form.randomRedPacketRulesArr){
+            let rulesJson = JSON.stringify(this.form.randomRedPacketRulesArr);
+            this.form.randomRedPacketRules =  rulesJson;
+          }
+          if (this.form.videoId != null) {
+            updateUserCourseVideo(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addUserCourseVideo(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
           }
         }
-      },
-      formatDuration(seconds) {
-        if (seconds === null || seconds === undefined) {
-          return '未上传视频'; // 或者您可以根据具体需求返回其他默认值
+      });
+    },
+    openUpdates() {
+      this.updateBatchData.form = {};
+      this.updateBatchData.open = true;
+    },
+    /** 提交按钮 */
+    updateBatch() {
+      this.updateBatchData.form.ids = this.ids;
+      if (this.updateBatchData.form.timeRange != null && this.updateBatchData.form.timeRange.length === 2) {
+        this.updateBatchData.form.viewStartTime = this.updateBatchData.form.timeRange[0];
+        this.updateBatchData.form.viewEndTime = this.updateBatchData.form.timeRange[1];
+      }
+      updates(this.updateBatchData.form).then(response => {
+        this.msgSuccess("修改成功");
+        this.updateBatchData.open = false;
+        this.getList();
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const videoIds = row.videoId || this.ids;
+      this.$confirm('是否确认删除视频编号为"' + videoIds + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return delUserCourseVideo(videoIds);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {
+      });
+    },
+    /** 同步模板数据*/
+    handleSync() {
+      const courseId = this.courseId;
+      this.$confirm('是否同步课程数据至模板', "确认", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return syncTemplate(courseId);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("正在同步模板中!!请稍后!");
+      }).catch(() => {
+      });
+    },
+
+    handleCourseSort() {
+
+      getVideoListByCourseIdAll(this.queryParams.courseId).then(response => {
+
+        response.rows.forEach((item) => item.newCourseSort = item.courseSort);
+        this.userCourseVideoSortList = response.rows.sort((a, b) => a.courseSort - b.courseSort);
+        if (this.userCourseVideoSortList == null || this.userCourseVideoSortList.length == 0) {
+          this.$message.error("暂无课节天数")
+        } else {
+          this.openVideoSort = true;
         }
-        const hours = Math.floor(seconds / 3600);
-        const minutes = Math.floor((seconds % 3600) / 60);
-        const remainingSeconds = seconds % 60;
+      })
 
-        const formattedHours = hours > 0 ? hours.toString() + ':' : '';
-        const formattedMinutes = minutes.toString().padStart(2, '0');
-        const formattedSeconds = remainingSeconds.toString().padStart(2, '0');
 
-        return `${formattedHours}${formattedMinutes}:${formattedSeconds}`;
-      },
-      getDetails(courseId,courseName) {
-        this.courseName = courseName
-        this.courseId = courseId;
-        this.queryParams.courseId = courseId;
+    },
+
+    onDragEndDay() {
+      this.userCourseVideoSortList.forEach((item, index) => {
+        item.newCourseSort = index + 1;
+      })
+      this.$forceUpdate()
+    },
+
+    saveSorts() {
+      let list = this.userCourseVideoSortList.filter(e => e.courseSort != e.newCourseSort).map(e => {
+        return {courseSort: e.newCourseSort, videoId: e.videoId}
+      })
+      this.loading3 = true;
+      sortCourseVideo(list).then(e => {
         this.getList();
-      },
-      getList() {
-        this.loading = true;
-        getVideoListByCourseId(this.queryParams).then(response => {
-          this.userCourseVideoList = response.rows;
-          this.total = response.total;
-          this.loading = false;
+      }).finally(() => {
+        this.userCourseVideoSortList = [];
+        this.openVideoSort = false;
+      })
+    },
+
+    openAdds() {
+      this.addBatchData.open = true;
+      this.getRootTypeList();
+      this.addBatchData.form = {
+        courseId: this.courseId,
+      };
+      // 重置选择顺序数组
+      this.addBatchData.select = [];
+      this.resourceList();
+    },
+    openBatchWatchIntegralDialog() {
+      if (!this.ids || this.ids.length === 0) {
+        this.$message.warning('请先勾选要修改的课节');
+        return;
+      }
+      this.batchWatchIntegral.form = {
+        watchDurationMinutes: 0,
+        integralReward: 0
+      };
+      this.batchWatchIntegral.open = true;
+    },
+    submitBatchWatchIntegral() {
+      const w = this.batchWatchIntegral.form.watchDurationMinutes;
+      const i = this.batchWatchIntegral.form.integralReward;
+      if (w === null || w === undefined || i === null || i === undefined) {
+        this.$message.warning('请填写观看时长与积分奖励');
+        return;
+      }
+      const videoIds = (this.ids || []).map(id => (typeof id === 'string' ? parseInt(id, 10) : id));
+      batchUpdateWatchIntegral({
+        courseId: this.courseId,
+        videoIds: videoIds,
+        watchDurationMinutes: w,
+        integralReward: i
+      }).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess('修改成功');
+          this.batchWatchIntegral.open = false;
+          this.getList();
+        } else {
+          this.$message.error(response.msg || '修改失败');
+        }
+      });
+    },
+    getRootTypeList() {
+      const req = this.usePublicVideoLibrary ? getPublicCatePidList() : getCatePidList()
+      req.then(response => {
+        this.addBatchData.typeOptions = response.data
+      });
+    },
+    async changeCateType(val) {
+      this.addBatchData.queryParams.typeSubId = null
+      this.addBatchData.typeSubOptions = []
+      if (!val) {
+        return
+      }
+      const subReq = this.usePublicVideoLibrary ? getPublicCateListByPid(val) : getCateListByPid(val)
+      await subReq.then(response => {
+        this.addBatchData.typeSubOptions = response.data
+      })
+    },
+    resourceList() {
+      this.addBatchData.loading = true;
+      const api = this.usePublicVideoLibrary ? listPublicVideoResource : listVideoResource
+      api(this.addBatchData.queryParams).then(response => {
+        this.addBatchData.loading = false;
+        this.addBatchData.list = response.rows;
+        this.addBatchData.total = response.total;
+      });
+    },
+    batchVideoSave() {
+      if (this.addBatchData.select.length === 0) {
+        this.$message({
+          message: '请选择视频!!',
+          type: 'warning'
         });
-      },
-      // 取消按钮
-      cancel() {
-        this.open = false;
-        this.reset();
-      },
-      updateMoney(row){
-        this.redPacketMoneyForm.redPacketMoney=row.companyRedPacketMoney;
-        this.redPacketMoneyForm.videoId=row.videoId;
+        return
+      }
+      this.addBatchData.form.ids = this.addBatchData.select; // 按用户选择顺序提交
+      batchSaveVideo(this.addBatchData.form).then(response => {
+        this.addBatchData.open = false;
+        this.getList();
+      })
+    },
+    updateRedPageckeOpen() {
+      this.redData.open = true;
+      this.redData.loading = true;
+      this.redData.queryParams.courseId = this.courseId;
+      getVideoListByCourseId(this.redData.queryParams).then(response => {
+        if(!!response.rows && response.rows.length >0){
+          for(let i = 0; i < response.rows.length; i++){
+            if(!!response.rows[i].randomRedPacketRules){
+             this.$set(response.rows[i], 'randomRedPacketRulesArr', JSON.parse(response.rows[i].randomRedPacketRules)) ;
+            }
+          }
+        }
+         this.redData.list = response.rows;
+         console.log(this.redData.list);
+        this.redData.loading = false;
+      });
+    },
+    batchRedSave() {
+      batchUpdateRed(this.redData.list).then(response => {
+        this.redData.open = false;
+        this.getList();
+      })
+    },
+    /** 查看评论按钮操作 */
+    handleComment(row) {
+      this.commentDialog.courseId = row.courseId || this.courseId;
+      this.commentDialog.videoId = row.videoId;
+      this.commentDialog.title = `查看评论 - ${row.title}`;
+      this.commentDialog.open = true;
+    },
+    // 实时过滤金额输入,只允许两位小数
+    handleAmountInput(rule, field) {
+      let value = rule[field];
+      if (value === null || value === undefined) return;
 
-        this.moneyOpen=true;
-      },
-      submitForm(){
+      // 转换为字符串处理
+      let str = value.toString();
 
-       updatePacketMoney(this.redPacketMoneyForm).then(response => {
-          this.msgSuccess("修改成功");
-          this.moneyOpen=false;
-          this.getList()
-          });
-      },
-      updateMoneycancel(){
-        this.moneyOpen=false;
-      },
+      // 移除除数字和小数点外的所有字符
+      str = str.replace(/[^0-9.]/g, '');
 
-      // 表单重置
-      reset() {
-        this.form = {
-          videoId: null,
-          title: null,
-          description: null,
-          url: null,
-          thumbnail: null,
-          duration: null,
-          createTime: null,
-          uploadType:null,
-          lineOne:null,
-          lineTwo:null,
-          lineThree:null,
-          fileName:null,
-          userId: null,
-          cateId: null,
-          courseId: null,
-          likes: null,
-          views: null,
-          comments: null,
-          status: 0,
-          courseSort: 1,
-          isHot: null,
-          isShow: null,
-          isAudit: null,
-          auditBy: null,
-          auditTime: null,
-          updateTime: null,
-          source: null,
-          isDel: null,
-          shares: null,
-          tags: null,
-          productId: null,
-          productJson: null,
-          questionBankId:null,
-          questionBankList:[],
-        };
-        this.videoURL = '';
-        this.progress=0;
-        this.resetForm("form");
-      },
-      /** 搜索按钮操作 */
-      handleQuery() {
-        this.queryParams.pageNum = 1;
+      // 只保留一个小数点
+      const dotIndex = str.indexOf('.');
+      if (dotIndex !== -1) {
+        str = str.substring(0, dotIndex + 1) + str.substring(dotIndex + 1).replace(/\./g, '');
+      }
+
+      // 限制小数点后最多两位
+      if (dotIndex !== -1 && str.length > dotIndex + 3) {
+        str = str.substring(0, dotIndex + 3);
+      }
+
+      // 转换回数字并更新
+      rule[field] = parseFloat(str) || 0;
+    },
+      deleteRule(index) {
+      this.$confirm("确定要删除这个区间吗?", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+      }).then(() => {
+        this.form.randomRedPacketRulesArr.splice(index, 1);
+        this.$message({
+          type: "success",
+          message: "删除成功!",
+        });
+      });
+    },
+      addRule(index) {
+      // 在当前行的后面插入一个新行
+      this.form.randomRedPacketRulesArr.splice(index + 1, 0, {
+        minAmount: 0.01,
+        maxAmount: 0.01,
+        weight: 100,
+      });
+    },
+       // 自定义校验规则:确保最大金额大于最小金额
+    validateMinAmount(rule, value, callback) {
+      // debugger;
+      // const maxAmount = this.form29.rules[].maxAmount
+
+      const index = rule.index;
+      const maxAmount = this.form.randomRedPacketRulesArr[index].maxAmount;
+
+      if (value > maxAmount) {
+        callback(new Error("最小金额不能大于最大金额"));
+      } else {
+        callback();
+      }
+    },
+      validateRules() {
+      this.form.randomRedPacketRulesArr.forEach((rule) => {
+        if (rule.minAmount === undefined || rule.minAmount < 0.01) {
+          rule.minAmount = 0.01;
+        }
+        if (rule.maxAmount === undefined || rule.maxAmount < rule.minAmount) {
+          rule.maxAmount = rule.minAmount;
+        }
+        if (rule.weight === undefined || rule.weight < 1) {
+          rule.weight = 1;
+        }
+      });
+    },
+    /** 下架 **/
+    handleDown() {
+      const videoIds = this.ids;
+      this.$confirm('是否确认下架视频编号为"' + videoIds + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return batchDownUserCourseVideo(videoIds);
+      }).then(() => {
         this.getList();
-      },
-      /** 重置按钮操作 */
-      resetQuery() {
-        this.resetForm("queryForm");
-        this.queryParams.title = null;
-        this.handleQuery();
-      },
-      // 多选框选中数据
-      handleSelectionChange(selection) {
-        this.ids = selection.map(item => item.courseId)
-        this.single = selection.length!==1
-        this.multiple = !selection.length
-      },
-      /** 修改按钮操作 */
-      handleDetails(row) {
-        this.open=true;
-        setTimeout(() => {
-          this.$refs.userCourseVideoDetails.getDetails(row.videoId);
-        }, 500);
+        this.msgSuccess("下架成功");
+      }).catch(() => {
+      });
+    },
+    /** 上架按钮操作 */
+    handleUp() {
+      const videoIds = this.ids;
+      this.$confirm('是否确认上架视频编号为"' + videoIds + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return batchUpUserCourseVideo(videoIds);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("上架成功");
+      }).catch(function () {
+      });
+    },
+    /** 修改封面 **/
+    handleEditCover() {
+      this.batchEditCoverDialog.form = {
+        thumbnail: null
+      }
+      this.batchEditCoverDialog.visible = true
+    },
+    handleCoverSuccess(res, file) {
+      if (res.code === 200) {
+        this.batchEditCoverDialog.form.thumbnail = res.url;
+        this.$forceUpdate()
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+    submitEditCoverForm() {
+      this.$refs["batchEditCoverDialogForm"].validate(valid => {
+        if (!valid) {
+          return;
+        }
 
+        const thumbnail = this.batchEditCoverDialog.form.thumbnail
+        const videoIds = this.ids
+
+        if (!thumbnail || thumbnail === '') {
+          this.$message({
+            message: '请上传封面!',
+            type: 'warning'
+          });
+          return
+        }
+
+        if (!videoIds || videoIds.length === 0) {
+          this.$message({
+            message: '请选择小节!',
+            type: 'warning'
+          });
+          return
+        }
+
+        const params = {
+          thumbnail: thumbnail,
+          videoIds: videoIds
+        }
+
+        batchEditCover(params).then(response => {
+          this.msgSuccess("修改成功")
+          this.batchEditCoverDialog.visible = false
+          this.getList();
+        });
+      });
+    },
+    cancelEditCoverForm() {
+      this.batchEditCoverDialog.visible = false
+      this.batchEditCoverDialog.form = {
+        thumbnail: null,
       }
-    }
+    },
+    // 复制销售易链接
+    openXsyDialog(row) {
+      // 构建JSON字符串
+      const courseData = {
+        videoId: row.videoId,
+        courseId: row.courseId,
+        companyId: 0
+      };
+
+      this.xsyParams = `pages_course/videoxsy?course=${JSON.stringify(courseData)}`;
+      this.xsyDialogVisible = true;
+    },
+    // 复制销售易参数
+    copyXsyParams() {
+      this.copyLink(this.xsyParams);
+    },
+    copyLink(url) {
+      const link = url;
+      // navigator clipboard 需要https等安全上下文
+      if (navigator.clipboard && window.isSecureContext) {
+        // navigator clipboard 向剪贴板写文本
+        navigator.clipboard.writeText(link).then(() => {
+          this.$message.success('链接已复制到剪贴板');
+        });
+      } else {
+        // document.execCommand('copy') 向剪贴板写文本
+        let input = document.createElement('input')
+        input.style.position = 'fixed'
+        input.style.top = '-10000px'
+        input.style.zIndex = '-999'
+        document.body.appendChild(input)
+        input.value = link
+        input.focus()
+        input.select()
+        try {
+          let result = document.execCommand('copy')
+          document.body.removeChild(input)
+          if (!result || result === 'unsuccessful') {
+            this.$message.error('复制失败');
+            console.log('复制失败')
+          } else {
+            this.$message.success('链接已复制到剪贴板');
+            console.log('复制成功')
+          }
+        } catch (e) {
+          document.body.removeChild(input)
+          alert('当前浏览器不支持复制功能,请检查更新或更换其他浏览器操作')
+        }
+      }
+    },
+    // 课程介绍图片上传成功
+    handleCourseIntroSuccess(res) {
+      if (res.code == 200) {
+        this.form.courseIntroImg = res.url;
+        this.$forceUpdate();
+      } else {
+        this.$message.error(res.msg || '上传失败');
+      }
+    },
+    // 课程介绍图片上传前校验(10MB限制)
+    beforeCourseIntroUpload(file) {
+      const isLt10M = file.size / 1024 / 1024 < 10;
+      if (!isLt10M) {
+        this.$message.error('课程介绍图片大小不能超过10MB!');
+      }
+      const isImage = file.type.startsWith('image/');
+      if (!isImage) {
+        this.$message.error('只能上传图片格式文件!');
+      }
+      return isLt10M && isImage;
+    },
+    // 打开精选留言导入弹窗
+    openUserCommentDialog() {
+      if (!this.form.courseId || !this.form.videoId) {
+        this.$message.warning('请先保存课节后再查看用户留言')
+        return
+      }
+      this.userCommentDialog.courseId = this.form.courseId
+      this.userCommentDialog.videoId = this.form.videoId
+      this.userCommentDialog.title = `用户留言 - ${this.form.title || ''}`
+      this.userCommentDialog.open = true
+    },
+    onUserCommentDialogOpened() {
+      this.$nextTick(() => {
+        if (this.$refs.userCourseSectionComment) {
+          this.$refs.userCourseSectionComment.handleQuery()
+        }
+      })
+    },
+    openCommentImportDialog() {
+      this.commentImportDialog.visible = true;
+      this.commentImportDialog.file = null;
+      this.commentImportDialog.fileList = [];
+      this.commentImportDialog.courseId = this.form.courseId;
+      this.commentImportDialog.videoId = this.form.videoId;
+      // 加载历史留言
+      listFeaturedComments(this.form.courseId, this.form.videoId).then(res => {
+        this.commentImportDialog.historyList = res.rows || [];
+      });
+    },
+    // 选择导入文件
+    handleCommentFileChange(file) {
+      this.commentImportDialog.file = file.raw;
+    },
+    // 删除精选留言
+    handleDeleteComment(row) {
+      this.$confirm('确认删除该条精选留言?', '提示', {type: 'warning'}).then(() => {
+        delUserCourseComment(row.commentId).then(res => {
+          if (res.code === 200) {
+            this.$message.success('删除成功');
+            // 刷新历史列表
+            listFeaturedComments(this.commentImportDialog.courseId, this.commentImportDialog.videoId).then(res2 => {
+              this.commentImportDialog.historyList = res2.rows || [];
+            });
+          } else {
+            this.$message.error(res.msg || '删除失败');
+          }
+        });
+      }).catch(() => {});
+    },
+    // 下载Word导入模板
+    handleDownloadTemplate() {
+      downloadCommentImportTemplate().then(res => {
+        const blob = new Blob([res], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
+        const url = window.URL.createObjectURL(blob);
+        const link = document.createElement('a');
+        link.href = url;
+        link.download = '精选留言导入模板.xlsx';
+        link.click();
+        window.URL.revokeObjectURL(url);
+      });
+    },
+    // 确认导入精选留言
+    handleImportComments() {
+      if (!this.commentImportDialog.file) {
+        this.$message.warning('请先选择要导入的Excel文件');
+        return;
+      }
+      this.commentImportDialog.loading = true;
+      importComments(this.commentImportDialog.courseId, this.commentImportDialog.videoId, this.commentImportDialog.file).then(res => {
+        if (res.code === 200) {
+          this.$message.success(res.msg);
+          // 刷新历史列表
+          listFeaturedComments(this.commentImportDialog.courseId, this.commentImportDialog.videoId).then(res2 => {
+            this.commentImportDialog.historyList = res2.rows || [];
+          });
+        } else {
+          this.$message.error(res.msg);
+        }
+      }).finally(() => {
+        this.commentImportDialog.loading = false;
+      });
+    },
+
   }
+}
 </script>
+<style scoped>
+.form-unit {
+  margin-left: 8px;
+  color: #606266;
+}
+.form-hint {
+  margin-left: 12px;
+  color: #909399;
+  font-size: 12px;
+}
+.batch-watch-tip {
+  margin: 0 0 16px 0;
+  color: #606266;
+  font-size: 13px;
+}
+.avatar-uploader-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.xsy-textarea >>> .el-textarea__inner {
+ background-color: #f5f5f5 !important;
+ resize: none !important;
+}
+</style>
 <style>
-  .avatar-uploader .el-upload {
-       border: 1px dashed #d9d9d9;
-       border-radius: 6px;
-       cursor: pointer;
-       position: relative;
-       overflow: hidden;
-     }
-     .avatar-uploader .el-upload:hover {
-       border-color: #409EFF;
-     }
-
-     .avatar-uploader-icon {
-       font-size: 28px;
-       color: #8c939d;
-       width: 150px;
-       height: 150px;
-       line-height: 150px;
-       text-align: center;
-     }
+.avatar-uploader .el-upload {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+}
+
+.avatar-uploader .el-upload:hover {
+  border-color: #409EFF;
+}
+
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 150px;
+  height: 150px;
+  line-height: 150px;
+  text-align: center;
+}
+
+
+.red:hover {
+  color: #dbdbdb !important;
+}
+
+.red {
+  background-color: #F56C6C !important;
+  color: #fff !important;
+}
 
 </style>

+ 246 - 0
src/views/components/course/userCourseSectionComment.vue

@@ -0,0 +1,246 @@
+<template>
+  <div class="app-container user-section-comment">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="72px">
+      <el-form-item label="用户昵称" prop="nickName">
+        <el-input
+          v-model="queryParams.nickName"
+          placeholder="请输入用户昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table v-loading="loading" :data="commentList" border>
+      <el-table-column label="用户昵称" prop="nickName" width="120" show-overflow-tooltip />
+      <el-table-column label="类型" width="72" align="center">
+        <template slot-scope="scope">
+          <el-tag size="mini" :type="scope.row.type === 2 ? 'info' : ''">
+            {{ scope.row.type === 2 ? '回复' : '评论' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="留言内容" min-width="360">
+        <template slot-scope="scope">
+          <div class="comment-content-cell">
+            <template v-for="(part, idx) in parseCommentContent(scope.row.content)">
+              <p v-if="part.kind === 'text'" :key="'t-' + scope.row.commentId + '-' + idx" class="comment-text">
+                {{ part.value }}
+              </p>
+              <el-image
+                v-else-if="part.kind === 'image'"
+                :key="'i-' + scope.row.commentId + '-' + idx"
+                class="comment-media comment-image"
+                :src="part.value"
+                :preview-src-list="[part.value]"
+                fit="contain"
+              />
+              <video
+                v-else-if="part.kind === 'video'"
+                :key="'v-' + scope.row.commentId + '-' + idx"
+                class="comment-media comment-video"
+                :src="part.value"
+                controls
+                preload="metadata"
+              />
+              <a
+                v-else-if="part.kind === 'link'"
+                :key="'l-' + scope.row.commentId + '-' + idx"
+                :href="part.value"
+                target="_blank"
+                rel="noopener"
+                class="comment-link"
+              >{{ part.value }}</a>
+            </template>
+            <span v-if="!scope.row.content" class="comment-empty">—</span>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="点赞" prop="likes" width="70" align="center" />
+      <el-table-column label="可见范围" width="100" align="center">
+        <template slot-scope="scope">
+          <el-tag size="mini" :type="scope.row.visibleAll === 1 ? 'success' : 'info'">
+            {{ scope.row.visibleAll === 1 ? '全部人可见' : '自己可见' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="留言时间" prop="createTime" width="160" align="center" />
+      <el-table-column label="操作" width="180" align="center" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            type="text"
+            size="mini"
+            :disabled="scope.row.visibleAll === 1"
+            @click="handleUpdateVisible(scope.row, 1)"
+          >全部人可见</el-button>
+          <el-button
+            type="text"
+            size="mini"
+            :disabled="scope.row.visibleAll === 0 || scope.row.visibleAll == null"
+            @click="handleUpdateVisible(scope.row, 0)"
+          >自己可见</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import { listSectionUserComments, updateCommentVisibleAll } from '@/api/course/userCourseComment'
+
+const IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i
+const VIDEO_EXT = /\.(mp4|webm|ogg|mov|m4v|m3u8)(\?.*)?$/i
+
+export default {
+  name: 'UserCourseSectionComment',
+  props: {
+    courseId: {
+      type: [String, Number],
+      required: true
+    },
+    videoId: {
+      type: [String, Number],
+      required: true
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      total: 0,
+      commentList: [],
+      visibleUpdatingId: null,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        courseId: null,
+        videoId: null,
+        nickName: null
+      }
+    }
+  },
+  watch: {
+    courseId: {
+      immediate: true,
+      handler(val) {
+        this.queryParams.courseId = val
+      }
+    },
+    videoId: {
+      immediate: true,
+      handler(val) {
+        this.queryParams.videoId = val
+      }
+    }
+  },
+  methods: {
+    getList() {
+      if (!this.queryParams.courseId || !this.queryParams.videoId) {
+        return
+      }
+      this.loading = true
+      listSectionUserComments(this.queryParams).then(response => {
+        this.commentList = response.rows || []
+        this.total = response.total || 0
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.queryParams.nickName = null
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleUpdateVisible(row, visibleAll) {
+      const label = visibleAll === 1 ? '全部人可见' : '自己可见'
+      this.$confirm(`确认将该留言设置为「${label}」?`, '提示', { type: 'warning' }).then(() => {
+        this.visibleUpdatingId = row.commentId
+        updateCommentVisibleAll({
+          commentId: row.commentId,
+          visibleAll: visibleAll
+        }).then(() => {
+          this.$message.success('修改成功')
+          this.getList()
+        }).finally(() => {
+          this.visibleUpdatingId = null
+        })
+      }).catch(() => {})
+    },
+    parseCommentContent(content) {
+      if (!content || !String(content).trim()) {
+        return []
+      }
+      const lines = String(content).split(/\r?\n/).map(s => s.trim()).filter(Boolean)
+      const parts = []
+      lines.forEach(line => {
+        if (!/^https?:\/\//i.test(line)) {
+          parts.push({ kind: 'text', value: line })
+          return
+        }
+        if (VIDEO_EXT.test(line) || /\/comment\/video\//i.test(line)) {
+          parts.push({ kind: 'video', value: line })
+        } else if (IMAGE_EXT.test(line) || /\/comment\/image\//i.test(line)) {
+          parts.push({ kind: 'image', value: line })
+        } else {
+          parts.push({ kind: 'link', value: line })
+        }
+      })
+      return parts
+    }
+  }
+}
+</script>
+
+<style scoped>
+.user-section-comment {
+  padding: 0;
+}
+.comment-content-cell {
+  text-align: left;
+  line-height: 1.6;
+}
+.comment-text {
+  margin: 0 0 6px;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.comment-media {
+  display: block;
+  margin: 6px 0;
+  max-width: 280px;
+  border-radius: 4px;
+}
+.comment-image {
+  max-height: 160px;
+}
+.comment-video {
+  max-height: 200px;
+  background: #000;
+}
+.comment-link {
+  display: block;
+  margin: 4px 0;
+  color: #409eff;
+  word-break: break-all;
+}
+.comment-empty {
+  color: #909399;
+}
+</style>

+ 108 - 12
src/views/course/courseQuestionBank/QuestionBank.vue

@@ -29,23 +29,33 @@
           />
         </el-select>
       </el-form-item>
-<!--      <el-form-item label="状态" prop="status">-->
-<!--        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">-->
-<!--          <el-option-->
-<!--            v-for="dict in statusOptions"-->
-<!--            :key="dict.dictValue"-->
-<!--            :label="dict.dictLabel"-->
-<!--            :value="dict.dictValue"-->
-<!--          />-->
-<!--        </el-select>-->
-<!--      </el-form-item>-->
+		<el-form-item label="题目类别" prop="questionType">
+	    <el-select v-model="queryParams.questionType" placeholder="请选择类别" @change="changeCateType" clearable size="small">
+	      <el-option
+	        v-for="dict in questionRootTypeOptions"
+	        :key="dict.dictValue"
+	        :label="dict.dictLabel"
+	        :value="dict.dictValue"
+	      />
+	    </el-select>
+	  </el-form-item>
+      <el-form-item label="题目子类别" prop="questionSubType" label-width="100px">
+        <el-select v-model="queryParams.questionSubType" ref="typeSelect" placeholder="请选择子类别" clearable size="small">
+          <el-option
+            v-for="item in questionSubTypeOptions"
+            :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="handleQuery">搜索</el-button>
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
-    <el-table border v-loading="loading" ref="table"   :data="courseQuestionBankList" @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="55" align="center"/>
+    <el-table border v-loading="loading" ref="table"   :data="courseQuestionBankList" >
+<!--      <el-table-column type="selection" width="55" align="center"/>-->
       <el-table-column label="标题" align="center" prop="title" >
         <template slot-scope="scope">
           <el-tooltip class="item" effect="dark" :content="scope.row.title" placement="top">
@@ -60,6 +70,11 @@
           <dict-tag :options="typeOptions" :value="scope.row.type"/>
         </template>
       </el-table-column>
+	  <el-table-column label="题目类别" align="center" prop="questionType">
+	    <template slot-scope="scope">
+	      <dict-tag :options="questionTypeOptions" :value="scope.row.questionType"/>
+	    </template>
+	  </el-table-column>
       <el-table-column label="状态" align="center" prop="status">
         <template slot-scope="scope">
           <dict-tag :options="statusOptions" :value="scope.row.status"/>
@@ -183,6 +198,7 @@
 
 <script>
 import { listCourseQuestionBank, getCourseQuestionBank, delCourseQuestionBank, addCourseQuestionBank, updateCourseQuestionBank, exportCourseQuestionBank } from "@/api/course/courseQuestionBank";
+import { getCateListByPid, getCatePidList, listUserCourseCategory } from '@/api/course/userCourseCategory'
 
 export default {
   name: "CourseQuestionBank",
@@ -224,6 +240,9 @@ export default {
       total: 0,
       // 题库表格数据
       courseQuestionBankList: [],
+	  questionTypeOptions: [],
+      questionRootTypeOptions: [],
+      questionSubTypeOptions: [],
       // 弹出层标题
       title: "",
       // 是否显示弹出层
@@ -237,6 +256,8 @@ export default {
         type: null,
         status: null,
         question: null,
+		questionType: null,
+        questionSubType: null,
         answer: null,
       },
       // 表单参数
@@ -260,9 +281,84 @@ export default {
     this.getDicts("sys_company_status").then(response => {
       this.statusOptions = response.data;
     });
+	this.getDicts("sys_course_question_type").then(response => {
+	  this.questionTypeOptions = response.data;
+	});
+    this.getCategoryType()
   },
   methods: {
+    getOptionLabel(index) {
+      return this.alphabet[index];
+    },
+    getCategoryType() {
+      getCatePidList().then(response => {
+        this.questionRootTypeOptions = response.data
+      });
+    },
+    changeCateType(val) {
+      this.questionSubTypeOptions = []
+      this.queryParams.questionSubType = null
+      if (!val) {
+        return
+      }
+      getCateListByPid(val).then(response => {
+        this.questionSubTypeOptions = response.data
+      })
+    },
+    //选择此课
+    chooseQuestion(val){
+      this.$emit('questionBankResult',val)
+    },
+    //题目类型发生变化
+    changeType(){
+        this.selectedAnswers = [];
+        this.selectedAnswer = null;
+        this.question.forEach(item => {
+          item.isAnswer = 0; // 清除所有选项的选中状态
+        });
+        // 重置答案的显示值
+        this.form.answer = null;
+    },
+    //选择答案
+    handleAnswerChange(row, index) {
+      if (this.form.type === 1) {
+        // 单选模式:记录选中的答案并取消其他选项
+        this.selectedAnswer = row;
+        this.question.forEach((item) => {
+          if (item !== row) {
+            item.isAnswer = 0;
+          }
+        });
+        // 保存单选的答案
+        // this.form.answer = this.getOptionLabel(index);
+        this.form.answer = row.name;
+      } else if (this.form.type === 2) {
+
+        // 多选模式:将所有被选中的答案存储到 selectedAnswers 数组 先检查当前选项是否已经在选中答案列表中
+        if (row.isAnswer === 1) {
+          // 如果选中,将该答案加入到选中数组
+          this.selectedAnswers.push(row);
+        } else {
+          // 如果取消选中,从选中数组中移除
+          this.selectedAnswers = this.selectedAnswers.filter(item => item.indexId !== row.indexId);
+        }
+
+        // 对 selectedAnswers 进行排序 (假设按 row.indexId 排序)
+        this.selectedAnswers.sort((a, b) => a.indexId - b.indexId);
 
+        // 更新 form.answer,保存所有选中的答案
+        this.form.answer = this.selectedAnswers.map(item => item.name);
+      }
+
+    },
+    addRow(val) {
+      this.question.push({ name: '', isAnswer: 0,indexId:val});},
+    deleteRow(index) {
+      if (this.form.type === 1 && this.question[index] === this.selectedAnswer) {
+        this.selectedAnswer = null;
+      }
+      this.question.splice(index, 1);
+    },
     /** 查询题库列表 */
     getList() {
       this.loading = true;

+ 276 - 18
src/views/course/courseQuestionBank/index.vue

@@ -29,6 +29,28 @@
           />
         </el-select>
       </el-form-item>
+	  <el-form-item label="题目类别" prop="questionType">
+	    <el-select v-model="queryParams.questionType" ref="typeSelect" placeholder="请选择类别" @change="changeCateType" clearable size="small">
+	      <el-option
+          v-for="item in questionRootTypeOptions"
+          :key="item.dictValue"
+          :label="item.dictLabel"
+          :value="item.dictValue"
+          />
+	    </el-select>
+	  </el-form-item>
+    <el-form-item label="题目子类别" prop="questionSubType" label-width="100px">
+      <el-select v-model="queryParams.questionSubType" ref="typeSelect" placeholder="请选择子类别" clearable size="small">
+        <el-option
+          v-for="item in questionSubTypeOptions"
+          :key="item.dictValue"
+          :label="item.dictLabel"
+          :value="item.dictValue"
+        />
+      </el-select>
+    </el-form-item>
+
+
 <!--      <el-form-item label="状态" prop="status">-->
 <!--        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">-->
 <!--          <el-option-->
@@ -89,6 +111,16 @@
           v-hasPermi="['course:courseQuestionBank:export']"
         >导出</el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button
+          plain
+          type="info"
+          icon="el-icon-upload2"
+          size="mini"
+          @click="handleImport"
+          v-hasPermi="['course:courseQuestionBank:importData']"
+        >导入</el-button>
+      </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
@@ -100,13 +132,23 @@
           <dict-tag :options="typeOptions" :value="scope.row.type"/>
         </template>
       </el-table-column>
+	  <el-table-column label="题目类别" align="center" prop="questionType">
+	    <template slot-scope="scope">
+        <el-tag v-if="scope.row.questionType">{{ getCategoryName(scope.row.questionType) }}</el-tag>
+	    </template>
+	  </el-table-column>
+    <el-table-column label="题目子类别" align="center" prop="questionType">
+      <template slot-scope="scope">
+        <el-tag v-if="scope.row.questionSubType">{{ getCategoryName(scope.row.questionSubType) }}</el-tag>
+      </template>
+    </el-table-column>
       <el-table-column label="状态" align="center" prop="status">
         <template slot-scope="scope">
           <dict-tag :options="statusOptions" :value="scope.row.status"/>
         </template>
       </el-table-column>
       <el-table-column label="排序" align="center" prop="sort" />
-      <el-table-column label="答案" align="center" prop="answer" />
+      <el-table-column v-if="!hideAnswerFields" label="答案" align="center" prop="answer" />
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button
@@ -137,15 +179,15 @@
 
     <!-- 添加或修改题库对话框 -->
     <el-dialog :title="title" :visible.sync="open" width="1000px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+      <el-form ref="form" :model="form" :rules="dynamicRules" label-width="80px">
         <el-form-item label="标题" prop="title">
           <el-input v-model="form.title" placeholder="请输入问题" />
         </el-form-item>
         <el-form-item label="排序" prop="sort">
           <el-input-number v-model="form.sort"  :min="0" label="请输入排序"></el-input-number>
         </el-form-item>
-        <el-form-item label="题目类别 " prop="type">
-          <el-select v-model="form.type" placeholder="请选择题目类别" @change="changeType">
+        <el-form-item label="类别 " prop="type">
+          <el-select v-model="form.type" placeholder="请选择类别" @change="changeType">
             <el-option
               v-for="dict in typeOptions"
               :key="dict.dictValue"
@@ -154,6 +196,24 @@
             ></el-option>
           </el-select>
         </el-form-item>
+		<el-form-item label="题目类别 " prop="questionType">
+		  <el-select v-model="form.questionType" clearable placeholder="请选择题目类别" @change="changeCateType">
+        <el-option
+          v-for="item in questionRootTypeOptions"
+          :key="item.dictValue"
+          :label="item.dictLabel"
+          :value="item.dictValue"
+        />
+		  </el-select>
+      <el-select v-model="form.questionSubType" clearable placeholder="请选择题目子类别" >
+        <el-option
+          v-for="item in questionSubTypeOptions"
+          :key="item.dictValue"
+          :label="item.dictLabel"
+          :value="item.dictValue"
+        />
+      </el-select>
+		</el-form-item>
         <el-form-item label="状态">
           <el-radio-group v-model="form.status">
             <el-radio
@@ -167,6 +227,7 @@
         <el-alert v-if="!form.type" title="选择题目类别后添加选项" type="warning" show-icon></el-alert>
 
         <el-form-item label="选项"  v-if="form.type">
+          <el-button @click="addRow" size="mini" type="primary">新增选项</el-button>
           <el-table border :data="question"  :cell-style="{ textAlign: 'center' }"		 :header-cell-style="{textAlign: 'center'}"   >
             <el-table-column label="序号" width="65px" >
               <template slot-scope="scope">
@@ -178,7 +239,7 @@
                 <el-input v-model="scope.row.name" :placeholder="getOptionLabel(scope.$index) + ':' + '请输入标题'" ></el-input>
               </template>
             </el-table-column>
-            <el-table-column label="是否为答案" prop="isWrite" >
+            <el-table-column v-if="!hideAnswerFields" label="是否为答案" prop="isWrite" >
               <template slot-scope="scope">
                 <el-tooltip
                   v-if="!scope.row.name"
@@ -207,17 +268,14 @@
             <el-table-column label="操作">
               <template slot-scope="scope">
                 <el-button @click="deleteRow(scope.$index)"   size="mini" type="text" v-if="question.length>1">删除</el-button>
-                <el-button @click="addRow(scope.$index+1)" size="mini" type="text" >新增</el-button>
               </template>
             </el-table-column>
           </el-table>
         </el-form-item>
-        <el-form-item label="答案:" prop="answer">
-          <template slot-scope="scope">
-            <span style="background-color: #faedc9; font-size: 20px;">
-              {{ form.answer }}
-            </span>
-          </template>
+        <el-form-item v-if="!hideAnswerFields" label="答案:" prop="answer">
+          <span style="background-color: #faedc9; font-size: 20px;">
+            {{ form.answer }}
+          </span>
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
@@ -225,11 +283,60 @@
         <el-button @click="cancel">取 消</el-button>
       </div>
     </el-dialog>
+
+    <!-- 导入 -->
+    <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
+      <el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
+        <i class="el-icon-upload"></i>
+        <div class="el-upload__text">
+          将文件拖到此处,或
+          <em>点击上传</em>
+        </div>
+        <div class="el-upload__tip" slot="tip">
+          <el-link type="info" style="font-size:12px" @click="importTemplate">下载模板</el-link>
+        </div>
+        <div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入“xls”或“xlsx”格式文件!</div>
+      </el-upload>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" :loading="upload.isUploading" :disabled="upload.isUploading" @click="submitFileForm">确 定</el-button>
+        <el-button @click="upload.open = false">取 消</el-button>
+      </div>
+    </el-dialog>
+    <el-dialog title="导入结果" :close-on-press-escape="false" :close-on-click-modal="false" :visible.sync="importMsgOpen" width="500px" append-to-body>
+      <div class="import-msg" v-html="importMsg">
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="importMsgOpen = false">关 闭</el-button>
+        <el-button
+          v-if="failList && failList.length > 0"
+          type="primary"
+          @click="handleExportFailList(failList)"
+          :loading="exportFailLoading"
+        >导出失败信息</el-button>
+      </div>
+    </el-dialog>
+
   </div>
 </template>
 
 <script>
-import { listCourseQuestionBank, getCourseQuestionBank, delCourseQuestionBank, addCourseQuestionBank, updateCourseQuestionBank, exportCourseQuestionBank } from "@/api/course/courseQuestionBank";
+import {
+  listCourseQuestionBank,
+  getCourseQuestionBank,
+  delCourseQuestionBank,
+  addCourseQuestionBank,
+  updateCourseQuestionBank,
+  exportCourseQuestionBank,
+  importTemplate,
+  exportFail
+} from "@/api/course/courseQuestionBank";
+import { getToken } from "@/utils/auth";
+import {
+  listUserCourseCategory,
+  getCatePidList,
+  getCateListByPid
+} from '@/api/course/userCourseCategory'
+import { getConfigByKey } from '@/api/system/config'
 
 export default {
   name: "CourseQuestionBank",
@@ -237,6 +344,22 @@ export default {
     alphabet() {
       return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
     },
+    /** 点播配置:不校验答案时隐藏列表/表单中的答案相关项(与 validateAnswerWhenWatch 为 false / "0" 一致) */
+    hideAnswerFields() {
+      const cfg = this.courseConfig;
+      if (!cfg || !Object.prototype.hasOwnProperty.call(cfg, 'validateAnswerWhenWatch')) {
+        return false;
+      }
+      const v = cfg.validateAnswerWhenWatch;
+      return v === false || v === '0' || v === 0 || v === 'false';
+    },
+    dynamicRules() {
+      const r = { ...this.rules };
+      if (this.hideAnswerFields) {
+        delete r.answer;
+      }
+      return r;
+    },
   },
   watch: {
   },
@@ -244,7 +367,9 @@ export default {
   },
   data() {
     return {
-
+      /** course.config 解析后的对象 */
+      courseConfig: {},
+      exportFailLoading: false,
       //单选
       selectedAnswer:null,
       //多选
@@ -254,6 +379,9 @@ export default {
       statusOptions: [],
       question:[
         { name: "", isAnswer: 0, indexId:0},
+        { name: "", isAnswer: 0, indexId:1},
+        { name: "", isAnswer: 0, indexId:2},
+        { name: "", isAnswer: 0, indexId:3},
       ],
       // 遮罩层
       loading: true,
@@ -271,6 +399,9 @@ export default {
       total: 0,
       // 题库表格数据
       courseQuestionBankList: [],
+	  questionTypeOptions: [],
+      questionRootTypeOptions: [],
+	    questionSubTypeOptions: [],
       // 弹出层标题
       title: "",
       // 是否显示弹出层
@@ -284,6 +415,8 @@ export default {
         type: null,
         status: null,
         question: null,
+		    questionType:null,
+        questionSubType: null,
         answer: null,
       },
       // 表单参数
@@ -296,24 +429,96 @@ export default {
         status: [{ required: true, message: "不能为空", trigger: "blur" }],
         question: [{ required: true, message: "不能为空", trigger: "blur" }],
         answer: [{ required: true, message: "不能为空", trigger: "blur" }],
+      },
+      // 导入
+      importMsgOpen:false,
+      failList:[],
+      importMsg: '',
+      upload: {
+        // 是否显示弹出层(文件导入)
+        open: false,
+        // 弹出层标题(文件导入)
+        title: "",
+        // 是否禁用上传
+        isUploading: false,
+        // 设置上传的请求头部
+        headers: { Authorization: "Bearer " + getToken() },
+        // 上传的地址
+        url: process.env.VUE_APP_BASE_API + "/course/courseQuestionBank/importData",
       }
     };
   },
   created() {
     this.getList();
+    getConfigByKey('course.config').then(response => {
+      if (response.data && response.data.configValue) {
+        try {
+          this.courseConfig = JSON.parse(response.data.configValue) || {};
+        } catch (e) {
+          this.courseConfig = {};
+        }
+      }
+    }).catch(() => { this.courseConfig = {}; });
     this.getDicts("sys_course_temp_type").then(response => {
       this.typeOptions = response.data;
     });
     this.getDicts("sys_company_status").then(response => {
       this.statusOptions = response.data;
     });
+    this.getCategoryTree()
   },
   methods: {
     getOptionLabel(index) {
       return this.alphabet[index];
     },
+// 导出失败信息
+    handleExportFailList(failList) {
+      if (!failList || failList.length === 0) {
+        this.msgWarning("没有失败信息可导出");
+        return;
+      }
 
 
+      this.exportFailLoading = true;
+      this.$confirm('是否确认导出失败信息?', "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "info"
+      }).then(() => {
+        // 调用导出失败信息的API
+        exportFail(failList).then(response => {
+          this.download(response.msg);
+          this.msgSuccess("失败信息导出成功");
+          this.exportFailLoading = false;
+        }).catch(() => {
+          this.msgError("失败信息导出失败");
+          this.exportFailLoading = false;
+        });
+      }).catch(() => {
+        this.exportFailLoading = false;
+      });
+    },
+
+    // 分类树
+    getCategoryTree() {
+      listUserCourseCategory().then(response => {
+        this.questionTypeOptions = response.data
+      });
+      getCatePidList().then(response => {
+        this.questionRootTypeOptions = response.data
+      });
+    },
+    changeCateType(val) {
+      if (!val) {
+        return
+      }
+      getCateListByPid(val).then(response => {
+        this.questionSubTypeOptions = response.data
+      })
+    },
+    getCategoryName(id) {
+      return this.questionTypeOptions.find(item => item.cateId === id)?.cateName || '';
+    },
     //题目类型发生变化
     changeType(){
         this.selectedAnswers = [];
@@ -356,8 +561,8 @@ export default {
       }
 
     },
-    addRow(val) {
-      this.question.push({ name: '', isAnswer: 0,indexId:val});},
+    addRow() {
+      this.question.push({ name: '', isAnswer: 0,indexId: this.question.length});},
     deleteRow(index) {
       if (this.form.type === 1 && this.question[index] === this.selectedAnswer) {
         this.selectedAnswer = null;
@@ -377,6 +582,7 @@ export default {
     cancel() {
       this.open = false;
       this.reset();
+      this.changeCateType(this.queryParams.questionType)
     },
     // 表单重置
     reset() {
@@ -392,6 +598,12 @@ export default {
         createBy: null
       };
       this.resetForm("form");
+      this.question = [
+        { name: "", isAnswer: 0, indexId:0},
+        { name: "", isAnswer: 0, indexId:1},
+        { name: "", isAnswer: 0, indexId:2},
+        { name: "", isAnswer: 0, indexId:3},
+      ]
     },
     /** 搜索按钮操作 */
     handleQuery() {
@@ -412,6 +624,7 @@ export default {
     /** 新增按钮操作 */
     handleAdd() {
       this.reset();
+      this.questionSubTypeOptions = []
       this.open = true;
       this.title = "添加题库";
     },
@@ -421,7 +634,7 @@ export default {
       const id = row.id || this.ids
       getCourseQuestionBank(id).then(response => {
         this.form = response.data;
-
+        this.changeCateType(this.form.questionType)
 
         //初始化多选的选择结果,单选的选择的时候,就取消其他的了 selectedAnswers
         if (this.form.type===2) {
@@ -438,6 +651,23 @@ export default {
     submitForm() {
       this.$refs["form"].validate(valid => {
         if (valid) {
+
+          if (this.question.some(q => q.name === "")) {
+            this.$message.error("不能存在空选项")
+            return
+          }
+
+          if (this.hideAnswerFields) {
+            this.question.forEach(q => { q.isAnswer = 0 });
+            this.selectedAnswer = null;
+            this.selectedAnswers = [];
+            if (this.form.type === 2) {
+              this.form.answer = [];
+            } else {
+              this.form.answer = '';
+            }
+          }
+
           this.form.question=JSON.stringify(this.question)
 
           if (this.form.type===2){
@@ -489,7 +719,35 @@ export default {
           this.download(response.msg);
           this.exportLoading = false;
         }).catch(() => {});
-    }
+    },
+    // 下载模板
+    importTemplate() {
+      importTemplate().then((response) => {
+        this.download(response.msg);
+      });
+    },
+    // 导入
+    handleImport() {
+      this.upload.title = "导入题目";
+      this.upload.open = true;
+    },
+    submitFileForm() {
+      this.$refs.upload.submit();
+    },
+    // 文件上传中处理
+    handleFileUploadProgress(event, file, fileList) {
+      this.upload.isUploading = true;
+    },
+    // 文件上传成功处理
+    handleFileSuccess(response, file, fileList) {
+      this.upload.open = false;
+      this.upload.isUploading = false;
+      this.$refs.upload.clearFiles();
+      this.importMsgOpen=true;
+      this.importMsg = response.data.message
+      this.failList = response.data.failList
+      this.getList();
+    },
   }
 };
 </script>

+ 4 - 4
src/views/course/userCourse/public.vue

@@ -601,9 +601,9 @@ export default {
     });
 
 
-    // getSelectableRange().then(e => {
-    //   this.startTimeRange = e.data;
-    // })
+    getSelectableRange().then(e => {
+      this.startTimeRange = e.data;
+    })
     // this.getTreeselect();
     this.getDicts("sys_spec_show").then(response => {
       this.specShowOptions = response.data;
@@ -632,7 +632,7 @@ export default {
 
     },
     talentMethod(query) {
-      if (query !== '') {
+      if (/^\d{11}$/.test(query)) {
         this.talentParam.phone = query;
         listBySearch(this.talentParam).then(response => {
           this.talentList = response.data;