Ver Fonte

版本更新

吴树波 há 2 meses atrás
pai
commit
a8365a33b2
36 ficheiros alterados com 13326 adições e 46 exclusões
  1. 18 1
      src/api/course/courseQuestionBank.js
  2. 53 0
      src/api/course/fsUserCoursePeriodDays.js
  3. 54 0
      src/api/course/userCourseCamp.js
  4. 176 0
      src/api/course/userCoursePeriod.js
  5. 54 0
      src/api/course/videoResource.js
  6. 54 0
      src/api/qw/material.js
  7. 53 0
      src/api/qw/materialGroup.js
  8. 105 0
      src/api/qw/sop.js
  9. 88 0
      src/api/qw/sopTemp.js
  10. 53 0
      src/api/qw/userVideo.js
  11. 53 0
      src/api/sop/companySopRole.js
  12. BIN
      src/assets/logo/logo.png
  13. BIN
      src/assets/logo/logo2.png
  14. 25 0
      src/router/index.js
  15. 166 25
      src/views/course/courseQuestionBank/index.vue
  16. 126 0
      src/views/course/userCoursePeriod/batchRedPacket.vue
  17. 1711 0
      src/views/course/userCoursePeriod/index.vue
  18. 173 0
      src/views/course/userCoursePeriod/redPacket.vue
  19. 338 0
      src/views/course/userCoursePeriod/statistics.vue
  20. 1901 0
      src/views/course/videoResource/index.vue
  21. 427 0
      src/views/qw/friendMaterial/index.vue
  22. 9 1
      src/views/qw/sop/ImageUpload.vue
  23. 331 0
      src/views/qw/sopTemp/addAiChatTemp.vue
  24. 981 0
      src/views/qw/sopTemp/addSopTemp.vue
  25. 734 0
      src/views/qw/sopTemp/addSopTempOld.vue
  26. 194 0
      src/views/qw/sopTemp/addTemp.vue
  27. 680 0
      src/views/qw/sopTemp/index.vue
  28. 177 0
      src/views/qw/sopTemp/sopTemp.vue
  29. 327 0
      src/views/qw/sopTemp/updateAiChatTemp.vue
  30. 1741 0
      src/views/qw/sopTemp/updateSopTemp.vue
  31. 906 0
      src/views/qw/sopTemp/updateSopTemp2.vue
  32. 664 0
      src/views/qw/sopTemp/updateSopTempOld.vue
  33. 201 0
      src/views/qw/sopTemp/updateTemp.vue
  34. 274 0
      src/views/qw/userVideo/userVideo.vue
  35. 310 0
      src/views/sop/companySopRole/index.vue
  36. 169 19
      src/views/system/config/config.vue

+ 18 - 1
src/api/course/courseQuestionBank.js

@@ -50,4 +50,21 @@ export function exportCourseQuestionBank(query) {
     method: 'get',
     params: query
   })
-}
+}
+
+// 下载模板
+export function importTemplate() {
+  return request({
+    url: '/course/courseQuestionBank/importTemplate',
+    method: 'get'
+  })
+}
+
+// 根据ids查询
+export function getByIds(query) {
+  return request({
+    url: '/course/courseQuestionBank/getByIds',
+    method: 'get',
+    params: query
+  })
+}

+ 53 - 0
src/api/course/fsUserCoursePeriodDays.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询营期课程列表
+export function listFsUserCoursePeriodDays(query) {
+  return request({
+    url: '/course/fsUserCoursePeriodDays/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询营期课程详细
+export function getFsUserCoursePeriodDays(id) {
+  return request({
+    url: '/course/fsUserCoursePeriodDays/' + id,
+    method: 'get'
+  })
+}
+
+// 新增营期课程
+export function addFsUserCoursePeriodDays(data) {
+  return request({
+    url: '/course/fsUserCoursePeriodDays',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改营期课程
+export function updateFsUserCoursePeriodDays(data) {
+  return request({
+    url: '/course/fsUserCoursePeriodDays',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除营期课程
+export function delFsUserCoursePeriodDays(id) {
+  return request({
+    url: '/course/fsUserCoursePeriodDays/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出营期课程
+export function exportFsUserCoursePeriodDays(query) {
+  return request({
+    url: '/course/fsUserCoursePeriodDays/export',
+    method: 'get',
+    params: query
+  })
+}

+ 54 - 0
src/api/course/userCourseCamp.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 训练营列表
+export function listCamp(query) {
+  return request({
+    url: '/course/trainingCamp/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 新增训练营
+export function addCamp(data) {
+  return request({
+    url: '/course/trainingCamp',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改训练营
+export function editCamp(data) {
+  return request({
+    url: '/course/trainingCamp',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除训练营
+export function delCamp(trainingCampId) {
+  return request({
+    url: `/course/trainingCamp/${trainingCampId}`,
+    method: 'delete'
+  })
+}
+
+// 复制训练营
+export function copyCamp(trainingCampId) {
+  return request({
+    url: `/course/trainingCamp/copy/${trainingCampId}`,
+    method: 'post'
+  })
+}
+
+// 获取训练营选项列表
+export function getCampListLikeName(query) {
+  return request({
+    url: '/course/trainingCamp/getCampListLikeName',
+    method: 'get',
+    params: query
+  })
+}
+

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

@@ -0,0 +1,176 @@
+import request from '@/utils/request'
+
+// 查询会员营期列表
+export function listPeriod(query) {
+  return request({
+    url: '/course/period/list',
+    method: 'get',
+    params: query
+  })
+}
+// 查询会员营期列表
+export function getDays(query) {
+  return request({
+    url: '/course/period/getDays',
+    method: 'get',
+    params: query
+  })
+}
+
+// 自定义查询主列表分页
+export function pagePeriod(data) {
+  return request({
+    url: '/course/period/page',
+    method: 'post',
+    data: data
+  })
+}
+
+// 查询会员营期详细
+export function getPeriod(periodId) {
+  return request({
+    url: '/course/period/' + periodId,
+    method: 'get'
+  })
+}
+
+// 新增会员营期
+export function addPeriod(data) {
+  return request({
+    url: '/course/period',
+    method: 'post',
+    data: data
+  })
+}
+
+// 新增会员营期
+export function addCourse(data) {
+  return request({
+    url: '/course/period/addCourse',
+    method: 'post',
+    data: data
+  })
+}
+
+// 新增会员营期
+export function updateCourseTime(data) {
+  return request({
+    url: '/course/period/updateCourseTime',
+    method: 'post',
+    data: data
+  })
+}
+// 新增会员营期
+export function updateCourseDate(data) {
+  return request({
+    url: '/course/period/updateCourseDate',
+    method: 'post',
+    data: data
+  })
+}
+// 新增会员营期
+export function updateListCourseData(data) {
+  return request({
+    url: '/course/period/updateListCourseData',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改会员营期
+export function updatePeriod(data) {
+  return request({
+    url: '/course/period',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除会员营期
+export function delPeriod(periodId) {
+  return request({
+    url: '/course/period/' + periodId,
+    method: 'delete'
+  })
+}
+
+// 导出会员营期
+export function exportPeriod(query) {
+  return request({
+    url: '/course/period/export',
+    method: 'get',
+    params: query
+  })
+}
+
+// 根据营期id获取公司红包金额列表
+export function getPeriodCompanyList(query) {
+  return request({
+    url: '/course/period/companyList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 按照课程批量设置红包金额
+export function batchSaveRedPacket(data) {
+  return request({
+    url: '/course/period/batchRedPacket',
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取设置红包金额列表展示
+export function getPeriodRedPacketList(query) {
+  return request({
+    url: '/course/period/redPacketList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 按照营期批量设置红包金额
+export function batchSaveRedPacketByPeriod(data) {
+  return request({
+    url: '/course/period/batchRedPacket/byPeriod',
+    method: 'post',
+    data: data
+  })
+}
+
+// 按照营期、按课程统计
+export function periodCountSelect(data) {
+  return request({
+    url: '/course/period/periodCount',
+    method: 'post',
+    data: data
+  })
+}
+
+// 获取营期选项列表
+export function getPeriodListLikeName(query) {
+  return request({
+    url: '/course/period/getPeriodListLikeName',
+    method: 'get',
+    params: query
+  })
+}
+
+// 营期课程上移下移
+export function periodCourseMove(data) {
+  return request({
+    url: '/course/period/courseMove',
+    method: 'put',
+    params: data
+  })
+}
+
+// 结束营期
+export function closePeriod(query) {
+  return request({
+    url: '/course/period/closePeriod',
+    method: 'post',
+    params: query
+  })
+}

+ 54 - 0
src/api/course/videoResource.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 查询视频资源列表
+export function listVideoResource(query) {
+  return request({
+    url: '/course/videoResource/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询视频资源详细
+export function getVideoResource(resourceId) {
+  return request({
+    url: '/course/videoResource/' + resourceId,
+    method: 'get'
+  })
+}
+
+// 新增视频资源
+export function addVideoResource(data) {
+  return request({
+    url: '/course/videoResource',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改视频资源
+export function updateVideoResource(data) {
+  return request({
+    url: '/course/videoResource',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除视频资源
+export function deleteVideoResource(resourceId) {
+  return request({
+    url: '/course/videoResource/' + resourceId,
+    method: 'delete'
+  })
+}
+
+// 批量新增视频资源
+export function batchAddVideoResource(data) {
+  return request({
+    url: '/course/videoResource/batchAddVideoResource',
+    method: 'post',
+    data: data
+  })
+}
+

+ 54 - 0
src/api/qw/material.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 查询素材库列表
+export function listMaterial(query) {
+  return request({
+    url: '/qw/material/list',
+    method: 'get',
+    params: query
+  })
+}
+
+
+// 查询素材库详细
+export function getMaterial(materialId) {
+  return request({
+    url: '/qw/material/' + materialId,
+    method: 'get'
+  })
+}
+
+// 新增素材库
+export function addMaterial(data) {
+  return request({
+    url: '/qw/material',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改素材库
+export function updateMaterial(data) {
+  return request({
+    url: '/qw/material',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除素材库
+export function delMaterial(materialId) {
+  return request({
+    url: '/qw/material/' + materialId,
+    method: 'delete'
+  })
+}
+
+// 导出素材库
+export function exportMaterial(query) {
+  return request({
+    url: '/qw/material/export',
+    method: 'get',
+    params: query
+  })
+}

+ 53 - 0
src/api/qw/materialGroup.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询企业微信素材分组列表
+export function listMaterialGroup(query) {
+  return request({
+    url: '/qw/materialGroup/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询企业微信素材分组详细
+export function getMaterialGroup(materialGroupId) {
+  return request({
+    url: '/qw/materialGroup/' + materialGroupId,
+    method: 'get'
+  })
+}
+
+// 新增企业微信素材分组
+export function addMaterialGroup(data) {
+  return request({
+    url: '/qw/materialGroup',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改企业微信素材分组
+export function updateMaterialGroup(data) {
+  return request({
+    url: '/qw/materialGroup',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除企业微信素材分组
+export function delMaterialGroup(materialGroupId) {
+  return request({
+    url: '/qw/materialGroup/' + materialGroupId,
+    method: 'delete'
+  })
+}
+
+// 导出企业微信素材分组
+export function exportMaterialGroup(query) {
+  return request({
+    url: '/qw/materialGroup/export',
+    method: 'get',
+    params: query
+  })
+}

+ 105 - 0
src/api/qw/sop.js

@@ -8,6 +8,92 @@ export function listSop(query) {
     params: query
   })
 }
+// 查询企微sop模板列表
+export function listAiChatSop(query) {
+  return request({
+    url: '/qw/sop/listAiChatSop',
+    method: 'get',
+    params: query
+  })
+}
+
+
+export function courseList() {
+  return request({
+    url: '/qw/sop/courseList',
+    method: 'get',
+  })
+}
+export function videoList(id) {
+  return request({
+    url: '/qw/sop/videoList/' + id,
+    method: 'get'
+  })
+}
+// 查询企微sop详细
+export function getSop(id) {
+  return request({
+    url: '/qw/sop/' + id,
+    method: 'get'
+  })
+}
+
+// 新增企微sop
+export function addSop(data) {
+  return request({
+    url: '/qw/sop',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改企微sop
+export function updateSop(data) {
+  return request({
+    url: '/qw/sop',
+    method: 'put',
+    data: data
+  })
+}
+//修改企微sop自动创建时间
+export function updateAutoSopTime(data) {
+  return request({
+    url: '/qw/sop/updateAutoSopTime',
+    method: 'post',
+    data: data
+  })
+}
+
+//修改状态
+export function updateSopStatus(data) {
+  return request({
+    url: '/qw/sop/updateSopStatus',
+    method: 'post',
+    data: data
+  })
+}
+// 删除企微sop
+export function delSop(id) {
+  return request({
+    url: '/qw/sop/' + id,
+    method: 'delete'
+  })
+}
+
+//批量执行企微sop
+export function executeSop(ids) {
+  return request({
+    url: '/qw/sop/executeSop/' + ids,
+    method: 'put'
+  })
+}
+//批量执行企微sop先改状态
+export function updateStatus(ids) {
+  return request({
+    url: '/qw/sop/updateStatus/' + ids,
+    method: 'get'
+  })
+}
 // 导出企微sop
 export function exportSop(query) {
   return request({
@@ -16,3 +102,22 @@ export function exportSop(query) {
     params: query
   })
 }
+
+// 导出企微sop
+export function getSopVoiceList(params) {
+  return request({
+    url: '/qw/sop/getSopVoiceList',
+    method: 'get',
+    params
+  })
+}
+
+
+// 修改企微sop
+export function updateSopQwUser(data) {
+  return request({
+    url: '/qw/sop/updateSopQwUser',
+    method: 'post',
+    data: data
+  })
+}

+ 88 - 0
src/api/qw/sopTemp.js

@@ -8,6 +8,14 @@ export function listSopTemp(query) {
     params: query
   })
 }
+// 查询sop模板列表
+export function redList(id) {
+  return request({
+    url: '/qw/sopTemp/redList',
+    method: 'get',
+    params: {id}
+  })
+}
 
 // 查询sop模板详细
 export function getSopTemp(id) {
@@ -16,6 +24,37 @@ export function getSopTemp(id) {
     method: 'get'
   })
 }
+export function dayListFun(id) {
+  return request({
+    url: '/qw/sopTemp/dayList',
+    method: 'get',
+    params: {id}
+  })
+}
+// 查询sop模板详细
+export function sortDay(list) {
+  return request({
+    url: '/qw/sopTemp/sortDay',
+    method: 'post',
+    data: list
+  })
+}
+// 查询sop模板详细
+export function selectRulesInfo(id, name, dayNum) {
+  return request({
+    url: '/qw/sopTemp/selectRulesInfo',
+    method: 'get',
+    params: {id, name, dayNum}
+  })
+}
+// 查询sop模板详细
+export function delRules(id, name, dayNum) {
+  return request({
+    url: '/qw/sopTemp/delRules',
+    method: 'get',
+    params: {id, name, dayNum}
+  })
+}
 
 // 新增sop模板
 export function addSopTemp(data) {
@@ -25,7 +64,48 @@ export function addSopTemp(data) {
     data: data
   })
 }
+// 新增sop模板
+export function updateRedPackage(data) {
+  return request({
+    url: '/qw/sopTemp/updateRedPackage',
+    method: 'post',
+    data: data
+  })
+}
+// 新增sop模板
+export function addTemp(data) {
+  return request({
+    url: '/qw/sopTemp/add',
+    method: 'post',
+    data: data
+  })
+}
 
+// 新增sop模板
+export function copyTemplate(data) {
+  return request({
+    url: '/qw/sopTemp/copyTemplate',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改sop模板
+export function updateTemp(data) {
+  return request({
+    url: '/qw/sopTemp/update',
+    method: 'post',
+    data: data
+  })
+}
+// 修改sop模板
+export function addOrUpdateSetting(data) {
+  return request({
+    url: '/qw/sopTemp/addOrUpdateSetting',
+    method: 'post',
+    data: data
+  })
+}
 // 修改sop模板
 export function updateSopTemp(data) {
   return request({
@@ -60,3 +140,11 @@ export function exportSopTemp(query) {
     params: query
   })
 }
+
+// 导出sop模板
+export function getSelectableRange() {
+  return request({
+    url: '/qw/sopTemp/getSelectableRange',
+    method: 'get'
+  })
+}

+ 53 - 0
src/api/qw/userVideo.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询企业微信的视频号列表
+export function listQwUserVideo(query) {
+  return request({
+    url: '/qw/userVideo/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询企业微信的视频号详细
+export function getQwUserVideo(id) {
+  return request({
+    url: '/qw/userVideo/' + id,
+    method: 'get'
+  })
+}
+
+// 新增企业微信的视频号
+export function addQwUserVideo(data) {
+  return request({
+    url: '/qw/userVideo',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改企业微信的视频号
+export function updateQwUserVideo(data) {
+  return request({
+    url: '/qw/userVideo',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除企业微信的视频号
+export function delQwUserVideo(id) {
+  return request({
+    url: '/qw/userVideo/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出企业微信的视频号
+export function exportQwUserVideo(query) {
+  return request({
+    url: '/qw/userVideo/export',
+    method: 'get',
+    params: query
+  })
+}

+ 53 - 0
src/api/sop/companySopRole.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询公司SOP权限列表
+export function listCompanySopRole(query) {
+  return request({
+    url: '/sop/companySopRole/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询公司SOP权限详细
+export function getCompanySopRole(id) {
+  return request({
+    url: '/sop/companySopRole/' + id,
+    method: 'get'
+  })
+}
+
+// 新增公司SOP权限
+export function addCompanySopRole(data) {
+  return request({
+    url: '/sop/companySopRole',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改公司SOP权限
+export function updateCompanySopRole(data) {
+  return request({
+    url: '/sop/companySopRole',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除公司SOP权限
+export function delCompanySopRole(id) {
+  return request({
+    url: '/sop/companySopRole/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出公司SOP权限
+export function exportCompanySopRole(query) {
+  return request({
+    url: '/sop/companySopRole/export',
+    method: 'get',
+    params: query
+  })
+}

BIN
src/assets/logo/logo.png


BIN
src/assets/logo/logo2.png


+ 25 - 0
src/router/index.js

@@ -164,6 +164,31 @@ export const constantRoutes = [
       }
     ]
   },
+  {
+    path: '/qw/sopTempe',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: 'updateSopTemp/:id/:type(\\d+)', // 确保 :type 的正则匹配数字
+        component: (resolve) => require(['@/views/qw/sopTemp/updateSopTemp'], resolve),
+        name: 'updateSopTemp',
+        meta: { title: '改动SOP模板', activeMenu: '/qw/addSopTemp' }
+      },
+      {
+        path: 'updateTemp/:id/:type(\\d+)', // 确保 :type 的正则匹配数字
+        component: () => import('@/views/qw/sopTemp/updateTemp'),
+        name: 'updateTemp',
+        meta: { title: '改动SOP模板', activeMenu: '/qw/updateTemp' }
+      },
+      {
+        path: 'updateAiChatTemp/:id/:type(\\d+)', // 确保 :type 的正则匹配数字
+        component: () => import('@/views/qw/sopTemp/updateAiChatTemp'),
+        name: 'updateAiChatTemp',
+        meta: { title: '改动SOP模板', activeMenu: '/qw/addSopTemp' }
+      }
+    ]
+  },
   {
     path: '/fastGpt/fastGptRole1',
     component: Layout,

+ 166 - 25
src/views/course/courseQuestionBank/index.vue

@@ -30,15 +30,25 @@
         </el-select>
       </el-form-item>
 	  <el-form-item label="题目类别" prop="questionType">
-	    <el-select v-model="queryParams.questionType" placeholder="请选择类别" clearable size="small">
+	    <el-select v-model="queryParams.questionType" ref="typeSelect" placeholder="请选择类别" @change="changeCateType" clearable size="small">
 	      <el-option
-	        v-for="dict in questionTypeOptions"
-	        :key="dict.dictValue"
-	        :label="dict.dictLabel"
-	        :value="dict.dictValue"
-	      />
+          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">-->
@@ -101,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:import']"
+        >导入</el-button>
+      </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
@@ -114,9 +134,14 @@
       </el-table-column>
 	  <el-table-column label="题目类别" align="center" prop="questionType">
 	    <template slot-scope="scope">
-	      <dict-tag :options="questionTypeOptions" :value="scope.row.questionType"/>
+        <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"/>
@@ -172,14 +197,22 @@
           </el-select>
         </el-form-item>
 		<el-form-item label="题目类别 " prop="questionType">
-		  <el-select v-model="form.questionType" placeholder="请选择题目类别" >
-		    <el-option
-		      v-for="dict in questionTypeOptions"
-		      :key="dict.dictValue"
-		      :label="dict.dictLabel"
-		      :value="parseInt(dict.dictValue)"
-		    ></el-option>
+		  <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">
@@ -194,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">
@@ -234,7 +268,6 @@
             <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>
@@ -252,11 +285,41 @@
         <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" @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>
+    </el-dialog>
+
   </div>
 </template>
 
 <script>
-import { listCourseQuestionBank, getCourseQuestionBank, delCourseQuestionBank, addCourseQuestionBank, updateCourseQuestionBank, exportCourseQuestionBank } from "@/api/course/courseQuestionBank";
+import { listCourseQuestionBank, getCourseQuestionBank, delCourseQuestionBank, addCourseQuestionBank, updateCourseQuestionBank, exportCourseQuestionBank, importTemplate } from "@/api/course/courseQuestionBank";
+import { getToken } from "@/utils/auth";
+import {
+  listUserCourseCategory,
+  getCatePidList,
+  getCateListByPid
+} from '@/api/course/userCourseCategory'
 
 export default {
   name: "CourseQuestionBank",
@@ -281,6 +344,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,
@@ -299,6 +365,8 @@ export default {
       // 题库表格数据
       courseQuestionBankList: [],
 	  questionTypeOptions: [],
+      questionRootTypeOptions: [],
+	    questionSubTypeOptions: [],
       // 弹出层标题
       title: "",
       // 是否显示弹出层
@@ -313,6 +381,7 @@ export default {
         status: null,
         question: null,
 		    questionType:null,
+        questionSubType: null,
         answer: null,
       },
       // 表单参数
@@ -325,6 +394,21 @@ export default {
         status: [{ required: true, message: "不能为空", trigger: "blur" }],
         question: [{ required: true, message: "不能为空", trigger: "blur" }],
         answer: [{ required: true, message: "不能为空", trigger: "blur" }],
+      },
+      // 导入
+      importMsgOpen:false,
+      importMsg: '',
+      upload: {
+        // 是否显示弹出层(文件导入)
+        open: false,
+        // 弹出层标题(文件导入)
+        title: "",
+        // 是否禁用上传
+        isUploading: false,
+        // 设置上传的请求头部
+        headers: { Authorization: "Bearer " + getToken() },
+        // 上传的地址
+        url: process.env.VUE_APP_BASE_API + "/course/courseQuestionBank/importData",
       }
     };
   },
@@ -336,17 +420,33 @@ 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.getCategoryTree()
   },
   methods: {
     getOptionLabel(index) {
       return this.alphabet[index];
     },
 
-
+    // 分类树
+    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 = [];
@@ -389,8 +489,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;
@@ -410,6 +510,7 @@ export default {
     cancel() {
       this.open = false;
       this.reset();
+      this.changeCateType(this.queryParams.questionType)
     },
     // 表单重置
     reset() {
@@ -425,6 +526,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() {
@@ -445,6 +552,7 @@ export default {
     /** 新增按钮操作 */
     handleAdd() {
       this.reset();
+      this.questionSubTypeOptions = []
       this.open = true;
       this.title = "添加题库";
     },
@@ -454,7 +562,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) {
@@ -471,6 +579,12 @@ export default {
     submitForm() {
       this.$refs["form"].validate(valid => {
         if (valid) {
+
+          if (this.question.some(q => q.name === "")) {
+            this.$message.error("不能存在空选项")
+            return
+          }
+
           this.form.question=JSON.stringify(this.question)
 
           if (this.form.type===2){
@@ -522,7 +636,34 @@ 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.msg
+      this.getList();
+    },
   }
 };
 </script>

+ 126 - 0
src/views/course/userCoursePeriod/batchRedPacket.vue

@@ -0,0 +1,126 @@
+<template>
+  <el-dialog
+    title="批量设置红包"
+    :visible.sync="visible"
+    width="800px"
+    append-to-body
+  >
+    <el-table
+      v-loading="loading"
+      :data="tableData"
+      border
+      style="width: 100%"
+    >
+      <el-table-column
+        type="index"
+        label="序号"
+        width="80"
+        align="center"
+      />
+      <el-table-column
+        prop="periodName"
+        label="营期"
+        align="center"
+      />
+      <el-table-column
+        label="金额"
+        align="center"
+        width="200"
+      >
+        <template slot-scope="scope">
+          <el-input-number
+            v-model="scope.row.amount"
+            :min="0.01"
+            :precision="2"
+            :step="0.01"
+            size="small"
+            style="width: 150px"
+          />
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="handleClose">取 消</el-button>
+      <el-button type="primary" @click="handleSave">保 存</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { batchSaveRedPacketByPeriod } from "@/api/course/userCoursePeriod";
+
+export default {
+  name: 'BatchRedPacket',
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    selectedData: {
+      type: Array,
+      default: () => []
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      tableData: []
+    }
+  },
+  watch: {
+    visible(val) {
+      if (val) {
+        this.initTableData()
+      }
+    }
+  },
+  methods: {
+    // 初始化表格数据
+    initTableData() {
+      this.tableData = this.selectedData.map(item => ({
+        ...item,
+        amount: 0.01
+      }))
+    },
+    // 保存
+    handleSave() {
+      this.$confirm(`是否确定?确定后营期下的所有公司红包金额都将设置成对应值`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.loading = true;
+        const saveData = this.tableData.map(item => ({
+          periodId: item.periodId,
+          redPacketMoney: item.amount
+        }));
+
+        batchSaveRedPacketByPeriod(saveData).then(response => {
+          if (response.code === 200) {
+            this.$message.success('设置成功');
+            this.$emit('success');
+            this.handleClose();
+          } else {
+            this.$message.error(response.msg || '批量设置失败');
+          }
+        }).catch(error => {
+          this.$message.error('批量设置失败:' + error.message);
+        }).finally(() => {
+          this.loading = false;
+        });
+      })
+    },
+    // 关闭
+    handleClose() {
+      this.$emit('update:visible', false)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.el-input-number {
+  width: 100%;
+}
+</style>

+ 1711 - 0
src/views/course/userCoursePeriod/index.vue

@@ -0,0 +1,1711 @@
+<template>
+  <div class="app-container">
+    <el-container>
+      <!-- 左侧区域 -->
+      <el-aside width="360px" class="left-aside">
+        <!-- 顶部区域 -->
+        <div class="left-header">
+          <div class="left-header-top">
+            <el-button type="primary" class="search-btn" @click="handleLeftQuery">搜索</el-button>
+            <el-button type="primary" style="width: 50%" icon="el-icon-plus" @click="handleAddTrainingCamp">新建训练营</el-button>
+          </div>
+          <div class="search-input-wrapper">
+            <el-input
+              v-model="leftQueryParams.trainingCampName"
+              placeholder="请输入训练营名称"
+              prefix-icon="el-icon-search"
+              clearable
+              size="small"
+              @keyup.enter.native="handleLeftQuery"
+            />
+          </div>
+          <div class="sort-wrapper">
+            <span class="sort-label">排序方式</span>
+            <el-select v-model="leftQueryParams.scs"
+            placeholder="按序号倒序"
+            size="small"
+            class="sort-select"
+            @change="handleSortChange"
+            >
+              <el-option label="按序号倒序" value="order_number(desc),training_camp_id(desc)" />
+              <el-option label="按序号顺序" value="order_number(asc),training_camp_id(desc)" />
+            </el-select>
+          </div>
+        </div>
+
+        <!-- 训练营列表 -->
+        <div class="camp-list" ref="campList" @scroll="handleScroll" v-loading="leftLoading">
+          <div
+            v-for="(item, index) in campList"
+            :key="index"
+            class="camp-item"
+            :class="{ 'active': activeCampIndex === index }"
+            @click="selectCamp(index)"
+          >
+            <div class="camp-content">
+              <div class="camp-title">
+                <i class="el-icon-s-flag camp-icon"></i>
+                {{ item.trainingCampName }}
+              </div>
+              <div class="camp-info">
+                <span>序号:{{ item.orderNumber }}</span>
+                <span>最新营期开课:{{ item.recentDate || '-' }}</span>
+              </div>
+              <div class="camp-stats">
+                <span class="stat-item">
+                  <i class="el-icon-s-data"></i>
+                  营期数:{{ item.periodCount || 0 }}
+                </span>
+                <span class="stat-item">
+                  <i class="el-icon-user"></i>
+                  会员总数:{{ item.vipCount || 0 }}
+                </span>
+              </div>
+            </div>
+            <div class="camp-actions">
+              <el-button type="text" class="action-btn delete-btn" @click.stop="handleDeleteCamp(item)">删除</el-button>
+              <el-button type="text" class="action-btn copy-btn" @click.stop="handleCopyCamp(item)">复制</el-button>
+              <el-button type="text" class="action-btn copy-btn" @click.stop="handleEditCamp(item)">编辑</el-button>
+            </div>
+          </div>
+          <!-- 底部加载更多提示 -->
+          <div v-if="loadingMore" class="loading-more">
+            <i class="el-icon-loading"></i>
+            <span>加载中...</span>
+          </div>
+
+          <!-- 所有数据加载完毕提示 -->
+          <div v-if="campList.length > 0 && !leftQueryParams.hasNextPage && !loadingMore" class="no-more-data">
+            <span>—— 已加载全部训练营 ——</span>
+          </div>
+        </div>
+      </el-aside>
+
+      <!-- 右侧区域 -->
+      <el-main>
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+          <el-form-item label="营期名称" prop="periodName">
+            <el-input
+              v-model="queryParams.periodName"
+              placeholder="请输入营期名称"
+              clearable
+              size="small"
+              @keyup.enter.native="handleQuery"
+            />
+          </el-form-item>
+          <el-form-item label="公司" prop="companyIdList">
+            <el-select v-model="queryParams.companyIdList" placeholder="请选择公司" clearable size="small" multiple>
+              <el-option
+                v-for="item in companyOptions"
+                :key="item.companyId"
+                :label="item.companyName"
+                :value="item.companyId"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="开营日期开始" prop="periodStartingTime" label-width="120px">
+            <el-date-picker clearable size="small" style="width: 200px"
+              v-model="queryParams.periodStartingTime"
+              type="date"
+              value-format="yyyy-MM-dd"
+              placeholder="请选择开营日期开始时间">
+            </el-date-picker>
+          </el-form-item>
+          <el-form-item label="开营日期结束" prop="periodEndTime" label-width="120px">
+            <el-date-picker clearable size="small" style="width: 300px"
+              v-model="queryParams.periodEndTime"
+              type="date"
+              value-format="yyyy-MM-dd"
+              placeholder="请选择开营日期结束时间">
+            </el-date-picker>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+            <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
+
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button
+              type="primary"
+              plain
+              icon="el-icon-plus"
+              size="mini"
+              @click="handleAdd"
+              v-hasPermi="['course:period:add']"
+            >新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button
+              type="warning"
+              plain
+              icon="el-icon-download"
+              size="mini"
+              @click="handleExport"
+              v-hasPermi="['course:period:export']"
+            >导出</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button
+              type="primary"
+              plain
+              icon="el-icon-edit"
+              size="mini"
+              @click="handleBatchSetRedPacket"
+              v-hasPermi="['course:period:export']"
+              :disabled="batchSetRedPacketDisabled"
+            >批量设置红包</el-button>
+          </el-col>
+          <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+
+        <el-table v-loading="loading" :data="periodList" @selection-change="handleSelectionChange">
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column label="营期名称" align="center" prop="periodName" />
+          <el-table-column label="公司名称" align="center" prop="companyName" />
+          <el-table-column label="营期状态" align="center" prop="periodStatus" width="100" :formatter="periodStatusFormatter" />
+          <el-table-column label="开营开始时间" align="center" prop="periodStartingTime" width="180" />
+          <el-table-column label="开营结束时间" align="center" prop="periodEndTime" width="180" />
+          <el-table-column label="创建时间" align="center" prop="createTime" width="180" />
+          <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+            <template slot-scope="scope">
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-edit"
+                @click="handleUpdate(scope.row)"
+                v-hasPermi="['course:period:edit']"
+              >修改</el-button>
+    <!--              <el-button-->
+    <!--                size="mini"-->
+    <!--                type="text"-->
+    <!--                icon="el-icon-edit"-->
+    <!--                @click="handleCourse(scope.row)"-->
+    <!--                v-hasPermi="['course:period:edit']"-->
+    <!--              >课程管理</el-button>-->
+    <!--              <el-button-->
+    <!--                size="mini"-->
+    <!--                type="text"-->
+    <!--                icon="el-icon-money"-->
+    <!--                @click="setRedPacket(scope.row)"-->
+    <!--              >设置红包</el-button>-->
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-setting"
+                @click="handlePeriodSettings(scope.row)"
+              >营期相关设置</el-button>
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-circle-close"
+                @click="handleClosePeriod(scope.row)"
+              >结束营期</el-button>
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-delete"
+                @click="handleDelete(scope.row)"
+                v-hasPermi="['course:period:remove']"
+              >删除</el-button>
+            </template>
+          </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+      </el-main>
+    </el-container>
+
+    <!-- 添加或修改会员营期对话框-->
+    <el-drawer :title="title" :visible.sync="open" size="700px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+        <el-form-item label="营期名称" prop="periodName">
+          <el-input v-model="form.periodName" placeholder="请输入营期名称" />
+        </el-form-item>
+         <el-form-item label="公司" prop="companyId">
+          <el-select v-model="form.companyId" placeholder="请选择公司" multiple>
+            <el-option
+              v-for="item in companyOptions"
+              :key="item.companyId"
+              :label="item.companyName"
+              :value="item.companyId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="课程风格" prop="courseStyle">
+          <image-upload v-model="form.courseStyle" :limit="1" />
+        </el-form-item>
+        <el-form-item label="直播间风格" prop="liveRoomStyle">
+          <image-upload v-model="form.liveRoomStyle" :limit="1" />
+        </el-form-item>
+        <el-form-item label="红包发放方式" prop="redPacketGrantMethod">
+          <el-radio-group v-model="form.redPacketGrantMethod">
+            <el-radio :label="1" >按课程</el-radio>
+            <el-radio :label="2" >按营期</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="营期类型" prop="periodType">
+          <el-radio-group v-model="form.periodType">
+            <el-radio :label="1" >多课程</el-radio>
+            <el-radio :label="2" >单课程</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="销售可查看天数" prop="periodType">
+          <el-input-number :min="0" v-model="form.maxViewNum" style="width: 200px" />
+        </el-form-item>
+        <el-form-item label="开营日期" prop="periodStartingTime">
+          <el-date-picker
+            :disabled = "isDisabledDateRange"
+            :style="{display: form.periodType == 1 ? '' : 'none !important'}"
+            v-model="form.dateRange"
+            @change="timeChange(1)"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="yyyy-MM-dd">
+          </el-date-picker>
+          <el-date-picker
+            :style="{display: form.periodType == 2 ? '' : 'none !important'}"
+            @change="timeChange(2)"
+            v-model="form.date"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择日期">
+          </el-date-picker>
+        </el-form-item>
+
+<!--        <el-form-item label="看课时间" prop="timeRange">-->
+<!--          <el-time-picker-->
+<!--            is-range-->
+<!--            v-model="form.timeRange"-->
+<!--            @input="$forceUpdate()"-->
+<!--            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="form.lastJoinTime"-->
+<!--            :selectableRange="form.lastJoinTime"-->
+<!--            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 class="drawer-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-drawer>
+
+    <!-- 添加训练营对话框 -->
+    <el-dialog :title="campForm.trainingCampId ? '修改训练营' : '新建训练营'" :visible.sync="campDialogVisible" width="500px" append-to-body>
+      <el-form ref="campForm" :model="campForm" :rules="campRules" label-width="100px">
+        <el-form-item label="训练营名称" prop="trainingCampName">
+          <el-input v-model="campForm.trainingCampName" placeholder="请输入训练营名称" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitCampForm">确 定</el-button>
+        <el-button @click="cancelCampForm">取 消</el-button>
+      </div>
+    </el-dialog>
+
+<!--    &lt;!&ndash; 添加或修改会员营期对话框&ndash;&gt;-->
+<!--    <el-dialog title="课程管理" :visible.sync="course.open" width="75%" top="10px" append-to-body style="padding-bottom: 10px">-->
+<!--      <el-row :gutter="10" class="mb8">-->
+<!--        <el-col :span="1.5">-->
+<!--          <el-button-->
+<!--            v-if="(getDiff(course.row.periodStartingTime, course.row.periodEndTime) - course.total) > 0"-->
+<!--            type="primary"-->
+<!--            icon="el-icon-plus"-->
+<!--            size="mini"-->
+<!--            @click="handleAddCourse"-->
+<!--            v-hasPermi="['course:period:add']"-->
+<!--          >添加课程</el-button>-->
+<!--        </el-col>-->
+<!--      </el-row>-->
+<!--      <el-table v-loading="course.loading" :data="course.list">-->
+<!--        <el-table-column label="课程" align="center" prop="courseName" width="180" />-->
+<!--        <el-table-column label="小节" align="center" prop="videoName" />-->
+<!--        <el-table-column label="营期时间" align="center" prop="dayDate" width="150"  />-->
+<!--        <el-table-column label="创建时间" align="center" prop="createTime" width="150" />-->
+<!--      </el-table>-->
+
+<!--      <div slot="footer" class="dialog-footer">-->
+<!--&lt;!&ndash;        <el-button type="primary" @click="saveCourseData">保存</el-button>&ndash;&gt;-->
+<!--      </div>-->
+<!--    </el-dialog>-->
+
+    <!-- 添加课程对话框-->
+    <el-dialog title="添加课程" :visible.sync="course.addOpen" width="500px" append-to-body>
+      <el-form ref="courseAddForm" :model="course.form" label-width="100px">
+        <el-form-item label="课程" prop="courseId">
+          <el-select filterable  v-model="course.form.courseId" placeholder="请选择课程"  clearable size="small" @change="courseChange(course.form.courseId)" style="width: 100%" :value-key="'dictValue'">
+            <el-option
+              v-for="dict in courseList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="小节" prop="videoId">
+          <el-select filterable  v-model="course.form.videoIds" placeholder="请选择小节" :multiple-limit="getDiff(course.row.periodStartingTime, course.row.periodEndTime) - course.total" multiple clearable size="small" style="width: 100%" :value-key="'dictValue'">
+            <el-option
+              v-for="dict in videoList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="看课时间" prop="timeRange">
+          <el-time-picker
+            is-range
+            v-model="course.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="course.form.joinTime"
+            :selectableRange="course.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="submitCourseForm">确 定</el-button>
+        <el-button @click="closeAddCourse">取 消</el-button>
+      </div>
+    </el-dialog>
+    <el-dialog title="修改看课时间" :visible.sync="updateCourse.open" width="500px" append-to-body>
+      <el-form ref="courseUpdateForm" :model="updateCourse.form" label-width="100px">
+        <el-form-item label="看课时间" prop="timeRange">
+          <el-time-picker
+            is-range
+            v-model="updateCourse.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="updateCourse.form.joinTime"
+            :selectableRange="updateCourse.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="submitUpdateCourseForm">确 定</el-button>
+        <el-button @click="closeUpdateCourse">取 消</el-button>
+      </div>
+    </el-dialog>
+    <el-dialog title="修改营期时间" :visible.sync="updateDateOpen" width="500px" append-to-body>
+      <el-form ref="courseUpdateForm" :model="form" label-width="100px">
+        <el-form-item label="营期时间" prop="dayDate">
+          <el-date-picker
+            v-model="form.dayDate"
+            :selectableRange="form.dayDate"
+            value-format="yyyy-MM-dd"
+            type="date"
+            placeholder="选择时间">
+          </el-date-picker>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="updateDate">确 定</el-button>
+        <el-button @click="updateDateOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+<!--    <red-packet-->
+<!--      :visible.sync="redPacketVisible"-->
+<!--      :periodId="currentRedPacketData.periodId"-->
+<!--      :videoId="currentRedPacketData.videoId"-->
+<!--      @success="handleRedPacketSuccess"-->
+<!--    />-->
+
+    <!-- 营期相关设置抽屉 -->
+  <el-drawer
+    title="营期相关设置"
+    :visible.sync="periodSettingsVisible"
+    direction="rtl"
+    size="70%"
+    :destroy-on-close="true"
+    append-to-body
+    custom-class="period-settings-drawer"
+  >
+    <div class="drawer-content" style="margin-left: 25px">
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+        <el-tab-pane label="课程管理" name="course">
+          <el-row :gutter="10" class="mb8">
+            <el-col :span="1.5">
+              <el-button
+                v-if="(getDiff(periodSettingsData.periodStartingTime, periodSettingsData.periodEndTime) - course.total) > 0"
+                type="primary"
+                icon="el-icon-plus"
+                size="mini"
+                @click="handleAddCourse"
+                v-hasPermi="['course:period:add']"
+              >添加课程</el-button>
+            </el-col>
+            <el-col :span="1.5">
+              <el-button
+                type="primary"
+                size="mini"
+                :disabled="updateCourse.ids.length <= 0"
+                @click="handleUpdateCourse"
+                v-hasPermi="['course:period:add']"
+              >修改看课时间</el-button>
+            </el-col>
+          </el-row>
+          <el-table v-loading="course.loading" :data="course.list" @selection-change="handleSelectionCourseChange">
+            <el-table-column type="selection" width="55" align="center" />
+            <el-table-column label="课程" align="center" prop="courseName" width="180" />
+            <el-table-column label="小节" align="center" prop="videoName" />
+            <el-table-column label="营期时间" align="center" prop="dayDate" />
+            <el-table-column label="开始时间" align="center" prop="startDateTime" width="100">
+              <template slot-scope="scope">
+                <el-tag>{{parseTime(scope.row.startDateTime, '{h}:{i}:{s}')}}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="结束时间" align="center" prop="endDateTime" width="100">
+              <template slot-scope="scope">
+                <el-tag type="success">{{parseTime(scope.row.endDateTime, '{h}:{i}:{s}')}}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="领取红包时间" align="center" prop="lastJoinTime" width="100">
+              <template slot-scope="scope">
+                <el-tag type="danger">{{parseTime(scope.row.lastJoinTime, '{h}:{i}:{s}')}}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+              <template slot-scope="scope">
+                <el-button
+                  size="mini"
+                  type="text"
+                  icon="el-icon-top"
+                  @click="handleTop(scope.row)"
+                >上移</el-button>
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-bottom"
+                @click="handleBottom(scope.row)"
+              >下移</el-button>
+<!--              <el-button-->
+<!--                size="mini"-->
+<!--                type="text"-->
+<!--                icon="el-icon-edit"-->
+<!--                @click="handleUpdateDate(scope.row)"-->
+<!--              >修改营期时间</el-button>-->
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+        <el-tab-pane label="公司列表" name="company">
+          <red-packet
+            :visible.sync="redPacketVisible"
+            :activeTab="activeTab"
+            :periodId="periodSettingsData.periodId"
+            @success="handleRedPacketSuccess"
+          />
+        </el-tab-pane>
+        <el-tab-pane label="课程统计" name="statistics">
+          <course-statistics
+            :periodId="periodSettingsData.periodId"
+            :active="activeTab === 'statistics'"
+          />
+        </el-tab-pane>
+      </el-tabs>
+    </div>
+  </el-drawer>
+
+  <batch-red-packet
+    :visible.sync="batchRedPacketVisible"
+    :selected-data="selectedPeriods"
+    @success="handleBatchRedPacketSuccess"
+  />
+
+  </div>
+</template>
+
+<script>
+import {addPeriod, delPeriod, exportPeriod, getPeriod, pagePeriod, updatePeriod, getDays, addCourse, updateCourseTime, updateCourseDate, updateListCourseData, periodCourseMove, closePeriod} from "@/api/course/userCoursePeriod";
+import {getCompanyList} from "@/api/company/company";
+import { listCamp, addCamp, editCamp, delCamp, copyCamp } from "@/api/course/userCourseCamp";
+import { courseList,videoList } from '@/api/course/courseRedPacketLog'
+import RedPacket from './redPacket.vue'
+import BatchRedPacket from './batchRedPacket.vue'
+import CourseStatistics from './statistics.vue'
+
+export default {
+  name: "Period",
+  components: {
+    RedPacket,
+    BatchRedPacket,
+    CourseStatistics
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      updateDateOpen: false,
+      // 左侧遮罩层
+      leftLoading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 左侧总条数
+      leftTotal: 0,
+      // 会员营期表格数据
+      periodList: [],
+      // 左侧列表数据
+      leftList: [],
+      videoList: [],
+      // 弹出层标题
+      title: "",
+      isDisabledDateRange: false, //是否禁用开营日期
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        periodName: null,
+        periodStartingTime: null,
+        periodEndTime: null,
+        companyIdList: []
+      },
+      // 左侧查询参数
+      leftQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        hasNextPage: false,
+        scs: 'order_number(desc),training_camp_id(desc)',
+        trainingCampName: null
+      },
+      // 表单参数
+      form: {},
+      course: {
+        open: false,
+        row:{},
+        list:[],
+        queryParams: {
+          pageNum: 1,
+          pageSize: 10,
+        },
+        loading: true,
+        total: 0,
+        addOpen: false,
+        form: {},
+      },
+      updateCourse: {
+        open: false,
+        loading: true,
+        ids: [],
+        form: {},
+      },
+      // 表单校验
+      rules: {
+      },
+      // 公司选项
+      companyOptions: [],
+      // 训练营列表
+      campList: [],
+      // 激活的训练营索引
+      activeCampIndex: null,
+      // 训练营对话框是否显示
+      campDialogVisible: false,
+      courseList: false,
+      // 训练营表单
+      campForm: {
+        trainingCampId: null,
+        trainingCampName: ''
+      },
+      // 训练营表单校验
+      campRules: {
+        trainingCampName: [
+          { required: true, message: '训练营名称不能为空', trigger: 'blur' },
+          { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
+        ]
+      },
+      // 滚动节流标志
+      scrollThrottle: false,
+      // 加载更多状态
+      loadingMore: false,
+      // 设置红包对话框
+      redPacketVisible: false,
+      periodCompanyList: [],
+      currentRedPacketData: {
+        periodId: '',
+        videoId: ''
+      },
+      // 营期相关设置抽屉
+      periodSettingsVisible: false,
+      activeTab: 'course',
+      periodSettingsData: {},
+      companyList: [],
+      courseDialogVisible: false,
+      redPacketList: [],
+      currentCompany: null,
+      // 选中的营期数据
+      selectedPeriods: [],
+      // 批量设置红包按钮是否禁用
+      batchSetRedPacketDisabled: true,
+      // 批量设置红包弹出框
+      batchRedPacketVisible: false,
+    };
+  },
+  created() {
+
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+    // this.getList();
+    this.getLeftList();
+    this.getCompanyList();
+
+  },
+  methods: {
+    /** 查询会员营期列表 */
+    getList() {
+      this.loading = true;
+      const params = { ...this.queryParams };
+      pagePeriod(params).then(response => {
+        this.periodList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    /** 查询左侧列表 */
+    getLeftList() {
+      this.leftLoading = true;
+      // 重置页码和加载更多状态
+      this.leftQueryParams.pageNum = 1;
+      this.loadingMore = false;
+
+      // 训练营数据
+      listCamp(this.leftQueryParams).then(response => {
+        if (response && response.code === 200) {
+          this.campList = response.data.list || [];
+          this.leftQueryParams.hasNextPage = response.data.hasNextPage;
+          this.activeCampIndex = this.campList.length > 0 ? 0 : null;
+          this.selectCamp(this.activeCampIndex);
+          // 如果当前显示的列表高度不足以触发滚动,但还有更多数据,自动加载下一页
+          this.$nextTick(() => {
+            const scrollEl = this.$refs.campList;
+            if (scrollEl && this.leftQueryParams.hasNextPage && scrollEl.scrollHeight <= scrollEl.clientHeight) {
+              this.loadMoreCamps();
+            }
+          });
+        } else {
+          this.$message.error(response.msg || '获取训练营列表失败');
+          this.campList = [];
+          this.leftQueryParams.hasNextPage = false;
+        }
+        this.leftLoading = false;
+      }).catch(error => {
+        console.error('获取训练营列表失败:', error);
+        this.$message.error('获取训练营列表失败');
+        this.campList = [];
+        this.leftQueryParams.hasNextPage = false;
+        this.leftLoading = false;
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 左侧搜索按钮操作 */
+    handleLeftQuery() {
+      // 重置页码和列表
+      this.leftQueryParams.pageNum = 1;
+      this.campList = [];
+      this.getLeftList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.companyIdList = [];
+      this.handleQuery();
+    },
+    /** 多选框选中数据 */
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.periodId)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+      // 更新批量设置红包相关数据
+      this.selectedPeriods = selection;
+      this.batchSetRedPacketDisabled = selection.length === 0;
+    },
+    handleSelectionCourseChange(selection) {
+      this.updateCourse.ids = selection.map(item => item.id)
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加会员营期";
+      this.isDisabledDateRange = false;
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const periodId = row.periodId || this.ids
+      getPeriod(periodId).then(response => {
+        this.form = response.data;
+        if (this.form.companyId) {
+          this.form.companyId = this.form.companyId.split(',').map(id => Number(id));
+        }
+        // 设置看课时间范围(回显)
+        if (this.form.viewStartTime && this.form.viewEndTime) {
+          this.form.timeRange = [this.form.viewStartTime, this.form.viewEndTime];
+        }
+        if(this.form.periodType == 1){
+          this.form.dateRange = [this.form.periodStartingTime, this.form.periodEndTime];
+        }
+        if(this.form.periodType == 1){
+          this.form.date = this.form.periodStartingTime;
+        }
+        this.open = true;
+        this.title = "修改会员营期";
+        this.isDisabledDateRange = true;
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          let data = JSON.parse(JSON.stringify(this.form));
+          // 处理看课时间范围
+          if (data.timeRange && data.timeRange.length === 2) {
+            data.viewStartTime = data.timeRange[0];
+            data.viewEndTime = data.timeRange[1];
+          }
+          data.companyId = data.companyId.join()
+          data.trainingCampId = this.queryParams.trainingCampId
+          if (data.periodId != null) {
+            updatePeriod(data).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          } else {
+            addPeriod(data).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      //添加删除判断,只能删除未开始的营期
+      console.log(row.periodStatus)
+      if(row.periodStatus !== 1){
+        this.$message.error('营期处于进行中或者结束,不能删除');
+        return;
+      }
+      const periodIds = row.periodId || this.ids;
+      this.$confirm('是否确认删除该营期?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delPeriod(periodIds);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(function() {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有会员营期数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return exportPeriod(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+        }).catch(function() {});
+    },
+
+    /** 批量设置红包 */
+    handleBatchSetRedPacket() {
+      if (this.selectedPeriods.length === 0) {
+        this.$message.warning('请至少选择一个营期');
+        return;
+      }
+      this.batchRedPacketVisible = true;
+    },
+
+    /** 处理批量设置红包保存 */
+    // handleBatchRedPacketSave(data) {
+    //   // 这里等待接口提供后补充具体实现
+    //   // 示例代码:
+    //   // batchSetRedPacket(data).then(response => {
+    //   //   if (response.code === 200) {
+    //   //     this.$message.success('批量设置成功');
+    //   //     this.getList();
+    //   //   }
+    //   // });
+    //   this.batchRedPacketVisible = false;
+    // },
+
+    /** 获取公司下拉列表*/
+    getCompanyList() {
+      this.loading = true;
+      getCompanyList().then(response => {
+        this.companyOptions = response.data;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        periodId: null,
+        periodName: null,
+        companyId: null,
+        courseId: null,
+        videoId: null,
+        trainingCampId: null,
+        createTime: null,
+        updateTime: null,
+        courseStyle: null,
+        liveRoomStyle: null,
+        redPacketGrantMethod: 1,
+        periodType: 1,
+        periodStartingTime: null,
+        dateRange: [],
+        date: null,
+        days: [],
+        maxViewNum: 0,
+        periodEndTime: null,
+        timeRange: [], // 看课时间范围
+        viewStartTime: null, // 看课开始时间
+        viewEndTime: null, // 看课结束时间
+        lastJoinTime: null // 领取红包时间
+      };
+      this.resetForm("form");
+    },
+    // 处理训练营列表的逻辑
+    handleDeleteCamp(item) {
+      this.$confirm(`确定要删除训练营"${item.trainingCampName}"吗?`, '提示', {
+        confirmButtonText: '删除',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        // 调用删除训练营API
+        const trainingCampId = item.trainingCampId;
+        this.leftLoading = true;
+
+        delCamp(trainingCampId).then(response => {
+          if (response.code === 200) {
+            this.$message.success('删除成功');
+            // 从列表中移除
+            const index = this.campList.findIndex(camp => camp.trainingCampId === trainingCampId);
+            if (index !== -1) {
+              this.campList.splice(index, 1);
+            }
+            // 如果删除的是当前选中的训练营,则重置选中状态
+            if (this.activeCampIndex === index) {
+              this.activeCampIndex = this.campList.length > 0 ? 0 : null;
+              if (this.activeCampIndex !== null) {
+                // 更新右侧列表
+                this.selectCamp(this.activeCampIndex);
+              } else {
+                // 没有训练营了,清空右侧列表
+                this.periodList = [];
+              }
+            }
+          } else {
+            this.$message.error(response.msg || '删除失败');
+          }
+          this.leftLoading = false;
+        }).catch(error => {
+          this.$message.error('删除失败: ' + error.message);
+          this.leftLoading = false;
+        });
+      }).catch(() => {
+        this.$message.info('已取消删除');
+      });
+    },
+    /** 复制训练营 */
+    handleCopyCamp(item) {
+      // 调用添加训练营API
+      copyCamp(item.trainingCampId).then(response => {
+        if (response.code === 200) {
+          this.$message.success('复制成功');
+          // 重新加载训练营列表
+          this.getLeftList();
+        } else {
+          this.$message.error(response.msg || '复制训练营失败');
+        }
+      }).catch(error => {
+        this.$message.error('复制训练营失败: ' + error.message);
+      });
+    },
+    /** 修改训练营按钮操作 */
+    handleEditCamp(item) {
+      this.resetCampForm();
+      this.leftLoading = true;
+
+      // 获取最新的训练营数据
+      const trainingCampId = item.trainingCampId;
+      // 应该调用获取训练营详情的API
+      setTimeout(() => {
+        // 填充表单数据
+        this.campForm = {
+          trainingCampId: item.trainingCampId,
+          trainingCampName: item.trainingCampName,
+          orderNumber: item.orderNumber || 1,
+          status: item.status !== undefined ? item.status : 1
+        };
+        this.campDialogVisible = true;
+        this.leftLoading = false;
+      }, 300);
+    },
+    /** 新建训练营按钮操作 */
+    handleAddTrainingCamp() {
+      this.resetCampForm();
+      this.campDialogVisible = true;
+    },
+    /** 重置训练营表单 */
+    resetCampForm() {
+      this.campForm = {
+        trainingCampId: null,
+        trainingCampName: ''
+      };
+      // 如果表单已经创建,则重置校验结果
+      if (this.$refs.campForm) {
+        this.$refs.campForm.resetFields();
+      }
+    },
+    /** 取消训练营表单 */
+    cancelCampForm() {
+      this.campDialogVisible = false;
+      this.resetCampForm();
+    },
+    /** 提交训练营表单 */
+    submitCampForm() {
+      this.$refs.campForm.validate(valid => {
+        if (valid) {
+          // 显示加载中
+          this.leftLoading = true;
+
+          // 准备提交的数据
+          const submitData = JSON.parse(JSON.stringify(this.campForm));
+
+          // 判断是新增还是修改
+          if (submitData.trainingCampId) {
+            // 修改训练营
+            editCamp(submitData).then(response => {
+              if (response.code === 200) {
+                this.$message.success('修改训练营成功');
+                this.campDialogVisible = false;
+
+                // 更新列表中的数据
+                const index = this.campList.findIndex(camp => camp.trainingCampId === submitData.trainingCampId);
+                if (index !== -1) {
+                  this.campList[index] = { ...this.campList[index], ...submitData };
+                  // 如果修改的是当前选中的训练营,更新右侧列表
+                  if (this.activeCampIndex === index) {
+                    this.selectCamp(index);
+                  }
+                }
+
+                // 重新加载训练营列表
+                this.getLeftList();
+              } else {
+                this.$message.error(response.msg || '修改训练营失败');
+                this.leftLoading = false;
+              }
+            }).catch(error => {
+              this.$message.error('修改训练营失败: ' + (error.message || '未知错误'));
+              this.leftLoading = false;
+            });
+          } else {
+            // 新增训练营
+            addCamp(submitData).then(response => {
+              if (response.code === 200) {
+                this.$message.success('新建训练营成功');
+                this.campDialogVisible = false;
+                // 重新加载训练营列表
+                this.getLeftList();
+              } else {
+                this.$message.error(response.msg || '新建训练营失败');
+                this.leftLoading = false;
+              }
+            }).catch(error => {
+              this.$message.error('新建训练营失败: ' + (error.message || '未知错误'));
+              this.leftLoading = false;
+            });
+          }
+        }
+      });
+    },
+    /** 排序方式改变 */
+    handleSortChange(value) {
+      this.leftQueryParams.scs = value;
+      // 重置页码和列表
+      this.leftQueryParams.pageNum = 1;
+      this.campList = [];
+      this.getLeftList();
+    },
+    /** 选中训练营 */
+    selectCamp(index) {
+      if(index == null || index == undefined) return;
+      this.activeCampIndex = index;
+      // 加载对应的训练营营期数据
+      const selectedCamp = this.campList[index];
+      this.queryParams.trainingCampId = selectedCamp.trainingCampId;
+      this.getList();
+    },
+    /** 处理滚动事件,实现滚动到底部加载更多 */
+    handleScroll() {
+      // 如果正在节流中或者正在加载中,则不处理
+      if (this.scrollThrottle || this.loadingMore) return;
+
+      // 设置节流,200ms内不再处理滚动事件
+      this.scrollThrottle = true;
+      setTimeout(() => {
+        this.scrollThrottle = false;
+      }, 200);
+
+      const scrollEl = this.$refs.campList;
+      if (!scrollEl) return;
+
+      // 判断是否滚动到底部:滚动高度 + 可视高度 >= 总高度 - 30(添加30px的容差,提前触发加载)
+      const isBottom = scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 30;
+
+      // 如果滚动到底部,且有下一页数据,且当前不在加载中,则加载更多
+      if (isBottom && this.leftQueryParams.hasNextPage && !this.leftLoading && !this.loadingMore) {
+        this.loadMoreCamps();
+      }
+    },
+    /** 加载更多训练营数据 */
+    loadMoreCamps() {
+      // 已在加载中,防止重复加载
+      if (this.leftLoading || this.loadingMore) return;
+
+      // 设置加载状态
+      this.loadingMore = true;
+
+      // 页码加1
+      this.leftQueryParams.pageNum += 1;
+
+      // 加载下一页数据
+      listCamp(this.leftQueryParams).then(response => {
+        if (response && response.code === 200) {
+          // 将新数据追加到列表中
+          const newList = response.data.list || [];
+          if (newList.length > 0) {
+            this.campList = [...this.campList, ...newList];
+          }
+
+          // 更新是否有下一页的标志
+          this.leftQueryParams.hasNextPage = response.data.hasNextPage;
+
+          // 如果当前显示的列表高度不足以触发滚动,但还有更多数据,自动加载下一页
+          this.$nextTick(() => {
+            const scrollEl = this.$refs.campList;
+            if (scrollEl && this.leftQueryParams.hasNextPage && scrollEl.scrollHeight <= scrollEl.clientHeight) {
+              // 延迟一点再加载下一页,避免过快加载
+              setTimeout(() => {
+                this.loadMoreCamps();
+              }, 300);
+            }
+          });
+        } else {
+          this.$message.error(response.msg || '加载更多训练营失败');
+        }
+        this.loadingMore = false;
+      }).catch(error => {
+        console.error('加载更多训练营失败:', error);
+        this.$message.error('加载更多训练营失败');
+        this.loadingMore = false;
+      });
+    },
+    timeChange(type){
+      if(type == 1){
+        this.form.periodStartingTime = this.form.dateRange[0];
+        this.form.periodEndTime = this.form.dateRange[1];
+
+
+         // 转换为天数
+        let days = this.getDiff(this.form.periodStartingTime, this.form.periodEndTime);
+        for (let i = 0; i < days; i++) {
+          this.form.days.push({lesson: i + 1});
+        }
+      }
+      if(type == 2){
+        this.form.periodStartingTime = this.form.date;
+        this.form.periodEndTime = this.form.date;
+      }
+    },
+    getDiff(start, end) {
+      if(start == null || start == undefined || start == '') return 0;
+      if(end == null || end == undefined || end == '') return 0;
+      if(start == end) 1;
+      const startDate = this.getUTCDate(start);
+      const endDate = this.getUTCDate(end);
+
+      const timeDiff = endDate - startDate;
+      return (Math.floor(timeDiff / (1000 * 3600 * 24))) + 1; // 直接取整
+    },
+    getUTCDate(dateStr) {
+      const [year, month, day] = dateStr.split('-').map(Number);
+      return new Date(Date.UTC(year, month - 1, day)); // 月份从0开始
+    },
+    handleCourse(row){
+      this.course = {
+        open: false,
+        row:{},
+        list:[],
+        queryParams: {
+          pageNum: 1,
+          pageSize: 9999,
+        },
+        loading: true,
+        total: 0,
+        addOpen: false,
+        form: {},
+      };
+      this.course.open = true;
+      this.course.row = row;
+      this.course.queryParams.periodId = row.periodId;
+      this.getCourseList();
+    },
+    getCourseList(){
+      this.course.loading = true;
+      getDays(this.course.queryParams).then(e => {
+        this.course.list = e.rows;
+        this.course.total = e.total;
+        this.course.loading = false;
+      });
+    },
+    handleAddCourse() {
+      this.course.addOpen = true;
+      this.course.form = {
+        periodId: this.course.queryParams.periodId,
+        courseId: null,
+        videoIds: []
+      };
+      // 重置表单
+      this.$nextTick(() => {
+        if (this.$refs.courseAddForm) {
+          this.$refs.courseAddForm.resetFields();
+        }
+      });
+    },
+    handleUpdateCourse() {
+      this.updateCourse.open = true;
+      this.updateCourse.form = {
+        ids: this.updateCourse.ids,
+        joinTime: [],
+      };
+    },
+    closeAddCourse() {
+      this.course.addOpen = false;
+      this.course.form = {
+        periodId: null,
+        courseId: null,
+        videoIds: []
+      };
+      // 重置表单
+      if (this.$refs.courseAddForm) {
+        this.$refs.courseAddForm.resetFields();
+      }
+    },
+    closeUpdateCourse() {
+      this.course.open = false;
+    },
+    courseChange(row){
+      this.course.form.videoIds = [];
+      videoList(row).then(response => {
+        this.videoList=response.list
+      });
+    },
+    submitCourseForm(){
+      this.$refs.courseAddForm.validate(valid => {
+        if (valid) {
+          if(this.course.form.timeRange != null && this.course.form.timeRange.length === 2){
+            this.course.form.startTime = this.course.form.timeRange[0];
+            this.course.form.endTime1 = this.course.form.timeRange[1];
+          }
+          // 提交数据
+          addCourse(this.course.form).then(response => {
+            this.$message.success('添加成功');
+            this.course.addOpen = false;
+            // 重新加载训练营列表
+            this.getCourseList();
+          });
+        }
+      });
+    },
+    submitUpdateCourseForm(){
+      this.$refs.courseUpdateForm.validate(valid => {
+        if (valid) {
+          if(this.updateCourse.form.timeRange != null && this.updateCourse.form.timeRange.length === 2){
+            this.updateCourse.form.startTime = this.updateCourse.form.timeRange[0];
+            this.updateCourse.form.endTime1 = this.updateCourse.form.timeRange[1];
+          }
+          // 提交数据
+          updateCourseTime(this.updateCourse.form).then(response => {
+            this.$message.success('添加成功');
+            this.updateCourse.open = false;
+            // 重新加载训练营列表
+            this.getCourseList();
+          });
+        }
+      });
+    },
+    updateDate(){
+      updateCourseDate(this.form).then(response => {
+          this.$message.success('修改成功');
+          this.updateDateOpen = false;
+          // 重新加载训练营列表
+          this.getCourseList();
+        });
+    },
+    saveCourseData(){
+      updateListCourseData(this.course.list).then(response => {
+        this.$message.success('保存成功');
+        this.getCourseList();
+      });
+    },
+    setRedPacket(row) {
+      this.currentRedPacketData = {
+        periodId: row.periodId
+        // videoId: row.videoId
+      };
+      this.redPacketVisible = true;
+    },
+    handleRedPacketSuccess() {
+      this.getCourseList();
+    },
+    handlePeriodSettings(row) {
+      this.periodSettingsData = row;
+      this.periodSettingsVisible = true;
+      // 初始化课程列表
+      this.course.queryParams.periodId = row.periodId;
+      // 根据当前激活的tab加载对应数据
+      this.handleTabClick({ name: this.activeTab });
+    },
+    // 结束营期
+    handleClosePeriod(row) {
+      const msg = `注: 1.确认结束营期,该营期的开营结束时间改为当天24点。2.当天正在播放中的课程不变。3.第二天如有未开始的课程,统一改为已结束。是否确认结束 ${row.periodName} 营期吗?`
+      this.$confirm(msg, "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        closePeriod({id: row.periodId}).then(response => {
+          if (response.code === 200) {
+            this.getList()
+          } else {
+            this.$message.error(response.msg)
+          }
+        })
+      }).catch(() => {})
+    },
+    handleBatchRedPacketSuccess() {
+      this.batchRedPacketVisible = false;
+      this.getCourseList();
+    },
+    /** 处理tab切换 */
+    handleTabClick(tab) {
+      if (tab.name === 'course') {
+        this.getCourseList();
+      } else if (tab.name === 'company') {
+        this.redPacketVisible = true;
+      }
+    },
+    /** 上移课程 */
+    handleTop(row) {
+      const currentIndex = this.course.list.findIndex(item => item.id === row.id);
+      if (currentIndex <= 0) {
+        this.$message.warning('已经是第一条数据');
+        return;
+      }
+
+      // 获取上一条数据
+      const prevRow = this.course.list[currentIndex - 1];
+      console.log({
+        id: row.id,
+        targetId: prevRow.id,
+        type: 1 //上移
+      })
+      periodCourseMove({
+        id: row.id,
+        targetId: prevRow.id,
+        type: 1 //上移
+      }).then(response => {
+        if (response.code === 200) {
+          this.$message.success('上移成功');
+          this.getCourseList();
+        } else {
+          this.$message.error(response.msg || '上移失败');
+        }
+      }).catch(() => {
+        this.$message.error('上移失败');
+      });
+    },
+    /** 下移课程 */
+    handleBottom(row) {
+      const currentIndex = this.course.list.findIndex(item => item.id === row.id);
+      if (currentIndex === -1 || currentIndex >= this.course.list.length - 1) {
+        this.$message.warning('已经是最后一条数据');
+        return;
+      }
+
+      // 获取下一条数据
+      const nextRow = this.course.list[currentIndex + 1];
+
+      periodCourseMove({
+        id: row.id,
+        targetId: nextRow.id,
+        type: 2 //下移
+      }).then(response => {
+        if (response.code === 200) {
+          this.$message.success('下移成功');
+          this.getCourseList(); // 重新加载列表
+        } else {
+          this.$message.error(response.msg || '下移失败');
+        }
+      }).catch(() => {
+        this.$message.error('下移失败');
+      });
+    },
+    /** 营期状态格式化 */
+    periodStatusFormatter(row) {
+      const statusMap = {
+        1: '未开始',
+        2: '进行中',
+        3: '已结束'
+      };
+      return statusMap[row.periodStatus] || '未知状态';
+    },
+    /** 开课状态格式化 */
+    courseStatusFormatter(row) {
+      const statusMap = {
+        0: '未开始',
+        1: '进行中',
+        2: '已结束'
+      };
+      return statusMap[row.status] || '未知状态';
+    },
+    /** 营期状态格式化 */
+    handleUpdateDate(row) {
+      this.form = {id: row.id, dayDate: row.dayDate};
+      this.updateDateOpen = true;
+    },
+  },
+};
+</script>
+
+<style scoped>
+.left-aside {
+  background-color: #fff;
+  border-right: 1px solid #EBEEF5;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  height: 800px;
+}
+
+.left-header {
+  padding: 10px;
+  border-bottom: 1px solid #EBEEF5;
+}
+
+.left-header-top {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.search-btn {
+  width: 50%;
+  height: 36px;
+  background-color: #409EFF;
+  color: white;
+  border: none;
+}
+
+.search-input-wrapper {
+  margin-bottom: 10px;
+}
+
+.sort-wrapper {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.sort-label {
+  width: 70px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #909399;
+}
+
+.sort-select {
+  margin-left: 10px;
+  width: 280px;
+}
+
+.color-wrapper {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.color-label {
+  width: 70px;
+  color: #606266;
+}
+
+.color-hint {
+  margin-left: 10px;
+  color: #606266;
+  font-size: 12px;
+}
+
+.color-help {
+  margin-left: 5px;
+  color: #909399;
+  cursor: pointer;
+}
+
+.hint-text {
+  color: #606266;
+  font-size: 12px;
+  margin-top: 5px;
+}
+
+.camp-list {
+  flex: 1;
+  overflow-y: auto;
+  padding: 3px;
+}
+
+.camp-item {
+  margin-bottom: 5px;
+  padding: 15px;
+  background-color: #ffffff;
+  position: relative;
+  cursor: pointer;
+  display: flex;
+  justify-content: space-between;
+  border: 1px solid #eaedf2;
+}
+
+.camp-item:last-child {
+  margin-bottom: 0;
+}
+
+.camp-item:hover {
+  background-color: #f5f9ff;
+}
+
+.camp-item.active {
+  background-color: #eaf4ff;
+  border-left: 1px solid #75b8fc;
+}
+
+.camp-content {
+  flex: 1;
+  padding-right: 10px;
+}
+
+.camp-title {
+  font-weight: bold;
+  font-size: 16px;
+  margin-bottom: 8px;
+  color: #333;
+  display: flex;
+  align-items: center;
+}
+
+.camp-icon {
+  font-size: 16px;
+  margin-right: 6px;
+  color: #409EFF;
+}
+
+.camp-info {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8px;
+  font-size: 12px;
+  color: #c4c1c1;
+  line-height: 1.5;
+}
+
+.camp-stats {
+  display: flex;
+  justify-content: space-between;
+  font-size: 12px;
+  color: #666;
+  background-color: #f5f9ff;
+  padding: 6px 10px;
+  border-radius: 4px;
+  line-height: 1.5;
+}
+
+.stat-item {
+  display: flex;
+  align-items: center;
+}
+
+.stat-item i {
+  margin-right: 4px;
+  font-size: 14px;
+  color: #409EFF;
+}
+
+.camp-actions {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: flex-end;
+  gap: 8px;
+  border-left: 1px dashed #eaedf2;
+  padding-left: 12px;
+  min-width: 50px;
+}
+
+.action-btn {
+  padding: 2px 5px;
+  font-size: 12px;
+  border-radius: 4px;
+  transition: all 0.2s;
+}
+
+.action-btn:hover {
+  background-color: rgba(255, 255, 255, 0.8);
+}
+
+.delete-btn {
+  color: #f56c6c;
+}
+
+.delete-btn:hover {
+  background-color: rgba(245, 108, 108, 0.1);
+}
+
+.copy-btn {
+  color: #409EFF;
+}
+
+.copy-btn:hover {
+  background-color: rgba(64, 158, 255, 0.1);
+}
+
+.warning-icon {
+  color: #E6A23C;
+  font-size: 16px;
+  margin-left: 5px;
+}
+
+.el-main {
+  padding: 10px;
+}
+
+/* 添加训练营表单样式 */
+.drawer-footer {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding: 20px;
+  background: #fff;
+  text-align: right;
+  border-top: 1px solid #e8e8e8;
+}
+
+.el-input-number {
+  width: 100%;
+}
+
+/* 加载更多样式 */
+.loading-more {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 12px 0;
+  color: #909399;
+  font-size: 14px;
+}
+
+.loading-more i {
+  margin-right: 5px;
+  font-size: 16px;
+}
+
+/* 无更多数据提示 */
+.no-more-data {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 12px 0;
+  color: #c0c4cc;
+  font-size: 13px;
+}
+
+.no-more-data span {
+  position: relative;
+  display: flex;
+  align-items: center;
+}
+</style>

+ 173 - 0
src/views/course/userCoursePeriod/redPacket.vue

@@ -0,0 +1,173 @@
+<template>
+  <div>
+    <!-- 公司列表弹窗 -->
+<!--    <el-dialog title="设置红包" :visible.sync="companyDialogVisible" width="800px" append-to-body>-->
+      <el-table :data="companyList" border>
+        <el-table-column type="index" label="序号" width="60" align="center" />
+        <el-table-column label="公司名称" prop="companyName" align="center" />
+        <el-table-column label="操作" align="center" width="120">
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="text"
+              @click="handleInputAmount(scope.row)"
+            >设置红包</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+<!--      <div slot="footer" class="dialog-footer">-->
+<!--      </div>-->
+<!--    </el-dialog>-->
+
+    <!-- 课程红包设置弹窗 -->
+    <el-dialog title="设置红包金额" :visible.sync="courseDialogVisible" width="1200px" append-to-body>
+      <el-table :data="redPacketList" border>
+        <el-table-column type="index" label="序号" width="60" align="center" />
+        <el-table-column label="课程" prop="courseName" align="center" />
+        <el-table-column label="小节" prop="videoName" align="center" />
+        <el-table-column label="营期日期" prop="dayDate" align="center"/>
+        <el-table-column label="红包金额" width="200px" align="center">
+          <template slot-scope="scope">
+            <el-input-number
+              v-model="scope.row.amount"
+              :min="0"
+              :precision="2"
+              :step="0.01"
+              size="small"
+              style="width: 150px"
+            >
+              <template slot="append">元</template>
+            </el-input-number>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="courseDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="handleSave">保 存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getPeriodCompanyList, getDays, batchSaveRedPacket, getPeriodRedPacketList } from "@/api/course/userCoursePeriod";
+import redPacket from "@/views/course/userCoursePeriod/redPacket.vue";
+
+export default {
+  name: "RedPacket",
+  computed: {
+    // redPacket() {
+    //   return redPacket
+    // }
+  },
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    activeTab: {
+      type: String,
+      default: ''
+    },
+    periodId: {
+      type: [String, Number],
+      default: ''
+    },
+    // videoId: {
+    //   type: [String, Number],
+    //   default: ''
+    // }
+  },
+  data() {
+    return {
+      // companyDialogVisible: false,
+      // activeTab: '',
+      courseDialogVisible: false,
+      companyList: [],
+      redPacketList: [],
+      currentCompany: null
+    };
+  },
+  created() {
+    if(this.activeTab == "company"){
+      this.getCompanyList();
+    }
+  },
+  watch: {
+    activeTab(val) {
+      this.activeTab = val;
+      if (val == "company") {
+        this.getCompanyList();
+      }
+    },
+    companyDialogVisible(val) {
+      if (!val) {
+        this.$emit('update:visible', false);
+      }
+    }
+  },
+  methods: {
+    // 获取公司列表
+    getCompanyList() {
+      getPeriodCompanyList({
+        periodId: this.periodId
+      }).then(response => {
+        this.companyList = response.data || [];
+      });
+    },
+    // 点击录入金额
+    handleInputAmount(row) {
+      this.currentCompany = row;
+      this.courseDialogVisible = true;
+      this.getCourseList();
+    },
+    // 获取课程列表
+    getCourseList() {
+      getPeriodRedPacketList({
+        periodId: this.periodId,
+        companyId: this.currentCompany.companyId
+      }).then(response => {
+        this.redPacketList = (response.data || []).map(item => ({
+          ...item,
+          amount: item.amount || 0
+        }));
+      });
+    },
+    // 保存红包金额
+    handleSave() {
+      const saveData = this.redPacketList
+        .filter(item => item.amount > 0)
+        .map(item => ({
+          companyId: this.currentCompany.companyId,
+          redPacketMoney: item.amount,
+          videoId: item.videoId,
+          periodId: this.periodId,
+          dataType: 2
+        }));
+
+      if (saveData.length === 0) {
+        this.$message.warning('请至少设置一个红包金额');
+        return;
+      }
+
+      batchSaveRedPacket(saveData).then(response => {
+        if (response.code === 200) {
+          this.$message.success('保存成功');
+          this.courseDialogVisible = false;
+          this.$emit('success');
+        } else {
+          this.$message.error(response.msg || "保存失败");
+        }
+      }).catch(error => {
+        this.$message.error("保存失败:" + error.message);
+      });
+    }
+  }
+};
+</script>
+
+<style scoped>
+.el-input-number {
+  width: 100%;
+}
+</style>

+ 338 - 0
src/views/course/userCoursePeriod/statistics.vue

@@ -0,0 +1,338 @@
+<template>
+  <div class="statistics-container">
+    <!-- 营期课程选择 -->
+    <div class="fixed-header">
+      <el-form :inline="true" :model="queryParams" class="demo-form-inline">
+        <el-form-item label="营期课程">
+          <el-select
+            v-model="queryParams.videoIdList"
+            multiple
+            placeholder="请选择营期课程"
+            style="width: 400px"
+          >
+            <el-option
+              v-for="item in courseOptions"
+              :key="item.videoId"
+              :label="item.videoName"
+              :value="item.videoId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery">查询</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 统计数据展示 -->
+      <el-row :gutter="20" class="statistics-row">
+        <el-col :span="3">
+          <div class="statistics-item">
+            <div class="statistics-title">完播人数</div>
+            <div class="statistics-value">{{ statistics.courseCompleteNum || 0 }}</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="statistics-item">
+            <div class="statistics-title">观看人数</div>
+            <div class="statistics-value">{{ statistics.courseWatchNum || 0 }}</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="statistics-item">
+            <div class="statistics-title">完播率</div>
+            <div class="statistics-value">{{ statistics.completeRate || '0%' }}</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="statistics-item">
+            <div class="statistics-title">观看总次数</div>
+            <div class="statistics-value">{{ statistics.courseWatchTimes || 0 }}</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="statistics-item">
+            <div class="statistics-title">答题总次数</div>
+            <div class="statistics-value">{{ statistics.answerTimes || 0 }}</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="statistics-item">
+            <div class="statistics-title">答题正确总次数</div>
+            <div class="statistics-value">{{ statistics.answerRightTimes || 0 }}</div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="statistics-item">
+            <div class="statistics-title">奖励金额总计(元)</div>
+            <div class="statistics-value">{{ statistics.redPacketAmount || 0 }}</div>
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+
+    <!-- 列表统计展示 -->
+    <div class="table-wrapper">
+      <el-table v-loading="loading" :data="list" border height="calc(100vh - 450px)">
+        <el-table-column type="index" label="序号" width="50" align="center" fixed/>
+        <el-table-column prop="title" label="课程名称" align="center" min-width="250" fixed/>
+        <el-table-column prop="dayDate" label="营期日期" align="center" min-width="120" fixed/>
+        <el-table-column prop="countDetailsVO.courseWatchTimes" label="观看次数" align="center" min-width="100"/>
+        <el-table-column prop="countDetailsVO.courseCompleteTimes" label="完播次数" align="center" min-width="100"/>
+        <el-table-column prop="countDetailsVO.courseWatchNum" label="观看人数" align="center" min-width="100"/>
+        <el-table-column prop="countDetailsVO.courseCompleteNum" label="完播人数" align="center" min-width="100"/>
+        <el-table-column prop="countDetailsVO.completeRate" label="完播率" align="center" min-width="100">
+          <template slot-scope="scope">
+            {{ scope.row.countDetailsVO.completeRate || 0 }}%
+          </template>
+        </el-table-column>
+        <el-table-column prop="countDetailsVO.answerTimes" label="答题次数" align="center" min-width="100"/>
+        <el-table-column prop="countDetailsVO.answerNum" label="答题人数" align="center" min-width="100"/>
+        <el-table-column prop="countDetailsVO.answerRightNum" label="正确人数" align="center" min-width="100"/>
+        <el-table-column prop="countDetailsVO.answerRightRate" label="正确率" align="center" min-width="100">
+          <template slot-scope="scope">
+            {{ scope.row.countDetailsVO.answerRightRate || 0 }}%
+          </template>
+        </el-table-column>
+        <el-table-column prop="countDetailsVO.redPacketNum" label="答题红包个数" align="center" min-width="120"/>
+        <el-table-column prop="countDetailsVO.redPacketAmount" label="答题红包金额(元)" align="center" min-width="150"/>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="custom-pagination-container">
+        <pagination
+          v-show="total > 0"
+          :total="total"
+          :page.sync="queryParams.pageNum"
+          :limit.sync="queryParams.pageSize"
+          @pagination="getCountList"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {getDays, periodCountSelect} from "@/api/course/userCoursePeriod";
+
+export default {
+  name: "CourseStatistics",
+  props: {
+    periodId: {
+      type: [String, Number],
+      default: ''
+    },
+    active: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: false,
+      // 总条数
+      total: 0,
+      // 课程选项
+      courseOptions: [],
+      // 统计数据
+      statistics: {
+        courseCompleteNum: 0,
+        courseWatchNum: 0,
+        completeRate: '0%',
+        courseWatchTimes: 0,
+        answerTimes: 0,
+        answerRightTimes: 0,
+        redPacketAmount: 0
+      },
+      // 列表数据
+      list: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        videoIdList: [],
+        // videoId: '',
+        periodId: ''
+      },
+      // 是否已初始化
+      initialized: false
+    };
+  },
+  watch: {
+    periodId: {
+      handler(newVal) {
+        this.queryParams.periodId = newVal;
+        if (this.active && !this.initialized) {
+          this.initializeData();
+        }
+      },
+      immediate: true
+    },
+    active: {
+      handler(newVal) {
+        if (newVal && !this.initialized) {
+          this.initializeData();
+        }
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    /** 初始化数据 */
+    initializeData() {
+      this.getCourseOptions();
+      this.getCountList();
+      this.initialized = true;
+    },
+    /** 获取课程选项 */
+    getCourseOptions() {
+      this.loading = true;
+      getDays(this.queryParams).then(r => {
+        if (r.code === 200) {
+          this.courseOptions = r.rows;
+          this.loading = false;
+        } else {
+          this.$message.error(r.msg || '获取数据失败');
+        }
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+    /** 查询按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getCountList();
+    },
+    /** 课程选择变化 */
+    handleCourseChange() {
+    },
+    /** 获取列表数据 */
+    getCountList() {
+      this.loading = true;
+      periodCountSelect(this.queryParams).then(response => {
+        if (response.code === 200) {
+          // 设置列表数据
+          this.list = response.rows;
+          this.total = response.total || 0;
+
+          // 计算总统计数据
+          this.calculateTotalStatistics();
+
+          console.log('列表数据:', this.list);
+        } else {
+          this.$message.error(response.msg || '获取数据失败');
+        }
+        this.loading = false;
+      }).catch(error => {
+        console.error('获取数据失败:', error);
+        this.$message.error('获取数据失败');
+        this.loading = false;
+      });
+    },
+    /** 计算总统计数据 */
+    calculateTotalStatistics() {
+      // 初始化统计数据
+      this.statistics = {
+        courseCompleteNum: 0,
+        courseWatchNum: 0,
+        completeRate: '0%',
+        courseWatchTimes: 0,
+        answerTimes: 0,
+        answerRightTimes: 0,
+        redPacketAmount: 0
+      };
+
+      // 如果没有数据,直接返回
+      if (!this.list || this.list.length === 0) {
+        return;
+      }
+
+      // 遍历列表数据,累加各项统计数据
+      this.list.forEach(item => {
+        const details = item.countDetailsVO || {};
+
+        // 累加各项数据
+        this.statistics.courseCompleteNum += details.courseCompleteNum || 0;
+        this.statistics.courseWatchNum += details.courseWatchNum || 0;
+        this.statistics.courseWatchTimes += details.courseWatchTimes || 0;
+        this.statistics.answerTimes += details.answerTimes || 0;
+        this.statistics.answerRightTimes += details.answerRightNum || 0;
+        this.statistics.redPacketAmount += details.redPacketAmount || 0;
+      });
+
+      // 计算完播率
+      if (this.statistics.courseWatchNum > 0) {
+        const rate = (this.statistics.courseCompleteNum / this.statistics.courseWatchNum * 100).toFixed(2);
+        this.statistics.completeRate = rate + '%';
+      }
+    }
+  }
+};
+</script>
+
+<style scoped>
+.statistics-container {
+  height: 100%;
+  overflow: hidden;
+  position: relative;
+}
+
+.fixed-header {
+  position: sticky;
+  top: 0;
+  z-index: 10;
+  background-color: #fff;
+  padding: 10px 0;
+  border-bottom: 1px solid #EBEEF5;
+}
+
+.table-wrapper {
+  height: calc(100% - 220px);
+  overflow: visible;
+  position: relative;
+}
+
+.custom-pagination-container {
+  padding: 10px 0 12px 0;
+  text-align: right;
+  background-color: #fff;
+  position: relative;
+  z-index: 1;
+}
+
+/* 覆盖原有的pagination-container样式 */
+:deep(.pagination-container) {
+  height: auto !important;
+  margin-bottom: 0 !important;
+  margin-top: 0 !important;
+  padding: 0 !important;
+}
+
+.statistics-row {
+  margin: 20px 0;
+}
+
+.statistics-item {
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  padding: 15px;
+  text-align: center;
+  height: 100px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.statistics-title {
+  font-size: 14px;
+  color: #606266;
+  margin-bottom: 10px;
+}
+
+.statistics-value {
+  font-size: 20px;
+  font-weight: bold;
+  color: #303133;
+}
+</style>

+ 1901 - 0
src/views/course/videoResource/index.vue

@@ -0,0 +1,1901 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="素材名称" prop="resourceName">
+        <el-input
+          v-model="queryParams.resourceName"
+          placeholder="请输入素材名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="文件名称" prop="fileName">
+        <el-input
+          v-model="queryParams.fileName"
+          placeholder="请输入素材名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="分类" prop="typeId">
+        <el-select v-model="queryParams.typeId" clearable placeholder="请选择分类" @change="changeCateType">
+          <el-option
+            v-for="item in rootTypeList"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="item.dictValue">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="子分类" prop="typeSubId">
+        <el-select v-model="queryParams.typeSubId" clearable placeholder="请选择子分类">
+          <el-option
+            v-for="item in subTypeList"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="item.dictValue">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['course:videoResource:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleBatchAdd"
+          v-hasPermi="['course:videoResource:add']"
+        >批量新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['course:videoResource:remove']"
+        >删除</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="resourceList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="序号" width="55" align="center">
+              <template slot-scope="scope">
+                {{ scope.$index + 1 }}
+              </template>
+            </el-table-column>
+      <el-table-column label="素材名称" align="center" :show-overflow-tooltip="true" prop="resourceName" width="300"/>
+      <el-table-column label="文件名称" align="center" :show-overflow-tooltip="true" prop="fileName" width="300"/>
+      <el-table-column label="分类" align="center" width="120">
+        <template slot-scope="scope">
+          <span v-if="scope.row.typeId">{{ getTypeName(scope.row.typeId) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="子分类" align="center" width="120">
+        <template slot-scope="scope">
+          <span v-if="scope.row.typeSubId">{{ getTypeName(scope.row.typeSubId) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="视频文件" align="center" width="120">
+        <template slot-scope="scope">
+          <a
+            @click="handleVideoPreview(scope.row.videoUrl)"
+            style="background-color: #409EFF; color: white; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; display: inline-block; text-decoration: none;">
+            查看文件
+          </a>
+        </template>
+      </el-table-column>
+      <el-table-column label="CDN" align="center" width="120">
+        <template slot-scope="scope">
+          <a
+            @click="copy(scope.row.videoUrl)"
+            style="color: #409EFF; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; display: inline-block; text-decoration: none;">
+            复制链接
+          </a>
+        </template>
+      </el-table-column>
+      <el-table-column label="关联题目" align="center" width="150">
+        <template slot-scope="scope">
+          <a
+            @click="handleViewProject(scope.row, 3)"
+            :style="scope.row.projectIds ? {
+              backgroundColor: '#409EFF',
+              color: 'white',
+              border: 'none',
+              borderRadius: '4px',
+              padding: '4px 12px',
+              cursor: 'pointer',
+              fontSize: '12px',
+              display: 'inline-block',
+              textDecoration: 'none'
+            } : {
+              backgroundColor: 'rgb(154 156 159)',
+              color: 'white',
+              border: 'none',
+              cursor: 'pointer',
+              borderRadius: '4px',
+              padding: '4px 12px',
+              fontSize: '12px',
+              display: 'inline-block',
+              textDecoration: 'none'
+            }">
+            {{ scope.row.projectIds ? '查看详情' : '未关联题目' }}
+          </a>
+        </template>
+      </el-table-column>
+      <el-table-column label="视频时长" align="center" width="150">
+        <template slot-scope="scope">
+          <div style="padding: 4px 12px;background: linear-gradient(to right, rgb(196 219 255), #409EFF)">{{ formatDuration(scope.row.duration) }}</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['course:videoResource:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['course:videoResource:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改视频素材库对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="700px" append-to-body :before-close="cancel">
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="素材名称" prop="resourceName" style="margin-top: 20px">
+          <el-input v-model="form.resourceName" placeholder="请输入" />
+        </el-form-item>
+
+        <el-form-item label="文件名称" prop="fileName" style="margin-top: 20px;display: none">
+          <el-input v-model="form.fileName" placeholder="请输入" />
+        </el-form-item>
+
+        <el-form-item label="分类" prop="typeId">
+          <el-select v-model="form.typeId" placeholder="请选择分类" style="width: 100%" @change="changeCateType">
+            <el-option
+              v-for="item in rootTypeList"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue">
+            </el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="子分类" prop="typeSubId">
+          <el-select v-model="form.typeSubId" clearable placeholder="请选择子分类" style="width: 100%">
+            <el-option
+              v-for="item in subTypeList"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue">
+            </el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="关联题目" prop="projectIds">
+          <el-select
+            ref="customSelect"
+            class="custom-select-class"
+            v-model="form.projectIds"
+            multiple
+            placeholder="请选择关联题目"
+            @click.native.stop="openProjectDialog(form.projectIds, 0)"
+            style="width: 100%;">
+            <el-option
+              v-for="item in projectShowList"
+              :key="item.id"
+              :label="item.title"
+              :value="item.id">
+            </el-option>
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="上传视频" prop="videoUrl" required>
+          <el-upload
+            ref="videoUpload"
+            action="#"
+            list-type="picture-card"
+            :http-request="videoUpload"
+            :file-list="fileList"
+            :on-change="handleFileChange"
+            accept=".mp4">
+            <i slot="default" class="el-icon-plus"></i>
+            <div slot="file" slot-scope="{file}">
+              <div
+                class="el-upload-list__item-thumbnail"
+                style="display: flex; justify-content: center; align-items: center; background-color: #f5f7fa; height: 146px;"
+              >
+                <i class="el-icon-video-camera" style="font-size: 48px;" />
+              </div>
+
+              <span v-if="file.status === 'success'" class="el-upload-list__item-status-label">
+                <i class="el-icon-upload-success el-icon--check"></i>
+              </span>
+              <span v-if="file.status === 'fail'" class="el-upload-list__item-status-label">
+                <i class="el-icon-circle-close"></i>
+              </span>
+
+              <span class="el-upload-list__item-actions">
+                <span
+                  class="el-upload-list__item-preview"
+                  @click="handleVideoPreview(form.videoUrl)"
+                >
+                  <i class="el-icon-zoom-in"></i>
+                </span>
+                <span
+                  class="el-upload-list__item-delete"
+                  @click="handleRemove(file)"
+                >
+                  <i class="el-icon-delete"></i>
+                </span>
+              </span>
+
+              <div v-if="file.status === 'uploading'" class="el-upload-list__item-progress">
+                <el-progress
+                  :percentage="file.percentage"
+                  :show-text="false"
+                  :width="52"
+                ></el-progress>
+              </div>
+            </div>
+          </el-upload>
+        </el-form-item>
+
+        <el-form-item label="时长">
+          <span>{{ formatDuration(form.duration) }}</span>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="cancel">取消</el-button>
+        <el-button type="primary" @click="submitForm">保存</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog
+      title="视频预览"
+      :visible.sync="videoPreviewVisible"
+      append-to-body
+      :close-on-click-modal="true"
+      width="700px"
+      class="video-preview-dialog"
+      :modal-append-to-body="false"
+      :before-close="handleCloseVideoPreview">
+      <video ref="up-video" id="video" width="100%" height="400px" controls :src="videoPreviewUrl" />
+    </el-dialog>
+
+    <!-- 批量选择视频弹窗 -->
+    <el-dialog :title="'选择视频'" :visible.sync="batchAddVisible" width="1200px" append-to-body class="batch-dialog" :close-on-click-modal="false" :before-close="cancelBeforeBatch">
+      <div class="filter-container">
+        <el-button type="primary" icon="el-icon-plus" size="small" @click="showUploadPanel">上传视频</el-button>
+      </div>
+
+      <el-table
+        v-loading="batchLoading"
+        :data="videoList"
+        height="350">
+        <el-table-column label="序号" width="60" align="center">
+          <template slot-scope="scope">
+            {{ scope.$index + 1 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="素材名称" align="center" prop="resourceName" min-width="120" />
+        <el-table-column label="文件名称" align="center" prop="fileName" min-width="120" />
+        <el-table-column label="分类" align="center" min-width="100">
+          <template slot-scope="scope">
+            {{ getTypeName(scope.row.typeId) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="关联项目" align="center" min-width="100">
+          <template slot-scope="scope">
+          <a
+            @click="handleViewProject(scope.row, 4)"
+            :style="scope.row.projectIds.length > 0 ? {
+              backgroundColor: '#409EFF',
+              color: 'white',
+              border: 'none',
+              borderRadius: '4px',
+              padding: '4px 12px',
+              cursor: 'pointer',
+              fontSize: '12px',
+              display: 'inline-block',
+              textDecoration: 'none'
+            } : {
+              backgroundColor: 'rgb(154 156 159)',
+              color: 'white',
+              border: 'none',
+              cursor: 'pointer',
+              borderRadius: '4px',
+              padding: '4px 12px',
+              fontSize: '12px',
+              display: 'inline-block',
+              textDecoration: 'none'
+            }">
+            {{ scope.row.projectIds.length > 0 ? '查看详情' : '未关联题目' }}
+          </a>
+        </template>
+        </el-table-column>
+        <el-table-column label="视频文件" align="center" prop="fileName" min-width="120">
+          <template slot-scope="scope">
+            <a
+              @click="handleVideoPreview(scope.row.videoUrl)"
+              style="background-color: #409EFF; color: white; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; display: inline-block; text-decoration: none;">
+              查看文件
+            </a>
+          </template>
+        </el-table-column>
+        <el-table-column label="视频时长" align="center" width="80">
+          <template slot-scope="scope">
+            {{ formatDuration(scope.row.duration) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="上传进度" align="center" width="120">
+          <template slot-scope="scope">
+            <el-progress :percentage="scope.row.progress" :status="scope.row.progress === 100 ? 'success' : undefined"></el-progress>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="150">
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-edit"
+              @click="handleEditVideo(scope.row)">编辑</el-button>
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-delete"
+              @click="handleDeleteVideo(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="dialog-footer">
+        <el-button @click="cancelBatch">取 消</el-button>
+        <el-button type="primary" @click="submitBatchAdd">保 存</el-button>
+      </div>
+
+      <!-- 批量上传视频弹窗 -->
+      <el-dialog
+        title="批量上传视频"
+        :visible.sync="showUpload"
+        width="500px"
+        append-to-body
+        :close-on-click-modal="false"
+        class="upload-dialog">
+        <el-form :model="batchUploadForm" ref="batchUploadForm" label-width="80px">
+          <el-form-item style="margin-top: 20px" label="分类" prop="typeId" :rules="[{ required: true, message: '请选择分类', trigger: 'blur' }]">
+            <el-select v-model="batchUploadForm.typeId" placeholder="请选择分类" style="width: 100%" @change="changeCateType">
+              <el-option
+                v-for="item in rootTypeList"
+                :key="item.dictValue"
+                :label="item.dictLabel"
+                :value="item.dictValue">
+              </el-option>
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="子分类" prop="typeSubId" :rules="[{ required: true, message: '请选择子分类', trigger: 'blur' }]">
+            <el-select v-model="batchUploadForm.typeSubId" clearable placeholder="请选择子分类" style="width: 100%">
+              <el-option
+                v-for="item in subTypeList"
+                :key="item.dictValue"
+                :label="item.dictLabel"
+                :value="item.dictValue">
+              </el-option>
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="关联题目" prop="projectIds" style="display: none">
+            <el-select
+              ref="customSelect"
+              class="custom-select-class"
+              v-model="batchUploadForm.projectIds"
+              multiple
+              placeholder="请选择关联题目"
+              @click.native.stop="openProjectDialog(batchUploadForm.projectIds, 1)"
+              style="width: 100%;">
+              <el-option
+                v-for="item in projectShowList"
+                :key="item.id"
+                :label="item.title"
+                :value="item.id">
+              </el-option>
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="上传视频" prop="files">
+            <el-upload
+              ref="batchVideoUpload"
+              action="#"
+              :http-request="batchVideoUpload"
+              :file-list="batchFileList"
+              :on-change="handleBatchFileChange"
+              multiple
+              accept=".mp4">
+              <el-button type="primary" :disabled="batchUploadForm.typeSubId === null">选择文件</el-button>
+              <div slot="tip" class="el-upload__tip">选择分类后才可以上传视频</div>
+            </el-upload>
+          </el-form-item>
+        </el-form>
+      </el-dialog>
+
+      <!-- 添加或修改视频素材库对话框 -->
+      <el-dialog :title="batchEditDialog.title" :visible.sync="batchEditDialog.open" width="600px" append-to-body :before-close="batchEditCancel">
+        <el-form ref="batchEditDialogForm" :model="batchEditDialog.form" :rules="batchEditDialog.rules" label-width="80px">
+          <el-form-item label="素材名称" prop="resourceName" style="margin-top: 20px">
+            <el-input v-model="batchEditDialog.form.resourceName" placeholder="请输入" />
+          </el-form-item>
+
+          <el-form-item label="文件名称" prop="fileName" style="margin-top: 20px;display: none">
+            <el-input v-model="batchEditDialog.form.fileName" placeholder="请输入" />
+          </el-form-item>
+
+          <el-form-item label="关联题目" prop="projectIds">
+            <el-select
+              ref="customSelect"
+              class="custom-select-class"
+              v-model="batchEditDialog.form.projectIds"
+              multiple
+              placeholder="请选择关联题目"
+              @click.native.stop="openProjectDialog(batchEditDialog.form.projectIds, 2)"
+              style="width: 100%;">
+              <el-option
+                v-for="item in projectShowList"
+                :key="item.id"
+                :label="item.title"
+                :value="item.id">
+              </el-option>
+            </el-select>
+          </el-form-item>
+        </el-form>
+        <div slot="footer" class="dialog-footer">
+          <el-button @click="batchEditCancel">取消</el-button>
+          <el-button type="primary" @click="batchEditSubmitForm">保存</el-button>
+        </div>
+      </el-dialog>
+
+    </el-dialog>
+
+    <!-- 项目选择弹窗 -->
+    <el-dialog
+      title="选择题目"
+      :visible.sync="projectDialogVisible"
+      width="1000px"
+      append-to-body
+      @close="cancelSelectProject"
+      :close-on-click-modal="false">
+      <div class="project-container">
+        <!-- 左侧分类树 -->
+        <div class="category-tree">
+          <div class="tree-fixed-header">
+            <el-input
+              placeholder="请输入分类名称"
+              v-model="categoryFilterText"
+              size="small"
+              clearable
+              prefix-icon="el-icon-search">
+            </el-input>
+          </div>
+          <div class="tree-content">
+            <el-tree
+              ref="categoryTree"
+              :data="categoryTreeData"
+              node-key="cateId"
+              highlight-current
+              :filter-node-method="filterNode"
+              :props="{ label: 'cateName', children: 'children' }"
+              @node-click="handleCategoryClick"
+              :expand-on-click-node="false"
+              default-expand-all>
+              <span class="custom-tree-node" slot-scope="{ node, data }">
+                <span>{{ node.label }}</span>
+              </span>
+            </el-tree>
+          </div>
+        </div>
+
+        <!-- 右侧题目列表 -->
+        <div class="project-list">
+          <div class="filter-container">
+            <el-form :inline="true" :model="projectQueryParams" ref="projectForm">
+              <el-form-item>
+                <el-input prefix-icon="el-icon-search" @input="searchProjects" v-model="projectQueryParams.title" placeholder="请输入题目标题" clearable size="small" />
+              </el-form-item>
+            </el-form>
+          </div>
+
+          <el-table
+            ref="projectTable"
+            height="350"
+            :data="projectList"
+            row-key="id"
+            v-loading="projectLoading"
+            border
+            @select="handleProjectSelect"
+            @select-all="handleProjectSelect">
+            <el-table-column type="selection" reserve-selection width="55" align="center" />
+            <el-table-column label="序号" width="60" align="center">
+              <template slot-scope="scope">
+                {{ scope.$index + 1 }}
+              </template>
+            </el-table-column>
+            <el-table-column label="题目标题" align="center" prop="title" min-width="200" show-overflow-tooltip />
+          </el-table>
+
+          <div class="table-footer">
+            <el-pagination
+              style="text-align: right"
+              v-show="projectListTotal>0"
+              :pager-count="5"
+              background
+              @size-change="handleProjectPageSizeChange"
+              @current-change="handleProjectPageChange"
+              :current-page="projectQueryParams.pageNum"
+              :page-sizes="[10, 20, 30, 50]"
+              :page-size="projectQueryParams.pageSize"
+              layout="total, sizes, prev, pager, next, jumper"
+              :total="projectListTotal">
+            </el-pagination>
+          </div>
+        </div>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <span style="float: left; color: #606266; font-size: 13px;">
+          共选择 <span style="color: #409EFF; font-weight: bold;">{{ selectedProjectIds.length }}</span> 个题目
+        </span>
+        <el-button @click="projectDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmSelectProject">确定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 项目列表弹窗 -->
+    <el-dialog
+      title="题目列表"
+      :visible.sync="projectListDialogVisible"
+      width="600px"
+      append-to-body
+      :close-on-click-modal="false">
+      <div class="project-list-container" style="max-height: 500px; overflow-y: auto; padding: 10px;">
+        <!-- 题目列表 -->
+        <div v-for="(item, index) in projectShowList" :key="index" class="question-card">
+          <!-- 题目标题 -->
+          <div class="question-header">
+            <span class="question-index">{{ index + 1 }}</span>
+            <span class="question-title">{{ item.title }}</span>
+          </div>
+
+          <!-- 题目类型 -->
+          <div class="question-type">
+            <el-tag size="small" type="primary">{{ item.type === 1 ? '单选' : '多选' }}</el-tag>
+          </div>
+
+          <!-- 题目内容 -->
+          <div class="question-content" v-if="item.question && item.question.length > 0">
+            <div class="content-title">题目内容:</div>
+            <div v-for="(q, qIndex) in JSON.parse(item.question)" :key="qIndex" class="question-item"
+                 :style=" q.isAnswer === 1 ? 'background-color: rgb(234 245 251)' : ''">
+              <div class="question-item-header">
+                <span class="item-index">{{ convertToLetter(qIndex) }}</span>
+                <span class="item-name">{{ q.name }}</span>
+              </div>
+            </div>
+          </div>
+
+          <!-- 答案 -->
+          <div class="question-answer" v-if="item.answer">
+            <span class="answer-label">答案:</span>
+            <span class="answer-content">{{ item.answer }}</span>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  addVideoResource,
+  deleteVideoResource,
+  getVideoResource,
+  listVideoResource,
+  updateVideoResource,
+  batchAddVideoResource
+} from '@/api/course/videoResource'
+import {listUserCourseCategory,getCatePidList,getCateListByPid} from '@/api/course/userCourseCategory'
+import {getByIds, listCourseQuestionBank} from '@/api/course/courseQuestionBank'
+import {getThumbnail} from "@/api/course/userVideo";
+import {uploadObject} from "@/utils/cos.js";
+import {uploadToOBS} from "@/utils/obs.js";
+
+export default {
+  name: 'VideoResource',
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 视频素材库表格数据
+      resourceList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        resourceName: null,
+        fileName: null,
+        typeId: null
+      },
+      // 表单参数
+      form: {
+        id: null,
+        resourceName: null,
+        fileName: null,
+        thumbnail: null,
+        line1: null,
+        line2: null,
+        line3: null,
+        duration: null,
+        fileSize: null,
+        fileKey: null,
+        videoUrl: null,
+        typeId: null,
+        typeSubId: null,
+        projectIds: []
+      },
+      // 表单校验
+      rules: {
+        resourceName: [
+          { required: true, message: "素材名称不能为空", trigger: "blur" }
+        ],
+        fileName: [
+          { required: true, message: "文件名称不能为空", trigger: "blur" }
+        ],
+        typeId: [
+          { required: true, message: "请选择分类", trigger: "change" }
+        ],
+        typeSubId: [
+          { required: true, message: "请选择子分类", trigger: "change" }
+        ],
+        videoUrl: [
+          { required: true, message: "请上传视频", trigger: "change" }
+        ]
+      },
+      // 课程列表数据
+      projectList: [],
+      projectShowList: [],
+      // 分类
+      typeList: [],
+      rootTypeList: [],
+      subTypeList: [],
+      // 视频上传
+      videoDisabled: false,
+      videoPreviewVisible: false,
+      videoPreviewUrl: '',
+      fileList: [],
+      // 批量添加相关
+      batchAddVisible: false,
+      batchLoading: false,
+      videoList: [],
+
+      // 批量上传相关
+      showUpload: false,
+      batchUploadForm: {
+        typeId: null,
+        projectIds: [],
+        files: []
+      },
+      batchFileList: [],
+
+      // 弹窗选择项目相关
+      projectDialogVisible: false,
+      projectQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        questionType: null,
+        title: null
+      },
+      projectLoading: false,
+      projectListTotal: 0,
+      selectedProjectIds: [],
+      selectedType: 0,
+      currentRow: null,
+      // 分类树相关数据
+      categoryFilterText: '',
+      categoryTreeData: [],
+      add: false,
+      // 题目列表
+      projectListDialogVisible: false,
+      // 修改视频记录
+      batchEditDialog: {
+        title: '修改视频',
+        open: false,
+        form: {},
+        rules: {
+          resourceName: [
+            { required: true, message: "素材名称不能为空", trigger: "blur" }
+          ],
+          fileName: [
+            { required: true, message: "文件名称不能为空", trigger: "blur" }
+          ]
+        },
+      },
+    }
+  },
+  watch: {
+    categoryFilterText(val) {
+      this.$refs.categoryTree.filter(val);
+    }
+  },
+  created() {
+    this.getTypeList();
+    this.getRootTypeList()
+    this.getList();
+  },
+  methods: {
+    /** 将数字索引转换为字母序号 (0->A, 1->B, 等) */
+    convertToLetter(index) {
+      // 确保索引是数字
+      let numIndex = parseInt(index, 10);
+      // 如果无法转换成数字,返回原始值
+      if (isNaN(numIndex)) {
+        return index;
+      }
+      // 处理负数情况
+      if (numIndex < 0) {
+        return index;
+      }
+      // 直接使用索引计算ASCII码(0对应A,1对应B,以此类推)
+      return String.fromCharCode(65 + numIndex); // 65 是大写字母 A 的ASCII码
+    },
+    /** 查询视频素材库列表 */
+    getList() {
+      this.loading = true;
+      listVideoResource(this.queryParams).then(response => {
+        this.resourceList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.$refs.videoUpload.clearFiles()
+      this.open = false;
+      this.reset();
+      this.changeCateType(this.queryParams.typeId)
+    },
+    // 表单重置
+    reset() {
+      // 初始化表单对象
+      this.form = {
+        id: null,
+        resourceName: null,
+        fileName: null,
+        thumbnail: null,
+        line1: null,
+        line2: null,
+        line3: null,
+        duration: null,
+        fileSize: null,
+        fileKey: null,
+        videoUrl: null,
+        typeId: null,
+        projectIds: []
+      };
+      // 重置表单验证状态
+      this.resetForm("form");
+      this.add = false
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      // 先清空文件列表
+      this.fileList = [];
+      this.projectShowList = [];
+
+      // 先重置表单
+      this.reset();
+      this.subTypeList = []
+
+      // 重置上传组件
+      if (this.$refs.videoUpload) {
+        this.$refs.videoUpload.clearFiles();
+      }
+
+      // 所有重置完成后再打开弹窗
+      this.open = true;
+      this.title = "添加视频素材库";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      // 先清空文件列表
+      this.fileList = [];
+      this.projectShowList = [];
+
+      // 先重置表单
+      this.reset();
+      this.subTypeList = []
+
+      const id = row.id
+
+      // 获取数据并设置表单
+      getVideoResource(id).then(async response => {
+        this.form = response.data;
+        await this.changeCateType(this.form.typeId)
+
+        // 处理projectIds,确保是数组格式
+        if (this.form.projectIds && typeof this.form.projectIds === 'string') {
+          this.form.projectIds = this.form.projectIds.split(',').map(id => parseInt(id));
+        } else if (!this.form.projectIds) {
+          this.form.projectIds = [];
+        }
+
+        // 如果存在关联项目,获取项目详情用于回显
+        if (this.form.projectIds && this.form.projectIds.length > 0) {
+          // 加载项目列表信息用于回显
+          await getByIds({ids: this.form.projectIds.join(',')}).then(reponse => {
+            this.projectShowList = reponse.data
+          });
+        }
+
+        // 如果存在视频URL,设置fileList
+        if (this.form.videoUrl) {
+          this.fileList = [{
+            name: this.form.fileName || '视频文件',
+            url: this.form.videoUrl,
+            thumbnail: this.form.thumbnail,
+            status: 'success' // 设置为成功状态
+          }];
+        }
+
+        // 所有设置完成后再打开弹窗
+        this.open = true;
+        this.title = "修改视频素材库";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.add){
+            this.$message.warning("文件上传中,请稍后再试")
+            return
+          }
+
+          const params = Object.assign({}, this.form);
+          params.projectIds = this.form.projectIds.join(',');
+          if (this.form.id != null) {
+            updateVideoResource(params).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          } else {
+            addVideoResource(params).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除视频素材库编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return deleteVideoResource(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(function() {});
+    },
+    /** 查询视频分类列表 */
+    getTypeList() {
+      listUserCourseCategory().then(response => {
+        this.typeList = response.data;
+      });
+    },
+    getRootTypeList() {
+      getCatePidList().then(response => {
+        this.rootTypeList = response.data
+      });
+    },
+    async changeCateType(val) {
+      if (!val) {
+        this.subTypeList = []
+        return
+      }
+      await getCateListByPid(val).then(response => {
+        this.subTypeList = response.data
+      })
+    },
+    /** 预览视频 */
+    handleVideoPreview(url) {
+      this.videoPreviewVisible = true;
+      this.videoPreviewUrl = url || this.form.videoUrl;
+    },
+    /** 格式化视频时长 */
+    formatDuration(seconds) {
+      if (!seconds) return '00:00';
+
+      const minutes = Math.floor(seconds / 60);
+      const remainingSeconds = seconds % 60;
+
+      return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
+    },
+    /** 移除视频 */
+    handleRemove(file) {
+      this.fileList.splice(this.fileList.indexOf(file), 1);
+      this.form.videoUrl = '';
+      this.form.thumbnail = '';
+      this.form.duration = 0;
+      this.form.fileSize = 0;
+      this.form.fileName = '';
+      this.form.line1 = '';
+      this.form.line_2 = '';
+      this.form.line_3 = '';
+      this.uploadType = null
+      this.fileSize = null
+      this.fileKey = null
+    },
+    //获取第一帧封面
+    async getFirstThumbnail(file, form){
+      getThumbnail(file).then(response => {
+        form.thumbnail = response.url;
+      })
+    },
+    //上传腾讯云Pcdn
+    async uploadVideoToTxPcdn(file, form, onProgress) {
+      try {
+        const data = await uploadObject(file, (progress) => {
+          const progressEvent = {
+            percent: Math.floor(progress.percent * 100 / 2), // COS SDK 百分比是 0-1,el-upload 需要 0-100
+            loaded: progress.loaded,
+            total: progress.total,
+            lengthComputable: true // 文件上传通常总大小可知
+          };
+          onProgress(progressEvent);
+        }, 1);
+
+        let line_1 = `https://myhktcpv.ylrzcloud.com${data.urlPath}`;
+
+        form.fileKey = data.urlPath.substring(1);
+        form.videoUrl = line_1;
+        form.line1 = line_1;
+
+        this.$message.success("线路一上传成功");
+      } catch (error) {
+        this.$message.error("线路一上传失败");
+      }
+    },
+    //上传华为云Obs
+    async uploadVideoToHwObs(file, form, onProgress) {
+      try {
+        const data = await uploadToOBS(file, (progress) => {
+          const progressEvent = {
+            percent: Math.floor(progress / 2) + 50,
+            loaded: progress,
+            total: progress,
+            lengthComputable: true // 文件上传通常总大小可知
+          };
+          onProgress(progressEvent);
+        }, 1);
+
+        form.line2 = `https://myhkobs.ylrztop.com/${data.urlPath}`;
+        this.$message.success("线路二上传成功");
+      } catch (error) {
+        this.$message.error("线路二上传失败");
+      }
+    },
+    // 上传视频
+    async videoUpload(options) {
+      this.add = true
+      const file = options.file;
+      this.getMediaDuration(file)
+
+      // 获取第一帧图片
+      await this.getFirstThumbnail(file, this.form);
+
+      // 上传腾讯云pcdn
+      await this.uploadVideoToTxPcdn(file, this.form, options.onProgress);
+
+      // 上传华为obs
+      await this.uploadVideoToHwObs(file, this.form, options.onProgress);
+
+      this.form.fileName = file.name;
+      this.form.fileSize = file.size;
+
+      this.add = false
+    },
+    // 获取媒体文件时长
+    getMediaDuration(file) {
+      // 处理视频
+      const video = document.createElement('video');
+      video.preload = 'metadata';
+      video.onloadedmetadata = () => {
+        this.form.duration = Math.round(video.duration);
+      };
+      video.src = URL.createObjectURL(file);
+    },
+    handleFileChange(file, files) {
+      // 保留最后一个文件
+      this.fileList = files.slice(-1);
+    },
+    /** 批量新增 */
+    handleBatchAdd() {
+      this.batchAddVisible = true;
+      this.videoList = []; // 清空之前的视频列表
+    },
+    cancelBeforeBatch(done, cancel) {
+      if (!this.videoList || this.videoList.length === 0) {
+        done()
+        return
+      }
+      this.$confirm('关闭窗口视频需要重新上传,确定关闭吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        done()
+      }).catch(() => {
+        cancel()
+      });
+    },
+    /** 取消批量 */
+    cancelBatch() {
+      if (!this.videoList || this.videoList.length === 0) {
+        this.batchAddVisible = false
+        return
+      }
+      this.$confirm('关闭窗口视频需要重新上传,确定关闭吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+        }).then(() => {
+          this.batchAddVisible = false
+          this.changeCateType(this.queryParams.typeId)
+      }).catch(() => {
+      });
+    },
+
+    /** 提交批量添加 */
+    submitBatchAdd() {
+      // 检查是否所有选中的视频都已上传完成
+      const incompleteVideos = this.videoList.filter(item => (item.progress || 0) < 100);
+
+      if (incompleteVideos.length > 0) {
+        this.$message.warning('有未完成上传的视频,请先完成上传');
+        return;
+      }
+
+      // 调用批量添加API
+      const videoList = JSON.parse(JSON.stringify(this.videoList));
+      videoList.forEach(item => {
+        item.projectIds = item.projectIds.join(",");
+      });
+      batchAddVideoResource(videoList).then(response => {
+        if (response.code === 200) {
+          this.$message.success('批量添加成功');
+          this.batchAddVisible = false;
+          this.getList();
+        }
+      });
+    },
+
+    /** 获取分类名称 */
+    getTypeName(typeId) {
+      const type = this.typeList.find(item => item.cateId === typeId);
+      return type ? type.cateName : '';
+    },
+    /** 编辑视频信息 */
+    handleEditVideo(row) {
+      this.batchEditDialog.form = Object.assign({}, row)
+      this.changeCateType(row.typeId)
+      this.batchEditDialog.open = true
+    },
+    batchEditCancel() {
+      this.resetForm('batchEditDialogForm')
+      this.batchEditDialog.open = false
+    },
+    batchEditSubmitForm() {
+      let temp = this.videoList.find(item => item.tempId === this.batchEditDialog.form.tempId)
+      Object.assign(temp, this.batchEditDialog.form)
+      this.batchEditDialog.open = false
+    },
+    /** 删除视频 */
+    handleDeleteVideo(row) {
+      this.$confirm('确认要从列表中删除该视频吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        // 从视频列表中删除该项
+        const index = this.videoList.findIndex(item => {
+          // 通过文件名和大小确定唯一性,因为临时视频可能没有id
+          return item.tempId === row.tempId
+        });
+
+        if (index !== -1) {
+          this.videoList.splice(index, 1);
+          this.$message({
+            type: 'success',
+            message: '已从列表中移除该视频'
+          });
+        }
+      }).catch(() => {
+        // 取消删除
+      });
+    },
+
+    /** 显示上传面板 */
+    showUploadPanel() {
+      this.showUpload = true;
+      this.batchUploadForm = {
+        typeId: null,
+        typeSubId: null,
+        projectIds: [],
+        files: []
+      };
+      this.batchFileList = [];
+      this.subTypeList = []
+      if (this.$refs.batchVideoUpload) {
+        this.$refs.batchVideoUpload.clearFiles();
+      }
+    },
+
+    /** 批量文件变更 */
+    handleBatchFileChange(file, fileList) {
+      this.batchFileList = fileList;
+    },
+    /** 批量上传视频 */
+    async batchVideoUpload(options) {
+      const file = options.file;
+
+      // 创建临时视频对象添加到列表中
+      const tempVideo = {
+        tempId: Math.random().toString(36).substring(2, 15),
+        resourceName: file.name.split('.')[0], // 使用文件名作为视频名称
+        fileName: file.name,
+        thumbnail: null,
+        line1: null,
+        line2: null,
+        line3: null,
+        duration: 0, // 先默认为0,后续获取真实时长
+        fileSize: file.size,
+        fileKey: null,
+        videoUrl: null,
+        typeId: this.batchUploadForm.typeId, // 使用选择的分类
+        typeSubId: this.batchUploadForm.typeSubId, // 使用选择的子分类
+        projectIds: this.batchUploadForm.projectIds, // 使用选择的项目
+        progress: 0, // 上传进度
+        file: file // 保存文件引用
+      };
+
+      // 添加到视频列表
+      this.videoList.unshift(tempVideo);
+
+      // 获取视频时长
+      const video = document.createElement('video');
+      video.preload = 'metadata';
+      video.onloadedmetadata = () => {
+        const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
+        if (index !== -1) {
+          tempVideo.duration = Math.round(video.duration);
+        }
+      };
+      video.src = URL.createObjectURL(file);
+
+      // 关闭上传弹窗,返回批量选择视频弹窗
+      this.showUpload = false;
+
+      try {
+        // 获取封面
+        await this.getFirstThumbnail(file, tempVideo);
+
+        // 上传到第一个服务器
+        await this.uploadVideoToTxPcdn(file, tempVideo, (event) => {
+            tempVideo.progress = event.percent
+        });
+
+        // 上传到第二个服务器
+        await this.uploadVideoToHwObs(file, tempVideo, (event) => {
+          tempVideo.progress = event.percent
+        });
+
+      } catch (error) {
+        this.$message.error(`文件 ${file.name} 上传失败: ${error.message || '未知错误'}`);
+      }
+
+      if (this.$refs.batchVideoUpload) {
+        this.$refs.batchVideoUpload.clearFiles();
+      }
+    },
+    /** 复制视频链接到剪贴板 */
+    copy(url) {
+      // 创建一个临时的input元素
+      const input = document.createElement('input');
+      // 设置input的值为要复制的视频链接
+      input.value = url;
+      // 将input添加到DOM中
+      document.body.appendChild(input);
+      // 选中input的值
+      input.select();
+      // 执行复制命令
+      document.execCommand('copy');
+      // 从DOM中移除input元素
+      document.body.removeChild(input);
+      // 提示用户复制成功
+      this.$message({
+        message: '已复制到剪贴板',
+        type: 'success',
+        duration: 1500
+      });
+    },
+    getProjectList() {
+      this.projectLoading = true
+      listCourseQuestionBank(this.projectQueryParams).then(response => {
+        this.projectList = response.rows;
+        this.projectListTotal = response.total;
+
+        // 如果存在已选择的项目,预选中表格中对应的行
+        this.$nextTick(() => {
+          // 获取表格组件实例
+          const projectTable = this.$refs.projectTable;
+          if (projectTable) {
+            if (this.selectedProjectIds.length > 0) {
+              // 遍历项目列表,找到匹配的ID并选中对应行
+              const selectedIds = this.selectedProjectIds;
+              this.projectList.forEach(row => {
+                if (selectedIds.includes(row.id)) {
+                  projectTable.toggleRowSelection(row, true);
+                }
+              });
+            }
+          }
+        });
+        this.projectLoading = false
+      });
+    },
+    /** 打开项目选择弹窗 */
+    openProjectDialog(projectIds, type) {
+      this.$nextTick(() => {
+        if (this.$refs.customSelect) {
+          this.$refs.customSelect.blur();
+        }
+      });
+      // 重置查询参数
+      this.projectQueryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        questionType: null,
+        title: null
+      };
+
+      this.selectedType = type
+
+      // 设置选中的项目IDs
+      if (projectIds) {
+        if (typeof projectIds === 'string') {
+          this.selectedProjectIds = projectIds.split(',').map(id => parseInt(id));
+        } else if (Array.isArray(projectIds)) {
+          this.selectedProjectIds = [...projectIds];
+        } else if (typeof projectIds === 'number') {
+          this.selectedProjectIds = [projectIds];
+        }
+      } else {
+        this.selectedProjectIds = [];
+      }
+
+      // 显示弹窗
+      this.projectDialogVisible = true;
+
+      // 加载分类树数据
+      this.initCategoryTree();
+
+      // 加载项目列表
+      this.getProjectList();
+    },
+
+    /** 确认选择项目 */
+    confirmSelectProject() {
+      // 更新表单中的项目ID
+      if (this.selectedType === 0) {
+        this.form.projectIds = this.selectedProjectIds;
+      }
+
+      else if (this.selectedType === 1) {
+        this.batchUploadForm.projectIds = this.selectedProjectIds;
+      }
+
+      else if (this.selectedType === 2) {
+        this.batchEditDialog.form.projectIds = this.selectedProjectIds;
+      }
+
+      else if (this.selectedType === 3) {
+        const params = {
+          id: this.currentRow.id,
+          projectIds: this.selectedProjectIds.join(",")
+        }
+        updateVideoResource(params).then(response => {
+          if (response.code === 200) {
+            this.msgSuccess("修改成功");
+            this.open = false;
+            this.getList();
+          }
+        });
+      }
+
+      else if (this.selectedType === 4) {
+        this.currentRow.projectIds = this.selectedProjectIds
+      }
+
+      this.projectDialogVisible = false;
+    },
+
+    /** 取消选择项目 */
+    cancelSelectProject() {
+      const projectTable = this.$refs.projectTable;
+      if (projectTable) {
+        // 清空表格数据
+        projectTable.clearSelection();
+      }
+      this.projectDialogVisible = false;
+    },
+    handleProjectSelect(selection) {
+      this.selectedProjectIds = selection.map(item => item.id);
+      selection.forEach(item => {
+        // 检查是否已存在该项目,不存在则添加
+        if (!this.projectShowList.some(p => p.id === item.id)) {
+          this.projectShowList.push(item);
+        }
+      });
+    },
+    /** 项目列表页码变更 */
+    handleProjectPageChange(page) {
+      this.projectQueryParams.pageNum = page;
+      this.getProjectList();
+    },
+
+    /** 项目列表每页条数变更 */
+    handleProjectPageSizeChange(size) {
+      this.projectQueryParams.pageNum = 1;
+      this.projectQueryParams.pageSize = size;
+      this.getProjectList();
+    },
+
+    /** 处理查看项目 */
+    handleViewProject(row, type) {
+      // 保存当前选择的行,以便后续操作
+      this.currentRow = row;
+
+      // 设置form对象的projectIds为当前行的项目IDs
+      this.projectShowList = [];
+
+      if (!row.projectIds || row.projectIds.length === 0) {
+        this.openProjectDialog(null, type)
+        return;
+      }
+
+      let projectIds = row.projectIds
+      if (Array.isArray(row.projectIds)) {
+        projectIds = projectIds.join(',');
+      }
+
+      getByIds({ids: projectIds}).then(response => {
+          this.projectShowList = response.data;
+        })
+
+      // 打开弹窗展示列表
+      this.projectListDialogVisible = true;
+    },
+    /** 初始化分类树 */
+    initCategoryTree() {
+      // 获取分类列表
+      listUserCourseCategory().then(response => {
+        const treeDate = this.handleTree(response.data, "cateId", "pid");
+        this.categoryTreeData = [{
+          cateId: 0,
+          cateName: '全部',
+          children: treeDate
+        }];
+      });
+    },
+    filterNode(value, data) {
+      if (!value) return true;
+      return data.cateName.indexOf(value) !== -1;
+    },
+    /** 处理分类点击 */
+    handleCategoryClick(data) {
+      // 更新查询参数
+      this.projectQueryParams.pageNum = 1;
+
+      // 如果是全部分类,则清空分类过滤
+      if (data.cateId === 0) {
+        this.projectQueryParams.questionType = null;
+      } else {
+        this.projectQueryParams.questionType = data.cateId;
+      }
+
+      // 重新加载项目列表
+      this.getProjectList();
+    },
+
+    /** 搜索项目 */
+    searchProjects() {
+      this.projectQueryParams.pageNum = 1;
+      this.getProjectList();
+    },
+
+    /** 视频预览弹窗关闭前的处理函数 */
+    handleCloseVideoPreview(done) {
+      // 停止视频播放
+      const video = document.getElementById('video');
+      if (video) {
+        video.pause();
+        video.currentTime = 0;
+      }
+      // 关闭弹窗
+      this.videoPreviewVisible = false;
+      done();
+    },
+  }
+}
+</script>
+
+<style scoped>
+/* 自定义表格样式 */
+.el-table .video-cell {
+  padding: 5px;
+}
+
+/* 设置删除和修改按钮的间距 */
+.el-button + .el-button {
+  margin-left: 5px;
+}
+
+::v-deep .upload-icon {
+  font-size: 28px;
+  color: #8c939d;
+}
+
+::v-deep .upload-text {
+  font-size: 12px;
+  color: #606266;
+  margin-top: 10px;
+}
+
+/* 表单样式 */
+::v-deep .el-form-item {
+  margin-bottom: 22px;
+}
+
+/* 批量选择弹窗样式 */
+::v-deep .batch-dialog .el-dialog__body {
+  padding: 0 20px 20px;
+  max-height: 70vh;
+  overflow-y: auto;
+}
+
+::v-deep .el-dialog .el-dialog__body {
+  padding: 0 20px 20px;
+  max-height: 70vh;
+  overflow-y: auto;
+}
+
+.filter-container {
+  padding: 15px 0;
+  background-color: #fff;
+  border-bottom: 1px solid #ebeef5;
+  position: relative;
+  text-align: right;
+}
+
+.dialog-footer {
+  text-align: right;
+  margin-top: 20px;
+  padding-right: 20px;
+}
+
+::v-deep .batch-dialog .el-table {
+  margin-top: 10px;
+}
+
+/* 按钮样式 */
+::v-deep .el-button--primary {
+  background-color: #1890ff;
+  border-color: #1890ff;
+}
+
+::v-deep .el-button--primary:hover {
+  background-color: #40a9ff;
+  border-color: #40a9ff;
+}
+
+::v-deep .el-button--primary:focus {
+  background-color: #40a9ff;
+  border-color: #40a9ff;
+}
+
+/* 表格头部样式 */
+::v-deep .batch-dialog .el-table__header-wrapper th {
+  background-color: #f5f7fa;
+  color: #606266;
+  font-weight: 500;
+  padding: 8px 0;
+}
+
+::v-deep .el-table__header-wrapper th {
+  background-color: #f5f7fa;
+  color: #606266;
+  font-weight: 500;
+  padding: 8px 0;
+}
+
+/* 确保弹窗中表格内容垂直居中 */
+::v-deep .batch-dialog .el-table .cell {
+  line-height: 23px;
+}
+
+::v-deep .el-table .cell {
+  line-height: 23px;
+}
+
+/* 自定义滚动条样式 */
+::v-deep .batch-dialog .el-dialog__body::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+::v-deep .el-dialog .el-dialog__body::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+::v-deep .batch-dialog .el-dialog__body::-webkit-scrollbar-thumb {
+  background: #c0c4cc;
+  border-radius: 3px;
+}
+
+::v-deep .el-dialog .el-dialog__body::-webkit-scrollbar-thumb {
+  background: #c0c4cc;
+  border-radius: 3px;
+}
+
+::v-deep .batch-dialog .el-dialog__body::-webkit-scrollbar-track {
+  background: #f5f7fa;
+}
+
+::v-deep .el-dialog .el-dialog__body::-webkit-scrollbar-track {
+  background: #f5f7fa;
+}
+
+/* 批量上传视频弹窗样式 */
+.upload-dialog .el-dialog__body {
+  padding: 20px;
+}
+
+::v-deep .upload-dialog .el-dialog__header {
+  padding: 15px 20px;
+  background-color: #f9f9f9;
+  border-bottom: 1px solid #ebeef5;
+}
+
+::v-deep .upload-dialog .el-dialog__title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #303133;
+}
+
+/* 项目选择弹窗样式 */
+::v-deep .el-dialog__wrapper {
+  z-index: 2001 !important;
+}
+
+::v-deep .el-dialog__header {
+  padding: 15px 20px;
+  background-color: #f9f9f9;
+  border-bottom: 1px solid #ebeef5;
+}
+
+::v-deep .el-dialog__title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #303133;
+}
+
+::v-deep .el-pagination {
+  text-align: center;
+  margin-top: 10px;
+}
+
+/* 项目选择弹窗的样式 */
+.project-container {
+  display: flex;
+  height: 500px;
+}
+
+.category-tree {
+  width: 250px;
+  height: 100%;
+  border-right: 1px solid #ebeef5;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  flex-shrink: 0;
+}
+
+.tree-fixed-header {
+  padding: 10px;
+  background-color: #fff;
+  border-bottom: 1px solid #ebeef5;
+  z-index: 10;
+  box-shadow: 0 1px 4px rgba(0,0,0,0.05);
+}
+
+.tree-content {
+  flex: 1;
+  overflow-y: auto;
+  padding: 10px 10px 10px 10px;
+}
+
+.project-list {
+  flex: 1;
+  padding: 10px;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  width: calc(100% - 250px); /* 固定宽度为剩余空间 */
+}
+
+.project-list .filter-container {
+  padding: 0 0 15px 0;
+  text-align: left;
+}
+
+.project-list .table-footer {
+  margin-top: 15px;
+}
+
+::v-deep .el-tree-node__content {
+  height: 36px;
+  border-radius: 4px;
+  margin-bottom: 2px;
+}
+
+::v-deep .el-tree-node:focus > .el-tree-node__content {
+  background-color: #e6f7ff;
+}
+
+::v-deep .el-tree-node__content:hover {
+  background-color: #f0f7ff;
+}
+
+::v-deep .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
+  background-color: #e6f7ff;
+  color: #1890ff;
+}
+
+::v-deep .custom-tree-node {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  padding: 0 8px;
+}
+
+/* 视频预览弹窗样式 */
+::v-deep .video-preview-dialog {
+  z-index: 3000 !important;
+}
+
+::v-deep .video-preview-dialog .el-dialog {
+  margin-top: 10vh !important;
+}
+
+::v-deep .video-preview-dialog .el-dialog__header {
+  padding: 15px 20px;
+  background-color: #f9f9f9;
+  border-bottom: 1px solid #ebeef5;
+}
+
+::v-deep .video-preview-dialog .el-dialog__body {
+  padding: 15px;
+  background-color: #000;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+::v-deep .video-preview-dialog video {
+  max-width: 100%;
+  max-height: 70vh;
+}
+
+/* 题目列表样式 */
+.project-list-container {
+  background-color: #f5f7fa;
+  border-radius: 4px;
+}
+
+.question-card {
+  background-color: #ffffff;
+  border-radius: 4px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  padding: 15px;
+  margin-bottom: 15px;
+  position: relative;
+}
+
+.question-header {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 10px;
+  border-bottom: 1px solid #ebeef5;
+  padding-bottom: 10px;
+}
+
+.question-index {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 24px;
+  height: 24px;
+  background-color: #409EFF;
+  color: white;
+  border-radius: 50%;
+  font-size: 14px;
+  margin-right: 10px;
+  flex-shrink: 0;
+}
+
+.question-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #303133;
+  line-height: 1.5;
+  word-break: break-all;
+}
+
+.question-type {
+  margin-bottom: 15px;
+}
+
+.content-title {
+  font-weight: 500;
+  color: #606266;
+  margin-bottom: 10px;
+}
+
+.question-content {
+  margin-bottom: 15px;
+}
+
+.question-item {
+  background-color: #f8f8f8;
+  border-left: 3px solid #409EFF;
+  padding: 10px;
+  margin-bottom: 10px;
+  border-radius: 0 4px 4px 0;
+}
+
+.question-item-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 5px;
+}
+
+.item-index {
+  display: inline-block;
+  margin-right: 10px;
+  font-weight: 500;
+  color: #409EFF;
+}
+
+.item-name {
+  color: #303133;
+}
+
+.question-answer {
+  background-color: #f0f9eb;
+  padding: 10px;
+  border-radius: 4px;
+  border-left: 3px solid #67c23a;
+}
+
+.answer-label {
+  font-weight: 500;
+  color: #67c23a;
+  margin-right: 10px;
+}
+
+.answer-content {
+  color: #606266;
+}
+
+::v-deep .custom-select-class .el-select__tags {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  flex-wrap: nowrap;
+  max-height: 200px;
+  overflow-y: auto;
+}
+::v-deep .custom-select-class .el-tag {
+  margin-bottom: 4px;
+  margin-right: 0;
+}
+</style>

+ 427 - 0
src/views/qw/friendMaterial/index.vue

@@ -0,0 +1,427 @@
+<template>
+  <div v-if="type == 'image'">
+    <ul v-for="(item,index) in value" :key="index" class="el-upload-list el-upload-list--picture-card">
+      <li tabindex="0" class="el-upload-list__item is-ready" :style="'width: '+width+'px;height: '+height+'px'">
+        <div>
+          <img :src="item" alt="" class="el-upload-list__item-thumbnail">
+          <span class="el-upload-list__item-actions">
+            <span v-if="index != 0" class="el-upload-list__item-preview" @click="moveMaterial(index,'up')">
+              <i class="el-icon-back" />
+            </span>
+            <span class="el-upload-list__item-preview" @click="zoomMaterial(index)">
+              <i class="el-icon-view" />
+            </span>
+            <span class="el-upload-list__item-delete" @click="deleteMaterial(index)">
+              <i class="el-icon-delete" />
+            </span>
+            <span v-if="index != value.length-1" class="el-upload-list__item-preview" @click="moveMaterial(index,'down')">
+              <i class="el-icon-right" />
+            </span>
+          </span>
+        </div>
+      </li>
+    </ul>
+    <div v-if="num > value.length" tabindex="0" class="el-upload el-upload--picture-card" :style="'width: '+width+'px;height: '+height+'px;'+'line-height:'+height+'px;'" @click="toSeleteMaterial">
+      <i class="el-icon-plus" />
+    </div>
+    <!-- 查看 -->
+    <el-dialog
+      append-to-body
+      :visible.sync="dialogVisible"
+      width="35%"
+    >
+      <img :src="materialUrl" alt="" style="width: 100%">
+    </el-dialog>
+    <!-- 素材列表 -->
+    <el-dialog
+      title="朋友圈图片素材库"
+      append-to-body
+      :visible.sync="listDialogVisible"
+      width="70%"
+    >
+      <el-container>
+        <el-aside width="unset">
+          <div style="margin-bottom: 10px">
+            <el-button
+              class="el-icon-plus"
+              size="small"
+              @click="materialgroupAdd()"
+            >
+              添加分组
+            </el-button>
+          </div>
+          <div class="group-list">
+            <div class="group-item" v-for="(group) in materialGroupList">
+                <el-button  @click="selectGroup(group)" type="primary" plain >{{group.materialGroupName}}</el-button>
+            </div>
+          </div>
+        </el-aside>
+        <el-main>
+          <el-card>
+            <div slot="header">
+              <el-row>
+                <el-col :span="12">
+                  <span>{{ materialGroup.materialGroupName }}</span>
+                  <span v-if="materialGroup.materialGroupId >0">
+                    <el-button size="small" type="text" class="el-icon-edit" style="margin-left: 10px;" @click="materialgroupEdit(materialGroup)">重命名</el-button>
+                    <el-button size="small" type="text" class="el-icon-delete" style="margin-left: 10px;color: red" @click="materialgroupDelete(materialGroup)">删除</el-button>
+                  </span>
+                </el-col>
+                <el-col :span="12" style="text-align: right;">
+                  <el-upload
+                    :action="uploadUrl"
+                    :file-list="[]"
+                    :on-progress="handleProgress"
+                    :before-upload="beforeUpload"
+                    :on-success="handleSuccess"
+                    :data="{type: 1}"
+                    multiple
+                  >
+                    <el-button size="small" type="primary">批量上传</el-button>
+                  </el-upload>
+                </el-col>
+              </el-row>
+            </div>
+            <div v-loading="tableLoading">
+              <el-alert
+                v-if="tableData.length <= 0"
+                title="暂无数据"
+                type="info"
+                :closable="false"
+                center
+                show-icon
+              />
+              <el-row :gutter="5">
+                <el-checkbox-group v-model="urls" :max="num - value.length">
+                  <el-col v-for="(item,index) in tableData" :key="index" :span="4">
+                    <el-card :body-style="{ padding: '5px' }">
+                      <el-image
+                        style="width: 100%;height: 100px"
+                        :src="item.materialUrl"
+                        fit="contain"
+                        :preview-src-list="[item.materialUrl]"
+                        :z-index="9999"
+                      />
+                      <div>
+                        <el-checkbox class="material-name" :label="item.materialUrl">
+                          选择
+                        </el-checkbox>
+                        <el-row>
+                          <el-col :span="24" class="col-do">
+                            <el-button type="text" size="medium" @click="materialDel(item)">删除</el-button>
+                          </el-col>
+                        </el-row>
+
+                      </div>
+                    </el-card>
+                  </el-col>
+                </el-checkbox-group>
+              </el-row>
+               <pagination
+                  v-show="total>0"
+                  :total="total"
+                  :page.sync="queryParams.pageNum"
+                  :limit.sync="queryParams.pageSize"
+                  @pagination="getMaterialList"
+                />
+
+            </div>
+          </el-card>
+        </el-main>
+      </el-container>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="listDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submit">确 定</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {addMaterial, delMaterial, listMaterial,} from "@/api/qw/material";
+import {addMaterialGroup, delMaterialGroup, listMaterialGroup, updateMaterialGroup} from "@/api/qw/materialGroup";
+
+export default {
+  name: 'ImageSelect',
+  props: {
+    // 素材数据
+    value: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    // 素材类型
+    type: {
+      type: String
+    },
+    // 素材限制选择数量,最多9个
+    num: {
+      type: Number,
+      default() {
+        return 9
+      }
+    },
+    // 宽度
+    width: {
+      type: Number,
+      default() {
+        return 150
+      }
+    },
+    // 宽度
+    height: {
+      type: Number,
+      default() {
+        return 150
+      }
+    }
+  },
+  data() {
+    return {
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
+      dialogVisible: false,
+      materialUrl: '',
+      listDialogVisible: false,
+      //素材组列表
+      materialGroupList: [],
+      materialGroupLoading: false,
+      //选择的某个素材组
+      materialGroup:{},
+      tableData: [],
+      resultNumber: 0,
+      total: 0,
+      //查询素材列表
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        groupType:2,
+        materialGroupName: null,
+        materialGroupId:null,
+        materialUrl: null,
+        createUserId: null,
+      },
+      tableLoading: false,
+      urls: []
+    }
+  },
+  mounted(){
+    this.getAllMaterialGroup();
+  },
+  methods: {
+    //查询素材组下的素材列表
+    selectGroup(item){
+      this.materialGroup=item;
+      this.queryParams.materialGroupId=item.materialGroupId;
+      this.getMaterialList();
+    },
+    //添加分组
+    materialgroupAdd() {
+      const that = this
+      this.$prompt('请输入分组名', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消'
+      }).then(({ value }) => {
+        addMaterialGroup({
+          materialGroupName: value,
+          groupType:2
+        }).then(function() {
+          that.materialGroup={};
+          that.getAllMaterialGroup();
+        })
+      }).catch(() => {
+
+      })
+    },
+    //删除素材组
+    materialgroupDelete(materialgroupObj) {
+      const that = this
+      this.$confirm('是否确认删除该分组?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(function() {
+        delMaterialGroup(materialgroupObj.materialGroupId)
+          .then(function() {
+            that.materialGroup={};
+            that.getAllMaterialGroup()
+          })
+      })
+      this.resetGroup();
+      this.getAllMaterialGroup();
+    },
+    //修改素材组名称
+    materialgroupEdit(materialgroupObj) {
+      const that = this
+      this.$prompt('请输入分组名', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        inputValue: materialgroupObj.materialGroupName
+      }).then(({ value }) => {
+        updateMaterialGroup({
+          materialGroupId: materialgroupObj.materialGroupId,
+          materialGroupName: value,
+          groupType:2,
+        }).then(function() {
+          that.materialGroup={};
+          that.getAllMaterialGroup()
+        })
+      }).catch(() => {
+
+      })
+    },
+    //获取所有素材分组
+    getAllMaterialGroup() {
+      this.materialGroupLoading = true;
+      listMaterialGroup({groupType:2}).then(response => {
+        this.materialGroupList = response.rows
+        this.materialGroupLoading = false;
+      });
+    },
+    resetGroup(){
+      this.queryParams= {
+        pageNum: 1,
+          pageSize: 10,
+          groupType:2,
+          materialGroupName: null,
+          materialGroupId:null,
+          materialUrl: null,
+          createUserId: null,
+      };
+    },
+    //查询分组 下的素材
+    getMaterialList() {
+      this.tableLoading = true;
+      listMaterial(this.queryParams).then(response => {
+        this.tableData = response.rows;
+        this.total = response.total;
+        this.tableLoading = false;
+      });
+    },
+    //移动素材
+    moveMaterial(index, type) {
+      if (type == 'up') {
+        const tempOption = this.value[index - 1]
+        this.$set(this.value, index - 1, this.value[index])
+        this.$set(this.value, index, tempOption)
+      }
+      if (type == 'down') {
+        const tempOption = this.value[index + 1]
+        this.$set(this.value, index + 1, this.value[index])
+        this.$set(this.value, index, tempOption)
+      }
+    },
+    //缩小图片
+    zoomMaterial(index) {
+      this.dialogVisible = true
+      this.materialUrl = this.value[index]
+    },
+    //删除素材
+    deleteMaterial(index) {
+      const that = this
+      this.$confirm('是否确认删除?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(function() {
+        that.value.splice(index, 1)
+        that.urls = []
+      })
+    },
+
+    toSeleteMaterial() {
+      this.listDialogVisible = true
+      this.getAllMaterialGroup()
+      this.getMaterialList();
+    },
+    //删除素材
+    materialDel(item) {
+      const that = this
+      this.$confirm('是否确认删除该素材?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(function() {
+        delMaterial(item.materialId)
+          .then(function() {
+            that.queryParams.pageNum=0;
+            that.getMaterialList();
+          })
+      })
+    },
+
+    //上传进度
+    handleProgress(event, file, fileList) {
+    },
+    //上传成功后
+    handleSuccess(response, file, fileList) {
+      const that = this
+      addMaterial({
+        materialType: 'image',
+        materialGroupId: this.queryParams.materialGroupId,
+        materialName: file.name,
+        materialUrl: response.url,
+        groupType:2,
+      }).then(() => {
+        this.resultNumber++
+        if (fileList.length === this.resultNumber) {
+          that.getMaterialList()
+          this.resultNumber = 0
+        }
+      })
+    },
+    //上传之前
+    beforeUpload(file) {
+      const isPic =
+        file.type === 'image/jpeg' ||
+        file.type === 'image/png' ||
+        file.type === 'image/gif' ||
+        file.type === 'image/jpg'
+      const isLt2M = file.size / 1024 / 1024 < 10
+      if (!isPic) {
+        this.$message.error('上传图片只能是 JPG、JPEG、PNG、GIF 格式!')
+        return false
+      }
+      if (!isLt2M) {
+        this.$message.error('上传图片大小不能超过 10MB!')
+      }
+      return isPic && isLt2M
+    },
+    //提交
+    submit() {
+      this.urls.forEach(item => {
+        console.log("item",item)
+        console.log("this.value",this.value)
+        console.log("this.value.length",this.value.length)
+
+        this.$set(this.value, this.value.length, item)
+      })
+      this.listDialogVisible = false
+    }
+  }
+}
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+  ::v-deep .el-icon-circle-close{
+    color: red;
+  }
+  .material-name{
+    padding: 8px 0px;
+  }
+  .col-do{
+    text-align: center;
+  }
+  .button-do{
+    padding: unset!important;
+    font-size: 12px;
+  }
+  .group-list{
+    display: flex;
+    flex-direction:column;
+    align-items: flex-start;
+  }
+  .group-item{
+    margin: 5px;
+  }
+</style>

+ 9 - 1
src/views/qw/sop/ImageUpload.vue

@@ -9,6 +9,7 @@
       :on-error="handleUploadError"
       :on-exceed="handleExceed"
       name="file"
+      :disabled="disabled"
       :on-remove="handleRemove"
       :show-file-list="true"
       :file-list="fileList"
@@ -54,6 +55,11 @@ export default {
       type: Array,
       default: () => ["png", "jpg", "jpeg"],
     },
+    // 大小限制(MB)
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
     // 是否显示提示
     isShowTip: {
       type: Boolean,
@@ -66,7 +72,7 @@ export default {
       dialogVisible: false,
       hideUpload: false,
       baseUrl:"",
-      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
+      uploadUrl: process.env.VUE_APP_BASE_API + "/common/uploadOSS",
       headers: {
         Authorization: "Bearer " + getToken(),
       },
@@ -112,12 +118,14 @@ export default {
       if(findex > -1) {
         this.fileList.splice(findex, 1);
         this.$emit("input", this.listToString(this.fileList));
+        this.$emit("change", this.listToString(this.fileList));    // 新增change事件触发
       }
     },
     // 上传成功回调
     handleUploadSuccess(res,file) {
       this.fileList.push({ name: res.url, url: res.url });
       this.$emit("input", this.listToString(this.fileList));
+      this.$emit("change", this.listToString(this.fileList));    // 新增change事件触发
       this.loading.close();
     },
     // 上传前loading加载

+ 331 - 0
src/views/qw/sopTemp/addAiChatTemp.vue

@@ -0,0 +1,331 @@
+<template>
+  <div class="app-container">
+    <div style="margin: 30px;">sop规则【新客对话】模板</div>
+    <div style="margin-top: 10px;margin-left: 50px;margin-right: 100px;margin-bottom: 60px;">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入模板标题"/>
+        </el-form-item>
+
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="dict.dictValue"
+            >{{ dict.dictLabel }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <el-form-item label="推送方式">
+          <el-radio-group v-model="form.sendType">
+            <el-radio
+              v-for="dict in sysQwSopType.filter(item => parseInt(item.dictValue) === 4)"
+              :key="dict.dictValue"
+              :label="parseInt(dict.dictValue)"
+            >{{ dict.dictLabel }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <!--        <el-form-item label="间隔天数" prop="gap">
+                  <el-input-number v-model="form.gap" :min="1" label="间隔天数"></el-input-number>
+                </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="setting">
+          <el-timeline>
+            <el-timeline-item
+              :timestamp="'第'+(1+(form.gap*index))+'天'"
+              :color="'#0bbd87'"
+              placement="top"
+              v-for="(item, index) in setting"
+              :key="index"
+              style="margin-top: 10px;">
+              <el-row>
+                <el-col :span="22">
+                  <div style="background-color: #fbfbfb;padding: 15px; border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                    <el-form :model="item" label-width="80px">
+                      <el-form-item label="规则">
+                        <div v-for="(content, contentIndex) in item.content"
+                             :key="contentIndex"
+                             style="background-color: #fdfdfd;padding: 15px; border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                          <el-row>
+                            <el-col :span="22">
+                              <el-form :model="content" label-width="70px">
+                                <!-- 第一天不显示时间选择 -->
+                                <el-form-item label="时间" prop="time" v-if="index > 0">
+                                  <el-time-picker
+                                    class="custom-input"
+                                    v-model="content.time"
+                                    value-format="HH:mm"
+                                    format="HH:mm"
+                                    :picker-options="{ selectableRange: '00:00:00 - 23:59:59' }"
+                                    placeholder="时间"
+                                    style="width: 100px;height: 20px;">
+                                  </el-time-picker>
+                                </el-form-item>
+
+
+                                <div v-for="(setList, setIndex) in content.setting"
+                                     :key="setIndex"
+                                     style="background-color: #fdfdfd; border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                                  <el-row>
+                                    <el-col :span="22">
+                                      <el-form :model="setList" label-width="70px">
+                                        <el-form-item label="添加客服" prop="intervalTime" style="margin: 2%">
+                                          <el-input-number
+                                            v-model="setList.intervalTime"
+                                            :min="1"
+                                            :max="1440"
+                                            style="width:100;margin-top: 10px;"
+                                          >
+                                          </el-input-number>
+                                          <span class="tip">单位:分钟,最大1440分钟(24小时)</span>
+                                        </el-form-item>
+                                        <el-form-item label="内容" style="margin: 2%">
+                                          <el-input
+                                            v-model="setList.value"
+                                            type="textarea"
+                                            :rows="3"
+                                            placeholder="内容"
+                                            style="width: 90%;margin-top: 10px;"/>
+                                        </el-form-item>
+                                        <el-form-item label="交流状态" style="margin: 2%">
+                                          <el-select v-model="setList.talkType" placeholder="更改交流状态" size="mini"
+                                                     style=" margin-right: 10px;" clearable>
+                                            <el-option label="非首次交流" value="非首次交流"></el-option>
+                                            <el-option label="首次交流1" value="首次交流1"></el-option>
+                                            <el-option label="首次交流2" value="首次交流2"></el-option>
+                                            <el-option label="交流状态1" value="交流状态1"></el-option>
+                                            <el-option label="交流状态2" value="交流状态2"></el-option>
+                                            <el-option label="交流状态3" value="交流状态3"></el-option>
+                                          </el-select>
+                                        </el-form-item>
+                                      </el-form>
+                                    </el-col>
+                                    <el-col :span="1" :offset="1">
+                                      <i class="el-icon-delete"
+                                         @click="delSetList(index,contentIndex,setIndex)"
+                                         style="margin-top: 20px;"
+                                         v-if="content.setting.length>1"></i>
+                                    </el-col>
+                                  </el-row>
+                                </div>
+                                <el-link type="primary"
+                                         class="el-icon-plus"
+                                         :underline="false"
+                                         @click='addSetList(contentIndex,item.content)'>添加内容
+                                </el-link>
+                              </el-form>
+                            </el-col>
+                            <el-col :span="1" :offset="1">
+                              <i class="el-icon-delete"
+                                 @click="delContent(index,contentIndex)"
+                                 style="margin-top: 20px;"
+                                 v-if="item.content.length>1"></i>
+                            </el-col>
+                          </el-row>
+                        </div>
+                        <el-link type="primary"
+                                 v-if="index > 0"
+                                 class="el-icon-plus"
+                                 :underline="false"
+                                 @click='addContent(index)'>添加规则
+                        </el-link>
+                      </el-form-item>
+                    </el-form>
+                  </div>
+                </el-col>
+                <el-col :span="1" :offset="1">
+                  <i class="el-icon-delete"
+                     @click="delSetting(index)"
+                     v-if="setting.length>1"></i>
+                </el-col>
+              </el-row>
+            </el-timeline-item>
+          </el-timeline>
+          <!--          <el-link type="primary"
+                             class="el-icon-plus"
+                             :underline="false"
+                             @click='addSetting()'>添加天数</el-link> -->
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer" style="float: right;">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {addSopTemp} from "@/api/qw/sopTemp";
+
+export default {
+  name: "AddAiChatTemp",
+  data() {
+    return {
+      // 状态字典
+      statusOptions: [],
+      // 添加推送方式字典
+      sysQwSopType: [],
+      // 表单数据
+      form: {
+        name: null,
+        setting: null,
+        status: "1",
+        sort: 1,
+        gap: 1,
+        sendType: 4, // AI对话类型,固定值
+      },
+      // 规则设置
+      setting: [],
+      // 表单校验
+      rules: {
+        name: [
+          {required: true, message: '名称不能为空', trigger: 'blur'}
+        ],
+        status: [
+          {required: true, message: '状态不能为空', trigger: 'blur'}
+        ],
+        sort: [
+          {required: true, message: '排序不能为空', trigger: 'blur'}
+        ],
+        gap: [
+          {required: true, message: '间隔天数不能为空', trigger: 'blur'}
+        ]
+      }
+    };
+  },
+  created() {
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    // 获取推送方式字典
+    this.getDicts("sys_qw_sop_type").then(response => {
+      this.sysQwSopType = response.data;
+    });
+    this.getList();
+  },
+  methods: {
+    getList() {
+      this.setting.push({
+        name: null,
+        content: [{
+          type: 1,
+          contentType: '1',
+          setting: [{
+            intervalTime: 5,
+            contentType: '1',
+            value: ""
+          }]
+        }]
+      });
+    },
+
+    addSetList(contentIndex, content) {
+      content[contentIndex].setting.push({
+        intervalTime: 5,
+        contentType: '1',
+        value: ""
+      });
+    },
+
+    delSetList(index, contentIndex, setIndex) {
+      this.setting[index].content[contentIndex].setting.splice(setIndex, 1);
+    },
+
+    addContent(index) {
+      this.setting[index].content.push({
+        type: 1,
+        contentType: '1',
+        // 默认5分钟
+        setting: [{
+          intervalTime: 5,
+          contentType: '1',
+          value: ""
+        }]
+      });
+    },
+
+    delContent(index, contentIndex) {
+      this.setting[index].content.splice(contentIndex, 1);
+    },
+
+    addSetting() {
+      this.setting.push({
+        name: null,
+        content: [{
+          type: 1,
+          contentType: '1',
+          setting: [{
+            intervalTime: 5, // 默认5分钟
+            contentType: '1',
+            value: ""
+          }]
+        }]
+      });
+    },
+
+    delSetting(index) {
+      this.setting.splice(index, 1);
+    },
+
+    submitForm() {
+      this.$refs["formRef"].validate(valid => {
+        if (valid) {
+          // 验证内容
+          for (let day of this.setting) {
+            for (let content of day.content) {
+              if (content.time === undefined && this.setting.indexOf(day) > 0) {
+                return this.$message.error("时间不能为空");
+              }
+              for (let set of content.setting) {
+                if (!set.intervalTime) {
+                  return this.$message.error("间隔时间不能为空");
+                }
+                if (!set.value) {
+                  return this.$message.error("内容不能为空");
+                }
+              }
+            }
+          }
+
+          this.form.setting = JSON.stringify(this.setting);
+          addSopTemp(this.form).then(response => {
+            this.$message.success("新增成功");
+            window.location.replace('/qw/conversion/sopTemp')
+            this.reset();
+          });
+        }
+      });
+    },
+
+    cancel() {
+      this.$router.push('/qw/conversion/sopTemp');
+    }
+  }
+};
+</script>
+
+<style scoped>
+.custom-input /deep/ .el-input__inner {
+  height: 20px;
+  text-align: center;
+}
+
+.custom-input /deep/ .el-input__icon {
+  line-height: 10px;
+}
+
+.tip {
+  color: #909399;
+  font-size: 12px;
+  margin-left: 10px;
+}
+</style>

+ 981 - 0
src/views/qw/sopTemp/addSopTemp.vue

@@ -0,0 +1,981 @@
+<template>
+  <div class="app-container">
+
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 "> sop规则【企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 "> sop规则【AI插件】模板</div>
+
+    <div style="margin-top: 10px;margin-left: 50px;margin-right: 100px;margin-bottom: 60px;">
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入模板标题" />
+        </el-form-item>
+
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="dict.dictValue"
+            >{{dict.dictLabel}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="间隔天数" prop="gap">
+          <el-input-number v-model="form.gap"  :min="1" label="间隔天数"></el-input-number>
+        </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="setting">
+          <el-timeline >
+            <el-timeline-item :timestamp="'第'+(1+(form.gap*index))+'天'" :color="'#0bbd87'" placement="top" v-for="(item, index) in setting"  style="margin-top: 10px;">
+              <el-row>
+                <el-col :span="22">
+                  <div style="background-color: #fbfbfb;padding: 15px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                    <el-form :model="item"  label-width="80px">
+
+                      <el-form-item label="内容名称"  style="height: 50px;">
+                        <el-input v-model="item.name" placeholder="内容名称,仅内部可见" />
+                      </el-form-item>
+
+                      <el-form-item label="规则"  >
+                        <div v-for="(content, contentIndex) in item.content" style="background-color: #fdfdfd;padding: 15px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
+
+                          <el-row>
+
+                            <el-col el-col :span="22" >
+                              <el-form :model="content" label-width="70px">
+                                <el-form-item label="时间"  prop="time"  >
+                                  <el-time-picker
+                                    class="custom-input"
+                                    v-model="content.time"
+                                    value-format="HH:mm"
+                                    format="HH:mm"
+                                    :picker-options="{ selectableRange: '00:21:00 - 23:59:59' }"
+                                    placeholder="时间"
+                                    style="width: 100px;height: 20px;" >
+                                  </el-time-picker>
+                                </el-form-item>
+                                <el-form-item label="消息类别"  >
+                                  <el-radio-group v-model="content.type" @change="() => content.contentType = '1'" >
+                                    <el-radio
+                                      :label="1"
+
+                                    >普通</el-radio>
+                                    <el-radio
+                                      :label="2"
+
+                                    >课程</el-radio>
+                                    <el-radio
+                                      :label="3"
+
+                                    >订单</el-radio>
+                                    <el-radio
+                                      :label="4"
+
+                                    >AI触达</el-radio>
+                                  </el-radio-group>
+                                </el-form-item>
+
+                                <el-form-item label="课程"  v-if="content.type == 2 ">
+                                  <el-select  v-model="content.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"  @change="courseChange(content,index,contentIndex)">
+                                    <el-option
+                                      v-for="dict in courseList"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+                                  <el-select  v-model="content.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;" @change="videoIdChange(content,index,contentIndex)" >
+                                    <el-option
+                                      v-for="dict in videoList[index][contentIndex]"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+                                  <el-select  v-model="content.courseType" placeholder="请选择消息类型" size="mini" style=" margin-right: 10px;" v-if="content.type != 4 ">
+                                    <el-option
+                                      v-for="dict in sysFsSopWatchStatus"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+
+
+
+
+                                </el-form-item>
+                                <el-form-item label="Ai触达"  v-if="content.type == 4 ">
+                                  <el-select  v-model="content.aiTouch" placeholder="请选择Ai触达类型" size="mini" style=" margin-right: 10px;" v-if="content.type == 4 ">
+                                    <el-option label="非首次交流" value="非首次交流"></el-option>
+                                    <el-option label="首次交流1" value="首次交流1"></el-option>
+                                    <el-option label="首次交流2" value="首次交流2"></el-option>
+                                    <el-option label="交流状态1" value="交流状态1"></el-option>
+                                    <el-option label="交流状态2" value="交流状态2"></el-option>
+                                    <el-option label="交流状态3" value="交流状态3"></el-option>
+                                  </el-select>
+                                </el-form-item>
+
+
+
+                                <!--                                <el-form-item label="内容类型"  >-->
+                                <div v-for="(setList, setIndex) in content.setting" :key="setIndex" style="background-color: #fdfdfd; border: 1px solid #e6e6e6; margin-bottom: 20px;" v-if="content.type != 4 ">
+                                  <el-row>
+                                    <el-col :span="22">
+                                      <el-form :model="setList" label-width="70px">
+                                        <el-form-item label="内容类别" style="margin: 2%">
+                                          <div v-if="form.sendType == 1 ">
+                                            <el-radio-group  v-model="setList.contentType" >
+                                              <el-radio
+                                                v-for="item in sysQwSopContentType"
+                                                :label="item.dictValue"
+                                                :disabled="item.dictValue === '1' && content.setting.some(s => s.contentType == '1')">
+                                                {{ item.dictLabel }}
+                                              </el-radio>
+                                            </el-radio-group>
+                                          </div>
+                                          <div v-if="form.sendType == 2 ">
+                                            <!--                                              <div v-if="content.type == 2 ">-->
+                                            <!--                                                <el-radio-group  v-model="setList.contentType" @change="handleContentTypeChange(setList,index,setIndex)">-->
+                                            <!--                                                  <el-radio  :label="item.dictValue" v-for="item in sysQwSopSettingType">{{item.dictLabel}}</el-radio>-->
+                                            <!--                                                </el-radio-group>-->
+                                            <!--                                              </div>-->
+                                            <!--                                              <div v-else>-->
+                                            <el-radio-group  v-model="setList.contentType" @change="handleContentTypeChange(content,index,contentIndex,setIndex)">
+                                              <el-radio   :label="item.dictValue" v-for="item in sysQwSopAiContentType">{{item.dictLabel}}</el-radio>
+                                            </el-radio-group>
+                                            <!--                                              </div>-->
+
+                                          </div>
+                                        </el-form-item>
+                                        <el-form-item label="内容"  >
+                                          <el-input v-if="setList.contentType == 1 " v-model="setList.value" type="textarea" :rows="3" placeholder="内容" style="width: 90%;margin-top: 10px;"/>
+
+                                          <ImageUpload v-if="setList.contentType == 2 " v-model="setList.imgUrl" type="image" :num="1"  :width="150" :height="150" />
+
+                                          <div v-if="setList.contentType == 3 ">
+                                            <el-card class="box-card">
+                                              <el-form-item label="链接标题:"  label-width="100px">
+                                                <el-input v-model="setList.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
+                                              </el-form-item>
+                                              <el-form-item label="链接描述:"   label-width="100px" >
+                                                <el-input type="textarea" :rows="3" v-model="setList.linkDescribe" placeholder="请输入链接描述" style="width: 90%;margin-top: 1%;"/>
+                                              </el-form-item>
+                                              <el-form-item label="链接封面:"   label-width="100px">
+                                                <ImageUpload v-model="setList.linkImageUrl" type="image" :num="1" :file-size="2" :width="150" :height="150" style="margin-top: 1%;" />
+                                              </el-form-item>
+                                              <div v-if="content.type!=2" style="margin-top: 1%">
+                                                <el-form-item label="链接地址:"  label-width="100px" >
+                                                  <el-input v-model="setList.linkUrl" placeholder="请输入链接地址" style="width: 90%;"/>
+                                                </el-form-item>
+                                              </div>
+                                              <div v-if="content.type==2">
+                                                <el-form-item label="链接地址:"  label-width="100px" >
+                                                  <el-tag type="warning" v-model="setList.isBindUrl=1">选择的课程小节 即为卡片链接地址</el-tag>
+                                                </el-form-item>
+                                              </div>
+                                            </el-card>
+                                          </div>
+
+                                          <div v-if="setList.contentType == 4">
+                                            <el-card class="box-card">
+                                              <el-form-item label="标题" prop="miniprogramTitle">
+                                                <el-input v-model="setList.miniprogramTitle" placeholder="请输入小程序消息标题,最长为64字"  />
+                                              </el-form-item>
+                                              <el-form-item label="封面" prop="miniprogramPicUrl">
+                                                <ImageUpload v-model="setList.miniprogramPicUrl"  type="image" :num="10" :width="150" :height="150" />
+                                              </el-form-item>
+                                              <el-form-item label="appid" prop="miniprogramAppid" v-show="false" >
+                                                <el-input v-model="setList.miniprogramAppid='wx73f85f8d62769119' " disabled />
+                                              </el-form-item>
+                                              <el-form-item label="page路径" prop="miniprogramPage" v-show="false" label-width="100px" style="margin-left: -30px">
+                                                <el-input v-model="setList.miniprogramPage" placeholder="小程序消息打开后的路径" disabled />
+                                              </el-form-item>
+                                            </el-card>
+                                          </div>
+
+                                          <div v-if="setList.contentType == 5 ">
+
+                                            <el-form-item label="上传文件:" prop="fileUrl" label-width="100px">
+                                              <el-upload
+                                                v-model="setList.fileUrl"
+                                                class="avatar-uploader"
+                                                :action="uploadUrl"
+                                                :show-file-list="false"
+                                                :on-success="(res, file) => handleAvatarSuccessFile(res, file, setList)"
+                                                :before-upload="beforeAvatarUploadFile">
+                                                <i class="el-icon-plus avatar-uploader-icon"></i>
+                                              </el-upload>
+                                              <el-link v-if="setList.fileUrl" type="primary" :href="downloadUrl(setList.fileUrl)" download>
+                                                {{setList.fileUrl}}
+                                              </el-link>
+                                            </el-form-item>
+
+                                          </div>
+
+                                          <div v-if="setList.contentType == 6 ">
+                                            <el-form-item label="上传视频:" prop="videoUrl" label-width="100px">
+                                              <el-upload
+                                                v-model="setList.videoUrl"
+                                                class="avatar-uploader"
+                                                :action="uploadUrl"
+                                                :show-file-list="false"
+                                                :on-success="(res, file) => handleAvatarSuccessVideo(res, file, setList)"
+                                                :before-upload="beforeAvatarUploadVideo">
+                                                <i class="el-icon-plus avatar-uploader-icon"></i>
+                                              </el-upload>
+                                              <video v-if="setList.videoUrl"
+                                                     :src="setList.videoUrl"
+                                                     controls style="width: 200px;height: 100px">
+                                              </video>
+                                            </el-form-item>
+                                          </div>
+                                          <div v-if="setList.contentType == 7 ">
+                                              <el-input
+                                                        v-model="setList.value"
+                                                        type="textarea" :rows="3" maxlength="66" show-word-limit
+                                                        placeholder="输入要转为语音的内容" style="width: 90%;margin-top: 10px;"
+                                                        @input="handleInputVideoText(setList.value,setList)"/>
+                                          </div>
+                                          <div v-if="setList.contentType == 8 ">
+                                            <el-button type="primary" style="margin-bottom: 1%" @click="hanldeSelectVideoNum(content,index,contentIndex,setIndex)">选择视频号</el-button>
+                                            <el-card class="box-card" v-if="setList.coverUrl">
+                                              <el-form-item label="封面标题:"  label-width="100px">
+                                                <el-input v-model="setList.nickname" style="width: 90%;margin-bottom: 1%" disabled/>
+                                              </el-form-item>
+                                              <el-form-item label="头像:"   label-width="100px" >
+                                                <el-image
+                                                  v-if="setList.avatar != null"
+                                                  :src="setList.avatar"
+                                                  :preview-src-list="[setList.avatar]"
+                                                  :style="{ width: '50px', height: '50px' }"
+                                                ></el-image>
+                                              </el-form-item>
+                                              <el-form-item label="封面:"   label-width="100px" >
+                                                <el-image
+                                                  v-if="setList.coverUrl != null"
+                                                  :src="setList.coverUrl"
+                                                  :preview-src-list="[setList.coverUrl]"
+                                                  :style="{ width: '200px', height: '200px' }"
+                                                ></el-image>
+
+                                              </el-form-item>
+                                              <el-form-item label="简介:"   label-width="100px" >
+                                                <el-input type="textarea" :rows="3" v-model="setList.desc"  style="width: 90%;margin-top: 1%;" disabled />
+                                              </el-form-item>
+                                              <el-form-item label="视频地址:"  label-width="100px" style="margin-top: 1%" >
+                                                <el-input v-model="setList.url" style="width: 90%;" disabled />
+                                              </el-form-item >
+
+                                            </el-card>
+                                          </div>
+
+                                        </el-form-item>
+
+                                        <el-form-item label="添加短链" v-if="content.type == 2 && setList.contentType == 1 "  >
+                                          <el-tooltip content="请先根据课程选定课程小节之后再添加" effect="dark" :disabled="!!content.videoId">
+                                            <el-switch
+                                              v-model="setList.isBindUrl"
+                                              :disabled="!content.videoId"
+                                              active-color="#13ce66"
+                                              inactive-color="#DCDFE6"
+                                              active-value="1"
+                                              inactive-value="2">
+                                            </el-switch>
+                                          </el-tooltip>
+
+                                          <span v-if="setList.isBindUrl == '1'" style="margin-left: 10px; color: #13ce66">添加URL</span>
+                                          <span v-if="setList.isBindUrl == '2'" style="margin-left: 10px; color: #b1b4ba">不加URL</span>
+                                        </el-form-item>
+
+                                        <el-form-item label="课节过期时间" v-if="content.type == 2 && setList.isBindUrl == '1' && setList.contentType != 2  && setList.contentType != 5  && setList.contentType != 6 && setList.contentType != 8 " style="margin-top: 1%">
+                                          <el-row>
+                                            <el-input-number  v-model="setList.expiresDays"  :min="0" :max="100" ></el-input-number>
+                                            (天)
+                                          </el-row>
+                                          <el-row>
+                                            <span class="tip">填写0或不填时,默认为系统配置的默认时间</span>
+                                          </el-row>
+                                        </el-form-item>
+
+                                      </el-form>
+                                    </el-col>
+                                    <el-col :span="1" :offset="1">
+                                      <i class="el-icon-delete" @click="delSetList(index,contentIndex,setIndex)" style="margin-top: 20px;" v-if="content.setting.length>1"></i>
+                                    </el-col>
+                                  </el-row>
+
+                                </div>
+                                <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetList(contentIndex,item.content)' v-if="content.type != 4 ">添加内容</el-link>
+                                <!--                                </el-form-item>-->
+
+                              </el-form>
+
+                            </el-col>
+                            <el-col :span="1" :offset="1">
+                              <i class="el-icon-delete" @click="delContent(index,contentIndex)" style="margin-top: 20px;" v-if="item.content.length>1"></i>
+                            </el-col>
+                          </el-row>
+                        </div>
+                        <el-link type="primary" class="el-icon-plus" :underline="false" @click='addContent(index)'>添加规则</el-link>
+                      </el-form-item>
+                    </el-form>
+                  </div>
+                </el-col>
+                <el-col :span="1" :offset="1">
+                  <i class="el-icon-delete-solid" @click="delSetting(index)"  v-if="setting.length>1"></i>
+                </el-col>
+              </el-row>
+            </el-timeline-item>
+
+
+          </el-timeline>
+          <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetting()'>添加天数</el-link>
+        </el-form-item>
+
+      </el-form>
+      <div slot="footer" class="dialog-footer" style="float: right;">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </div>
+
+
+    <el-dialog :title="videoNumOptions.title" :visible.sync="videoNumOptions.open" style="width: 1500px;height: 100%" append-to-body>
+      <userVideo ref="QwUserVideo" @videoResult="qwUserVideoResult" ></userVideo>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import {addSopTemp, delSopTemp, exportSopTemp, getSopTemp, listSopTemp, updateSopTemp} from "@/api/qw/sopTemp";
+import {courseList, videoList} from "@/api/qw/sop";
+import ImageUpload from "@/views/qw/sop/ImageUpload";
+import FriendMaterial from "@/views/qw/friendMaterial/index.vue";
+import userVideo from "@/views/qw/userVideo/userVideo.vue";
+
+export default {
+  name: "addSopTemp",
+  components: { ImageUpload,FriendMaterial,userVideo},
+  data() {
+    return {
+
+      //图片放大
+      dialogVisible: false,
+      dialogImageUrl:null,
+
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS2",
+      uploadUrlByVoice:process.env.VUE_APP_BASE_API+"/common/uploadOSSByHOOKVoice",
+      //上传语音的遮罩层
+      voiceLoading :false,
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      sysFsSopWatchStatus: [],
+      //消息内容类型 企微版
+      sysQwSopContentType:[],
+      //插件版
+      sysQwSopAiContentType:[],
+      //链接版类别
+      sysQwSopSettingType:[],
+
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // sop模板表格数据
+      setting: [],
+      courseList:[],
+      videoList:[],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 状态字典
+      statusOptions: [],
+      // 查询参数
+      form: {
+        name: null,
+        setting: null,
+        status: "1",
+        sort: 1,
+        companyId: null,
+        gap:1,
+      },
+      videoNumOptions:{
+        title:'选择视频号',
+        open:false,
+        content:null,
+        contentIndex:null,
+        setIndex:null,
+      },
+      // 表单校验
+      rules: {
+        name: [
+          { required: true, message: '名称不能为空', trigger: 'blur' }
+        ],
+        status: [
+          { required: true, message: '状态不能为空', trigger: 'blur' }
+        ],
+        sort: [
+          { required: true, message: '排序不能为空', trigger: 'blur' }
+        ],
+        gap: [
+          { required: true, message: '间隔天数不能为空', trigger: 'blur' }
+        ],
+      },
+      contentRules:{
+        time:[{ required: true, message: '时间不能为空', trigger: 'blur' }],
+      }
+    };
+  },
+  created() {
+
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+    this.getDicts("sys_fs_sop_watch_status").then(response => {
+      this.sysFsSopWatchStatus = response.data;
+    });
+
+    this.getDicts("sys_qwSopAi_contentType").then(response => {
+      this.sysQwSopAiContentType = response.data;
+    });
+
+    this.getDicts("sys_qwSop_contentType").then(response => {
+      this.sysQwSopContentType = response.data;
+    });
+    this.getDicts("sys_qwSop_settingType").then(response => {
+      this.sysQwSopSettingType = response.data;
+    });
+
+
+    this.form.sendType = this.$route.params && this.$route.params.command;
+
+    setTimeout(() => {
+      this.getList();
+    }, 200);
+
+  },
+  methods: {
+    handleInputVideoText(value,content){
+      // 允许的字符:中文、英文(大小写)、数字和指定标点符号(,。!?)
+      const regex = /^[\u4e00-\u9fa5,。!?,!?]+$/;
+
+      // 删除不符合条件的字符
+      const filteredValue = value.split('').filter(char => regex.test(char)).join('');
+
+      this.$set(content, 'value', filteredValue);
+
+    },
+    addSetList(contentIndex, content) {
+
+      if (this.form.sendType == 1) {
+        for (let i = 0; i < content.length; i++) {
+
+          if (content[i].setting.length > 9) {
+            return this.$message.error("因为企微接口限制,企微模板一条消息只能设置最多~9个内容!!")
+          } else {
+
+            // 动态生成新的设置项
+            const newSetting = content[i].setting.some(item => item.contentType === '1')
+              ? {contentType: '2', imgUrl: ''}  // 如果存在 contentType = '1',添加 contentType = '2'
+              : {contentType: '1', value: ''};  // 如果不存在 contentType = '1',添加 contentType = '1'
+
+            // 将新设置项添加到 content[i].setting 数组中
+            content[i].setting.push(newSetting);
+          }
+
+        }
+      } else {
+
+        const newSetting = {
+          contentType: '1',
+          value: '',
+        };
+
+        // 将新设置项添加到 content.setting 数组中
+        content[contentIndex].setting.push(newSetting);
+
+      }
+
+    },
+    delSetList(index, contentIndex, setIndex) {
+      this.setting[index].content[contentIndex].setting.splice(setIndex, 1)
+    },
+
+    addContent(index) {
+
+      if (this.setting[index].content.length > 0 && this.form.sendType == 1) {
+        return this.$message.error("因为企微接口限制,企微模板一天只能设置一条消息~")
+      } else {
+        this.setting[index].content.push({type: 1, contentType: '1', setting: [{contentType: '1', value: "",}]})
+        this.videoList[index].push([])
+      }
+
+    },
+
+    delContent(index, contentIndex) {
+      this.setting[index].content.splice(contentIndex, 1)
+      this.videoList[index].splice(contentIndex, 1)
+    },
+    addSetting() {
+
+      this.setting.push({name: null, content: [{type: 1, contentType: '1', setting: [{contentType: '1', value: "",}]}]})
+      this.videoList.push([])
+
+    },
+    delSetting(index) {
+      this.setting.splice(index, 1)
+      this.videoList.splice(index, 1)
+    },
+    //上传文件
+    handleAvatarSuccessFile(res, file, content) {
+      if (res.code === 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'fileUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadFile(file) {
+      const isLt1M = file.size / 1024 / 1024 < 10;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 10MB!');
+      }
+      return isLt1M;
+    },
+
+    //下载文件
+    downloadUrl(materialUrl) {
+      // 直接返回文件 URL
+      return materialUrl;
+    },
+
+    //选择视频号
+    hanldeSelectVideoNum(content,index,contentIndex,setIndex){
+      this.videoNumOptions.content = content;
+      this.videoNumOptions.contentIndex = contentIndex;
+      this.videoNumOptions.setIndex = setIndex;
+        this.videoNumOptions.open=true;
+    },
+
+    qwUserVideoResult(val){
+
+      // 根据选中的内容,将返回的数据更新到相应的表单项
+      const content = this.videoNumOptions.content;
+      const setList = content.setting[this.videoNumOptions.setIndex];
+
+      setList.nickname = val.nickname;
+      setList.avatar = val.avatar;
+      setList.coverUrl = val.coverUrl;
+      setList.thumbUrl = val.thumbUrl;
+      setList.desc = val.desc;
+      setList.url = val.url;
+      setList.extras = val.extras;
+
+      this.videoNumOptions.open=false;
+    },
+
+    handleAvatarSuccessVideo(res, file, content) {
+      if (res.code == 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'videoUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+    handleAvatarSuccessVoice(res, file, content) {
+      if (res.code == 200) {
+
+        // 创建 Audio 对象加载音频
+        const audio = new Audio(res.mp3Url);
+        audio.addEventListener('loadedmetadata', () => {
+          // 获取音频时长
+          this.$set(content, 'voiceDuration', Math.ceil(audio.duration));
+        });
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'voiceUrl', res.silkUrl);
+        this.$set(content, 'mp3Url', res.mp3Url);
+      } else {
+        this.msgError(res.msg);
+      }
+      this.voiceLoading = false;
+    },
+    beforeAvatarUploadVideo(file) {
+      const isLt30M = file.size / 1024 / 1024 < 10;
+      const isMP4 = file.type === 'video/mp4';
+
+      if (!isMP4) {
+        this.$message.error('仅支持上传 MP4 格式的视频文件!');
+        return false;
+      }
+
+      if (!isLt30M) {
+        this.$message.error('上传大小不能超过 10MB!');
+        return false;
+      }
+
+      return true;
+    },
+    beforeAvatarUploadVoice(file) {
+      return new Promise((resolve, reject) => {
+        const isLt10M = file.size / 1024 / 1024 < 10; // 假设语音文件大小限制为10MB
+        const isVoiceType = ['audio/mp3', 'audio/mpeg', 'audio/wav', 'audio/x-wav'].includes(file.type);
+
+        if (!isVoiceType) {
+          this.$message.error('仅支持上传 MP3, WAV, X-WAV 格式的语音文件!');
+          return reject(false); // 不允许继续上传
+        }
+
+        if (!isLt10M) {
+          this.$message.error('上传大小不能超过 10MB!');
+          return reject(false); // 不允许继续上传
+        }
+
+        // 使用 FileReader 读取文件
+        const reader = new FileReader();
+        reader.onload = (event) => {
+          const audio = new Audio(event.target.result);
+          audio.addEventListener('loadedmetadata', () => {
+            // 获取时长并保存
+            if (Math.ceil(audio.duration) > 30) {
+              this.$message.error('音频时长不能超过30秒!');
+              this.voiceLoading = false;
+              return reject(false); // 不允许继续上传
+            }
+            resolve(true); // 允许上传
+          });
+        };
+
+        reader.onerror = () => {
+          this.$message.error('无法读取音频文件!请上传正确的音频文件');
+          reject(false); // 不允许继续上传
+        };
+        this.voiceLoading = true;
+        reader.readAsDataURL(file); // 开始读取文件
+      });
+    },
+
+    handleClosegroupUser(list) {
+      const index = this.userSelectList.findIndex(t => t === list);
+      if (index !== -1) {
+        this.userSelectList.splice(index, 1);
+      }
+    },
+    courseChange(content, index, countIndex) {
+      this.$set(content, 'videoId', null);
+      let selectedCourse;
+      if (content.courseId != null) {
+        // 查找选中的课程对应的 label
+        selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === content.courseId);
+      }
+
+      for (let i = 0; i < content.setting.length; i++) {
+        //如果是链接的才上
+        if (selectedCourse && content.type == 2 && content.courseId != null) {
+
+          if (content.setting[i].contentType == 3){
+            //响应式直接给链接的标题/封面上值
+              this.$set(content.setting[i], 'linkTitle', selectedCourse.dictLabel);
+              this.$set(content.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+
+          if (content.setting[i].contentType == 4){
+            this.$set(content.setting[i], 'miniprogramPicUrl', selectedCourse.dictImgUrl);
+          }
+
+
+        }
+      }
+      videoList(content.courseId).then(response => {
+
+        this.videoList[index].splice(countIndex, 1, response.list);
+
+      });
+    },
+
+    handleContentTypeChange(content, index, countIndex, setIndex) {
+
+      //如果是链接的才上
+      if (content.courseId != null && content.type == 2) {
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === content.courseId);
+        for (let i = 0; i < content.setting.length; i++) {
+          //响应式直接给链接的标题/封面上值
+          if (selectedCourse  && content.type == 2 && content.courseId != null) {
+
+            if (content.setting[i].contentType == 3){
+              this.$set(content.setting[i], 'linkTitle', selectedCourse.dictLabel);
+              this.$set(content.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+            }
+            if (this.setting[i].contentType == 4){
+              this.$set(content.setting[i], 'miniprogramPicUrl', selectedCourse.dictImgUrl);
+            }
+
+          }
+
+        }
+
+      }
+      if (content.videoId != null && content.type == 2) {
+        // 查找选中的课节对应的 label
+        const selectedVideo = this.videoList[index][countIndex].find(course => parseInt(course.dictValue) === content.videoId);
+
+        for (let i = 0; i < content.setting.length; i++) {
+          //响应式直接给链接的描述上值
+          if (selectedVideo  && content.type == 2 && content.videoId != null) {
+            if (content.setting[i].contentType == 3){
+              this.$set(content.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+            }
+            if (this.setting[i].contentType == 4){
+              this.$set(content.setting[i], 'miniprogramTitle', selectedVideo.dictLabel);
+            }
+          }
+        }
+      }
+
+
+    },
+    videoIdChange(content, index, countIndex) {
+      //选择了课程小节则 默认绑上
+
+      let selectedVideo;
+      // 查找选中的课程对应的 label
+      if (content.videoId != null) {
+        selectedVideo = this.videoList[index][countIndex].find(course => parseInt(course.dictValue) === content.videoId);
+      }
+      for (let i = 0; i < content.setting.length; i++) {
+        //课程消息-文本内容
+        if (content.setting[i].contentType == 1 && content.type == 2) {
+          this.$set(content.setting[i], 'isBindUrl', '1');
+        }
+        //如果是链接的才上
+        if (selectedVideo && content.type == 2 && content.videoId != null) {
+          //响应式直接给链接的描述上值
+          if (content.setting[i].contentType == 3 ){
+
+              this.$set(content.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+          }
+          if (content.setting[i].contentType == 4){
+              this.$set(content.setting[i], 'miniprogramTitle', selectedVideo.dictLabel);
+          }
+
+        }
+      }
+
+    },
+    /** 查询sop模板列表 */
+    getList() {
+      this.loading = true;
+      listSopTemp(this.queryParams).then(response => {
+        this.sopTempList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.$store.dispatch("tagsView/delView", this.$route);
+      this.$router.replace('/qw/conversion/sopTemp')
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        setting: null,
+        status: "0",
+        sort: null,
+        createTime: null,
+        createBy: null,
+        companyId: null,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加sop模板";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getSopTemp(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改sop模板";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+
+        if (valid) {
+          this.form.setting = JSON.stringify(this.setting)
+
+          if (this.setting.length <= 0) {
+            return this.$message("请添加规则")
+          }
+
+          for (let i = 0; i < this.setting.length; i++) {
+
+
+            for (let j = 0; j < this.setting[i].content.length; j++) {
+
+              if (this.setting[i].name == null || this.setting[i].name == "") {
+                return this.$message.error("内容名称不能为空")
+              }
+              if (this.setting[i].content[j].time == null || this.setting[i].content[j].time == "") {
+                return this.$message.error("时间不能为空")
+              }
+              if (this.setting[i].content[j].type != 4) {
+                for (let k = 0; k < this.setting[i].content[j].setting.length; k++) {
+                  if (this.setting[i].content[j].setting[k].contentType == 1 && (this.setting[i].content[j].setting[k].value == null || this.setting[i].content[j].setting[k].value == "")) {
+                    return this.$message.error("内容不能为空")
+                  }
+                  if (this.setting[i].content[j].setting[k].contentType == 2 && (this.setting[i].content[j].setting[k].imgUrl == null || this.setting[i].content[j].setting[k].imgUrl == "")) {
+                    return this.$message.error("图片不能为空")
+                  }
+                  if (this.setting[i].content[j].setting[k].contentType == 3 && (this.setting[i].content[j].setting[k].linkTitle == null || this.setting[i].content[j].setting[k].linkTitle == "")) {
+                    return this.$message.error("链接标题不能为空")
+                  }
+                  if (this.setting[i].content[j].setting[k].contentType == 3 && (this.setting[i].content[j].setting[k].linkDescribe == null || this.setting[i].content[j].setting[k].linkDescribe == "")) {
+                    return this.$message.error("链接描述不能为空")
+                  }
+                  if (this.setting[i].content[j].setting[k].contentType == 3 && (this.setting[i].content[j].setting[k].linkImageUrl == null || this.setting[i].content[j].setting[k].linkImageUrl == "")) {
+                    return this.$message.error("链接图片不能为空")
+                  }
+                  if (this.setting[i].content[j].setting[k].contentType == 3 && this.setting[i].content[j].setting[k].type == 1 && (this.setting[i].content[j].setting[k].linkUrl == null || this.setting[i].content[j].setting[k].linkUrl == "")) {
+                    return this.$message.error("链接地址不能为空")
+                  }
+
+                  if (this.setting[i].content[j].setting[k].contentType == 4 && (this.setting[i].content[j].setting[k].miniprogramTitle == null || this.setting[i].content[j].setting[k].miniprogramTitle == "")) {
+                    return this.$message.error("小程序消息标题不能为空")
+                  }
+                  if (this.setting[i].content[j].setting[k].contentType == 4 && (this.setting[i].content[j].setting[k].miniprogramPicUrl == null || this.setting[i].content[j].setting[k].miniprogramPicUrl == "")) {
+                    return this.$message.error("小程序封面地址不能为空")
+                  }
+                  if (this.setting[i].content[j].setting[k].contentType == 5 && (this.setting[i].content[j].setting[k].fileUrl == null || this.setting[i].content[j].setting[k].fileUrl == "")) {
+                    return this.$message.error("文件不能为空")
+                  }
+                  if (this.setting[i].content[j].setting[k].contentType == 6 && (this.setting[i].content[j].setting[k].videoUrl == null || this.setting[i].content[j].setting[k].videoUrl == "")) {
+                    return this.$message.error("视频不能为空")
+                  }
+                  if (this.setting[i].content[j].setting[k].contentType == 7 && (this.setting[i].content[j].setting[k].value == null || this.setting[i].content[j].setting[k].value == "")) {
+                    return this.$message.error("语音文本不能为空")
+                  }
+                  if (this.setting[i].content[j].setting[k].contentType == 8 && (this.setting[i].content[j].setting[k].url == null || this.setting[i].content[j].setting[k].url == "")) {
+                    return this.$message.error("视频号信息不能为空")
+                  }
+                }
+              } else {
+                if (this.setting[i].content[j].aiTouch == null || this.setting[i].content[j].aiTouch == '') {
+                  return this.$message.error("AI触达不能为空")
+                }
+              }
+            }
+
+
+          }
+
+          if (this.form.id != null) {
+            updateSopTemp(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.$store.dispatch("tagsView/delView", this.$route);
+              // this.$router.replace('/qw/conversion/sopTemp')
+              window.location.replace('/qw/conversion/sopTemp')
+              this.reset();
+
+            });
+          } else {
+
+            addSopTemp(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.$store.dispatch("tagsView/delView", this.$route);
+              // this.$router.replace('/qw/conversion/sopTemp')
+              window.location.replace('/qw/conversion/sopTemp')
+              this.reset();
+
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除sop模板编号为"' + ids + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return delSopTemp(ids);
+      }).then(() => {
+        this.getList();
+
+
+        this.msgSuccess("删除成功");
+      }).catch(() => {
+      });
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有sop模板数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportSopTemp(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {
+      });
+    }
+  }
+};
+</script>
+
+<style scoped>
+.custom-input /deep/ .el-input__inner {
+  height: 20px;
+  text-align: center;
+}
+
+.custom-input /deep/ .el-input__icon {
+  line-height: 10px;
+}
+</style>

+ 734 - 0
src/views/qw/sopTemp/addSopTempOld.vue

@@ -0,0 +1,734 @@
+<template>
+  <div class="app-container">
+
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 "> sop规则【企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 "> sop规则【AI插件】模板</div>
+
+    <div style="margin-top: 10px;margin-left: 50px;margin-right: 100px;margin-bottom: 60px;">
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入模板标题" />
+        </el-form-item>
+
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="dict.dictValue"
+            >{{dict.dictLabel}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="间隔天数" prop="gap">
+          <el-input-number v-model="form.gap"  :min="1" label="间隔天数"></el-input-number>
+        </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="setting">
+          <el-timeline >
+            <el-timeline-item :timestamp="'第'+(1+(form.gap*index))+'天'" :color="'#0bbd87'" placement="top" v-for="(item, index) in setting"  style="margin-top: 10px;">
+            <el-row>
+              <el-col :span="22">
+                <div style="background-color: #fbfbfb;padding: 15px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                  <el-form :model="item"  label-width="80px">
+
+                    <el-form-item label="内容名称"  style="height: 50px;">
+                       <el-input v-model="item.name" placeholder="内容名称,仅内部可见" />
+                    </el-form-item>
+
+                    <el-form-item label="规则"  >
+                       <div v-for="(content, contentIndex) in item.content" style="background-color: #fdfdfd;padding: 15px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
+
+                         <el-row>
+
+                           <el-col el-col :span="22" >
+                              <el-form :model="content" label-width="70px">
+                                <el-form-item label="时间"  prop="time"  >
+                                   <el-time-picker
+                                     class="custom-input"
+                                     v-model="content.time"
+                                     value-format="HH:mm"
+                                     format="HH:mm"
+                                     :picker-options="{ selectableRange: '00:00:00 - 23:59:59' }"
+                                     placeholder="时间"
+                                     style="width: 100px;height: 20px;" >
+                                   </el-time-picker>
+                                </el-form-item>
+                                <el-form-item label="消息类别"  >
+                                   <el-radio-group v-model="content.type" @change="() => content.contentType = '1'" >
+                                       <el-radio
+                                         :label="1"
+
+                                       >普通</el-radio>
+                                       <el-radio
+                                         :label="2"
+
+                                       >课程</el-radio>
+                                       <el-radio
+                                         :label="3"
+
+                                       >订单</el-radio>
+                                   </el-radio-group>
+                                </el-form-item>
+
+                                <el-form-item label="课程"  v-if="content.type == 2 ">
+                                   <el-select  v-model="content.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"  @change="courseChange(content,index,contentIndex)">
+                                     <el-option
+                                         v-for="dict in courseList"
+                                         :key="dict.dictValue"
+                                         :label="dict.dictLabel"
+                                         :value="parseInt(dict.dictValue)"
+                                       />
+                                   </el-select>
+                                   <el-select  v-model="content.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;" @change="videoIdChange(content,index,contentIndex)" >
+                                     <el-option
+                                         v-for="dict in videoList[index][contentIndex]"
+                                         :key="dict.dictValue"
+                                         :label="dict.dictLabel"
+                                         :value="parseInt(dict.dictValue)"
+                                       />
+                                   </el-select>
+                                   <el-select  v-model="content.courseType" placeholder="请选择消息类型" size="mini" style=" margin-right: 10px;">
+                                     <el-option
+                                         v-for="dict in sysFsSopWatchStatus"
+                                         :key="dict.dictValue"
+                                         :label="dict.dictLabel"
+                                         :value="parseInt(dict.dictValue)"
+                                       />
+                                   </el-select>
+
+                                </el-form-item>
+                                <el-form-item label="内容类别"  >
+
+<!--                                  <div v-for="(list, setIndex) in content.setting ">-->
+<!--                                    <el-row>-->
+<!--                                      <el-col el-col :span="22" >-->
+<!--                          -->
+<!--                                      </el-col>-->
+<!--                                      <el-col :span="1" :offset="1">-->
+<!--                                        <i class="el-icon-delete" @click="delContent(index,contentIndex)" style="margin-top: 20px;" v-if="item.content.length>1"></i>-->
+<!--                                      </el-col>-->
+<!--                                    </el-row>-->
+
+<!--                                  </div>-->
+
+                                  <div v-if="form.sendType == 1 ">
+                                    <el-radio-group  v-model="content.contentType" >
+                                      <el-radio  :label="item.dictValue" v-for="item in sysQwSopContentType">{{item.dictLabel}}</el-radio>
+                                    </el-radio-group>
+                                  </div>
+                                  <div v-if="form.sendType == 2 ">
+                                    <div v-if="content.type == 2 ">
+                                      <el-radio-group  v-model="content.contentType" @change="handleContentTypeChange(content,index,contentIndex)">
+                                        <el-radio  :label="item.dictValue" v-for="item in sysQwSopSettingType">{{item.dictLabel}}</el-radio>
+                                      </el-radio-group>
+                                    </div>
+                                    <div v-else>
+                                      <el-radio-group  v-model="content.contentType" @change="handleContentTypeChange(content,index,contentIndex)">
+                                        <el-radio   :label="item.dictValue" v-for="item in sysQwSopAiContentType">{{item.dictLabel}}</el-radio>
+                                      </el-radio-group>
+                                    </div>
+
+                                  </div>
+                                </el-form-item>
+                                <el-form-item label="内容"  >
+                                   <el-input v-if="content.contentType == 1 " v-model="content.value" type="textarea" :rows="3" placeholder="内容" style="width: 90%;margin-top: 10px;"/>
+
+                                    <ImageUpload v-if="content.contentType == 2 " v-model="content.imgUrl" type="image" :num="1" :width="150" :height="150" />
+
+                                    <div v-if="content.contentType == 3 ">
+                                      <el-card class="box-card">
+                                        <el-form-item label="链接标题:"  label-width="100px">
+                                          <el-input v-model="content.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
+                                        </el-form-item>
+                                        <el-form-item label="链接描述:"   label-width="100px" >
+                                          <el-input type="textarea" :rows="3" v-model="content.linkDescribe" placeholder="请输入链接描述" style="width: 90%;margin-top: 1%;"/>
+                                        </el-form-item>
+                                        <el-form-item label="链接封面:"   label-width="100px">
+                                          <ImageUpload v-model="content.linkImageUrl" type="image" :num="1" :width="150" :height="150" style="margin-top: 1%;" />
+                                      </el-form-item>
+                                    <div v-if="content.type!=2" style="margin-top: 1%">
+                                    <el-form-item label="链接地址:"  label-width="100px" >
+                                      <el-input v-model="content.linkUrl" placeholder="请输入链接地址" style="width: 90%;"/>
+                                    </el-form-item>
+                                    </div>
+                                    <div v-if="content.type==2">
+                                    <el-form-item label="链接地址:"  label-width="100px" >
+                                      <el-tag type="warning" v-model="content.isBindUrl=1">选择的课程小节 即为卡片链接地址</el-tag>
+                                    </el-form-item>
+                                    </div>
+                                    </el-card>
+                                    </div>
+                                    <div v-if="content.contentType == 5 ">
+
+                                      <el-form-item label="上传文件:" prop="fileUrl" label-width="100px">
+                                        <el-upload
+                                        v-model="content.fileUrl"
+                                        class="avatar-uploader"
+                                        :action="uploadUrl"
+                                        :show-file-list="false"
+                                        :on-success="(res, file) => handleAvatarSuccessFile(res, file, content)"
+                                        :before-upload="beforeAvatarUploadFile">
+                                        <i class="el-icon-plus avatar-uploader-icon"></i>
+                                        </el-upload>
+                                        <el-link v-if="content.fileUrl" type="primary" :href="downloadUrl(content.fileUrl)" download>
+                                        {{content.fileUrl}}
+                                      </el-link>
+                                      </el-form-item>
+
+                                    </div>
+
+                                    <div v-if="content.contentType == 6 ">
+                                      <el-form-item label="上传视频:" prop="videoUrl" label-width="100px">
+                                        <el-upload
+                                        v-model="content.videoUrl"
+                                        class="avatar-uploader"
+                                        :action="uploadUrl"
+                                        :show-file-list="false"
+                                        :on-success="(res, file) => handleAvatarSuccessVideo(res, file, content)"
+                                        :before-upload="beforeAvatarUploadVideo">
+                                        <i class="el-icon-plus avatar-uploader-icon"></i>
+                                        </el-upload>
+                                        <video v-if="content.videoUrl"
+                                        :src="content.videoUrl"
+                                        controls style="width: 200px;height: 100px">
+                                        </video>
+                                      </el-form-item>
+                                    </div>
+                                    <div v-if="content.contentType == 7 ">
+                                      <el-form-item label="上传语音:" prop="videoUrl" label-width="100px">
+                                        <el-upload
+                                          v-model="content.voiceUrl"
+                                          class="avatar-uploader"
+                                          :action="uploadUrl"
+                                          :show-file-list="false"
+                                          :on-success="(res, file) => handleAvatarSuccessVoice(res, file, content)"
+                                          :before-upload="beforeAvatarUploadVoice">
+                                          <i class="el-icon-plus avatar-uploader-icon"></i>
+                                        </el-upload>
+                                        <audio  v-if="content.voiceUrl"
+                                               :src="content.voiceUrl"
+                                               controls style="width: 300px;height: 50px">
+                                        </audio >
+                                      </el-form-item>
+                                    </div>
+
+
+                                </el-form-item>
+
+                                <el-form-item label="添加短链" v-if="content.type == 2 && content.contentType == 1 "  >
+                                  <el-tooltip content="请先根据课程选定课程小节之后再添加" effect="dark" :disabled="!!content.videoId">
+                                    <el-switch
+                                      v-model="content.isBindUrl"
+                                      :disabled="!content.videoId"
+                                      active-color="#13ce66"
+                                      inactive-color="#DCDFE6"
+                                      active-value="1"
+                                      inactive-value="2">
+                                    </el-switch>
+                                  </el-tooltip>
+
+                                  <span v-if="content.isBindUrl == '1'" style="margin-left: 10px; color: #13ce66">添加URL</span>
+                                  <span v-if="content.isBindUrl == '2'" style="margin-left: 10px; color: #b1b4ba">不加URL</span>
+                                </el-form-item>
+
+                                <el-form-item label="课节过期时间" v-if="content.type == 2 && content.isBindUrl == '1' && content.contentType != 2  && content.contentType != 5  && content.contentType != 6 " style="margin-top: 1%">
+                                  <el-row>
+                                    <el-input-number  v-model="content.expiresDays"  :min="0" :max="100" ></el-input-number>
+                                    (天)
+                                  </el-row>
+                                  <el-row>
+                                    <span class="tip">填写0或不填时,默认为系统配置的默认时间</span>
+                                  </el-row>
+                                </el-form-item>
+
+                              </el-form>
+
+                           </el-col>
+                           <el-col :span="1" :offset="1">
+                             <i class="el-icon-delete" @click="delContent(index,contentIndex)" style="margin-top: 20px;" v-if="item.content.length>1"></i>
+                           </el-col>
+                         </el-row>
+                       </div>
+                       <el-link type="primary" class="el-icon-plus" :underline="false" @click='addContent(index)'>添加规则</el-link>
+                    </el-form-item>
+                  </el-form>
+                </div>
+              </el-col>
+              <el-col :span="1" :offset="1">
+                 <i class="el-icon-delete-solid" @click="delSetting(index)"  v-if="setting.length>1"></i>
+              </el-col>
+            </el-row>
+          </el-timeline-item>
+
+
+          </el-timeline>
+           <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetting()'>添加天数</el-link>
+        </el-form-item>
+
+      </el-form>
+      <div slot="footer" class="dialog-footer" style="float: right;">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { listSopTemp, getSopTemp, delSopTemp, addSopTemp, updateSopTemp, exportSopTemp } from "@/api/qw/sopTemp";
+import { courseList,videoList } from "@/api/qw/sop";
+import ImageUpload from "@/views/qw/sop/ImageUpload";
+import FriendMaterial from "@/views/qw/friendMaterial/index.vue";
+export default {
+  name: "SopTemp",
+      components: { ImageUpload,FriendMaterial},
+  data() {
+    return {
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      sysFsSopWatchStatus: [],
+      //消息内容类型 企微版
+      sysQwSopContentType:[],
+      //插件版
+      sysQwSopAiContentType:[],
+      //类别
+      sysQwSopSettingType:[],
+      courseList:[],
+      videoList:[],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // sop模板表格数据
+      setting: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 状态字典
+      statusOptions: [],
+      // 查询参数
+      form: {
+        name: null,
+        setting: null,
+        status: "1",
+        sort: 1,
+        companyId: null,
+        gap:1,
+      },
+      // 表单校验
+      rules: {
+        name: [
+          { required: true, message: '名称不能为空', trigger: 'blur' }
+        ],
+        status: [
+          { required: true, message: '状态不能为空', trigger: 'blur' }
+        ],
+        sort: [
+          { required: true, message: '排序不能为空', trigger: 'blur' }
+        ],
+        gap: [
+          { required: true, message: '间隔天数不能为空', trigger: 'blur' }
+        ],
+      },
+      contentRules:{
+        time:[{ required: true, message: '时间不能为空', trigger: 'blur' }],
+      }
+    };
+  },
+  created() {
+
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+    this.getDicts("sys_fs_sop_watch_status").then(response => {
+      this.sysFsSopWatchStatus = response.data;
+    });
+
+    this.getDicts("sys_qwSopAi_contentType").then(response => {
+      this.sysQwSopAiContentType = response.data;
+    });
+
+    this.getDicts("sys_qwSop_contentType").then(response => {
+      this.sysQwSopContentType = response.data;
+    });
+    this.getDicts("sys_qwSop_settingType").then(response => {
+      this.sysQwSopSettingType = response.data;
+    });
+
+
+    this.form.sendType= this.$route.params && this.$route.params.command;
+
+    setTimeout(() => {
+      this.getList();
+    }, 200);
+
+  },
+  methods: {
+
+    addContent(index){
+
+      if (this.setting[index].content.length>0 && this.form.sendType==1){
+        return this.$message.error("因为企微接口限制,企微模板一天只能设置一条消息~")
+      }else {
+        this.setting[index].content.push({type:1,contentType:'1'})
+        this.videoList[index].push([])
+      }
+
+    },
+    delContent(index,contentIndex){
+      this.setting[index].content.splice(contentIndex,1)
+      this.videoList[index].splice(contentIndex,1)
+    },
+    addSetting(){
+
+      this.setting.push({name:null,content:[{type:1,contentType:'1'}]})
+      this.videoList.push([])
+
+    },
+    delSetting(index){
+      this.setting.splice(index,1)
+       this.videoList.splice(index,1)
+    },
+    //上传文件
+    handleAvatarSuccessFile(res, file, content) {
+      if (res.code === 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'fileUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadFile(file){
+      const isLt1M = file.size / 1024 / 1024 < 30;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 30MB!');
+      }
+      return isLt1M;
+    },
+
+    //下载文件
+    downloadUrl(materialUrl) {
+      // 直接返回文件 URL
+      return materialUrl;
+    },
+
+    handleAvatarSuccessVideo(res, file, content) {
+      if(res.code==200){
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'videoUrl', res.url);
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+    handleAvatarSuccessVoice(res, file, content){
+      if(res.code==200){
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'voiceUrl', res.url);
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadVideo(file){
+      const isLt30M = file.size / 1024 / 1024 < 50;
+      const isMP4 = file.type === 'video/mp4';
+
+      if (!isMP4) {
+        this.$message.error('仅支持上传 MP4 格式的视频文件!');
+        return false;
+      }
+
+      if (!isLt30M) {
+        this.$message.error('上传大小不能超过 50MB!');
+        return false;
+      }
+
+      return true;
+    },
+
+    beforeAvatarUploadVoice(file){
+      const isLt10M = file.size / 1024 / 1024 < 10; // 假设语音文件大小限制为10MB
+      const isVoiceType = ['audio/mp3'].includes(file.type); // 支持的语音文件类型
+
+      if (!isVoiceType) {
+        this.$message.error('仅支持上传 MP3 格式的语音文件!');
+        return false;
+      }
+
+      if (!isLt10M) {
+        this.$message.error('上传大小不能超过 10MB!');
+        return false;
+      }
+
+      return true;
+    },
+
+    handleClosegroupUser(list){
+      const index = this.userSelectList.findIndex(t => t === list);
+      if (index !== -1) {
+        this.userSelectList.splice(index, 1);
+      }
+    },
+    courseChange(content,index,countIndex){
+
+      this.$set(content, 'videoId', null);
+
+      //如果是链接的才上
+      if (content.contentType==3 && content.type==2 && content.courseId!=null){
+
+        // 查找选中的课程对应的 label
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === content.courseId);
+
+        //响应式直接给链接的标题/封面上值
+        if (selectedCourse) {
+          this.$set(content, 'linkTitle', selectedCourse.dictLabel);
+          this.$set(content, 'linkImageUrl', selectedCourse.dictImgUrl);
+        }
+      }
+
+      videoList(content.courseId).then(response => {
+
+        this.videoList[index].splice(countIndex, 1, response.list);
+
+      });
+    },
+
+    handleContentTypeChange(content,index,countIndex){
+
+      //如果是链接的才上
+      if (content.contentType==3 && content.type==2 ) {
+
+        if (content.courseId!=null) {
+            // 查找选中的课程对应的 label
+            const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === content.courseId);
+
+            //响应式直接给链接的标题/封面上值
+            if (selectedCourse) {
+              this.$set(content, 'linkTitle', selectedCourse.dictLabel);
+              this.$set(content, 'linkImageUrl', selectedCourse.dictImgUrl);
+            }
+        }
+        if (content.videoId!=null){
+            // 查找选中的课程对应的 label
+            const selectedVideo = this.videoList[index][countIndex].find(course => parseInt(course.dictValue) === content.videoId);
+
+            //响应式直接给链接的描述上值
+            if (selectedVideo) {
+              this.$set(content, 'linkDescribe', selectedVideo.dictLabel);
+            }
+        }
+
+      }
+    },
+    videoIdChange(content,index,countIndex){
+
+      //选择了课程小节则 默认绑上
+      this.$set(content,'isBindUrl','1');
+
+      //如果是链接的才上
+      if (content.contentType==3 && content.type==2 && content.videoId!=null ) {
+
+        // 查找选中的课程对应的 label
+        const selectedVideo = this.videoList[index][countIndex].find(course => parseInt(course.dictValue) === content.videoId);
+
+        //响应式直接给链接的描述上值
+        if (selectedVideo) {
+          this.$set(content, 'linkDescribe', selectedVideo.dictLabel);
+        }
+      }
+    },
+    /** 查询sop模板列表 */
+    getList() {
+      this.loading = true;
+      listSopTemp(this.queryParams).then(response => {
+        this.sopTempList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.$store.dispatch("tagsView/delView", this.$route);
+      this.$router.replace('/qw/conversion/sopTemp')
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        setting: null,
+        status: "0",
+        sort: null,
+        createTime: null,
+        createBy: null,
+        companyId: null,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加sop模板";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getSopTemp(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改sop模板";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+
+        if (valid) {
+          this.form.setting=JSON.stringify(this.setting)
+
+          if(this.setting.length<=0){
+            return this.$message("请添加规则")
+          }
+          for (let i = 0; i < this.setting.length; i++) {
+
+            for (let j = 0; j < this.setting[i].content.length; j++) {
+
+              if(this.setting[i].name==null||this.setting[i].name==""){
+                return this.$message.error("内容名称不能为空")
+              }
+              if(this.setting[i].content[j].time==null||this.setting[i].content[j].time==""){
+                return this.$message.error("时间不能为空")
+
+              }
+              if (this.setting[i].content[j].contentType==1 && (this.setting[i].content[j].value==null|| this.setting[i].content[j].value=="")){
+                return this.$message.error("内容不能为空")
+              }
+              if (this.setting[i].content[j].contentType==2 && (this.setting[i].content[j].imgUrl==null|| this.setting[i].content[j].imgUrl=="")){
+                return this.$message.error("图片不能为空")
+              }
+              if (this.setting[i].content[j].contentType==3 && (this.setting[i].content[j].linkTitle==null|| this.setting[i].content[j].linkTitle=="")){
+                return this.$message.error("链接标题不能为空")
+              }
+              if (this.setting[i].content[j].contentType==3 && (this.setting[i].content[j].linkDescribe==null|| this.setting[i].content[j].linkDescribe=="")){
+                return this.$message.error("链接描述不能为空")
+              }
+              if (this.setting[i].content[j].contentType==3 && (this.setting[i].content[j].linkImageUrl==null|| this.setting[i].content[j].linkImageUrl=="")){
+                return this.$message.error("链接图片不能为空")
+              }
+              if (this.setting[i].content[j].contentType==3 && this.setting[i].content[j].type==1 && (this.setting[i].content[j].linkUrl==null|| this.setting[i].content[j].linkUrl=="")){
+                return this.$message.error("链接地址不能为空")
+              }
+              if (this.setting[i].content[j].contentType==5 && (this.setting[i].content[j].fileUrl==null|| this.setting[i].content[j].fileUrl=="")){
+                return this.$message.error("文件不能为空")
+              }
+              if (this.setting[i].content[j].contentType==6 && (this.setting[i].content[j].videoUrl==null|| this.setting[i].content[j].videoUrl=="")){
+                return this.$message.error("视频不能为空")
+              }
+              if (this.setting[i].content[j].contentType==7 && (this.setting[i].content[j].voiceUrl==null|| this.setting[i].content[j].voiceUrl=="")){
+                return this.$message.error("语音不能为空")
+              }
+            }
+
+          }
+
+          if (this.form.id != null) {
+            updateSopTemp(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.$store.dispatch("tagsView/delView", this.$route);
+              // this.$router.replace('/qw/conversion/sopTemp')
+              window.location.replace('/qw/conversion/sopTemp')
+              this.reset();
+
+            });
+          }
+          else {
+
+            addSopTemp(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.$store.dispatch("tagsView/delView", this.$route);
+              // this.$router.replace('/qw/conversion/sopTemp')
+              window.location.replace('/qw/conversion/sopTemp')
+              this.reset();
+
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除sop模板编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delSopTemp(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有sop模板数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportSopTemp(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>
+
+<style scoped>
+.custom-input /deep/ .el-input__inner {
+  height: 20px;
+  text-align:center;
+}
+.custom-input /deep/ .el-input__icon {
+  line-height: 10px;
+}
+</style>

+ 194 - 0
src/views/qw/sopTemp/addTemp.vue

@@ -0,0 +1,194 @@
+<template>
+  <div class="app-container">
+
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 "> sop规则【企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 "> sop规则【AI插件】模板</div>
+
+    <div style="margin-top: 10px;margin-left: 50px;margin-right: 100px;margin-bottom: 60px;">
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入模板标题" />
+        </el-form-item>
+
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="dict.dictValue"
+            >{{dict.dictLabel}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="间隔天数" prop="gap">
+          <el-input-number v-model="form.gap"  :min="1" label="间隔天数"></el-input-number>
+        </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>
+      <div slot="footer" class="dialog-footer" style="float: right;">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {addTemp} from "@/api/qw/sopTemp";
+import ImageUpload from "@/views/qw/sop/ImageUpload";
+import FriendMaterial from "@/views/qw/friendMaterial/index.vue";
+import userVideo from "@/views/qw/userVideo/userVideo.vue";
+
+export default {
+  name: "addSopTemp",
+  components: { ImageUpload,FriendMaterial,userVideo},
+  data() {
+    return {
+
+      //图片放大
+      dialogVisible: false,
+      dialogImageUrl:null,
+
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS2",
+      uploadUrlByVoice:process.env.VUE_APP_BASE_API+"/common/uploadOSSByHOOKVoice",
+      //上传语音的遮罩层
+      voiceLoading :false,
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      sysFsSopWatchStatus: [],
+      //消息内容类型 企微版
+      sysQwSopContentType:[],
+      //插件版
+      sysQwSopAiContentType:[],
+      //链接版类别
+      sysQwSopSettingType:[],
+
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // sop模板表格数据
+      setting: [],
+      courseList:[],
+      videoList:[],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 状态字典
+      statusOptions: [],
+      // 查询参数
+      form: {
+        name: null,
+        setting: null,
+        status: "1",
+        sort: 1,
+        companyId: null,
+        gap:1,
+      },
+      videoNumOptions:{
+        title:'选择视频号',
+        open:false,
+        content:null,
+        contentIndex:null,
+        setIndex:null,
+      },
+      // 表单校验
+      rules: {
+        name: [
+          { required: true, message: '名称不能为空', trigger: 'blur' }
+        ],
+        status: [
+          { required: true, message: '状态不能为空', trigger: 'blur' }
+        ],
+        sort: [
+          { required: true, message: '排序不能为空', trigger: 'blur' }
+        ],
+        gap: [
+          { required: true, message: '间隔天数不能为空', trigger: 'blur' }
+        ],
+      },
+      contentRules:{
+        time:[{ required: true, message: '时间不能为空', trigger: 'blur' }],
+      }
+    };
+  },
+  created() {
+
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    this.form.sendType = this.$route.params && this.$route.params.command;
+    setTimeout(() => {
+      this.getList();
+    }, 200);
+
+  },
+  methods: {
+    // 取消按钮
+    cancel() {
+      this.$store.dispatch("tagsView/delView", this.$route);
+      this.$router.replace('/qw/conversion/sopTemp')
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        setting: null,
+        status: "0",
+        sort: null,
+        createTime: null,
+        createBy: null,
+        companyId: null,
+      };
+      this.resetForm("form");
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateTemp(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.$store.dispatch("tagsView/delView", this.$route);
+              // this.$router.replace('/qw/conversion/sopTemp')
+              window.location.replace('/qw/conversion/sopTemp')
+              this.reset();
+            });
+          } else {
+            addTemp(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.$store.dispatch("tagsView/delView", this.$route);
+              // this.$router.replace('/qw/conversion/sopTemp')
+              window.location.replace('/qw/conversion/sopTemp')
+              this.reset();
+            });
+          }
+        }
+      });
+    },
+  }
+};
+</script>
+
+<style scoped>
+.custom-input /deep/ .el-input__inner {
+  height: 20px;
+  text-align: center;
+}
+
+.custom-input /deep/ .el-input__icon {
+  line-height: 10px;
+}
+</style>

+ 680 - 0
src/views/qw/sopTemp/index.vue

@@ -0,0 +1,680 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="销售公司" prop="companyId">
+        <el-select filterable v-model="queryParams.companyId" clearable placeholder="请选择公司名" size="small">
+          <el-option
+            v-for="item in companys"
+            :key="item.companyId"
+            :label="item.companyName"
+            :value="item.companyId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="模板标题" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入模板标题"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </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="sendType">
+        <el-select v-model="queryParams.sendType" placeholder="请选择类型" clearable size="small">
+          <el-option
+            v-for="dict in sysQwSopType"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="排序" prop="sort">
+        <el-input
+          v-model="queryParams.sort"
+          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-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="['qw:sopTemp:add']"-->
+        <!--        >新增</el-button>-->
+
+        <el-dropdown
+          @command="handleCommand"
+          trigger="click"
+          placement="bottom-start"
+        >
+          <el-dropdown-menu slot="dropdown" style="width: 120px;">
+            <el-dropdown-item
+              v-for="option in sysQwSopType"
+              :key="option.dictValue"
+              :command="option.dictValue"
+            >
+              <i :class="option.iconClass" style="margin-right: 10px;"></i>
+              {{ option.dictLabel }}
+            </el-dropdown-item>
+          </el-dropdown-menu>
+          <span class="el-dropdown-link">
+              <el-button type="primary" icon="el-icon-plus" plain size="mini">
+                新增模板
+              </el-button>
+            </span>
+        </el-dropdown>
+      </el-col>
+
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['qw:sopTemp:remove']"
+        >删除
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['qw:sopTemp:export']"
+        >导出
+        </el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" border :data="sopTempList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center"/>
+      <el-table-column label="模板编号" align="center" prop="id"/>
+      <el-table-column label="销售公司" align="center">
+        <template slot-scope="scope">
+          <el-tag v-for="item in companys" v-if="scope.row.companyId == item.companyId">{{ item.companyName }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="模板标题" align="center" prop="name"/>
+      <el-table-column label="模板类型" align="center" prop="sendType">
+        <template slot-scope="scope">
+          <dict-tag :options="sysQwSopType" :value="scope.row.sendType"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="间隔天数" align="center" prop="gap"/>
+      <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="createTime"/>
+      <el-table-column label="修改时间" align="center" prop="updateTime"/>
+      <el-table-column label="排序" align="center" prop="sort"/>
+
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-connection"
+            @click="copyTemplate(scope.row)"
+            v-hasPermi="['qw:sopTemp:edit']"
+          >复制模板
+          </el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-s-promotion"
+            @click="handleQueryDetails(scope.row)"
+            v-hasPermi="['qw:sopTemp:list']"
+          >详情
+          </el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['qw:sopTemp:edit']"
+          >修改
+          </el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate2(scope.row)"
+            v-hasPermi="['qw:sopTemp:edit']"
+          >管理规则
+          </el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdateRed(scope.row)"
+            v-if="scope.row.sendType == 5"
+            v-hasPermi="['qw:sopTemp:edit']"
+          >红包设置
+          </el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['qw:sopTemp:remove']"
+          >删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入模板标题"/>
+        </el-form-item>
+        <el-form-item label="销售公司" prop="companyId">
+          <el-select filterable v-model="form.companyId" placeholder="请选择公司名" size="small">
+            <el-option
+              v-for="item in companys"
+              :key="item.companyId"
+              :label="item.companyName"
+              :value="item.companyId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="dict.dictValue"
+            >{{ dict.dictLabel }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="所属项目" prop="project" v-if="form.sendType != 5">
+          <el-select v-model="form.project" placeholder="请选择项目" filterable clearable size="small">
+            <el-option
+              v-for="dict in projectOptions"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="dict.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="课程" prop="courseId" v-if="form.sendType == 5 && !form.id">
+          <el-select v-model="form.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"
+                     filterable>
+            <el-option
+              v-for="dict in courseList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="间隔天数" prop="gap">
+          <el-input-number v-model="form.gap" :min="1" label="间隔天数"></el-input-number>
+        </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="num" v-if="form.sendType == 5 && !form.id">
+          <el-input-number v-model="form.num" :min="1" label="每天发送次数" @change="sendNumChange"></el-input-number>
+        </el-form-item>
+        <el-form-item label="发送时间" v-if="form.sendType == 5 && !form.id">
+          <el-time-picker
+            v-for="item in form.timeList"
+            class="custom-input"
+            v-model="item.value"
+            value-format="HH:mm"
+            format="HH:mm"
+            :picker-options="{ selectableRange: startTimeRange }"
+            placeholder="时间"
+            style="width: 100px;height: 20px;margin-left: 10px;margin-top: 10px">
+          </el-time-picker>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer" style="float: right;">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="课程红包" :visible.sync="redData.open" width="900px" append-to-body>
+      <el-table v-loading="redData.loading" border :data="redData.list" height="500px">
+        <el-table-column label="小节" align="left" prop="videoName"/>
+        <el-table-column label="金额" align="center">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.redPacketMoney" :min="0.01" step="0.01"
+                             label="红包金额"></el-input-number>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div slot="footer" class="dialog-footer" style="float: right;">
+        <el-button type="primary" @click="updateRedData" :disabled="redData.loading">保 存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  addTemp,
+  copyTemplate,
+  delSopTemp,
+  exportSopTemp,
+  getSelectableRange,
+  getSopTemp,
+  redList,
+  listSopTemp,
+  shareSopTemp,
+  updateRedPackage,
+  updateTemp
+} from "@/api/qw/sopTemp";
+import {getCompanyList} from "@/api/company/company";
+import {courseList} from "@/api/qw/sop";
+import fa from "element-ui/src/locale/lang/fa";
+
+export default {
+  name: "SopTemp",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      companysloading: false,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      courseList: [],
+      //选中的公司
+      companys: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      companyTotal: 0,
+      total: 0,
+      sendType: 0,
+      // sop模板表格数据
+      sopTempList: [],
+      sysQwSopType: [],
+      companyList: [],
+      projectOptions: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      command: 0,
+      // 状态字典
+      statusOptions: [],
+      startTimeRange: [],
+
+      shareOptions: {
+        title: '分享模板',
+        open: false,
+        templateId: null,
+      },
+      redData: {
+        open: false,
+        loading: true,
+        list: [],
+      },
+      queryCompanyParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyName: null,
+        status: null,
+      },
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        setting: null,
+        status: '1',
+        sort: null,
+        companyId: null
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        name: [
+          {required: true, message: '名称不能为空', trigger: 'blur'}
+        ],
+        status: [
+          {required: true, message: '状态不能为空', trigger: 'blur'}
+        ],
+        sort: [
+          {required: true, message: '排序不能为空', trigger: 'blur'}
+        ],
+        gap: [
+          {required: true, message: '间隔天数不能为空', trigger: 'blur'}
+        ],
+      },
+      contentRules: {
+        time: [{required: true, message: '时间不能为空', trigger: 'blur'}],
+      }
+    };
+  },
+  created() {
+    this.getList();
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    getSelectableRange().then(e => {
+      this.startTimeRange = e.data;
+    })
+
+    this.getDicts("sys_course_project").then(response => {
+      this.projectOptions = response.data;
+    });
+
+    this.getDicts("sys_qw_sop_type").then(response => {
+      this.sysQwSopType = response.data;
+    });
+
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+
+    getCompanyList().then(response => {
+      this.companys = response.data;
+    });
+  },
+  methods: {
+
+    handleCompanyQuery() {
+      this.queryCompanyParams.pageNum = 1;
+    },
+    resetCompanyQuery() {
+      this.resetForm("queryCompanyForm");
+      this.handleCompanyQuery();
+    },
+    /** 查询sop模板列表 */
+    getList() {
+      this.loading = true;
+      listSopTemp(this.queryParams).then(response => {
+        this.sopTempList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        gap: 1,
+        sendType: this.sendType,
+        sort: 0,
+        num: 1,
+        timeList: [{value: ""}],
+        status: "1",
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+
+    handleCommand(command) {
+      // if (command==4) {
+      //   this.$router.push('/qw/sopTemp/addAiChatTemp')
+      // }else{
+      this.sendType = command;
+      this.title = "新增";
+      this.reset();
+      this.open = true;
+      // this.$router.push('/qw/sopTemp/addTemp/'+command)
+      // }
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    //销售公司多选
+    handleSelectionCompany(selection) {
+      this.companys = selection.map(item => item.companyId)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    // handleAdd() {
+    //   this.$router.push('/qw/sopTemp/addSopTemp')
+    // },
+    /**
+     * 查看详情
+     */
+    handleQueryDetails(row) {
+      // if (row.sendType==4) {
+      //   this.$router.push(`/qw/sopTemp/updateAiChatTemp/${row.id}/3`)
+      // }else{
+      this.$router.push(`/qw/sopTempe/updateSopTemp/${row.id}/3`)
+      // }
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      // if (row.sendType==4) {
+      //   this.$router.push(`/qw/sopTemp/updateAiChatTemp/${row.id}/1`)
+      // }else{
+      this.getInfo(row.id, 1);
+      // this.$router.push(`/qw/sopTemp/updateTemp/${row.id}/1`)
+      // }
+
+    },
+    /** 修改按钮操作 */
+    handleUpdate2(row) {
+      // if (row.sendType==4) {
+      //   this.$router.push(`/qw/sopTemp/updateAiChatTemp/${row.id}/1`)
+      // }else{
+      let url = `/qw/sopTempe/updateSopTemp/${row.id}/1`;
+      console.info(url)
+      this.$router.push(url)
+      // }
+    },
+    /** 修改按钮操作 */
+    handleUpdateRed(row) {
+      this.redData.open = true;
+      redList(row.id).then(e => {
+        this.redData.loading = false;
+        this.redData.list = e.data;
+      })
+    },
+    /** 修改按钮操作 */
+    updateRedData() {
+      this.redData.loading = true;
+      updateRedPackage({list: this.redData.list}).then(e => {
+        this.redData.open = false;
+        this.getList();
+      }).catch(() => {
+        this.redData.loading = false;
+      });
+    },
+
+    /** 分享模板 */
+    shareTemplate(row) {
+      this.shareOptions.open = true;
+      this.shareOptions.templateId = row.id;
+    },
+    /**
+     * 复制模板
+     */
+    copyTemplate(row) {
+      // if(row.sendType==4){
+      //   this.$router.push(`/qw/sopTemp/updateAiChatTemp/${row.id}/2`)
+      // }else{
+      this.getInfo(row.id, 2);
+      // this.$router.push(`/qw/sopTemp/updateSopTemp/${row.id}/2`)
+      // }
+    },
+    getInfo(id, command) {
+      getSopTemp(id).then(response => {
+        this.command = command;
+        if (command == 2) {
+          this.title = "复制";
+        }
+        if (command == 1) {
+          this.title = "修改";
+        }
+        this.form = response.data;
+        this.open = true;
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      delete this.form.rules
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          let f = JSON.parse(JSON.stringify(this.form));
+          if (f.timeList && f.timeList.length > 0) {
+            f.timeList = f.timeList.map(item => item.value);
+          }
+          const loading = this.$loading({
+            lock: true,
+            text: 'Loading',
+            spinner: 'el-icon-loading',
+            background: 'rgba(0, 0, 0, 0.7)'
+          });
+          if (this.command == 2) {
+            copyTemplate(f).then(response => {
+              this.msgSuccess("复制成功");
+              this.open = false;
+              loading.close();
+              this.getList();
+            });
+          } else {
+            if (f.id != null) {
+              updateTemp(f).then(response => {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                loading.close();
+                this.getList();
+              });
+            } else {
+              addTemp(f).then(response => {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                loading.close();
+                this.getList();
+              });
+            }
+          }
+        }
+      });
+    },
+
+    handleShareTemplate() {
+      const companyIds = this.companys;
+      let templateId = this.shareOptions.templateId;
+      this.$confirm("确定将模板分享给 公司编号为:" + companyIds + "的公司?", "提醒", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return shareSopTemp({companyIds: companyIds, templeId: templateId});
+      }).then(() => {
+        this.companys = [];
+        this.shareOptions.open = false;
+        this.getList();
+        this.msgSuccess("分享成功");
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除sop模板编号为"' + ids + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return delSopTemp(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {
+      });
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有sop模板数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportSopTemp(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {
+      });
+    },
+    sendNumChange(val, old) {
+      if (val > old) {
+        for (let i = 0; i < val - old; i++) {
+          this.form.timeList.push({value: ""});
+        }
+      } else {
+        let len = old - val;
+        this.form.timeList.splice(Math.max(this.form.timeList.length - len, 0), len);
+      }
+    }
+  }
+};
+</script>

+ 177 - 0
src/views/qw/sopTemp/sopTemp.vue

@@ -0,0 +1,177 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="模板标题" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          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="sopTempList" ref="sopTempList" >
+      <el-table-column label="id" align="center" prop="id" />
+      <el-table-column label="模板标题" align="center" prop="name" />
+      <el-table-column label="每天首条消息是否发课" align="center" prop="name" v-if="cuoser">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.cuoser" type="success">是</el-tag>
+          <el-tag v-if="!scope.row.cuoser" type="danger">否</el-tag>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="medium"
+            type="primary"
+            plain
+            icon="el-icon-edit"
+            @click="handleSopTemp(scope.row)"
+            v-hasPermi="['qw:sopTemp:edit']"
+          >选择此模板</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getResetList"
+    />
+  </div>
+</template>
+
+<script>
+import { listSopTemp } from "@/api/qw/sopTemp";
+
+export default {
+  name: "SopTempComments",
+  data() {
+    return {
+      isUpdateSop:null,
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // sop模板表格数据
+      sopTempList: [],
+      sysQwSopType:[],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      cuoser: false,
+      // 状态字典
+      statusOptions: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        sendType:null,
+        status:1,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+
+    this.getDicts("sys_qw_sop_type").then(response => {
+      this.sysQwSopType = response.data;
+    });
+  },
+  methods: {
+    /** 查询sop模板列表 */
+    getList(sendType,isUpdate,cuoser) {
+      if(cuoser == null){
+        cuoser = false;
+      }
+      console.log(isUpdate)
+      this.isUpdateSop = isUpdate;
+      this.cuoser = cuoser;
+      this.loading = true;
+      this.queryParams.sendType= sendType;
+      this.queryParams.cuoser= cuoser;
+      listSopTemp(this.queryParams).then(response => {
+        this.sopTempList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+
+    getResetList(){
+      this.loading = true;
+      listSopTemp(this.queryParams).then(response => {
+        this.sopTempList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    handleSopTemp(val){
+      if (this.isUpdateSop === 1){
+        console.log("进入sop修改模板方法")
+        this.$emit("submitUpdateTemp",val)
+        this.$refs.sopTempList.clearSelection();
+      }else {
+        this.$emit("sopTemp",val)
+        this.$refs.sopTempList.clearSelection();
+        this.open=false;
+      }
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        status:1,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getResetList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+
+
+  }
+};
+</script>

+ 327 - 0
src/views/qw/sopTemp/updateAiChatTemp.vue

@@ -0,0 +1,327 @@
+<template>
+  <div class="app-container">
+    <div style="margin: 30px;">sop规则【AI对话】模板</div>
+    <div style="margin-top: 10px;margin-left: 50px;margin-right: 100px;margin-bottom: 60px;">
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入模板标题" />
+        </el-form-item>
+
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="dict.dictValue"
+            >{{dict.dictLabel}}</el-radio>
+          </el-radio-group>
+        </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="setting">
+          <el-timeline>
+            <el-timeline-item
+              :timestamp="'第'+(1+(form.gap*index))+'天'"
+              :color="'#0bbd87'"
+              placement="top"
+              v-for="(item, index) in setting"
+              :key="index"
+              style="margin-top: 10px;">
+              <el-row>
+                <el-col :span="22">
+                  <div style="background-color: #fbfbfb;padding: 15px; border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                    <el-form :model="item" label-width="80px">
+                      <el-form-item label="规则">
+                        <div v-for="(content, contentIndex) in item.content"
+                             :key="contentIndex"
+                             style="background-color: #fdfdfd;padding: 15px; border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                          <el-row>
+                            <el-col :span="22">
+                              <el-form :model="content" label-width="70px">
+                                <!-- 第一天不显示时间选择 -->
+                                <el-form-item label="时间" prop="time" v-if="index > 0">
+                                  <el-time-picker
+                                    class="custom-input"
+                                    v-model="content.time"
+                                    value-format="HH:mm"
+                                    format="HH:mm"
+                                    :picker-options="{ selectableRange: '00:00:00 - 23:59:59' }"
+                                    placeholder="时间"
+                                    style="width: 100px;height: 20px;">
+                                  </el-time-picker>
+                                </el-form-item>
+
+
+                                <div v-for="(setList, setIndex) in content.setting"
+                                     :key="setIndex"
+                                     style="background-color: #fdfdfd; border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                                  <el-row>
+                                    <el-col :span="22">
+                                      <el-form :model="setList" label-width="70px">
+										  <el-form-item label="间隔时间" prop="intervalTime" style="margin: 2%">
+										    <el-input-number
+										      v-model="setList.intervalTime"
+										      :min="1"
+										      :max="1440"
+										      style="width:100;margin-top: 10px;"
+										      >
+										    </el-input-number>
+										    <span class="tip">单位:分钟,最大1440分钟(24小时)</span>
+										  </el-form-item>
+                                        <el-form-item label="内容" style="margin: 2%">
+                                          <el-input
+                                            v-model="setList.value"
+                                            type="textarea"
+                                            :rows="3"
+                                            placeholder="内容"
+                                            style="width: 90%;margin-top: 10px;"/>
+                                        </el-form-item>
+										<el-form-item label="交流状态" style="margin: 2%" >
+										  <el-select  v-model="setList.talkType" placeholder="更改交流状态" size="mini" style=" margin-right: 10px;" clearable>
+											<el-option label="非首次交流" value="非首次交流"></el-option>
+											<el-option label="首次交流1" value="首次交流1"></el-option>
+											<el-option label="首次交流2" value="首次交流2"></el-option>
+										    <el-option label="交流状态1" value="交流状态1"></el-option>
+										    <el-option label="交流状态2" value="交流状态2"></el-option>
+										    <el-option label="交流状态3" value="交流状态3"></el-option>
+										  </el-select>
+										</el-form-item>
+                    </el-form>
+                        </el-col>
+                            <el-col :span="1" :offset="1">
+                              <i class="el-icon-delete"
+                                 @click="delSetList(index,contentIndex,setIndex)"
+                                 style="margin-top: 20px;"
+                                 v-if="content.setting.length>1 && (formType==1 || formType==2)"></i>
+                            </el-col>
+                      </el-row>
+                    </div>
+                                <el-link type="primary"
+                                        class="el-icon-plus"
+                                        :underline="false"
+                                        @click='addSetList(contentIndex,item.content)' v-if="(formType==1 || formType==2)">添加内容</el-link>
+                              </el-form>
+                            </el-col>
+                            <el-col :span="1" :offset="1">
+                              <i class="el-icon-delete"
+                                 @click="delContent(index,contentIndex)"
+                                 style="margin-top: 20px;"
+                                 v-if="item.content.length>1 && (formType==1 || formType==2)"></i>
+                            </el-col>
+                          </el-row>
+                        </div>
+                        <el-link type="primary"
+								                v-if="index > 0 && (formType==1 || formType==2)"
+                                class="el-icon-plus"
+                                :underline="false"
+                                @click='addContent(index)' >添加规则</el-link>
+                      </el-form-item>
+                    </el-form>
+                  </div>
+                </el-col>
+                <el-col :span="1" :offset="1">
+                  <i class="el-icon-delete"
+                     @click="delSetting(index)"
+                     v-if="setting.length>1 && (formType==1 || formType==2) "></i>
+                </el-col>
+              </el-row>
+            </el-timeline-item>
+          </el-timeline>
+<!--          <el-link type="primary"
+                   class="el-icon-plus"
+                   :underline="false"
+                   @click='addSetting()'>添加天数</el-link> -->
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer" style="float: right;"  v-if="this.formType==1 || this.formType==2 ">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { addSopTemp ,getSopTemp,updateSopTemp} from "@/api/qw/sopTemp";
+
+export default {
+  name: "AddAiChatTemp",
+  data() {
+    return {
+      // 状态字典
+      statusOptions: [],
+      // 添加推送方式字典
+      sysQwSopType: [],
+      // 表单数据
+      form: {
+        name: null,
+        setting: null,
+        status: "1",
+        sort: 1,
+        gap: 1,
+        sendType: 3, // AI对话类型,固定值
+      },
+      //1 修改 2 复制 3 查看
+      formType:null,
+
+      // 规则设置
+      setting: [],
+      // 表单校验
+      rules: {
+        name: [
+          { required: true, message: '名称不能为空', trigger: 'blur' }
+        ],
+        status: [
+          { required: true, message: '状态不能为空', trigger: 'blur' }
+        ],
+        sort: [
+          { required: true, message: '排序不能为空', trigger: 'blur' }
+        ],
+        gap: [
+          { required: true, message: '间隔天数不能为空', trigger: 'blur' }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    // 获取推送方式字典
+    this.getDicts("sys_qw_sop_type").then(response => {
+      this.sysQwSopType = response.data;
+    });
+	const id = this.$route.params && this.$route.params.id;
+	this.formType = this.$route.params && this.$route.params.type;
+	this.handleUpdate(id);
+
+  },
+  methods: {
+
+	handleUpdate(id) {
+	  getSopTemp(id).then(response => {
+		this.form = response.data;
+	    this.setting = JSON.parse(response.data.setting);
+	  });
+	},
+    addSetList(contentIndex, content) {
+      content[contentIndex].setting.push({
+		intervalTime: 5,
+        contentType: '1',
+        value: ""
+      });
+    },
+
+    delSetList(index, contentIndex, setIndex) {
+      this.setting[index].content[contentIndex].setting.splice(setIndex, 1);
+    },
+
+    addContent(index) {
+      this.setting[index].content.push({
+        type: 1,
+        contentType: '1',
+         // 默认5分钟
+        setting: [{
+          intervalTime: 5,
+          contentType: '1',
+          value: ""
+        }]
+      });
+    },
+
+    delContent(index, contentIndex) {
+      this.setting[index].content.splice(contentIndex, 1);
+    },
+
+    addSetting() {
+      this.setting.push({
+        content: [{
+          type: 1,
+          contentType: '1',
+
+          setting: [{
+			intervalTime: 5, // 默认5分钟
+            contentType: '1',
+            value: ""
+          }]
+        }]
+      });
+    },
+
+    delSetting(index) {
+      this.setting.splice(index, 1);
+    },
+
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          // 验证内容
+          for(let day of this.setting) {
+            for(let content of day.content) {
+              if(content.time === undefined && this.setting.indexOf(day) > 0) {
+                return this.$message.error("时间不能为空");
+              }
+              for(let set of content.setting) {
+				if(!set.intervalTime) {
+				  return this.$message.error("间隔时间不能为空");
+				}
+                if(!set.value) {
+                  return this.$message.error("内容不能为空");
+                }
+              }
+            }
+          }
+          this.form.setting = JSON.stringify(this.setting);
+
+		  if (this.formType == 1) {
+		    if (this.form.id != null) {
+		      updateSopTemp(this.form).then(response => {
+		        this.msgSuccess("修改成功");
+		        this.$store.dispatch("tagsView/delView", this.$route);
+		        window.location.replace('/qw/conversion/sopTemp')
+		        this.reset();
+		      });
+		    }
+		  } else {
+		    //id制空,防止键值重复-报错
+		    this.form.id = null
+		    //更新时间制空
+		    this.form.updateTime = null
+		    addSopTemp(this.form).then(response => {
+		     this.msgSuccess("复制成功");
+		     this.$store.dispatch("tagsView/delView", this.$route);
+		     window.location.replace('/qw/conversion/sopTemp')
+		     this.reset();
+		    });
+		  }
+
+
+
+        }
+      });
+    },
+
+    cancel() {
+      this.$router.push('/qw/conversion/sopTemp');
+    }
+  }
+};
+</script>
+
+<style scoped>
+.custom-input /deep/ .el-input__inner {
+  height: 20px;
+  text-align: center;
+}
+
+.custom-input /deep/ .el-input__icon {
+  line-height: 10px;
+}
+
+.tip {
+  color: #909399;
+  font-size: 12px;
+  margin-left: 10px;
+}
+</style>

+ 1741 - 0
src/views/qw/sopTemp/updateSopTemp.vue

@@ -0,0 +1,1741 @@
+<template>
+  <div class="app-container">
+
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 && formType==1 "> sop规则【修改企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 && formType==2 "> sop规则【复制企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 && formType==3 "> sop规则【查看企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 && formType==1 "> sop规则【修改群发助手】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 && formType==2 "> sop规则【复制群发助手】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 && formType==3 "> sop规则【查看群发助手】模板</div>
+    <div style="margin: 30px;">模板编号:【{{ this.form.id }}】</div>
+
+    <div style="margin-top: 10px;margin-left: 50px;margin-right: 100px;margin-bottom: 60px;">
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="名称" prop="name">
+          {{ form.name }}
+        </el-form-item>
+
+        <el-form-item label="状态">
+          <el-tag v-for="dict in statusOptions" v-if="dict.dictValue == form.status">{{ dict.dictLabel }}</el-tag>
+        </el-form-item>
+        <el-form-item label="间隔天数" prop="gap">
+          {{ form.gap }}天
+        </el-form-item>
+        <el-form-item label="规则" prop="setting">
+          <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetting()'
+                   v-if="form.sendType != 4 && formType != 3">添加天数
+          </el-link>
+          <el-link type="primary" class="el-icon-sort" :underline="false" @click='openUpdateDaySorts()'
+                   style="margin-left: 20px"
+                   v-if="form.sendType != 4 && formType != 3">修改天数排序
+          </el-link>
+          <el-link type="primary" class="el-icon-sort" :underline="false" @click='openUpdateSorts()'
+                   style="margin-left: 20px"
+                   v-if="form.sendType != 4 && formType != 3">修改规则排序
+          </el-link>
+          <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetting()'
+                   v-if="form.sendType == 4 && (formType != 3 && setting.length < 1)">添加天数
+          </el-link>
+          <el-tabs v-model="tabIndex" type="card" @tab-remove="delSetting" v-if="setting && setting.length > 0"
+                   :before-leave="leave" @tab-click="tabClick">
+            <el-tab-pane v-for="(item, index) in setting" :closable="formType != 3" :key="index" :name="index + ''">
+              <el-badge slot="label" :is-dot="!item.id" class="item" style="display: inline-block">
+                <span>{{ '第' + item.dayNum + '天' }}</span>
+              </el-badge>
+              <el-row v-loading="loading">
+                <el-col :span="22">
+                  <div
+                    style="background-color: #fbfbfb;padding: 15px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                    <el-form :model="item" label-width="80px">
+                      <el-form-item v-if="form.sendType != 4" label="内容名称" style="height: 50px;">
+                        <el-input :disabled="formType == 3" v-model="item.name"
+                                  placeholder="内容名称,仅内部可见"/>
+                      </el-form-item>
+                      <el-form-item label="课程" v-if="form.sendType == 5 && item.content && item.content.length > 0" required>
+                        <el-select :disabled="(formType == 3 || form.sendType == 5) && item.id != null" v-model="item.content[0].courseId"
+                                   placeholder="请选择课程" style=" margin-right: 10px;" size="mini" remote
+                                   filterable
+                                   @change="courseChangeUpdate(item.content[0], index, 0)">
+                          <el-option
+                            v-for="dict in courseList"
+                            :key="dict.dictValue"
+                            :label="dict.dictLabel"
+                            :value="parseInt(dict.dictValue)"
+                          />
+                        </el-select>
+                        <el-select :disabled="(formType == 3 || form.sendType == 5) && item.id != null" v-model="item.content[0].videoId"
+                                   placeholder="请选择小节" size="mini" style=" margin-right: 10px;" remote
+                                   filterable
+                                   @change="videoIdChange(item.content[0],index,0)">
+                          <el-option
+                            v-for="dict in videoList[0]"
+                            :key="dict.dictValue"
+                            :label="dict.dictLabel"
+                            :value="parseInt(dict.dictValue)"
+                          />
+                        </el-select>
+                      </el-form-item>
+                      <el-form-item label="规则">
+                        <div v-for="(content, contentIndex) in item.content" :key="contentIndex"
+                             style="background-color: #fdfdfd;padding: 15px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                          <el-row>
+                            <el-col el-col :span="22">
+                              <el-form :model="content" label-width="70px">
+                                <div v-if="item.dayNum==1">
+                                  <el-form-item label="时间" v-if="form.sendType != 4">
+                                    <el-time-picker
+                                      :disabled="formType == 3"
+                                      class="custom-input"
+                                      v-model="content.time"
+                                      value-format="HH:mm"
+                                      format="HH:mm"
+                                      :picker-options="{ selectableRange: startTimeRange }"
+                                      placeholder="时间"
+                                      style="width: 100px;height: 20px;">
+                                    </el-time-picker>
+                                  </el-form-item>
+                                </div>
+                                <div v-else>
+                                  <el-form-item label="时间" v-if="form.sendType != 4">
+                                    <el-time-picker
+                                      :disabled="formType == 3"
+                                      class="custom-input"
+                                      v-model="content.time"
+                                      value-format="HH:mm"
+                                      format="HH:mm"
+                                      placeholder="时间"
+                                      style="width: 100px;height: 20px;">
+                                    </el-time-picker>
+                                  </el-form-item>
+                                </div>
+                                <el-form-item label="官方群发" v-if="contentIndex==0 && content.type==2">
+                                  <el-switch
+                                    v-model="content.isOfficial"
+                                    active-color="#13ce66"
+                                    inactive-color="#DCDFE6"
+                                    active-value="1"
+                                    inactive-value="0">
+                                  </el-switch>
+                                </el-form-item>
+                                <el-form-item label="消息类型" v-if="form.sendType != 4">
+                                  <el-select :disabled="formType == 3" v-model="content.courseType"
+                                             placeholder="请选择消息类型" size="mini"
+                                             style=" margin-right: 10px;" v-if="content.type != 4 ">
+                                    <el-option
+                                      v-for="dict in sysFsSopWatchStatus"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="Number(dict.dictValue)"
+                                    />
+                                  </el-select>
+                                </el-form-item>
+
+                                <el-form-item label="消息类别" v-if="form.sendType != 4 && form.sendType != 5">
+                                  <el-radio-group v-model="content.type"
+                                                  :disabled="formType == 3 || content.isOfficial === '1'"
+                                                  @change="updateHtml(() => content.contentType = '1')">
+                                    <el-radio :label="1">普通</el-radio>
+                                    <el-radio :label="2">课程</el-radio>
+                                    <el-radio :label="4">AI触达</el-radio>
+                                    <el-radio :label="5">打标签</el-radio>
+                                  </el-radio-group>
+                                </el-form-item>
+                                <el-form-item label="课程" v-if="content.type == 2 && form.sendType != 5" required>
+                                  <el-select :disabled="formType == 3 || form.sendType == 5" v-model="content.courseId"
+                                             placeholder="请选择课程" style=" margin-right: 10px;" size="mini" remote
+                                             filterable
+                                             @change="courseChangeUpdate(content,index,contentIndex)">
+                                    <el-option
+                                      v-for="dict in courseList"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+                                  <el-select :disabled="formType == 3 || form.sendType == 5" v-model="content.videoId"
+                                             placeholder="请选择小节" size="mini" style=" margin-right: 10px;" remote
+                                             filterable
+                                             @change="videoIdChange(content,index,contentIndex)">
+                                    <el-option
+                                      v-for="dict in videoList[contentIndex]"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+                                  <el-select :disabled="formType == 3" v-model="content.courseType"
+                                             placeholder="请选择消息类型" size="mini"
+                                             style=" margin-right: 10px;" v-if="content.type != 4 ">
+                                    <el-option
+                                      v-for="dict in sysFsSopWatchStatus"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="Number(dict.dictValue)"
+                                    />
+                                  </el-select>
+
+                                </el-form-item>
+                                <el-form-item label="Ai触达" v-if="content.type == 4 ">
+                                  <el-select :disabled="formType == 3" v-model="content.aiTouch"
+                                             placeholder="请选择Ai触达类型" size="mini"
+                                             style=" margin-right: 10px;" v-if="content.type == 4 ">
+                                    <el-option label="非首次交流" value="非首次交流"></el-option>
+                                    <el-option label="首次交流1" value="首次交流1"></el-option>
+                                    <el-option label="首次交流2" value="首次交流2"></el-option>
+                                    <el-option label="交流状态1" value="交流状态1"></el-option>
+                                    <el-option label="交流状态2" value="交流状态2"></el-option>
+                                    <el-option label="交流状态3" value="交流状态3"></el-option>
+                                  </el-select>
+                                </el-form-item>
+                                <el-form-item label="添加标签" prop="addTag" v-if="content.type == 5 "
+                                              v-model="content.addTag">
+
+                                  <el-tag
+                                    :key="tag"
+                                    v-for="tag in content.addTag"
+                                    closable
+                                    :disable-transitions="false"
+                                    @close="handleClose(contentIndex,tag,content)">
+                                    {{ tag }}
+                                  </el-tag>
+                                  <el-input
+                                    style="width:110px"
+                                    class="input-new-tag"
+                                    v-if="addTag[contentIndex].inputVisible"
+                                    v-model="addTag[contentIndex].inputValue"
+                                    ref="saveTagInput"
+                                    size="small"
+                                    @keyup.enter.native="handleInputConfirm(contentIndex,content)"
+                                    @blur="handleInputConfirm(contentIndex,content)"
+                                  >
+                                  </el-input>
+                                  <el-button v-else class="button-new-tag" size="small" style="width: 110px"
+                                             @click="showInput(contentIndex)">新增标签
+                                  </el-button>
+                                </el-form-item>
+
+                                <el-form-item label="移除标签" prop="remarkMobiles" v-if="content.type == 5 ">
+
+                                  <el-tag
+                                    :key="tag"
+                                    v-for="tag in content.delTag"
+                                    closable
+                                    :disable-transitions="false"
+                                    @close="handleCloseDel(contentIndex,tag,content)">
+                                    {{ tag }}
+                                  </el-tag>
+                                  <el-input
+                                    style="width:110px"
+                                    class="input-new-tag"
+                                    v-if="addTag[contentIndex].delTagVisible"
+                                    v-model="addTag[contentIndex].delTagValue"
+                                    ref="saveTagInputDel"
+                                    size="small"
+                                    @keyup.enter.native="handleInputConfirmDel(contentIndex,content)"
+                                    @blur="handleInputConfirmDel(contentIndex,content)"
+                                  >
+                                  </el-input>
+                                  <el-button v-else class="button-new-tag" size="small" style="width: 110px"
+                                             @click="showInputDel(contentIndex)">新增标签
+                                  </el-button>
+
+                                </el-form-item>
+
+                                <div v-for="(setList, setIndex) in content.setting" :key="setIndex"
+                                     style="background-color: #fdfdfd; border: 1px solid #e6e6e6; margin-bottom: 20px;"
+                                     v-if="content.type != 4 && content.type != 5">
+                                  <el-row style="padding-bottom: 20px">
+                                    <el-col :span="22">
+                                      <el-form v-if="form.sendType != 4" :model="setList" label-width="70px">
+                                        <el-form-item label="内容类别" style="margin: 2%">
+                                          <div v-if="form.sendType == 1 ">
+                                            <el-radio-group v-model="setList.contentType" :disabled="formType == 3">
+                                              <!--                                              <el-radio  :label="item.dictValue" v-for="item in sysQwSopContentType">{{item.dictLabel}}</el-radio>-->
+                                              <el-radio
+                                                v-for="item in sysQwSopContentType"
+                                                :key="item.dictValue"
+                                                :label="item.dictValue"
+                                                :disabled="item.dictValue === '1' && content.setting.some(s => s.contentType == '1') ">
+                                                {{ item.dictLabel }}
+                                              </el-radio>
+                                            </el-radio-group>
+                                          </div>
+                                          <div v-if="form.sendType == 2">
+                                            <el-radio-group v-model="setList.contentType"
+                                                            :disabled="formType == 3"
+                                                            @change="handleContentTypeChange(content,index,contentIndex,setIndex, item, 'contentType', $event)">
+                                              <el-radio
+                                                :key="item.dictValue"
+                                                :label="item.dictValue"
+                                                :disabled="(content.type!=2 && item.dictValue === '9') || (content.isOfficial==1 && ['5','6','7','8','9'].includes(item.dictValue))"
+                                                v-for="item in sysQwSopAiContentType">{{ item.dictLabel }}
+                                              </el-radio>
+                                            </el-radio-group>
+                                          </div>
+                                          <div v-if="form.sendType == 5">
+                                            <el-radio-group v-model="setList.contentType"
+                                                            :disabled="formType == 3 || (form.sendType == 5 && contentIndex != 0 && setIndex == 0)"
+                                                            @change="handleContentTypeChange(content,index,contentIndex,setIndex, item, 'contentType', $event)">
+                                              <el-radio
+                                                :key="item.dictValue"
+                                                :label="item.dictValue"
+                                                :disabled="(content.type!=2 && item.dictValue === '9') || (content.isOfficial==1 && ['5','6','7','8','9'].includes(item.dictValue))"
+                                                v-for="item in sysQwSopAiContentType" v-if="setIndex == 0 ? courseTypeList.includes(item.dictValue) : !courseTypeList.includes(item.dictValue)">{{ item.dictLabel }}
+                                              </el-radio>
+                                            </el-radio-group>
+                                          </div>
+                                        </el-form-item>
+                                        <el-form-item label="内容">
+                                          <el-input :disabled="formType == 3" v-if="setList.contentType == 1 "
+                                                    v-model="setList.value"
+                                                    type="textarea" :rows="3" placeholder="内容"
+                                                    style="width: 90%;margin-top: 10px;"
+                                                    @keydown.native="handleKeydown($event, index, contentIndex, setIndex)"
+                                                    :ref="`textarea-${index}-${contentIndex}-${setIndex}`"
+                                          />
+
+                                          <!-- 修改按钮部分 -->
+                                          <el-link
+                                            v-if="setList.contentType == 1"
+                                            type="primary"
+                                            @click="toggleSalesCall(index, contentIndex, setIndex)"
+                                            style="margin-top: 10px;"
+                                          >
+                                            {{ setList.isSalesCallAdded ? '移除#销售称呼#' : '添加#销售称呼#' }}
+                                          </el-link>
+
+                                          <ImageUpload :disabled="formType == 3" v-if="setList.contentType == 2 "
+                                                       v-model="setList.imgUrl"
+                                                       type="image" :num="1" :width="150" :height="150"/>
+
+                                          <div
+                                            v-if="setList.contentType == 3  || (setList.contentType == 9 && content.type==2 )">
+                                            <el-card class="box-card">
+                                              <el-form-item label="链接标题:" label-width="100px" required>
+                                                <el-input :disabled="formType == 3 || (form.sendType == 5 && contentIndex != 0 && setIndex == 0)" v-model="setList.linkTitle"
+                                                          @change="updateAll(setIndex, item, 'linkTitle', $event)"
+                                                          placeholder="请输入链接标题"
+                                                          style="width: 90%;"/>
+                                              </el-form-item>
+                                              <el-form-item label="链接描述:" label-width="100px" required>
+                                                <el-input :disabled="formType == 3 || (form.sendType == 5 && contentIndex != 0 && setIndex == 0)" type="textarea" :rows="3"
+                                                          v-model="setList.linkDescribe"
+                                                          @change="updateAll(setIndex, item, 'linkDescribe', $event)"
+                                                          placeholder="请输入链接描述"
+                                                          style="width: 90%;margin-top: 1%;"/>
+                                              </el-form-item>
+                                              <el-form-item label="链接封面:" label-width="100px" required>
+                                                <ImageUpload :disabled="formType == 3 || (form.sendType == 5 && contentIndex != 0 && setIndex == 0)" v-model="setList.linkImageUrl"
+                                                             type="image" :num="1"
+                                                             @input="updateAll(setIndex, item, 'linkImageUrl', $event)"
+                                                             :file-size="2" :width="150" :height="150"
+                                                             style="margin-top: 1%;"/>
+                                              </el-form-item>
+                                              <div v-if="content.type != 2 " style="margin-top: 1%">
+                                                <el-form-item label="链接地址:" label-width="100px" required>
+                                                  <el-input :disabled="formType == 3" v-model="setList.linkUrl"
+                                                            placeholder="请输入链接地址"
+                                                            style="width: 90%;"/>
+                                                </el-form-item>
+                                              </div>
+                                              <div v-if="content.type == 2 ">
+                                                <el-form-item label="链接地址:" label-width="100px">
+                                                  <el-tag type="warning" v-model="setList.isBindUrl = 1 ">选择的课程小节
+                                                    即为卡片链接地址
+                                                  </el-tag>
+                                                </el-form-item>
+                                              </div>
+                                            </el-card>
+                                          </div>
+
+                                          <div v-if="setList.contentType == 4">
+                                            <el-card class="box-card">
+                                              <el-form-item label="标题" prop="miniprogramTitle">
+                                                <el-input v-model="setList.miniprogramTitle"
+                                                          :disabled="formType == 3 || (form.sendType == 5 && contentIndex != 0 && setIndex == 0)"
+                                                          @change="updateAll(setIndex, item, 'miniprogramTitle', $event)"
+                                                          placeholder="请输入小程序消息标题,最长为64字节" :rows="2"
+                                                          maxlength="64" type="textarea"
+                                                          @input="checkByteLength(content,setList.contentType,content.isOfficial)"/>
+                                              </el-form-item>
+                                              <el-form-item label="封面" prop="miniprogramPicUrl">
+                                                <ImageUpload v-if="content.isOfficial !== '1'"
+                                                             @change="updateAll(setIndex, item, 'miniprogramPicUrl', $event)"
+                                                             :disabled="formType == 3 || (form.sendType == 5 && contentIndex != 0 && setIndex == 0)"
+                                                             v-model="setList.miniprogramPicUrl" type="image" :num="10"
+                                                             :width="150" :height="150"/>
+                                              </el-form-item>
+                                              <el-form-item label="appid" prop="miniprogramAppid" v-show="false">
+                                                <el-input v-model="setList.miniprogramAppid='wx73f85f8d62769119' "
+                                                          disabled/>
+                                              </el-form-item>
+                                              <el-form-item label="page路径" prop="miniprogramPage" v-show="false"
+                                                            label-width="100px" style="margin-left: -30px">
+                                                <el-input v-model="setList.miniprogramPage"
+                                                          placeholder="小程序消息打开后的路径" disabled/>
+                                              </el-form-item>
+                                            </el-card>
+                                          </div>
+
+
+                                          <div v-if="setList.contentType == 5 ">
+
+                                            <el-form-item label="上传文件:" prop="fileUrl" label-width="100px">
+                                              <el-upload
+                                                :disabled="formType == 3"
+                                                v-model="setList.fileUrl"
+                                                class="avatar-uploader"
+                                                :action="uploadUrl"
+                                                :show-file-list="false"
+                                                :on-success="(res, file) => handleAvatarSuccessFile(res, file, setList)"
+                                                :before-upload="beforeAvatarUploadFile">
+                                                <i class="el-icon-plus avatar-uploader-icon"></i>
+                                              </el-upload>
+                                              <el-link v-if="setList.fileUrl" type="primary"
+                                                       :href="downloadUrl(setList.fileUrl)" download>
+                                                {{ setList.fileUrl }}
+                                              </el-link>
+                                            </el-form-item>
+
+                                          </div>
+
+                                          <div v-if="setList.contentType == 6 ">
+                                            <el-form-item label="上传视频:" prop="videoUrl" label-width="100px">
+                                              <el-upload
+                                                :disabled="formType == 3"
+                                                v-model="setList.videoUrl"
+                                                class="avatar-uploader"
+                                                :action="uploadUrl"
+                                                :show-file-list="false"
+                                                :on-success="(res, file) => handleAvatarSuccessVideo(res, file, setList)"
+                                                :before-upload="beforeAvatarUploadVideo">
+                                                <i class="el-icon-plus avatar-uploader-icon"></i>
+                                              </el-upload>
+                                              <video v-if="setList.videoUrl"
+                                                     :src="setList.videoUrl"
+                                                     controls style="width: 200px;height: 100px">
+                                              </video>
+                                            </el-form-item>
+                                          </div>
+                                          <div v-if="setList.contentType == 7 ">
+                                            <el-input
+                                              :disabled="formType == 3"
+                                              v-model="setList.value"
+                                              type="textarea" :rows="3" maxlength="66" show-word-limit
+                                              placeholder="输入要转为语音的内容"
+                                              style="width: 90%;margin-top: 10px;"
+                                              @input="handleInputVideoText(setList.value,setList)"/>
+
+
+                                          </div>
+                                          <div v-if="setList.contentType == 8 ">
+                                            <el-button :disabled="formType == 3" type="primary"
+                                                       style="margin-bottom: 1%"
+                                                       @click="hanldeSelectVideoNum(content,index,contentIndex,setIndex)">
+                                              选择视频号
+                                            </el-button>
+                                            <el-card class="box-card" v-if="setList.coverUrl">
+                                              <el-form-item label="封面标题:" label-width="100px">
+                                                <el-input :disabled="formType == 3" v-model="setList.nickname"
+                                                          style="width: 90%;margin-bottom: 1%" disabled/>
+                                              </el-form-item>
+                                              <el-form-item label="头像:" label-width="100px">
+                                                <el-image
+                                                  v-if="setList.avatar != null"
+                                                  :src="setList.avatar"
+                                                  :preview-src-list="[setList.avatar]"
+                                                  :style="{ width: '50px', height: '50px' }"
+                                                ></el-image>
+                                              </el-form-item>
+                                              <el-form-item label="封面:" label-width="100px">
+                                                <el-image
+                                                  v-if="setList.coverUrl != null"
+                                                  :src="setList.coverUrl"
+                                                  :preview-src-list="[setList.coverUrl]"
+                                                  :style="{ width: '200px', height: '200px' }"
+                                                ></el-image>
+
+                                              </el-form-item>
+                                              <el-form-item label="简介:" label-width="100px">
+                                                <el-input :disabled="formType == 3" type="textarea" :rows="3"
+                                                          v-model="setList.desc"
+                                                          style="width: 90%;margin-top: 1%;" disabled/>
+                                              </el-form-item>
+                                              <el-form-item label="视频地址:" label-width="100px"
+                                                            style="margin-top: 1%">
+                                                <el-input :disabled="formType == 3" v-model="setList.url"
+                                                          style="width: 90%;" disabled/>
+                                              </el-form-item>
+
+                                            </el-card>
+                                          </div>
+                                          <div v-if="setList.contentType == 10 ">
+                                            <el-card class="box-card">
+                                              <el-form-item label="链接标题:" label-width="100px" required>
+                                                <el-input :disabled="formType == 3" v-model="setList.linkTitle"
+                                                          placeholder="请输入链接标题"
+                                                          style="width: 90%;"/>
+                                              </el-form-item>
+                                              <el-form-item label="链接描述:" label-width="100px" required>
+                                                <el-input :disabled="formType == 3" type="textarea" :rows="3"
+                                                          v-model="setList.linkDescribe"
+                                                          placeholder="请输入链接描述"
+                                                          style="width: 90%;margin-top: 1%;"/>
+                                              </el-form-item>
+                                              <el-form-item label="链接封面:" label-width="100px" required>
+                                                <ImageUpload :disabled="formType == 3" v-model="setList.linkImageUrl"
+                                                             type="image" :num="1"
+                                                             :file-size="2" :width="150" :height="150"
+                                                             style="margin-top: 1%;"/>
+                                              </el-form-item>
+                                              <el-form-item label="链接地址:" label-width="100px">
+                                                <el-tag type="warning"> 链接地址自动生成
+                                                </el-tag>
+                                              </el-form-item>
+                                            </el-card>
+                                          </div>
+                                        </el-form-item>
+                                        <el-form-item label="添加短链"
+                                                      v-if="content.type == 2 && setList.contentType == 1  ">
+                                          <el-tooltip content="请先根据课程选定课程小节之后再添加" effect="dark"
+                                                      :disabled="!content.videoId">
+                                            <el-switch
+                                              @change="updateHtml"
+                                              v-model="setList.isBindUrl"
+                                              :disabled="!content.videoId && formType == 3"
+                                              active-color="#13ce66"
+                                              inactive-color="#DCDFE6"
+                                              active-value="1"
+                                              inactive-value="2">
+                                            </el-switch>
+                                          </el-tooltip>
+
+                                          <span v-if="setList.isBindUrl == '1'"
+                                                style="margin-left: 10px; color: #13ce66">添加URL</span>
+                                          <span v-if="setList.isBindUrl == '2'"
+                                                style="margin-left: 10px; color: #b1b4ba">不加URL</span>
+                                        </el-form-item>
+                                        <el-form-item label="课节过期时间"
+                                                      v-if="content.type == 2 && setList.isBindUrl == '1' && setList.contentType != 2  && setList.contentType != 5  && setList.contentType != 6 && setList.contentType != 8 && setList.contentType != 9 && setList.contentType != 10  ">
+                                          <el-row>
+                                            <el-input type="number" v-model="setList.expiresDays"
+                                                      :disabled="formType == 3 || (form.sendType == 5 && contentIndex != 0 && setIndex == 0)"
+                                                      @change="updateAll(setIndex, item, 'expiresDays', $event)"
+                                                      style="width: 200px">
+                                              <template slot="append">天</template>
+                                            </el-input>
+                                          </el-row>
+                                          <el-row>
+                                            <span class="tip">填写0或不填时,默认为系统配置的默认时间</span>
+                                          </el-row>
+                                        </el-form-item>
+
+                                      </el-form>
+                                      <el-form v-if="form.sendType == 4" :model="setList" label-width="70px">
+                                        <el-form-item label="添加客服" prop="intervalTime" style="margin: 2%">
+                                          <el-input-number
+                                            v-model="setList.intervalTime"
+                                            :min="1"
+                                            :max="1440"
+                                            style="width:100px;margin-top: 10px;"
+                                          >
+                                          </el-input-number>
+                                          <span class="tip">单位:分钟,最大1440分钟(24小时)</span>
+                                        </el-form-item>
+                                        <el-form-item label="内容" style="margin: 2%">
+                                          <el-input
+                                            v-model="setList.value"
+                                            type="textarea"
+                                            :rows="3"
+                                            placeholder="内容"
+                                            style="width: 90%;margin-top: 10px;"/>
+                                        </el-form-item>
+                                        <el-form-item label="交流状态" style="margin: 2%">
+                                          <el-select v-model="setList.talkType" placeholder="更改交流状态" size="mini"
+                                                     style=" margin-right: 10px;" clearable>
+                                            <el-option label="非首次交流" value="非首次交流"></el-option>
+                                            <el-option label="首次交流1" value="首次交流1"></el-option>
+                                            <el-option label="首次交流2" value="首次交流2"></el-option>
+                                            <el-option label="交流状态1" value="交流状态1"></el-option>
+                                            <el-option label="交流状态2" value="交流状态2"></el-option>
+                                            <el-option label="交流状态3" value="交流状态3"></el-option>
+                                          </el-select>
+                                        </el-form-item>
+                                      </el-form>
+                                    </el-col>
+                                    <el-col :span="1" :offset="1">
+                                      <i class="el-icon-delete" @click="delSetList(index,contentIndex,setIndex)"
+                                         style="margin-top: 20px;"
+                                         v-if="content.setting.length>1 && formType != 3 && !(form.sendType == 5 && setIndex == 0)"></i>
+                                    </el-col>
+                                  </el-row>
+                                </div>
+                                <el-link type="primary" class="el-icon-plus" :underline="false"
+                                         @click='addSetList(contentIndex,item.content)'
+                                         v-if="content.type != 4 && formType != 3">添加内容
+                                </el-link>
+                              </el-form>
+                            </el-col>
+                            <el-col :span="1" :offset="1">
+                              <i class="el-icon-delete" @click="delContent(index,contentIndex)"
+                                 style="margin-top: 20px;"
+                                 v-if="item.content.length>1 && formType != 3"></i>
+                            </el-col>
+                          </el-row>
+                        </div>
+                        <el-link type="primary" class="el-icon-plus" :underline="false" @click='addContent(index)'
+                                 v-if="formType != 3 && form.sendType != 4">添加规则
+                        </el-link>
+                      </el-form-item>
+                    </el-form>
+
+                  </div>
+                  <div style="float: right;" v-if="formType != 3">
+                    <el-button type="primary" @click="save" v-if="!item.voice || item.voice == 0">
+                      保存({{ '第' + (1 + (form.gap * index)) + '天' }})
+                    </el-button>
+                    <el-button type="primary" disabled v-if="item.voice == 1">语言生成中</el-button>
+                    <el-button type="primary" @click="leave(tabIndex)" v-if="item.voice == 1">刷新状态</el-button>
+                  </div>
+                </el-col>
+              </el-row>
+            </el-tab-pane>
+          </el-tabs>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <el-dialog :title="videoNumOptions.title" :visible.sync="videoNumOptions.open" style="width: 1500px;height: 100%"
+               append-to-body>
+      <userVideo ref="QwUserVideo" @videoResult="qwUserVideoResult"></userVideo>
+    </el-dialog>
+    <el-dialog title="修改天数排序" :visible.sync="openSort" style="width: 1500px" append-to-body>
+      <draggable v-model="dayList" @end="onDragEndDay" style="padding: 20px">
+        <el-button v-for="(item, index) in dayList" :class="item.newDay != item.dayNum ? 'red':''">第{{
+            item.newDay
+          }}天(第{{ item.dayNum }}天)
+        </el-button>
+      </draggable>
+
+      <div style="float: right;">
+        <el-button type="primary" @click="saveSorts">保存</el-button>
+      </div>
+    </el-dialog>
+    <el-dialog title="修改规则排序" :visible.sync="openSort2" style="width: 1500px" append-to-body>
+      <draggable v-model="ruleList" @end="onDragEnd" style="padding: 20px">
+        <el-button v-for="(item, index) in ruleList">{{ item.time }}</el-button>
+      </draggable>
+
+      <div style="float: right;">
+        <el-button @click="autoSortsRules">自动排序</el-button>
+        <el-button type="primary" @click="saveSortsRules">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+
+<style scoped>
+.el-button {
+  margin-left: 10px;
+}
+
+.red:hover {
+  color: #dbdbdb !important;
+}
+
+.red {
+  background-color: #F56C6C !important;
+  color: #fff !important;
+}
+
+.custom-input /deep/ .el-input__inner {
+  height: 20px;
+  text-align: center;
+}
+
+.custom-input /deep/ .el-input__icon {
+  line-height: 10px;
+}
+
+.el-icon-delete {
+  cursor: pointer;
+}
+
+/deep/ .el-badge__content.is-fixed {
+  top: 10px !important;
+  right: 0 !important;
+  width: 8px;
+}
+
+.tip {
+  color: #909399;
+  font-size: 12px;
+  margin-left: 10px;
+}
+
+.sortable-ghost {
+  background: rgb(26, 164, 255) !important;
+  color: #FFF !important;
+}
+</style>
+<script>
+import draggable from 'vuedraggable';
+import {
+  listSopTemp,
+  getSopTemp,
+  delSopTemp,
+  addSopTemp,
+  sortDay,
+  dayListFun,
+  updateSopTemp,
+  getSelectableRange,
+  exportSopTemp,
+  addOrUpdateSetting,
+  selectRulesInfo,
+  delRules
+} from "@/api/qw/sopTemp";
+import {courseList, videoList} from "@/api/qw/sop";
+import ImageUpload from "@/views/qw/sop/ImageUpload";
+import userVideo from "@/views/qw/userVideo/userVideo.vue";
+
+export default {
+  name: "updateSopTemp",
+  components: {ImageUpload, userVideo, draggable},
+  data() {
+    return {
+      addTag: [{
+        addTag: [],
+        inputVisible: false,
+        inputValue: '',
+        delTag: [],
+        delTagVisible: false,
+        delTagValue: ''
+      }],
+      uploadUrl: process.env.VUE_APP_BASE_API + "/common/uploadOSS",
+      uploadUrlByVoice: process.env.VUE_APP_BASE_API + "/common/uploadOSSByHOOKVoice",
+      //上传语音的遮罩层
+      voiceLoading: false,
+      openSort: false,
+      openSort2: false,
+      // 遮罩层
+      loading: false,
+      loading2: false,
+      loading3: false,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      dayList: [],
+      ruleList: [],
+      ids: [],
+      courseTypeList: ['3', '4', '9'],
+      sysFsSopWatchStatus: [],
+      //消息内容类型 企微版
+      sysQwSopContentType: [],
+      //插件版
+      sysQwSopAiContentType: [],
+
+      //类别
+      sysQwSopSettingType: [],
+
+      courseList: [],
+      videoList: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      defaultContentType: 1,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      id: "",
+      total: 0,
+      tabIndex: null,
+      // sop模板表格数据
+      setting: [],
+      data: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 状态字典
+      statusOptions: [],
+      startTimeRange: [],
+      videoNumOptions: {
+        title: '选择视频号',
+        open: false,
+        content: null,
+        contentIndex: null,
+        setIndex: null,
+      },
+      //1 修改 2 复制 3 查看
+      formType: null,
+      // 查询参数
+      form: {
+        name: null,
+        setting: null,
+        status: "1",
+        sort: 1,
+        companyId: null,
+        gap: 1,
+      },
+      // 表单校验
+      rules: {
+        name: [
+          {required: true, message: '名称不能为空', trigger: 'blur'}
+        ],
+        status: [
+          {required: true, message: '状态不能为空', trigger: 'blur'}
+        ],
+        sort: [
+          {required: true, message: '排序不能为空', trigger: 'blur'}
+        ],
+        gap: [
+          {required: true, message: '间隔天数不能为空', trigger: 'blur'}
+        ],
+      }
+    };
+  },
+  created() {
+    this.getDicts("sys_qwSopAi_contentType").then(response => {
+      this.sysQwSopAiContentType = response.data;
+    });
+    this.getDicts("sys_fs_sop_watch_status").then(response => {
+      this.sysFsSopWatchStatus = response.data;
+    });
+
+    this.getDicts("sys_qwSop_contentType").then(response => {
+      this.sysQwSopContentType = response.data;
+    });
+
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+
+    this.getDicts("sys_qwSop_settingType").then(response => {
+      this.sysQwSopSettingType = response.data;
+    });
+
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+
+    getSelectableRange().then(e => {
+      this.startTimeRange = e.data;
+    })
+    const id = this.$route.params && this.$route.params.id;
+    this.id = id;
+    this.formType = this.$route.params && this.$route.params.type;
+    this.handleUpdate(id);
+  },
+  methods: {
+    handleClose(index, tag, content) {
+      content.addTag.splice(content.addTag.indexOf(tag), 1);
+    },
+    showInput(index) {
+      this.addTag[index].inputVisible = true;
+    },
+    handleInputConfirm(index, content) {
+      let inputValue = this.addTag[index].inputValue;
+      if (inputValue) {
+        if (!content.hasOwnProperty('addTag')) {
+          this.$set(content, 'addTag', []);
+        }
+        content.addTag.push(inputValue);
+      }
+      this.addTag[index].inputVisible = false;
+      this.addTag[index].inputValue = '';
+    },
+
+    // 检查字节长度
+    checkByteLength(content, type, isOfficial) {
+
+      if (type == 4 && isOfficial == '1')
+        for (let i = 0; i < content.setting.length; i++) {
+          const text = content.setting[i].miniprogramTitle;
+          const byteLength = this.getByteLength(text); // 获取当前字节数
+          // 如果字节数超过64,截断输入内容
+          if (byteLength > 64) {
+            this.$set(content.setting[i], 'miniprogramTitle', this.truncateTextByByteLength(text, 60));
+          }
+
+        }
+
+    },
+
+    // 计算字符串的字节数
+    getByteLength(text) {
+      return new Blob([text]).size; // 使用 Blob 计算字节数
+    },
+
+    // 根据字节数截断字符串
+    truncateTextByByteLength(text, maxByteLength) {
+      let byteLength = 0;
+      let result = "";
+
+      for (let i = 0; i < text.length; i++) {
+        const char = text[i];
+        const charByteLength = this.getByteLength(char); // 获取当前字符的字节数
+
+        // 如果加上当前字符的字节数后不超过限制,则添加到结果中
+        if (byteLength + charByteLength <= maxByteLength) {
+          result += char;
+          byteLength += charByteLength;
+        } else {
+          break; // 超过限制时停止
+        }
+      }
+
+      return result;
+    },
+
+    handleCloseDel(index, tag, content) {
+      content.delTag.splice(content.delTag.indexOf(tag), 1);
+    },
+
+    showInputDel(index) {
+      this.addTag[index].delTagVisible = true;
+    },
+    handleInputConfirmDel(index, content) {
+
+      let delTagValue = this.addTag[index].delTagValue;
+
+      if (delTagValue) {
+        if (!content.hasOwnProperty('delTag')) {
+          this.$set(content, 'delTag', []);
+        }
+        content.delTag.push(delTagValue);
+
+      }
+      this.addTag[index].delTagVisible = false;
+      this.addTag[index].delTagValue = '';
+    },
+    saveSorts() {
+      let list = this.dayList.filter(e => e.dayNum != e.newDay).map(e => {
+        return {dayNum: e.newDay, id: e.id}
+      })
+      this.loading3 = true;
+      sortDay(list).then(e => {
+        window.location.reload();
+      })
+    },
+    saveSortsRules() {
+      this.$set(this.setting[this.tabIndex], 'content', this.ruleList);
+      this.openSort2 = false;
+    },
+    autoSortsRules() {
+      this.ruleList = this.ruleList.sort((a, b) => {
+        // 将 time 字段转换为 Date 对象进行比较
+        const timeA = new Date(`1970-01-01T${a.time}Z`);
+        const timeB = new Date(`1970-01-01T${b.time}Z`);
+
+        // 比较时间顺序
+        return timeA - timeB; // 升序排序
+      });
+    },
+    openUpdateDaySorts() {
+      dayListFun(this.id).then(response => {
+        response.data.forEach((item) => item.newDay = item.dayNum);
+        this.dayList = response.data;
+        if (this.dayList == null || this.dayList.length == 0) {
+          this.$message.error("暂无天数")
+        } else {
+          this.openSort = true;
+        }
+      })
+    },
+    openUpdateSorts() {
+      this.ruleList = JSON.parse(JSON.stringify(this.setting[this.tabIndex].content));
+      if (this.ruleList == null || this.ruleList.length == 0) {
+        this.$message.error("暂无规则")
+      } else {
+        this.openSort2 = true;
+      }
+    },
+    onDragEndDay() {
+      this.dayList.forEach((item, index) => {
+        item.newDay = (this.form.gap * index) + 1;
+      })
+      this.$forceUpdate()
+    },
+    onDragEnd() {
+      // 更新排序值
+      // this.setting[this.tabIndex].content.forEach((item, index) => {
+      //   item.sorts = index;
+      // });
+    },
+    // 切换标签保存数据
+    leave(index, oldIndex) {
+      const newData = this.setting[index]
+      if (newData.dayNum && newData.id) {
+        this.loading = true;
+        selectRulesInfo(newData.id).then(res => {
+          this.$nextTick(() => {
+            this.loading = false;
+            this.videoList = [];
+            this.addTag = [];
+            this.$set(this.setting, index, {
+              ...newData,
+              voice: res.data.voice,
+              content: res.data.list || [{type: this.defaultContentType, contentType: '1', setting: [{contentType: '1', value: ""}]}]
+            });
+
+            for (let j = 0; j < this.setting[index].content.length; j++) {
+
+              if (this.setting[index].content[j].addTag != null) {
+                this.setting[index].content[j].addTag = JSON.parse(this.setting[index].content[j].addTag)
+              }
+              if (this.setting[index].content[j].delTag != null) {
+                this.setting[index].content[j].delTag = JSON.parse(this.setting[index].content[j].delTag)
+              }
+              this.videoList.push([])
+              this.addTag.push({
+                addTag: [],
+                inputVisible: false,
+                inputValue: '',
+                delTag: [],
+                delTagVisible: false,
+                delTagValue: ''
+              })
+              if (this.setting[index].content[j].type == 2) {
+                if (this.setting[index].content[j].hasOwnProperty('courseId')) {
+                  this.courseChange(this.setting[index].content[j], index, j);
+                }
+              }
+            }
+          })
+        });
+      }
+
+    },
+    save() {
+      let data = this.setting[this.tabIndex];
+      let check = this.checkData(data);
+      if (!check) {
+        return;
+      }
+      for (let j = 0; j < data.content.length; j++) {
+        if (data.content[j].type != 4 && data.content[j].type != 5) {
+          for (let k = 0; k < data.content[j].setting.length; k++) {
+            if (data.content[j].setting[k].contentType == 4 && (data.content[j].setting[k].miniprogramTitle != null || data.content[j].setting[k].miniprogramTitle != "")) {
+              data.content[j].setting[k].miniprogramTitle = this.truncateTextByByteLength(data.content[j].setting[k].miniprogramTitle, 60)
+            }
+          }
+        }
+      }
+
+      let index = 0;
+      const dataList = data.content.map(e => {
+        e.name = data.name;
+        e.tempId = this.id;
+        e.sorts = Number(this.tabIndex);
+        e.dayNum = (this.form.gap * e.sorts) + 1;
+        e.sorts = index++;
+        data.dayNum = e.dayNum;
+        e.contentType = e.type;
+        e.settingList = e.setting;
+        e.settingList.forEach(e => e.content = JSON.stringify(e));
+        if (e.addTag != null) {
+          e.addTag = JSON.stringify(e.addTag);
+        }
+        if (e.delTag != null) {
+          e.delTag = JSON.stringify(e.delTag);
+        }
+        return e;
+      })
+      data.tempId = this.id;
+      data.list = dataList;
+      this.loading = true;
+      addOrUpdateSetting(data).then(e => {
+        let content = this.setting[this.tabIndex].content;
+        for (let j = 0; j < content.length; j++) {
+          if (content[j].addTag != null) {
+            content[j].addTag = JSON.parse(content[j].addTag);
+          }
+          if (content[j].delTag != null) {
+            content[j].delTag = JSON.parse(content[j].delTag);
+          }
+        }
+        this.loading = false;
+        // 使用Vue.set确保响应式更新
+        this.$set(this.setting, this.tabIndex, {
+          ...this.setting[this.tabIndex],
+          id: e.data.id,
+        });
+      });
+    },
+    checkData(data) {
+      if (this.form.sendType != 4) {
+
+        for (let j = 0; j < data.content.length; j++) {
+          if (data.name == null || data.name == "") {
+            this.$message.error("内容名称不能为空")
+            return false;
+          }
+          if (data.content[j].time == null || data.content[j].time == "") {
+            this.$message.error("时间不能为空")
+            return false;
+          }
+          if (data.content[j].type == 2) {
+            if (data.content[j].courseId == null || data.content[j].courseId == "") {
+              this.$message.error("请选择课程")
+              return false;
+            }
+            if (data.content[j].videoId == null || data.content[j].videoId == "") {
+              this.$message.error("请选择课节")
+              return false;
+            }
+            if (data.content[j].courseType == null || data.content[j].courseType === "") {
+              this.$message.error("请选择消息类型")
+              return false;
+            }
+          }
+
+          if (data.content[j].type != 4 && data.content[j].type != 5) {
+            for (let k = 0; k < data.content[j].setting.length; k++) {
+              if (data.content[j].setting[k].contentType == 1 && (data.content[j].setting[k].value == null || data.content[j].setting[k].value == "")) {
+                this.$message.error("内容不能为空")
+                return false;
+              }
+              if (data.content[j].setting[k].contentType == 2 && (data.content[j].setting[k].imgUrl == null || data.content[j].setting[k].imgUrl == "")) {
+                this.$message.error("图片不能为空")
+                return false;
+              }
+              if ((data.content[j].setting[k].contentType == 3 || data.content[j].setting[k].contentType == 9 || data.content[j].setting[k].contentType == 10) && (data.content[j].setting[k].linkTitle == null || data.content[j].setting[k].linkTitle == "")) {
+                this.$message.error("链接标题不能为空")
+                return false;
+              }
+              if ((data.content[j].setting[k].contentType == 3 || data.content[j].setting[k].contentType == 9 || data.content[j].setting[k].contentType == 10) && (data.content[j].setting[k].linkDescribe == null || data.content[j].setting[k].linkDescribe == "")) {
+                this.$message.error("链接描述不能为空")
+                return false;
+              }
+              if ((data.content[j].setting[k].contentType == 3 || data.content[j].setting[k].contentType == 9 || data.content[j].setting[k].contentType == 10) && (data.content[j].setting[k].linkImageUrl == null || data.content[j].setting[k].linkImageUrl == "")) {
+                this.$message.error("链接图片不能为空")
+                return false;
+              }
+              if (data.content[j].setting[k].contentType == 3 && data.content[j].setting[k].type == 1 && (data.content[j].setting[k].linkUrl == null || data.content[j].setting[k].linkUrl == "")) {
+                this.$message.error("链接地址不能为空")
+                return false;
+              }
+
+              if (data.content[j].setting[k].contentType == 4 && (data.content[j].setting[k].miniprogramTitle == null || data.content[j].setting[k].miniprogramTitle == "")) {
+                this.$message.error("小程序消息标题不能为空")
+                return false;
+              }
+              if (data.content[j].setting[k].contentType == 4 && data.content[j].isOfficial !== '1' && (data.content[j].setting[k].miniprogramPicUrl == null || data.content[j].setting[k].miniprogramPicUrl == "")) {
+                this.$message.error("小程序封面地址不能为空")
+                return false;
+              }
+
+              if (data.content[j].setting[k].contentType == 5 && (data.content[j].setting[k].fileUrl == null || data.content[j].setting[k].fileUrl == "")) {
+                this.$message.error("文件不能为空")
+                return false;
+              }
+              if (data.content[j].setting[k].contentType == 6 && (data.content[j].setting[k].videoUrl == null || data.content[j].setting[k].videoUrl == "")) {
+                this.$message.error("视频不能为空")
+                return false;
+              }
+              if (data.content[j].setting[k].contentType == 7 && (data.content[j].setting[k].value == null || data.content[j].setting[k].value == "")) {
+                this.$message.error("语音文本不能为空")
+                return false;
+              }
+              if (data.content[j].setting[k].contentType == 8 && (data.content[j].setting[k].url == null || data.content[j].setting[k].url == "")) {
+                this.$message.error("视频号信息不能为空")
+                return false;
+              }
+            }
+          } else if (data.content[j].type == 4) {
+            if (data.content[j].aiTouch == null || data.content[j].aiTouch == '') {
+              this.$message.error("AI触达不能为空")
+              return false;
+            }
+          }
+        }
+      } else {
+        for (let day of this.setting) {
+          for (let content of day.content) {
+            for (let set of content.setting) {
+              if (!set.intervalTime) {
+                this.$message.error("客服数量不能为空");
+                return false;
+              }
+              if (!set.value) {
+                this.$message.error("内容不能为空");
+                return false;
+              }
+            }
+          }
+        }
+      }
+
+      return true;
+    },
+    tabClick(obj) {
+
+    },
+    addSetList(contentIndex, content) {
+
+      if (this.form.sendType == 1) {
+        for (let i = 0; i < content.length; i++) {
+
+          if (content[i].setting.length > 9) {
+            return this.$message.error("因为企微接口限制,企微模板一条消息只能设置最多~9个内容!!")
+          } else {
+
+            // 动态生成新的设置项
+            const newSetting = content[i].setting.some(item => item.contentType === '1')
+              ? {contentType: '2', imgUrl: ''}  // 如果存在 contentType = '1',添加 contentType = '2'
+              : {contentType: '1', value: ''};  // 如果不存在 contentType = '1',添加 contentType = '1'
+
+            // 将新设置项添加到 content[i].setting 数组中
+            content[i].setting.push(newSetting);
+          }
+
+        }
+      } else {
+
+        const newSetting = {
+          contentType: '1',
+          value: '',
+        };
+        if (this.form.sendType == 4) {
+          newSetting.intervalTime = 5;
+        }
+        // 将新设置项添加到 content.setting 数组中
+        content[contentIndex].setting.push(newSetting);
+      }
+
+
+    },
+    delSetList(index, contentIndex, setIndex) {
+      this.setting[index].content[contentIndex].setting.splice(setIndex, 1)
+
+    },
+
+    addContent(index) {
+      if (this.setting[index].content.length > 0 && this.form.sendType == 1) {
+        return this.$message.error("因为企微接口限制,企微模板一天只能设置一条消息~")
+      } else {
+        this.setting[index].content.push({type: this.defaultContentType, contentType: 1, setting: [{contentType: '1', value: "",}]})
+        this.videoList.push([])
+        this.addTag.push({
+          addTag: [],
+          inputVisible: false,
+          inputValue: '',
+          delTag: [],
+          delTagVisible: false,
+          delTagValue: ''
+        })
+      }
+
+    },
+    delContent(index, contentIndex) {
+      this.setting[index].content.splice(contentIndex, 1)
+      this.videoList.splice(contentIndex, 1)
+      this.addTag.splice(contentIndex, 1)
+    },
+    addSetting() {
+      let content = [{type: this.defaultContentType, contentType: '1', setting: [{contentType: '1', value: "",}]}];
+      if (this.form.sendType == 4) {
+        content[0].setting[0].intervalTime = 5;
+      }
+      this.setting.push({
+        name: null,
+        dayNum: (this.form.gap * this.setting.length) + 1,
+        content
+      })
+
+    },
+    delSetting(index) {
+      const newData = this.setting[index]
+      if (newData.id && newData.dayNum) {
+        this.$confirm('此操作将永久删除, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          delRules(newData.id, newData.name, newData.dayNum).then(res => {
+            this.del(index);
+          });
+        });
+      } else {
+        this.del(index);
+      }
+
+    },
+    del(index) {
+      if (index == 0) {
+        this.tabIndex = "0";
+      } else {
+        this.tabIndex = (index - 1) + "";
+      }
+      this.setting.splice(index, 1)
+
+    },
+    handleClosegroupUser(list) {
+      const index = this.userSelectList.findIndex(t => t === list);
+      if (index !== -1) {
+        this.userSelectList.splice(index, 1);
+      }
+
+    },
+    //选择视频号
+    hanldeSelectVideoNum(content, index, contentIndex, setIndex) {
+      this.videoNumOptions.content = content;
+      this.videoNumOptions.contentIndex = contentIndex;
+      this.videoNumOptions.setIndex = setIndex;
+      this.videoNumOptions.open = true;
+
+    },
+
+    qwUserVideoResult(val) {
+
+      // 根据选中的内容,将返回的数据更新到相应的表单项
+      const content = this.videoNumOptions.content;
+      const setList = content.setting[this.videoNumOptions.setIndex];
+
+      setList.nickname = val.nickname;
+      setList.avatar = val.avatar;
+      setList.coverUrl = val.coverUrl;
+      setList.thumbUrl = val.thumbUrl;
+      setList.desc = val.desc;
+      setList.url = val.url;
+      setList.extras = val.extras;
+
+      this.videoNumOptions.open = false;
+
+    },
+    //首次刷新
+    courseChange(content, index, countIndex) {
+
+      this.courseUpdate(content, index, countIndex);
+    },
+
+    //修改
+    courseChangeUpdate(content, index, countIndex) {
+
+      this.$set(content, 'videoId', null);
+      // 查找选中的课程对应的 label
+      const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === content.courseId);
+      for (let i = 0; i < content.setting.length; i++) {
+
+
+        this.$set(content.setting[i], 'linkTitle', null);
+        this.$set(content.setting[i], 'linkDescribe', null);
+        this.$set(content.setting[i], 'linkImageUrl', null);
+
+
+        //如果是链接的才上
+        if (selectedCourse && content.type == 2 && content.courseId != null) {
+          //响应式直接给链接的标题/封面上值
+
+          if (content.setting[i].contentType == 3 || content.setting[i].contentType == 9) {
+            this.$set(content.setting[i], 'linkTitle', selectedCourse.dictLabel);
+            this.$set(content.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+          if (content.setting[i].contentType == 4) {
+            this.$set(content.setting[i], 'miniprogramPicUrl', selectedCourse.dictImgUrl);
+          }
+
+
+        }
+      }
+
+
+      this.courseUpdate(content, index, countIndex);
+    },
+    handleInputVideoText(value, content) {
+      // 允许的字符:中文、英文(大小写)、数字和指定标点符号(,。!?)
+      const regex = /^[\u4e00-\u9fa5,。!?,!?]+$/;
+
+      // 删除不符合条件的字符
+      const filteredValue = value.split('').filter(char => regex.test(char)).join('');
+
+      this.$set(content, 'value', filteredValue);
+
+    },
+    courseUpdate(content, index, countIndex) {
+      videoList(content.courseId).then(response => {
+        // this.$set(this.videoList, countIndex, response.list); // 响应式设置videoList
+        this.videoList.splice(countIndex, 1, response.list);
+
+      });
+
+    },
+    toggleSalesCall(itemIndex, contentIndex, setIndex) {
+      // 获取目标对象
+      const setItem = this.setting[itemIndex].content[contentIndex].setting[setIndex];
+      const salesCall = '#销售称呼#';
+      const refKey = `textarea-${itemIndex}-${contentIndex}-${setIndex}`;
+      const textarea = this.$refs[refKey][0]?.$refs?.textarea;
+
+      if (!textarea) return;
+
+      // 获取光标位置
+      const cursorPosition = textarea.selectionStart;
+
+      if (setItem.isSalesCallAdded) {
+        // 移除所有标签
+        setItem.value = setItem.value.replace(new RegExp(salesCall, 'g'), '');
+      } else {
+        // 插入到光标位置
+        setItem.value =
+          setItem.value.slice(0, cursorPosition) +
+          salesCall +
+          setItem.value.slice(cursorPosition);
+      }
+
+      // 切换状态
+      setItem.isSalesCallAdded = !setItem.isSalesCallAdded;
+
+      // 保持光标位置
+      this.$nextTick(() => {
+        const newCursorPos = cursorPosition + (setItem.isSalesCallAdded ? salesCall.length : 0);
+        textarea.setSelectionRange(newCursorPos, newCursorPos);
+      });
+    },
+    handleKeydown(event, itemIndex, contentIndex, setIndex) {
+
+      const setItem = this.setting[itemIndex].content[contentIndex].setting[setIndex];
+      const refKey = `textarea-${itemIndex}-${contentIndex}-${setIndex}`;
+      const textarea = this.$refs[refKey][0]?.$refs?.textarea;
+
+      if (!textarea) return;
+
+      const tags = ['#销售称呼#', '#客户称呼#'];
+      const key = event.key;
+      const value = setItem.value;
+      const cursorPosition = textarea.selectionStart;
+
+      if (key === 'Backspace' || key === 'Delete') {
+        tags.forEach(tag => {
+          let start, end;
+
+          // Backspace 处理
+          if (key === 'Backspace' && cursorPosition >= tag.length) {
+            start = cursorPosition - tag.length;
+            if (value.slice(start, cursorPosition) === tag) {
+              event.preventDefault();
+              setItem.value = value.slice(0, start) + value.slice(cursorPosition);
+              if (tag === '#销售称呼#') setItem.isSalesCallAdded = false;
+              this.$nextTick(() => textarea.setSelectionRange(start, start));
+            }
+          }
+
+          // Delete 处理
+          if (key === 'Delete') {
+            end = cursorPosition + tag.length;
+            if (value.slice(cursorPosition, end) === tag) {
+              event.preventDefault();
+              setItem.value = value.slice(0, cursorPosition) + value.slice(end);
+              if (tag === '#销售称呼#') setItem.isSalesCallAdded = false;
+            }
+          }
+        });
+      }
+    },
+    handleContentTypeChange(content, index, countIndex, setIndex, item, fieldName, val) {
+      //如果是链接的才上
+      if (content.courseId != null && content.type == 2) {
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === content.courseId);
+        for (let i = 0; i < content.setting.length; i++) {
+          //响应式直接给链接的标题/封面上值
+          if (selectedCourse && content.type == 2 && content.courseId != null) {
+            if (content.setting[i].contentType == 3 || content.setting[i].contentType == 9) {
+              this.$set(content.setting[i], 'linkTitle', selectedCourse.dictLabel);
+              this.$set(content.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+            }
+            if (content.setting[i].contentType == 4 && (content.isOfficial == '0' || content.isOfficial == null)) {
+              console.log(content.isOfficial);
+              this.$set(content.setting[i], 'miniprogramPicUrl', selectedCourse.dictImgUrl);
+            }
+
+          }
+
+        }
+
+      }
+      if (content.videoId != null && content.type == 2) {
+        // 查找选中的课节对应的 label
+        const selectedVideo = this.videoList[countIndex].find(course => parseInt(course.dictValue) === content.videoId);
+
+        for (let i = 0; i < content.setting.length; i++) {
+          //响应式直接给链接的描述上值
+          if (selectedVideo && content.type == 2 && content.videoId != null) {
+
+            if (content.setting[i].contentType == 3 || content.setting[i].contentType == 9) {
+              this.$set(content.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+            }
+            if (content.setting[i].contentType == 4) {
+              this.$set(content.setting[i], 'miniprogramTitle', this.truncateTextByByteLength(selectedVideo.dictLabel, 60));
+            }
+          }
+        }
+      }
+      if(countIndex == 0 && setIndex == 0){
+        this.updateAll(setIndex, item, fieldName, val);
+        for (let index in item.content) {
+          if(index != 0){
+            this.handleContentTypeChange(item.content[index], 0, index, 0, item, fieldName, val);
+          }
+        }
+      }
+    },
+    videoIdChange(content, index, countIndex) {
+
+      //选择了课程小节则 默认绑上
+      let selectedVideo;
+      // 查找选中的课程对应的 label
+      if (content.videoId != null) {
+        selectedVideo = this.videoList[countIndex].find(course => parseInt(course.dictValue) === content.videoId);
+      }
+      for (let i = 0; i < content.setting.length; i++) {
+        //课程消息-文本内容
+        if (content.setting[i].contentType == 1 && content.type == 2) {
+          this.$set(content.setting[i], 'isBindUrl', '1');
+        }
+        //如果是链接的才上
+        if (selectedVideo && content.type == 2 && content.videoId != null) {
+
+          if (content.setting[i].contentType == 3 || content.setting[i].contentType == 9) {
+            this.$set(content.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+          }
+          if (content.setting[i].contentType == 4) {
+            this.$set(content.setting[i], 'miniprogramTitle', this.truncateTextByByteLength(selectedVideo.dictLabel, 60));
+          }
+
+
+        }
+      }
+
+    },
+    //上传文件
+    handleAvatarSuccessFile(res, file, content) {
+      if (res.code === 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'fileUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+
+    },
+    beforeAvatarUploadFile(file) {
+      const isLt1M = file.size / 1024 / 1024 < 10;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 10MB!');
+      }
+
+      return isLt1M;
+    },
+
+    //下载文件
+    downloadUrl(materialUrl) {
+      // 直接返回文件 URL
+      return materialUrl;
+    },
+
+    handleAvatarSuccessVideo(res, file, content) {
+      if (res.code == 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'videoUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+
+    },
+    beforeAvatarUploadVideo(file) {
+      const isLt30M = file.size / 1024 / 1024 < 10;
+      const isMP4 = file.type === 'video/mp4';
+
+      if (!isMP4) {
+        this.$message.error('仅支持上传 MP4 格式的视频文件!');
+        return false;
+      }
+
+      if (!isLt30M) {
+        this.$message.error('上传大小不能超过 10MB!');
+        return false;
+      }
+
+      return true;
+    },
+
+    handleAvatarSuccessVoice(res, file, content) {
+      if (res.code == 200) {
+        // 创建 Audio 对象加载音频
+        const audio = new Audio(res.mp3Url);
+        audio.addEventListener('loadedmetadata', () => {
+          // 获取音频时长
+          this.$set(content, 'voiceDuration', Math.ceil(audio.duration));
+        });
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'voiceUrl', res.silkUrl);
+        this.$set(content, 'mp3Url', res.mp3Url);
+      } else {
+        this.msgError(res.msg);
+      }
+      this.voiceLoading = false;
+
+    },
+    beforeAvatarUploadVoice(file) {
+      return new Promise((resolve, reject) => {
+        const isLt10M = file.size / 1024 / 1024 < 10; // 假设语音文件大小限制为10MB
+        const isVoiceType = ['audio/mp3', 'audio/mpeg', 'audio/wav', 'audio/x-wav'].includes(file.type);
+
+        if (!isVoiceType) {
+          this.$message.error('仅支持上传 MP3, WAV, X-WAV 格式的语音文件!');
+          return reject(false); // 不允许继续上传
+        }
+
+        if (!isLt10M) {
+          this.$message.error('上传大小不能超过 10MB!');
+          return reject(false); // 不允许继续上传
+        }
+
+        // 使用 FileReader 读取文件
+        const reader = new FileReader();
+        reader.onload = (event) => {
+          const audio = new Audio(event.target.result);
+          audio.addEventListener('loadedmetadata', () => {
+            // 获取时长并保存
+            if (Math.ceil(audio.duration) > 30) {
+              this.$message.error('音频时长不能超过30秒!');
+              this.voiceLoading = false;
+              return reject(false); // 不允许继续上传
+            }
+            resolve(true); // 允许上传
+          });
+        };
+
+        reader.onerror = () => {
+          this.$message.error('无法读取音频文件!请上传正确的音频文件');
+          reject(false); // 不允许继续上传
+        };
+
+        this.voiceLoading = true;
+        reader.readAsDataURL(file); // 开始读取文件
+      });
+
+    },
+
+    /** 查询sop模板列表 */
+    getList() {
+      this.loading = true;
+      listSopTemp(this.queryParams).then(response => {
+        this.sopTempList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+
+    },
+    // 取消按钮
+    cancel() {
+      this.$store.dispatch("tagsView/delView", this.$route);
+      this.$router.replace('/qw/conversion/sopTemp')
+      // this.reset();
+
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        setting: null,
+        status: "0",
+        sort: null,
+        createTime: null,
+        createBy: null,
+        companyId: null
+      };
+      this.resetForm("form");
+
+    },
+
+
+    /** 修改按钮操作 */
+    handleUpdate(id) {
+      getSopTemp(id).then(response => {
+        this.videoList = []
+        this.form = response.data;
+        this.setting = this.form.list || [];
+
+        if (Array.isArray(this.setting)) {
+          this.setting.forEach(item => {
+            if (item && Array.isArray(item.content)) {
+              item.content.forEach(content => {
+                if (content && Array.isArray(content.setting)) {
+                  content.setting.forEach(setList => {
+                    if (setList && !Object.hasOwn(setList, 'isSalesCallAdded')) {
+                      this.$set(setList, 'isSalesCallAdded', false);
+                    }
+                  });
+                }
+              });
+            }
+          });
+        }
+        this.form.list.forEach(e => e.newDay = e.dayNum)
+        this.dayList = JSON.parse(JSON.stringify(this.form.list));
+        this.videoList.push([]);
+        this.addTag.push({
+          addTag: [],
+          inputVisible: false,
+          inputValue: '',
+          delTag: [],
+          delTagVisible: false,
+          delTagValue: ''
+        })
+      });
+
+    },
+    /** 提交按钮 */
+    submitForm() {
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除sop模板编号为"' + ids + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return delSopTemp(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {
+      });
+
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有sop模板数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportSopTemp(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {
+      });
+
+    },
+    updateHtml(val) {
+      val || val();
+
+    },
+    updateAll(setIndex, list, fieldName, newVal) {
+      if(this.form.sendType == 5 && setIndex == 0) {
+        console.info("更新数据", newVal)
+        for (let index in list.content) {
+          this.$set(list.content[index].setting[0], fieldName, newVal);
+        }
+      }
+    }
+  }
+};
+</script>

+ 906 - 0
src/views/qw/sopTemp/updateSopTemp2.vue

@@ -0,0 +1,906 @@
+<template>
+  <div class="app-container">
+
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 && formType==1 "> sop规则【修改企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 && formType==2 "> sop规则【复制企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 && formType==3 "> sop规则【查看企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 && formType==1 "> sop规则【修改AI插件】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 && formType==2 "> sop规则【复制AI插件】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 && formType==3 "> sop规则【查看AI插件】模板</div>
+
+    <div style="margin-top: 10px;margin-left: 50px;margin-right: 100px;margin-bottom: 60px;">
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入模板标题" />
+        </el-form-item>
+
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="dict.dictValue"
+            >{{dict.dictLabel}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="间隔天数" prop="gap">
+          <el-input-number v-model="form.gap"  :min="1" label="间隔天数"></el-input-number>
+        </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="setting">
+          <el-timeline >
+            <el-timeline-item :timestamp="'第'+(1+(form.gap*index))+'天'" :color="'#0bbd87'" placement="top" v-for="(item, index) in setting"  style="margin-top: 10px;">
+              <el-row>
+                <el-col :span="22">
+                  <div style="background-color: #fbfbfb;padding: 15px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                    <el-form :model="item"  label-width="80px">
+
+                      <el-form-item label="内容名称"  style="height: 50px;">
+                        <el-input v-model="item.name" placeholder="内容名称,仅内部可见" />
+                      </el-form-item>
+
+                      <el-form-item label="规则"  >
+                        <div v-for="(content, contentIndex) in item.content" style="background-color: #fdfdfd;padding: 15px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
+
+                          <el-row>
+
+                            <el-col el-col :span="22" >
+                              <el-form :model="content"  label-width="70px">
+                                <el-form-item label="时间" >
+                                  <el-time-picker
+                                    class="custom-input"
+                                    v-model="content.time"
+                                    value-format="HH:mm"
+                                    format="HH:mm"
+                                    :picker-options="{ selectableRange: '00:21:00 - 23:59:59' }"
+                                    placeholder="时间"
+                                    style="width: 100px;height: 20px;" >
+                                  </el-time-picker>
+                                </el-form-item>
+                                <el-form-item label="消息类别"  >
+                                  <el-radio-group v-model="content.type" @change="() => content.contentType = '1'" >
+                                    <el-radio
+                                      :label="1"
+                                    >普通</el-radio>
+                                    <el-radio
+                                      :label="2"
+                                    >课程</el-radio>
+                                    <el-radio
+                                      :label="3"
+                                    >订单</el-radio>
+                                    <el-radio
+                                      :label="4"
+                                    >AI触达</el-radio>
+                                  </el-radio-group>
+                                </el-form-item>
+
+                                <el-form-item label="课程"  v-if="content.type == 2 " required>
+                                  <el-select v-model="content.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"  @change="courseChangeUpdate(content,index,contentIndex)">
+                                    <el-option
+                                      v-for="dict in courseList"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+                                  <el-select v-model="content.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;"  @change="videoIdChange(content,index,contentIndex)">
+                                    <el-option
+                                      v-for="dict in videoList[index][contentIndex]"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+                                  <el-select v-model="content.courseType" placeholder="请选择消息类型" size="mini" style=" margin-right: 10px;" v-if="content.type != 4 ">
+                                    <el-option
+                                      v-for="dict in sysFsSopWatchStatus"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+
+                                </el-form-item>
+                                <el-form-item label="Ai触达"  v-if="content.type == 4 ">
+                                  <el-select  v-model="content.aiTouch" placeholder="请选择Ai触达类型" size="mini" style=" margin-right: 10px;" v-if="content.type == 4 ">
+                                    <el-option label="非首次交流" value="非首次交流"></el-option>
+                                    <el-option label="首次交流1" value="首次交流1"></el-option>
+                                    <el-option label="首次交流2" value="首次交流2"></el-option>
+                                    <el-option label="交流状态1" value="交流状态1"></el-option>
+                                    <el-option label="交流状态2" value="交流状态2"></el-option>
+                                    <el-option label="交流状态3" value="交流状态3"></el-option>
+                                  </el-select>
+                                </el-form-item>
+
+                                <div v-for="(setList, setIndex) in content.setting" :key="setIndex" style="background-color: #fdfdfd; border: 1px solid #e6e6e6; margin-bottom: 20px;" v-if="content.type != 4 ">
+                                  <el-row>
+                                    <el-col :span="22">
+                                      <el-form :model="setList" label-width="70px">
+                                        <el-form-item label="内容类别" style="margin: 2%">
+                                          <div v-if="form.sendType == 1 ">
+                                            <el-radio-group  v-model="setList.contentType" >
+                                              <!--                                              <el-radio  :label="item.dictValue" v-for="item in sysQwSopContentType">{{item.dictLabel}}</el-radio>-->
+                                              <el-radio
+                                                v-for="item in sysQwSopContentType"
+                                                :label="item.dictValue"
+                                                :disabled="item.dictValue === '1' && content.setting.some(s => s.contentType == '1')">
+                                                {{ item.dictLabel }}
+                                              </el-radio>
+                                            </el-radio-group>
+                                          </div>
+                                          <div v-if="form.sendType == 2 ">
+                                            <el-radio-group  v-model="setList.contentType" @change="handleContentTypeChange(content,index,contentIndex,setIndex)">
+                                              <el-radio   :label="item.dictValue" v-for="item in sysQwSopAiContentType">{{item.dictLabel}}</el-radio>
+                                            </el-radio-group>
+                                          </div>
+                                        </el-form-item>
+                                        <el-form-item label="内容"  >
+                                          <el-input v-if="setList.contentType == 1 " v-model="setList.value" type="textarea" :rows="3" placeholder="内容" style="width: 90%;margin-top: 10px;"/>
+                                            <ImageUpload v-if="setList.contentType == 2 " v-model="setList.imgUrl" type="image" :num="1" :width="150" :height="150" />
+                                          <div v-if="setList.contentType == 3 ">
+                                            <el-card class="box-card">
+                                              <el-form-item label="链接标题:"  label-width="100px" required >
+                                                <el-input v-model="setList.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
+                                              </el-form-item>
+                                              <el-form-item label="链接描述:"   label-width="100px" required >
+                                                <el-input type="textarea" :rows="3" v-model="setList.linkDescribe" placeholder="请输入链接描述" style="width: 90%;margin-top: 1%;"/>
+                                              </el-form-item>
+                                              <el-form-item label="链接封面:"   label-width="100px" required>
+                                                <ImageUpload  v-model="setList.linkImageUrl" type="image" :num="1" :file-size="2" :width="150" :height="150" style="margin-top: 1%;" />
+                                              </el-form-item>
+                                              <div v-if="content.type != 2 " style="margin-top: 1%" >
+                                                <el-form-item label="链接地址:"  label-width="100px" required>
+                                                  <el-input v-model="setList.linkUrl" placeholder="请输入链接地址" style="width: 90%;"/>
+                                                </el-form-item>
+                                              </div>
+                                              <div v-if="content.type == 2 ">
+                                                <el-form-item label="链接地址:"  label-width="100px" >
+                                                  <el-tag type="warning" v-model="setList.isBindUrl = 1 ">选择的课程小节 即为卡片链接地址</el-tag>
+                                                </el-form-item>
+                                              </div>
+                                            </el-card>
+                                          </div>
+                                          <div v-if="setList.contentType == 5 ">
+
+                                            <el-form-item label="上传文件:" prop="fileUrl" label-width="100px">
+                                              <el-upload
+                                                v-model="setList.fileUrl"
+                                                class="avatar-uploader"
+                                                :action="uploadUrl"
+                                                :show-file-list="false"
+                                                :on-success="(res, file) => handleAvatarSuccessFile(res, file, setList)"
+                                                :before-upload="beforeAvatarUploadFile">
+                                                <i class="el-icon-plus avatar-uploader-icon"></i>
+                                              </el-upload>
+                                              <el-link v-if="setList.fileUrl" type="primary" :href="downloadUrl(setList.fileUrl)" download>
+                                                {{setList.fileUrl}}
+                                              </el-link>
+                                            </el-form-item>
+
+                                          </div>
+
+                                          <div v-if="setList.contentType == 6 ">
+                                            <el-form-item label="上传视频:" prop="videoUrl" label-width="100px">
+                                              <el-upload
+                                                v-model="setList.videoUrl"
+                                                class="avatar-uploader"
+                                                :action="uploadUrl"
+                                                :show-file-list="false"
+                                                :on-success="(res, file) => handleAvatarSuccessVideo(res, file, setList)"
+                                                :before-upload="beforeAvatarUploadVideo">
+                                                <i class="el-icon-plus avatar-uploader-icon"></i>
+                                              </el-upload>
+                                              <video v-if="setList.videoUrl"
+                                                     :src="setList.videoUrl"
+                                                     controls style="width: 200px;height: 100px">
+                                              </video>
+                                            </el-form-item>
+                                          </div>
+                                          <div v-if="setList.contentType == 7 ">
+                                              <el-input
+                                                v-model="setList.value"
+                                                type="textarea" :rows="3" maxlength="66" show-word-limit
+                                                placeholder="输入要转为语音的内容" style="width: 90%;margin-top: 10px;"
+                                                @input="handleInputVideoText(setList.value,setList)"/>
+
+
+                                          </div>
+                                          <div v-if="setList.contentType == 8 ">
+                                            <el-button type="primary" style="margin-bottom: 1%" @click="hanldeSelectVideoNum(content,index,contentIndex,setIndex)">选择视频号</el-button>
+                                            <el-card class="box-card" v-if="setList.coverUrl">
+                                              <el-form-item label="封面标题:"  label-width="100px">
+                                                <el-input v-model="setList.nickname" style="width: 90%;margin-bottom: 1%" disabled/>
+                                              </el-form-item>
+                                              <el-form-item label="头像:"   label-width="100px" >
+                                                <el-image
+                                                  v-if="setList.avatar != null"
+                                                  :src="setList.avatar"
+                                                  :preview-src-list="[setList.avatar]"
+                                                  :style="{ width: '50px', height: '50px' }"
+                                                ></el-image>
+                                              </el-form-item>
+                                              <el-form-item label="封面:"   label-width="100px" >
+                                                <el-image
+                                                  v-if="setList.coverUrl != null"
+                                                  :src="setList.coverUrl"
+                                                  :preview-src-list="[setList.coverUrl]"
+                                                  :style="{ width: '200px', height: '200px' }"
+                                                ></el-image>
+
+                                              </el-form-item>
+                                              <el-form-item label="简介:"   label-width="100px" >
+                                                <el-input type="textarea" :rows="3" v-model="setList.desc"  style="width: 90%;margin-top: 1%;" disabled />
+                                              </el-form-item>
+                                              <el-form-item label="视频地址:"  label-width="100px" style="margin-top: 1%" >
+                                                <el-input v-model="setList.url" style="width: 90%;" disabled />
+                                              </el-form-item >
+
+                                            </el-card>
+                                          </div>
+                                        </el-form-item>
+                                        <el-form-item label="添加短链" v-if="content.type == 2 && setList.contentType == 1  "  >
+                                          <el-tooltip content="请先根据课程选定课程小节之后再添加" effect="dark" :disabled="!!content.videoId">
+                                            <el-switch
+                                              v-model="setList.isBindUrl"
+                                              :disabled="!content.videoId"
+                                              active-color="#13ce66"
+                                              inactive-color="#DCDFE6"
+                                              active-value="1"
+                                              inactive-value="2">
+                                            </el-switch>
+                                          </el-tooltip>
+
+                                          <span v-if="setList.isBindUrl == '1'" style="margin-left: 10px; color: #13ce66">添加URL</span>
+                                          <span v-if="setList.isBindUrl == '2'" style="margin-left: 10px; color: #b1b4ba">不加URL</span>
+                                        </el-form-item>
+                                        <el-form-item label="课节过期时间" v-if="content.type == 2 && setList.isBindUrl == '1' && setList.contentType != 2  && setList.contentType != 5  && setList.contentType != 6 && setList.contentType != 8 ">
+                                          <el-row>
+                                            <el-input-number  v-model="setList.expiresDays"  :min="0" :max="100" ></el-input-number>
+                                            (天)
+                                          </el-row>
+                                          <el-row>
+                                            <span class="tip">填写0或不填时,默认为系统配置的默认时间</span>
+                                          </el-row>
+                                        </el-form-item>
+
+                                      </el-form>
+                                    </el-col>
+                                    <el-col :span="1" :offset="1">
+                                      <i class="el-icon-delete" @click="delSetList(index,contentIndex,setIndex)" style="margin-top: 20px;" v-if="content.setting.length>1 && (formType==1 || formType==2)"></i>
+                                    </el-col>
+                                  </el-row>
+                                </div>
+                                <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetList(contentIndex,item.content)' v-if="content.type != 4 && (formType==1 || formType==2)">添加内容</el-link>
+                              </el-form>
+
+
+                            </el-col>
+                            <el-col :span="1" :offset="1">
+                              <i class="el-icon-delete" @click="delContent(index,contentIndex)" style="margin-top: 20px;" v-if="item.content.length>1 && (formType==1 || formType==2)"></i>
+                            </el-col>
+                          </el-row>
+                        </div>
+                        <el-link type="primary" class="el-icon-plus" :underline="false" @click='addContent(index)' v-if="formType==1 || formType==2 ">添加规则</el-link>
+                      </el-form-item>
+                    </el-form>
+                  </div>
+                </el-col>
+                <el-col :span="1" :offset="1">
+                  <i class="el-icon-delete-solid" @click="delSetting(index)"  v-if="setting.length>1 && (formType==1 || formType==2)"></i>
+                </el-col>
+              </el-row>
+            </el-timeline-item>
+
+
+          </el-timeline>
+          <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetting()' v-if="formType==1 || formType==2">添加天数</el-link>
+        </el-form-item>
+
+      </el-form>
+      <div slot="footer" class="dialog-footer" style="float: right;" v-if="formType==1 || formType==2 ">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </div>
+
+    <el-dialog :title="videoNumOptions.title" :visible.sync="videoNumOptions.open" style="width: 1500px;height: 100%" append-to-body>
+      <userVideo ref="QwUserVideo" @videoResult="qwUserVideoResult" ></userVideo>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listSopTemp, getSopTemp, delSopTemp, addSopTemp, updateSopTemp, exportSopTemp } from "@/api/qw/sopTemp";
+import { courseList,videoList } from "@/api/qw/sop";
+import ImageUpload from "@/views/qw/sop/ImageUpload";
+import userVideo from "@/views/qw/userVideo/userVideo.vue";
+export default {
+  name: "updateSopTemp",
+  components: { ImageUpload,userVideo},
+  data() {
+    return {
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS2",
+      uploadUrlByVoice:process.env.VUE_APP_BASE_API+"/common/uploadOSSByHOOKVoice",
+      //上传语音的遮罩层
+      voiceLoading :false,
+
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      sysFsSopWatchStatus: [],
+      //消息内容类型 企微版
+      sysQwSopContentType:[],
+      //插件版
+      sysQwSopAiContentType:[],
+
+      //类别
+      sysQwSopSettingType:[],
+
+      courseList:[],
+      videoList:[],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // sop模板表格数据
+      setting: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 状态字典
+      statusOptions: [],
+      videoNumOptions:{
+        title:'选择视频号',
+        open:false,
+        content:null,
+        contentIndex:null,
+        setIndex:null,
+      },
+      //1 修改 2 复制 3 查看
+      formType:null,
+      // 查询参数
+      form: {
+        name: null,
+        setting: null,
+        status: "1",
+        sort: 1,
+        companyId: null,
+        gap:1,
+      },
+      // 表单校验
+      rules: {
+        name: [
+          { required: true, message: '名称不能为空', trigger: 'blur' }
+        ],
+        status: [
+          { required: true, message: '状态不能为空', trigger: 'blur' }
+        ],
+        sort: [
+          { required: true, message: '排序不能为空', trigger: 'blur' }
+        ],
+        gap: [
+          { required: true, message: '间隔天数不能为空', trigger: 'blur' }
+        ],
+      }
+    };
+  },
+  created() {
+    this.getDicts("sys_qwSopAi_contentType").then(response => {
+      this.sysQwSopAiContentType = response.data;
+    });
+    this.getDicts("sys_fs_sop_watch_status").then(response => {
+      this.sysFsSopWatchStatus = response.data;
+    });
+
+    this.getDicts("sys_qwSop_contentType").then(response => {
+      this.sysQwSopContentType = response.data;
+    });
+
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+
+    this.getDicts("sys_qwSop_settingType").then(response => {
+      this.sysQwSopSettingType = response.data;
+    });
+
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+
+    const id = this.$route.params && this.$route.params.id;
+    this.formType = this.$route.params && this.$route.params.type;
+    this.handleUpdate(id);
+  },
+  methods: {
+    addSetList(contentIndex,content){
+
+      if (this.form.sendType==1){
+        for (let i = 0; i < content.length; i++) {
+
+          if (content[i].setting.length > 9) {
+            return this.$message.error("因为企微接口限制,企微模板一条消息只能设置最多~9个内容!!")
+          } else {
+
+            // 动态生成新的设置项
+            const newSetting = content[i].setting.some(item => item.contentType === '1')
+              ? {contentType: '2', imgUrl: ''}  // 如果存在 contentType = '1',添加 contentType = '2'
+              : {contentType: '1', value: ''};  // 如果不存在 contentType = '1',添加 contentType = '1'
+
+            // 将新设置项添加到 content[i].setting 数组中
+            content[i].setting.push(newSetting);
+          }
+
+        }
+      } else {
+
+        const newSetting = {
+          contentType: '1',
+          value: '',
+        };
+
+        // 将新设置项添加到 content.setting 数组中
+        content[contentIndex].setting.push(newSetting);
+
+      }
+
+    },
+    delSetList(index, contentIndex, setIndex) {
+      this.setting[index].content[contentIndex].setting.splice(setIndex, 1)
+    },
+
+    addContent(index) {
+
+      if (this.setting[index].content.length > 0 && this.form.sendType == 1) {
+        return this.$message.error("因为企微接口限制,企微模板一天只能设置一条消息~")
+      } else {
+        this.setting[index].content.push({type: 1, contentType: 1, setting: [{contentType: '1', value: "",}]})
+        this.videoList[index].push([])
+      }
+
+    },
+    delContent(index, contentIndex) {
+      this.setting[index].content.splice(contentIndex, 1)
+      this.videoList[index].splice(contentIndex, 1)
+    },
+    addSetting() {
+      this.setting.push({name: null, content: [{type: 1, contentType: '1', setting: [{contentType: '1', value: "",}]}]})
+      this.videoList.push([])
+
+    },
+    delSetting(index) {
+      this.setting.splice(index, 1)
+      this.videoList.splice(index, 1)
+    },
+    handleClosegroupUser(list) {
+      const index = this.userSelectList.findIndex(t => t === list);
+      if (index !== -1) {
+        this.userSelectList.splice(index, 1);
+      }
+    },
+    //选择视频号
+    hanldeSelectVideoNum(content,index,contentIndex,setIndex){
+      this.videoNumOptions.content = content;
+      this.videoNumOptions.contentIndex = contentIndex;
+      this.videoNumOptions.setIndex = setIndex;
+      this.videoNumOptions.open=true;
+    },
+
+    qwUserVideoResult(val){
+
+      // 根据选中的内容,将返回的数据更新到相应的表单项
+      const content = this.videoNumOptions.content;
+      const setList = content.setting[this.videoNumOptions.setIndex];
+
+      setList.nickname = val.nickname;
+      setList.avatar = val.avatar;
+      setList.coverUrl = val.coverUrl;
+      setList.thumbUrl = val.thumbUrl;
+      setList.desc = val.desc;
+      setList.url = val.url;
+      setList.extras = val.extras;
+
+      this.videoNumOptions.open=false;
+    },
+    //首次刷新
+    courseChange(content, index, countIndex) {
+
+      this.courseUpdate(content, index, countIndex);
+    },
+
+    //修改
+    courseChangeUpdate(content, index, countIndex) {
+      this.$set(content, 'videoId', null);
+      // 查找选中的课程对应的 label
+      const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === content.courseId);
+
+      for (let i = 0; i < content.setting; i++) {
+        this.$set(content.setting[i], 'linkTitle', null);
+        this.$set(content.setting[i], 'linkDescribe', null);
+        this.$set(content.setting[i], 'linkImageUrl', null);
+
+        //如果是链接的才上
+        if (content.setting[i].contentType == 3 && content.type == 2 && content.courseId != null) {
+
+          //响应式直接给链接的标题/封面上值
+          if (selectedCourse) {
+            this.$set(content.setting[i], 'linkTitle', selectedCourse.dictLabel);
+            this.$set(content.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+        }
+      }
+
+
+      this.courseUpdate(content, index, countIndex);
+    },
+    handleInputVideoText(value,content){
+      // 允许的字符:中文、英文(大小写)、数字和指定标点符号(,。!?)
+      const regex = /^[\u4e00-\u9fa5,。!?,!?]+$/;
+
+      // 删除不符合条件的字符
+      const filteredValue = value.split('').filter(char => regex.test(char)).join('');
+
+      this.$set(content, 'value', filteredValue);
+
+    },
+    courseUpdate(content, index, countIndex) {
+      videoList(content.courseId).then(response => {
+
+        this.videoList[index].splice(countIndex, 1, response.list);
+
+      });
+    },
+    handleContentTypeChange(content, index, countIndex) {
+
+      //如果是链接的才上
+      if (content.courseId != null && content.type == 2) {
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === content.courseId);
+        for (let i = 0; i < content.setting.length; i++) {
+          //响应式直接给链接的标题/封面上值
+          if (selectedCourse && content.setting[i].contentType == 3 && content.type == 2 && content.courseId != null) {
+            this.$set(content.setting[i], 'linkTitle', selectedCourse.dictLabel);
+            this.$set(content.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+
+        }
+
+      }
+      if (content.videoId != null && content.type == 2) {
+        // 查找选中的课节对应的 label
+        const selectedVideo = this.videoList[index][countIndex].find(course => parseInt(course.dictValue) === content.videoId);
+
+        for (let i = 0; i < content.setting.length; i++) {
+          //响应式直接给链接的描述上值
+          if (selectedVideo && content.setting[i].contentType == 3 && content.type == 2 && content.videoId != null) {
+            this.$set(content.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+          }
+        }
+      }
+
+    },
+    videoIdChange(content, index, countIndex) {
+      //选择了课程小节则 默认绑上
+      let selectedVideo;
+      // 查找选中的课程对应的 label
+      if (content.videoId != null) {
+        selectedVideo = this.videoList[index][countIndex].find(course => parseInt(course.dictValue) === content.videoId);
+      }
+      for (let i = 0; i < content.setting.length; i++) {
+        //课程消息-文本内容
+        if (content.setting[i].contentType == 1 && content.type == 2) {
+          this.$set(content.setting[i], 'isBindUrl', '1');
+        }
+        //如果是链接的才上
+        if (content.setting[i].contentType == 3 && content.type == 2 && content.videoId != null) {
+
+          //响应式直接给链接的描述上值
+          if (selectedVideo) {
+            this.$set(content.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+          }
+        }
+      }
+
+    },
+    //上传文件
+    handleAvatarSuccessFile(res, file, content) {
+      if (res.code === 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'fileUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadFile(file) {
+      const isLt1M = file.size / 1024 / 1024 < 10;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 10MB!');
+      }
+      return isLt1M;
+    },
+
+    //下载文件
+    downloadUrl(materialUrl) {
+      // 直接返回文件 URL
+      return materialUrl;
+    },
+
+    handleAvatarSuccessVideo(res, file, content) {
+      if (res.code == 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'videoUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadVideo(file) {
+      const isLt30M = file.size / 1024 / 1024 < 10;
+      const isMP4 = file.type === 'video/mp4';
+
+      if (!isMP4) {
+        this.$message.error('仅支持上传 MP4 格式的视频文件!');
+        return false;
+      }
+
+      if (!isLt30M) {
+        this.$message.error('上传大小不能超过 10MB!');
+        return false;
+      }
+
+      return true;
+    },
+
+    handleAvatarSuccessVoice(res, file, content) {
+      if (res.code == 200) {
+        // 创建 Audio 对象加载音频
+        const audio = new Audio(res.mp3Url);
+        audio.addEventListener('loadedmetadata', () => {
+          // 获取音频时长
+          this.$set(content, 'voiceDuration', Math.ceil(audio.duration));
+        });
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'voiceUrl', res.silkUrl);
+        this.$set(content, 'mp3Url', res.mp3Url);
+      } else {
+        this.msgError(res.msg);
+      }
+      this.voiceLoading = false;
+    },
+    beforeAvatarUploadVoice(file) {
+      return new Promise((resolve, reject) => {
+        const isLt10M = file.size / 1024 / 1024 < 10; // 假设语音文件大小限制为10MB
+        const isVoiceType = ['audio/mp3', 'audio/mpeg', 'audio/wav', 'audio/x-wav'].includes(file.type);
+
+        if (!isVoiceType) {
+          this.$message.error('仅支持上传 MP3, WAV, X-WAV 格式的语音文件!');
+          return reject(false); // 不允许继续上传
+        }
+
+        if (!isLt10M) {
+          this.$message.error('上传大小不能超过 10MB!');
+          return reject(false); // 不允许继续上传
+        }
+
+        // 使用 FileReader 读取文件
+        const reader = new FileReader();
+        reader.onload = (event) => {
+          const audio = new Audio(event.target.result);
+          audio.addEventListener('loadedmetadata', () => {
+            // 获取时长并保存
+            if (Math.ceil(audio.duration) > 30) {
+              this.$message.error('音频时长不能超过30秒!');
+              this.voiceLoading = false;
+              return reject(false); // 不允许继续上传
+            }
+            resolve(true); // 允许上传
+          });
+        };
+
+        reader.onerror = () => {
+          this.$message.error('无法读取音频文件!请上传正确的音频文件');
+          reject(false); // 不允许继续上传
+        };
+
+        this.voiceLoading = true;
+        reader.readAsDataURL(file); // 开始读取文件
+      });
+    },
+
+    /** 查询sop模板列表 */
+    getList() {
+      this.loading = true;
+      listSopTemp(this.queryParams).then(response => {
+        this.sopTempList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.$store.dispatch("tagsView/delView", this.$route);
+      this.$router.replace('/qw/conversion/sopTemp')
+      // this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        setting: null,
+        status: "0",
+        sort: null,
+        createTime: null,
+        createBy: null,
+        companyId: null
+      };
+      this.resetForm("form");
+    },
+
+
+    /** 修改按钮操作 */
+    handleUpdate(id) {
+
+      getSopTemp(id).then(response => {
+        this.videoList = []
+        this.form = response.data;
+        this.setting = JSON.parse(response.data.setting);
+        for (let i = 0; i < this.setting.length; i++) {
+          this.videoList.push([])
+          for (let j = 0; j < this.setting[i].content.length; j++) {
+            this.videoList[i].push([])
+            if (this.setting[i].content[j].type == 2) {
+              if (this.setting[i].content[j].hasOwnProperty('courseId')) {
+                this.courseChange(this.setting[i].content[j], i, j);
+              }
+            }
+          }
+        }
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+
+        this.form.setting = JSON.stringify(this.setting)
+
+        if (this.setting.length <= 0) {
+          return this.$message("请添加规则")
+        }
+
+        for (let i = 0; i < this.setting.length; i++) {
+
+
+          for (let j = 0; j < this.setting[i].content.length; j++) {
+
+            if (this.setting[i].name == null || this.setting[i].name == "") {
+              return this.$message.error("内容名称不能为空")
+            }
+            if (this.setting[i].content[j].time == null || this.setting[i].content[j].time == "") {
+              return this.$message.error("时间不能为空")
+            }
+            if (this.setting[i].content[j].type != 4) {
+              for (let k = 0; k < this.setting[i].content[j].setting.length; k++) {
+                if (this.setting[i].content[j].setting[k].contentType == 1 && (this.setting[i].content[j].setting[k].value == null || this.setting[i].content[j].setting[k].value == "")) {
+                  return this.$message.error("内容不能为空")
+                }
+                if (this.setting[i].content[j].setting[k].contentType == 2 && (this.setting[i].content[j].setting[k].imgUrl == null || this.setting[i].content[j].setting[k].imgUrl == "")) {
+                  return this.$message.error("图片不能为空")
+                }
+                if (this.setting[i].content[j].setting[k].contentType == 3 && (this.setting[i].content[j].setting[k].linkTitle == null || this.setting[i].content[j].setting[k].linkTitle == "")) {
+                  return this.$message.error("链接标题不能为空")
+                }
+                if (this.setting[i].content[j].setting[k].contentType == 3 && (this.setting[i].content[j].setting[k].linkDescribe == null || this.setting[i].content[j].setting[k].linkDescribe == "")) {
+                  return this.$message.error("链接描述不能为空")
+                }
+                if (this.setting[i].content[j].setting[k].contentType == 3 && (this.setting[i].content[j].setting[k].linkImageUrl == null || this.setting[i].content[j].setting[k].linkImageUrl == "")) {
+                  return this.$message.error("链接图片不能为空")
+                }
+                if (this.setting[i].content[j].setting[k].contentType == 3 && this.setting[i].content[j].setting[k].type == 1 && (this.setting[i].content[j].setting[k].linkUrl == null || this.setting[i].content[j].setting[k].linkUrl == "")) {
+                  return this.$message.error("链接地址不能为空")
+                }
+                if (this.setting[i].content[j].setting[k].contentType == 5 && (this.setting[i].content[j].setting[k].fileUrl == null || this.setting[i].content[j].setting[k].fileUrl == "")) {
+                  return this.$message.error("文件不能为空")
+                }
+                if (this.setting[i].content[j].setting[k].contentType == 6 && (this.setting[i].content[j].setting[k].videoUrl == null || this.setting[i].content[j].setting[k].videoUrl == "")) {
+                  return this.$message.error("视频不能为空")
+                }
+                if (this.setting[i].content[j].setting[k].contentType == 7 && (this.setting[i].content[j].setting[k].value == null || this.setting[i].content[j].setting[k].value == "")) {
+                  return this.$message.error("语音文本不能为空")
+                }
+                if (this.setting[i].content[j].setting[k].contentType == 8 && (this.setting[i].content[j].setting[k].url == null || this.setting[i].content[j].setting[k].url == "")) {
+                  return this.$message.error("视频号信息不能为空")
+                }
+              }
+            } else {
+              if (this.setting[i].content[j].aiTouch == null || this.setting[i].content[j].aiTouch == '') {
+                return this.$message.error("AI触达不能为空")
+              }
+            }
+          }
+
+
+        }
+        if (valid) {
+          if (this.formType == 1) {
+            if (this.form.id != null) {
+              updateSopTemp(this.form).then(response => {
+                this.msgSuccess("修改成功");
+                this.$store.dispatch("tagsView/delView", this.$route);
+                // this.$router.replace('/qw/conversion/sopTemp')
+                window.location.replace('/qw/conversion/sopTemp')
+                this.reset();
+
+              });
+            }
+          } else {
+            //id制空,防止键值重复-报错
+            this.form.id = null
+            //更新时间制空
+            this.form.updateTime = null
+            addSopTemp(this.form).then(response => {
+              this.msgSuccess("复制成功");
+              this.$store.dispatch("tagsView/delView", this.$route);
+              // this.$router.replace('/qw/conversion/sopTemp')
+              window.location.replace('/qw/conversion/sopTemp')
+              this.reset();
+
+            });
+          }
+
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除sop模板编号为"' + ids + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return delSopTemp(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {
+      });
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有sop模板数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportSopTemp(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {
+      });
+    }
+  }
+};
+</script>
+
+<style scoped>
+.custom-input /deep/ .el-input__inner {
+  height: 20px;
+  text-align: center;
+}
+
+.custom-input /deep/ .el-input__icon {
+  line-height: 10px;
+}
+</style>

+ 664 - 0
src/views/qw/sopTemp/updateSopTempOld.vue

@@ -0,0 +1,664 @@
+<template>
+  <div class="app-container">
+
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 "> sop规则【企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 "> sop规则【AI插件】模板</div>
+
+    <div style="margin-top: 10px;margin-left: 50px;margin-right: 100px;margin-bottom: 60px;">
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入模板标题" />
+        </el-form-item>
+
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="dict.dictValue"
+            >{{dict.dictLabel}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="间隔天数" prop="gap">
+          <el-input-number v-model="form.gap"  :min="1" label="间隔天数"></el-input-number>
+        </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="setting">
+          <el-timeline >
+            <el-timeline-item :timestamp="'第'+(1+(form.gap*index))+'天'" :color="'#0bbd87'" placement="top" v-for="(item, index) in setting"  style="margin-top: 10px;">
+              <el-row>
+                <el-col :span="22">
+                  <div style="background-color: #fbfbfb;padding: 15px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                    <el-form :model="item"  label-width="80px">
+
+                      <el-form-item label="内容名称"  style="height: 50px;">
+                        <el-input v-model="item.name" placeholder="内容名称,仅内部可见" />
+                      </el-form-item>
+
+                      <el-form-item label="规则"  >
+                        <div v-for="(content, contentIndex) in item.content" style="background-color: #fdfdfd;padding: 15px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
+
+                          <el-row>
+
+                            <el-col el-col :span="22" >
+                              <el-form :model="content"  label-width="70px">
+                                <el-form-item label="时间" >
+                                  <el-time-picker
+                                    class="custom-input"
+                                    v-model="content.time"
+                                    value-format="HH:mm"
+                                    format="HH:mm"
+                                    :picker-options="{ selectableRange: '00:00:00 - 23:59:59' }"
+                                    placeholder="时间"
+                                    style="width: 100px;height: 20px;" >
+                                  </el-time-picker>
+                                </el-form-item>
+                                <el-form-item label="消息类别"  >
+                                  <el-radio-group v-model="content.type" @change="() => content.contentType = '1'" >
+
+                                    <el-radio
+                                      :label="1"
+                                    >普通</el-radio>
+                                    <el-radio
+                                      :label="2"
+                                    >课程</el-radio>
+                                    <el-radio
+                                      :label="3"
+                                    >订单</el-radio>
+
+                                  </el-radio-group>
+                                </el-form-item>
+
+                                <el-form-item label="课程"  v-if="content.type == 2 " required>
+                                  <el-select v-model="content.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"  @change="courseChangeUpdate(content,index,contentIndex)">
+                                    <el-option
+                                      v-for="dict in courseList"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+                                  <el-select v-model="content.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;"  @change="videoIdChange(content,index,contentIndex)">
+                                    <el-option
+                                      v-for="dict in videoList[index][contentIndex]"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+                                  <el-select v-model="content.courseType" placeholder="请选择消息类型" size="mini" style=" margin-right: 10px;">
+                                    <el-option
+                                      v-for="dict in sysFsSopWatchStatus"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+
+                                </el-form-item>
+                                <el-form-item label="内容类别"  >
+                                  <div v-if="form.sendType == 1 ">
+                                    <el-radio-group v-model="content.contentType">
+                                      <el-radio :label="item.dictValue" v-for="item in sysQwSopContentType">{{item.dictLabel}}</el-radio>
+                                    </el-radio-group>
+                                  </div>
+                                  <div v-if="form.sendType == 2 ">
+                                    <div v-if="content.type == 2 ">
+                                      <el-radio-group  v-model="content.contentType" @change="handleContentTypeChange(content,index,contentIndex)">
+                                        <el-radio  :label="item.dictValue" v-for="item in sysQwSopSettingType">{{item.dictLabel}}</el-radio>
+                                      </el-radio-group>
+                                    </div>
+                                    <div v-else>
+                                      <el-radio-group  v-model="content.contentType" @change="handleContentTypeChange(content,index,contentIndex)">
+                                        <el-radio   :label="item.dictValue" v-for="item in sysQwSopAiContentType">{{item.dictLabel}}</el-radio>
+                                      </el-radio-group>
+                                    </div>
+                                  </div>
+                                  <!--                                   <el-radio-group v-model="content.contentType">-->
+                                  <!--                                       <el-radio-->
+                                  <!--                                         :label="1"-->
+                                  <!--                                       >文字</el-radio>-->
+                                  <!--                                       <el-radio-->
+                                  <!--                                         :label="2"-->
+                                  <!--                                       >图片</el-radio>-->
+                                  <!--                                   </el-radio-group>-->
+                                </el-form-item>
+
+                                <el-form-item label="内容"  >
+                                  <el-input v-if="content.contentType == 1 " v-model="content.value" type="textarea" :rows="3" placeholder="内容" style="width: 90%;margin-top: 10px;"/>
+                                  <ImageUpload v-if="content.contentType == 2 " v-model="content.imgUrl" type="image" :num="1" :width="150" :height="150" />
+                                  <div v-if="content.contentType == 3 ">
+                                    <el-card class="box-card">
+                                      <el-form-item label="链接标题:"  label-width="100px" required >
+                                        <el-input v-model="content.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
+                                      </el-form-item>
+                                      <el-form-item label="链接描述:"   label-width="100px" required >
+                                        <el-input type="textarea" :rows="3" v-model="content.linkDescribe" placeholder="请输入链接描述" style="width: 90%;margin-top: 1%;"/>
+                                      </el-form-item>
+                                      <el-form-item label="链接封面:"   label-width="100px" required>
+                                        <ImageUpload  v-model="content.linkImageUrl" type="image" :num="1" :width="150" :height="150" style="margin-top: 1%;" />
+                                      </el-form-item>
+                                      <div v-if="content.type != 2 " style="margin-top: 1%" >
+                                        <el-form-item label="链接地址:"  label-width="100px" required>
+                                          <el-input v-model="content.linkUrl" placeholder="请输入链接地址" style="width: 90%;"/>
+                                        </el-form-item>
+                                      </div>
+                                      <div v-if="content.type == 2 ">
+                                        <el-form-item label="链接地址:"  label-width="100px" >
+                                          <el-tag type="warning" v-model="content.isBindUrl = 1 ">选择的课程小节 即为卡片链接地址</el-tag>
+                                        </el-form-item>
+                                      </div>
+                                    </el-card>
+                                  </div>
+                                  <div v-if="content.contentType == 5 ">
+
+                                    <el-form-item label="上传文件:" prop="fileUrl" label-width="100px">
+                                      <el-upload
+                                        v-model="content.fileUrl"
+                                        class="avatar-uploader"
+                                        :action="uploadUrl"
+                                        :show-file-list="false"
+                                        :on-success="(res, file) => handleAvatarSuccessFile(res, file, content)"
+                                        :before-upload="beforeAvatarUploadFile">
+                                        <i class="el-icon-plus avatar-uploader-icon"></i>
+                                      </el-upload>
+                                      <el-link v-if="content.fileUrl" type="primary" :href="downloadUrl(content.fileUrl)" download>
+                                        {{content.fileUrl}}
+                                      </el-link>
+                                    </el-form-item>
+
+                                  </div>
+
+                                  <div v-if="content.contentType == 6 ">
+                                    <el-form-item label="上传视频:" prop="videoUrl" label-width="100px">
+                                      <el-upload
+                                        v-model="content.videoUrl"
+                                        class="avatar-uploader"
+                                        :action="uploadUrl"
+                                        :show-file-list="false"
+                                        :on-success="(res, file) => handleAvatarSuccessVideo(res, file, content)"
+                                        :before-upload="beforeAvatarUploadVideo">
+                                        <i class="el-icon-plus avatar-uploader-icon"></i>
+                                      </el-upload>
+                                      <video v-if="content.videoUrl"
+                                             :src="content.videoUrl"
+                                             controls style="width: 200px;height: 100px">
+                                      </video>
+                                    </el-form-item>
+                                  </div>
+                                  <div v-if="content.contentType == 7 ">
+                                    <el-form-item label="上传语音:" prop="videoUrl" label-width="100px">
+                                      <el-upload
+                                        v-model="content.voiceUrl"
+                                        class="avatar-uploader"
+                                        :action="uploadUrl"
+                                        :show-file-list="false"
+                                        :on-success="(res, file) => handleAvatarSuccessVoice(res, file, content)"
+                                        :before-upload="beforeAvatarUploadVoice">
+                                        <i class="el-icon-plus avatar-uploader-icon"></i>
+                                      </el-upload>
+                                      <audio  v-if="content.voiceUrl"
+                                              :src="content.voiceUrl"
+                                              controls style="width: 300px;height: 50px">
+                                      </audio >
+                                    </el-form-item>
+                                  </div>
+                                </el-form-item>
+                                <el-form-item label="添加短链" v-if="content.type == 2 && content.contentType == 1  "  >
+                                  <el-tooltip content="请先根据课程选定课程小节之后再添加" effect="dark" :disabled="!!content.videoId">
+                                    <el-switch
+                                      v-model="content.isBindUrl"
+                                      :disabled="!content.videoId"
+                                      active-color="#13ce66"
+                                      inactive-color="#DCDFE6"
+                                      active-value="1"
+                                      inactive-value="2">
+                                    </el-switch>
+                                  </el-tooltip>
+
+                                  <span v-if="content.isBindUrl == '1'" style="margin-left: 10px; color: #13ce66">添加URL</span>
+                                  <span v-if="content.isBindUrl == '2'" style="margin-left: 10px; color: #b1b4ba">不加URL</span>
+                                </el-form-item>
+                                <el-form-item label="课节过期时间" v-if="content.type == 2 && content.isBindUrl == '1' && content.contentType != 2  && content.contentType != 5  && content.contentType != 6 ">
+                                  <el-row>
+                                    <el-input-number  v-model="content.expiresDays"  :min="0" :max="100" ></el-input-number>
+                                    (天)
+                                  </el-row>
+                                  <el-row>
+                                    <span class="tip">填写0或不填时,默认为系统配置的默认时间</span>
+                                  </el-row>
+                                </el-form-item>
+                              </el-form>
+
+
+
+
+                            </el-col>
+                            <el-col :span="1" :offset="1">
+                              <i class="el-icon-delete" @click="delContent(index,contentIndex)" style="margin-top: 20px;" v-if="item.content.length>1"></i>
+                            </el-col>
+                          </el-row>
+                        </div>
+                        <el-link type="primary" class="el-icon-plus" :underline="false" @click='addContent(index)'>添加规则</el-link>
+                      </el-form-item>
+                    </el-form>
+                  </div>
+                </el-col>
+                <el-col :span="1" :offset="1">
+                  <i class="el-icon-delete-solid" @click="delSetting(index)"  v-if="setting.length>1"></i>
+                </el-col>
+              </el-row>
+            </el-timeline-item>
+
+
+          </el-timeline>
+          <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetting()'>添加天数</el-link>
+        </el-form-item>
+
+      </el-form>
+      <div slot="footer" class="dialog-footer" style="float: right;">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { listSopTemp, getSopTemp, delSopTemp, addSopTemp, updateSopTemp, exportSopTemp } from "@/api/qw/sopTemp";
+import { courseList,videoList } from "@/api/qw/sop";
+import ImageUpload from "@/views/qw/sop/ImageUpload";
+export default {
+  name: "SopTemp",
+  components: { ImageUpload},
+  data() {
+    return {
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      sysFsSopWatchStatus: [],
+      //消息内容类型 企微版
+      sysQwSopContentType:[],
+      //插件版
+      sysQwSopAiContentType:[],
+
+      //类别
+      sysQwSopSettingType:[],
+
+      courseList:[],
+      videoList:[],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // sop模板表格数据
+      setting: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 状态字典
+      statusOptions: [],
+      // 查询参数
+      form: {
+        name: null,
+        setting: null,
+        status: "1",
+        sort: 1,
+        companyId: null,
+        gap:1,
+      },
+      // 表单校验
+      rules: {
+        name: [
+          { required: true, message: '名称不能为空', trigger: 'blur' }
+        ],
+        status: [
+          { required: true, message: '状态不能为空', trigger: 'blur' }
+        ],
+        sort: [
+          { required: true, message: '排序不能为空', trigger: 'blur' }
+        ],
+        gap: [
+          { required: true, message: '间隔天数不能为空', trigger: 'blur' }
+        ],
+      }
+    };
+  },
+  created() {
+    this.getDicts("sys_qwSopAi_contentType").then(response => {
+      this.sysQwSopAiContentType = response.data;
+    });
+    this.getDicts("sys_fs_sop_watch_status").then(response => {
+      this.sysFsSopWatchStatus = response.data;
+    });
+
+    this.getDicts("sys_qwSop_contentType").then(response => {
+      this.sysQwSopContentType = response.data;
+    });
+
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+
+    this.getDicts("sys_qwSop_settingType").then(response => {
+      this.sysQwSopSettingType = response.data;
+    });
+
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+
+    const id = this.$route.params && this.$route.params.id;
+    this.handleUpdate(id);
+
+
+
+  },
+  methods: {
+    addContent(index){
+
+      if (this.setting[index].content.length>0 && this.form.sendType==1){
+        return this.$message.error("因为企微接口限制,企微模板一天只能设置一条消息~")
+      }else {
+        this.setting[index].content.push({type:1,contentType:1})
+        this.videoList[index].push([])
+      }
+
+    },
+    delContent(index,contentIndex){
+      this.setting[index].content.splice(contentIndex,1)
+      this.videoList[index].splice(contentIndex,1)
+    },
+    addSetting(){
+      this.setting.push({name:null,type:1,content:[],day:"0",hour:"0",minute:"0",time:""})
+      this.videoList.push([])
+
+    },
+    delSetting(index){
+      this.setting.splice(index,1)
+      this.videoList.splice(index,1)
+    },
+    handleClosegroupUser(list){
+      const index = this.userSelectList.findIndex(t => t === list);
+      if (index !== -1) {
+        this.userSelectList.splice(index, 1);
+      }
+    },
+
+    //首次刷新
+    courseChange(content,index,countIndex){
+
+      this.courseUpdate(content,index,countIndex);
+    },
+
+    //修改
+    courseChangeUpdate(content,index,countIndex){
+      this.$set(content, 'videoId', null);
+      this.$set(content, 'linkTitle', null);
+      this.$set(content, 'linkDescribe', null);
+      this.$set(content, 'linkImageUrl', null);
+      //如果是链接的才上
+      if (content.contentType==3 && content.type==2 && content.courseId!=null){
+
+        // 查找选中的课程对应的 label
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === content.courseId);
+
+        //响应式直接给链接的标题/封面上值
+        if (selectedCourse) {
+          this.$set(content, 'linkTitle', selectedCourse.dictLabel);
+          this.$set(content, 'linkImageUrl', selectedCourse.dictImgUrl);
+        }
+      }
+
+      this.courseUpdate(content,index,countIndex);
+    },
+
+    courseUpdate(content,index,countIndex){
+      videoList(content.courseId).then(response => {
+
+        this.videoList[index].splice(countIndex, 1, response.list);
+
+      });
+    },
+    handleContentTypeChange(content,index,countIndex){
+
+      //如果是链接的才上
+      if (content.contentType==3 && content.type==2 ) {
+
+        if (content.courseId!=null) {
+          // 查找选中的课程对应的 label
+          const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === content.courseId);
+
+          //响应式直接给链接的标题/封面上值
+          if (selectedCourse) {
+            this.$set(content, 'linkTitle', selectedCourse.dictLabel);
+            this.$set(content, 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+        }
+        if (content.videoId!=null){
+          // 查找选中的课程对应的 label
+          const selectedVideo = this.videoList[index][countIndex].find(course => parseInt(course.dictValue) === content.videoId);
+
+          //响应式直接给链接的描述上值
+          if (selectedVideo) {
+            this.$set(content, 'linkDescribe', selectedVideo.dictLabel);
+          }
+        }
+
+      }
+    },
+    videoIdChange(content,index,countIndex){
+
+      //选择了课程小节则 默认绑上
+      this.$set(content,'isBindUrl','1');
+
+      //如果是链接的才上
+      if (content.contentType==3 && content.type==2 && content.videoId!=null) {
+
+        // 查找选中的课程对应的 label
+        const selectedVideo = this.videoList[index][countIndex].find(course => parseInt(course.dictValue) === content.videoId);
+
+        //响应式直接给链接的描述上值
+        if (selectedVideo) {
+          this.$set(content, 'linkDescribe', selectedVideo.dictLabel);
+        }
+      }
+
+    },
+    //上传文件
+    handleAvatarSuccessFile(res, file, content) {
+      if (res.code === 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'fileUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadFile(file){
+      const isLt1M = file.size / 1024 / 1024 < 30;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 30MB!');
+      }
+      return isLt1M;
+    },
+
+    //下载文件
+    downloadUrl(materialUrl) {
+      // 直接返回文件 URL
+      return materialUrl;
+    },
+
+    handleAvatarSuccessVideo(res, file, content) {
+      if(res.code==200){
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'videoUrl', res.url);
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadVideo(file){
+      const isLt30M = file.size / 1024 / 1024 < 50;
+      const isMP4 = file.type === 'video/mp4';
+
+      if (!isMP4) {
+        this.$message.error('仅支持上传 MP4 格式的视频文件!');
+        return false;
+      }
+
+      if (!isLt30M) {
+        this.$message.error('上传大小不能超过 50MB!');
+        return false;
+      }
+
+      return true;
+    },
+
+    handleAvatarSuccessVoice(res, file, content){
+      if(res.code==200){
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'voiceUrl', res.url);
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadVoice(file){
+      const isLt10M = file.size / 1024 / 1024 < 10; //语音文件大小限制为10MB
+      const isVoiceType = ['audio/mp3'].includes(file.type); // 支持的语音文件类型
+
+      if (!isVoiceType) {
+        this.$message.error('仅支持上传 MP3 格式的语音文件!');
+        return false;
+      }
+
+      if (!isLt10M) {
+        this.$message.error('上传大小不能超过 10MB!');
+        return false;
+      }
+
+      return true;
+    },
+    /** 查询sop模板列表 */
+    getList() {
+      this.loading = true;
+      listSopTemp(this.queryParams).then(response => {
+        this.sopTempList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.$store.dispatch("tagsView/delView", this.$route);
+      this.$router.replace('/qw/conversion/sopTemp')
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        setting: null,
+        status: "0",
+        sort: null,
+        createTime: null,
+        createBy: null,
+        companyId: null
+      };
+      this.resetForm("form");
+    },
+
+
+
+    /** 修改按钮操作 */
+    handleUpdate(id) {
+
+      getSopTemp(id).then(response => {
+        this.videoList=[]
+        this.form=response.data;
+        this.setting = JSON.parse(response.data.setting);
+        for (let i = 0; i < this.setting.length; i++) {
+          this.videoList.push([])
+          for (let j = 0; j < this.setting[i].content.length; j++) {
+            this.videoList[i].push([])
+            if(this.setting[i].content[j].type==2){
+              if(this.setting[i].content[j].hasOwnProperty('courseId')){
+                this.courseChange(this.setting[i].content[j],i,j);
+              }
+            }
+          }
+        }
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        this.form.setting=JSON.stringify(this.setting)
+        if (valid) {
+          if (this.form.id != null) {
+            updateSopTemp(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.$store.dispatch("tagsView/delView", this.$route);
+              // this.$router.replace('/qw/conversion/sopTemp')
+              window.location.replace('/qw/conversion/sopTemp')
+              this.reset();
+
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除sop模板编号为"' + ids + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return delSopTemp(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有sop模板数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportSopTemp(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {});
+    }
+  }
+};
+</script>
+
+<style scoped>
+.custom-input /deep/ .el-input__inner {
+  height: 20px;
+  text-align:center;
+}
+.custom-input /deep/ .el-input__icon {
+  line-height: 10px;
+}
+</style>

+ 201 - 0
src/views/qw/sopTemp/updateTemp.vue

@@ -0,0 +1,201 @@
+<template>
+  <div class="app-container">
+
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 && formType==1 "> sop规则【修改企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 && formType==2 "> sop规则【复制企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 1 && formType==3 "> sop规则【查看企微接口】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 && formType==1 "> sop规则【修改AI插件】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 && formType==2 "> sop规则【复制AI插件】模板</div>
+    <div style="margin: 30px;" v-if="this.form.sendType == 2 && formType==3 "> sop规则【查看AI插件】模板</div>
+
+    <div style="margin-top: 10px;margin-left: 50px;margin-right: 100px;margin-bottom: 60px;">
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入模板标题" />
+        </el-form-item>
+
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="dict.dictValue"
+            >{{dict.dictLabel}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="间隔天数" prop="gap">
+          <el-input-number v-model="form.gap"  :min="1" label="间隔天数"></el-input-number>
+        </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>
+      <div slot="footer" class="dialog-footer" style="float: right;" v-if="formType==1 || formType==2 ">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { listSopTemp, getSopTemp, delSopTemp, updateTemp, exportSopTemp } from "@/api/qw/sopTemp";
+export default {
+  name: "updateSopTemp",
+  data() {
+    return {
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS2",
+      uploadUrlByVoice:process.env.VUE_APP_BASE_API+"/common/uploadOSSByHOOKVoice",
+      //上传语音的遮罩层
+      voiceLoading :false,
+
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      sysFsSopWatchStatus: [],
+      //消息内容类型 企微版
+      sysQwSopContentType:[],
+      //插件版
+      sysQwSopAiContentType:[],
+
+      //类别
+      sysQwSopSettingType:[],
+
+      courseList:[],
+      videoList:[],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // sop模板表格数据
+      setting: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 状态字典
+      statusOptions: [],
+      videoNumOptions:{
+        title:'选择视频号',
+        open:false,
+        content:null,
+        contentIndex:null,
+        setIndex:null,
+      },
+      //1 修改 2 复制 3 查看
+      formType:null,
+      // 查询参数
+      form: {
+        name: null,
+        setting: null,
+        status: "1",
+        sort: 1,
+        companyId: null,
+        gap:1,
+      },
+      // 表单校验
+      rules: {
+        name: [
+          { required: true, message: '名称不能为空', trigger: 'blur' }
+        ],
+        status: [
+          { required: true, message: '状态不能为空', trigger: 'blur' }
+        ],
+        sort: [
+          { required: true, message: '排序不能为空', trigger: 'blur' }
+        ],
+        gap: [
+          { required: true, message: '间隔天数不能为空', trigger: 'blur' }
+        ],
+      }
+    };
+  },
+  created() {
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    const id = this.$route.params && this.$route.params.id;
+    this.formType = this.$route.params && this.$route.params.type;
+    this.handleUpdate(id);
+  },
+  methods: {
+    // 取消按钮
+    cancel() {
+      this.$store.dispatch("tagsView/delView", this.$route);
+      this.$router.replace('/qw/conversion/sopTemp')
+      // this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        setting: null,
+        status: "0",
+        sort: null,
+        createTime: null,
+        createBy: null,
+        companyId: null
+      };
+      this.resetForm("form");
+    },
+
+
+    /** 修改按钮操作 */
+    handleUpdate(id) {
+      getSopTemp(id).then(response => {
+        this.form = response.data;
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.formType == 1) {
+            if (this.form.id != null) {
+              updateTemp(this.form).then(response => {
+                this.msgSuccess("修改成功");
+                this.$store.dispatch("tagsView/delView", this.$route);
+                // this.$router.replace('/qw/conversion/sopTemp')
+                window.location.replace('/qw/conversion/sopTemp')
+                this.reset();
+              });
+            }
+          } else {
+            //id制空,防止键值重复-报错
+            this.form.id = null
+            //更新时间制空
+            this.form.updateTime = null
+            addTemp(this.form).then(response => {
+              this.msgSuccess("复制成功");
+              this.$store.dispatch("tagsView/delView", this.$route);
+              // this.$router.replace('/qw/conversion/sopTemp')
+              window.location.replace('/qw/conversion/sopTemp')
+              this.reset();
+            });
+          }
+
+        }
+      });
+    },
+  }
+};
+</script>
+
+<style scoped>
+.custom-input /deep/ .el-input__inner {
+  height: 20px;
+  text-align: center;
+}
+
+.custom-input /deep/ .el-input__icon {
+  line-height: 10px;
+}
+</style>

+ 274 - 0
src/views/qw/userVideo/userVideo.vue

@@ -0,0 +1,274 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+        <el-alert
+          title="注意事项"
+          type="warning"
+          description="请先搜索相关【企业微信账号名称】,获取视频号信息"
+          :closable="false"
+          show-icon>
+        </el-alert>
+      <el-form-item label="企微账号名称" prop="qwUserName" >
+        <el-input
+          v-model="queryParams.qwUserName"
+          placeholder="请输入企微账号名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <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-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+        >删除</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="QwUserVideoList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="封面图片" align="center" prop="coverUrl">
+        <template slot-scope="scope">
+          <el-popover
+            placement="right"
+            title=""
+            trigger="hover"
+          >
+            <img slot="reference" :src="scope.row.coverUrl" width="100" >
+            <img :src="scope.row.coverUrl" style="max-width: 300px;">
+          </el-popover>
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar">
+        <template slot-scope="scope">
+          <el-popover
+            placement="right"
+            title=""
+            trigger="hover">
+            <img slot="reference" :src="scope.row.coverUrl" style="width: 50px;height: 50px">
+            <img :src="scope.row.coverUrl" style="max-width: 200px;max-height: 200px">
+          </el-popover>
+        </template>
+      </el-table-column>
+      <el-table-column label="封面标题" align="center" prop="nickname" />
+      <el-table-column label="简介" align="center" prop="desc" >
+        <template slot-scope="scope">
+          <el-tooltip class="item" effect="dark" :content="scope.row.desc" placement="top">
+            <div style="display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; text-overflow: ellipsis;">
+              <span>{{ scope.row.desc }}</span>
+            </div>
+          </el-tooltip>
+        </template>
+      </el-table-column>
+      <el-table-column label="视频地址" align="center" prop="url">
+        <template slot-scope="scope">
+          <el-tooltip class="item" effect="dark" :content="scope.row.url" placement="top">
+            <div style="display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; text-overflow: ellipsis;">
+              <span>{{ scope.row.url }}</span>
+            </div>
+          </el-tooltip>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-check"
+            @click="handleChangeVideoNum(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"
+    />
+
+  </div>
+</template>
+
+<script>
+import { listQwUserVideo, getQwUserVideo, delQwUserVideo, addQwUserVideo, updateQwUserVideo, exportQwUserVideo } from "@/api/qw/userVideo";
+
+export default {
+  name: "userVideo",
+  data() {
+    return {
+      // 遮罩层
+      loading: false,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 企业微信的视频号表格数据
+      QwUserVideoList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        appKey: null,
+        senderName: null,
+        qwUserName:null,
+        objectId: null,
+        coverUrl: null,
+        thumbUrl: null,
+        avatar: null,
+        nickname: null,
+        desc: null,
+        url: null,
+        extras: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+
+
+    /** 查询企业微信的视频号列表 */
+    getList() {
+      this.loading = true;
+      listQwUserVideo(this.queryParams).then(response => {
+        this.QwUserVideoList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        appKey: null,
+        senderName: null,
+        objectId: null,
+        coverUrl: null,
+        thumbUrl: null,
+        avatar: null,
+        nickname: null,
+        desc: null,
+        url: null,
+        extras: null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+
+    handleChangeVideoNum(val){
+        this.$emit("videoResult",val)
+    },
+
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateQwUserVideo(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addQwUserVideo(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除企业微信的视频号编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delQwUserVideo(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有企业微信的视频号数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportQwUserVideo(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>

+ 310 - 0
src/views/sop/companySopRole/index.vue

@@ -0,0 +1,310 @@
+<template>
+  <div class="app-container">
+<!--    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">-->
+<!--      <el-form-item label="销售公司" prop="companyId">-->
+<!--        <el-input-->
+<!--          v-model="queryParams.companyId"-->
+<!--          placeholder="请输入销售公司"-->
+<!--          clearable-->
+<!--          size="small"-->
+<!--          @keyup.enter.native="handleQuery"-->
+<!--        />-->
+<!--      </el-form-item>-->
+<!--      <el-form-item label="权限名称" prop="roleName">-->
+<!--        <el-input-->
+<!--          v-model="queryParams.roleName"-->
+<!--          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-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="['sop:companySopRole:add']"-->
+<!--        >新增</el-button>-->
+<!--      </el-col>-->
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="success"-->
+<!--          plain-->
+<!--          icon="el-icon-edit"-->
+<!--          size="mini"-->
+<!--          :disabled="single"-->
+<!--          @click="handleUpdate"-->
+<!--          v-hasPermi="['sop:companySopRole:edit']"-->
+<!--        >修改</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="['sop:companySopRole:remove']"-->
+<!--        >删除</el-button>-->
+<!--      </el-col>-->
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="warning"-->
+<!--          plain-->
+<!--          icon="el-icon-download"-->
+<!--          size="mini"-->
+<!--          :loading="exportLoading"-->
+<!--          @click="handleExport"-->
+<!--          v-hasPermi="['sop:companySopRole:export']"-->
+<!--        >导出</el-button>-->
+<!--      </el-col>-->
+<!--    </el-row>-->
+
+    <el-table border v-loading="loading" :data="companySopRoleList">
+      <el-table-column label="销售公司" align="center" prop="companyId">
+        <template slot-scope="scope">
+          <el-tag v-for="item in companyList" v-if="item.companyId == scope.row.companyId"> {{scope.row.companyName}}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="权限" align="center">
+        <template slot-scope="scope">
+          <div style="display: flex;flex-flow: wrap;justify-content: flex-start;">
+            <el-tag style="width: 30%;margin: 0 5px" v-for="item in scope.row.rules">{{roleMap[item]}}</el-tag>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['sop:companySopRole:edit']"
+          >修改</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"
+    />
+
+    <!-- 添加或修改公司SOP权限对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="700px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="销售公司" prop="companyId">
+          <el-select filterable  v-model="form.companyId" placeholder="请选择公司名" size="small">
+            <el-option
+              v-for="item in companyList"
+              :key="item.companyId"
+              :label="item.companyName"
+              :value="item.companyId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="权限" prop="rules">
+          <div style="margin: 15px 0;"></div>
+          <el-checkbox-group v-model="form.rules" style="display: flex;flex-flow: wrap;justify-content: flex-start;">
+            <el-checkbox style="width: 28%" v-for="item in roleOptions" :label="item.dictValue" :key="item.dictValue">{{item.dictLabel}}</el-checkbox>
+          </el-checkbox-group>
+        </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>
+  </div>
+</template>
+
+<script>
+import { listCompanySopRole, getCompanySopRole, delCompanySopRole, addCompanySopRole, updateCompanySopRole, exportCompanySopRole } from "@/api/sop/companySopRole";
+import { getCompanyList } from "@/api/company/company";
+
+export default {
+  name: "CompanySopRole",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 公司SOP权限表格数据
+      companySopRoleList: [],
+      roleOptions: [],
+      roleMap: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      companyList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyId: null,
+        roleName: null,
+        roleValue: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    this.getList();
+    this.getDicts("company_sop_role").then(response => {
+      this.roleOptions = response.data;
+      this.roleMap = response.data.reduce((obj, item) => {
+        obj[item.dictValue] = item.dictLabel;
+        return obj;
+      }, {});
+    });
+    getCompanyList().then(response => {
+      this.companyList = response.data;
+    });
+  },
+  methods: {
+    /** 查询公司SOP权限列表 */
+    getList() {
+      this.loading = true;
+      listCompanySopRole(this.queryParams).then(response => {
+        console.info(response.rows)
+        this.companySopRoleList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        companyId: null,
+        roleName: null,
+        roleValue: null,
+        createTime: null,
+        createBy: null,
+        updateBy: null,
+        updateTime: null,
+        rules: this.roleOptions.map(e => e.dictValue),
+        remark: null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加公司SOP权限";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      if(row.rules && row.rules.length > 0){
+        this.form.rules = row.rules;
+      }
+      this.form.companyId = row.companyId;
+      this.open = true;
+      this.title = "修改公司SOP权限";
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateCompanySopRole(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addCompanySopRole(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除公司SOP权限编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delCompanySopRole(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有公司SOP权限数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportCompanySopRole(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    },
+  }
+};
+</script>

+ 169 - 19
src/views/system/config/config.vue

@@ -1,14 +1,13 @@
 <template>
   <div class="app-container">
      <el-tabs v-model="activeName" @tab-click="handleClick" >
-      <el-tab-pane label="云存储配置" name="sys.oss.cloudStorage">
+      <el-tab-pane label="OSS配置" name="sys.oss.cloudStorage">
           <el-form ref="form1" :model="form1" :rules="rules1" label-width="160px">
             <el-form-item label="类型" prop="type">
                <el-radio-group v-model="form1.type">
                 <el-radio :label="1">七牛云</el-radio>
                 <el-radio :label="2">阿里云</el-radio>
                 <el-radio :label="3">腾讯云</el-radio>
-                 <el-radio :label="4">华为云</el-radio>
               </el-radio-group>
             </el-form-item>
             <el-form-item v-if="form1.type==1" label="七牛绑定的域名" prop="qiniuDomain">
@@ -61,22 +60,6 @@
             </el-form-item>
              <el-form-item v-if="form1.type==3" label="所属地区" prop="qcloudRegion">
                 <el-input  v-model="form1.qcloudRegion" label="所属地区不能为空"></el-input>
-            </el-form-item>
-            <el-form-item v-if="form1.type==4" label="华为云绑定的域名" prop="huaweiDomain">
-              <el-input  v-model="form1.huaweiDomain" label="华为云绑定的域名格式不正确"></el-input>
-            </el-form-item>
-            <el-form-item v-if="form1.type==4" label="华为云Endpoint" prop="huaweiBucketName">
-              <el-input  v-model="form1.huaweiEndpoint" label="华为云Endpoint不能为空"></el-input>
-            </el-form-item>
-
-            <el-form-item v-if="form1.type==4" label="华为云AK" prop="huaweiAK">
-              <el-input  v-model="form1.huaweiAK" label="华为云AK不能为空"></el-input>
-            </el-form-item>
-            <el-form-item v-if="form1.type==4" label="华为云SK" prop="huaweiSK">
-              <el-input  v-model="form1.huaweiSK" label="华为云SK不能为空"></el-input>
-            </el-form-item>
-            <el-form-item v-if="form1.type==4" label="华为云BucketName" prop="huaweiBucketName">
-              <el-input  v-model="form1.huaweiBucketName" label="华为云BucketName不能为空"></el-input>
             </el-form-item>
              <div   class="footer">
               <el-button type="primary" @click="submitForm1">提  交</el-button>
@@ -929,6 +912,36 @@
              </el-radio-group>
            </el-form-item>
 
+           <el-form-item label="红包金额" v-if="form18.rewardType == 1">
+             <el-tooltip class="item" effect="dark" content="课程默认红包金额" placement="top-end">
+               <el-input-number  v-model="form18.redPackageMoney" :min="0.01" ></el-input-number>
+             </el-tooltip>
+           </el-form-item>
+
+
+           <el-form-item label="禁止发送时间段">
+             <el-row v-for="(item, index) in form18.disabledTimeList" style="margin-top: 10px">
+               <el-time-picker
+                 class="custom-input"
+                 v-model="item.startDisabledTime"
+                 value-format="HH:mm"
+                 format="HH:mm"
+                 placeholder="开始时间"
+                 style="width: 130px;height: 20px;">
+               </el-time-picker>
+               <el-time-picker
+                 class="custom-input"
+                 v-model="item.endDisabledTime"
+                 value-format="HH:mm"
+                 format="HH:mm"
+                 placeholder="结束时间"
+                 style="width: 130px;height: 20px;margin-left: 10px">
+               </el-time-picker>
+               <el-button type="danger" icon="el-icon-delete" circle style="margin-left: 20px" @click="removeDisabledTime(index)"></el-button>
+             </el-row>
+             <el-button @click="addDisabledTime" style="margin-top: 10px">添加时间段</el-button>
+           </el-form-item>
+
            <el-form-item label="红包模式" v-if="form18.rewardType==1">
              <el-radio-group v-model="form18.redPacketMode">
                <el-radio label="1">总公司</el-radio>
@@ -942,6 +955,69 @@
            </div>
          </el-form>
        </el-tab-pane>
+       <el-tab-pane label="点播小程序配置" name="courseMa.config">
+         <el-button type="primary" @click="addCourseMaConfig" style="margin-bottom: 10px">添加配置</el-button>
+         <el-table  height="660" border v-loading="courseMaConfigLoading" :data="courseMaConfigList">
+           <el-table-column label="小程序名称" align="center" prop="name">
+             <template slot-scope="scope">
+               <el-input v-model="scope.row.name" placeholder="请输入小程序名称" :disabled="!scope.row.editing" />
+             </template>
+           </el-table-column>
+           <el-table-column label="AppId" align="center" prop="appid">
+             <template slot-scope="scope">
+               <el-input v-model="scope.row.appid" placeholder="请输入AppId" :disabled="!scope.row.editing" />
+             </template>
+           </el-table-column>
+           <el-table-column label="Secret" align="center" prop="secret">
+             <template slot-scope="scope">
+               <el-input v-model="scope.row.secret" placeholder="请输入Secret" :disabled="!scope.row.editing" />
+             </template>
+           </el-table-column>
+           <el-table-column label="token" align="center" prop="token">
+             <template slot-scope="scope">
+               <el-input v-model="scope.row.token" placeholder="请输入token" :disabled="!scope.row.editing" />
+             </template>
+           </el-table-column>
+           <el-table-column label="aesKey" align="center" prop="aesKey">
+             <template slot-scope="scope">
+               <el-input v-model="scope.row.aesKey" placeholder="请输入aesKey" :disabled="!scope.row.editing" />
+             </template>
+           </el-table-column>
+           <el-table-column label="msgDataFormat" align="center" prop="msgDataFormat">
+             <template slot-scope="scope">
+               <el-input v-model="scope.row.msgDataFormat" placeholder="请输入消息格式" :disabled="!scope.row.editing" />
+             </template>
+           </el-table-column>
+           <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+             <template slot-scope="scope">
+               <el-button
+                 v-if="!scope.row.editing"
+                 size="mini"
+                 type="text"
+                 icon="el-icon-edit"
+                 @click="handleEdit(scope.$index, scope.row)"
+               >编辑</el-button>
+               <el-button
+                 v-if="scope.row.editing"
+                 size="mini"
+                 type="text"
+                 icon="el-icon-check"
+                 @click="handleSave(scope.$index, scope.row)"
+               >保存</el-button>
+               <el-button
+                 size="mini"
+                 type="text"
+                 icon="el-icon-delete"
+                 @click="deleteCourseMaConfig(scope.$index)"
+               >删除</el-button>
+             </template>
+           </el-table-column>
+         </el-table>
+         <div class="footer">
+
+           <el-button type="primary" @click="submitCourseMaConfig">提 交</el-button>
+         </div>
+       </el-tab-pane>
        <el-tab-pane label="红包商户配置" name="redPacket.config" >
          <el-form ref="form19" :model="form19"  label-width="150px">
            <el-form-item   label="红包接口类型" prop="isNew">
@@ -1096,6 +1172,8 @@ export default {
   },
   data() {
     return {
+      courseMaConfigLoading:false,
+      courseMaConfigList:[],
       deliveryGift:{
         open:false,
         title:"商品选择"
@@ -1194,6 +1272,24 @@ export default {
     },
   },
   methods: {
+    submitCourseMaConfig() {
+      // 验证所有必填字段
+      const isValid = this.courseMaConfigList.every(item => {
+        return item.name && item.appid && item.secret;
+      });
+
+      if (!isValid) {
+        this.$message.error('请填写所有必填字段');
+        return;
+      }
+
+      var param = {configId: this.configId, configValue: JSON.stringify(this.courseMaConfigList)};
+      updateConfigByKey(param).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess("修改成功");
+        }
+      });
+    },
       deleteInquirySubType(index,row){
         this.form2.inquirySubType.splice(index, 1);
       },
@@ -1319,6 +1415,18 @@ export default {
           if(key=="qwRating.config"){
             this.form20 =JSON.parse(response.data.configValue);
           }
+          if(key=="courseMa.config"){
+            this.courseMaConfigLoading = true;
+            if(response.data && response.data.configValue) {
+              this.courseMaConfigList = JSON.parse(response.data.configValue).map(item => ({
+                ...item,
+                editing: false
+              }));
+            } else {
+              this.courseMaConfigList = [];
+            }
+            this.courseMaConfigLoading = false;
+          }
         });
      },
     /** 提交按钮 */
@@ -1525,7 +1633,49 @@ export default {
           this.msgSuccess("清理成功");
         }
       });
-    }
+    },
+    deleteCourseMaConfig(index) {
+      this.$confirm('确认删除该配置?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.courseMaConfigList.splice(index, 1);
+        this.$message.success('删除成功');
+      }).catch(() => {});
+    },
+    addCourseMaConfig() {
+      this.courseMaConfigList.push({
+        name: "",
+        appid: "",
+        secret: "",
+        token: "",
+        aesKey: "",
+        msgDataFormat: "",
+        editing: true
+      });
+    },
+    handleEdit(index, row) {
+      this.$set(this.courseMaConfigList[index], 'editing', true);
+    },
+    handleSave(index, row) {
+      // 验证必填字段
+      if (!row.name || !row.appid || !row.secret) {
+        this.$message.error('请填写所有必填字段');
+        return;
+      }
+      this.$set(this.courseMaConfigList[index], 'editing', false);
+    },
+    addDisabledTime(){
+      if(this.form18.disabledTimeList == null || this.form18.disabledTimeList == undefined){
+        this.form18.disabledTimeList = []
+      }
+      this.form18.disabledTimeList.push({startDisabledTime: "", endDisabledTime: ""});
+      this.$forceUpdate();
+    },
+    removeDisabledTime(index){
+      this.form18.disabledTimeList.splice(index, 1);
+    },
   }
 };
 </script>