Просмотр исходного кода

feat:app独立看课迁移到销售端、添加销售公司相关数据过滤功能

caoliqin 12 часов назад
Родитель
Сommit
9662c65014
66 измененных файлов с 27115 добавлено и 9 удалено
  1. 9 0
      src/api/app/article/index.js
  2. 47 0
      src/api/app/course/courseFinishTemp.js
  3. 17 0
      src/api/app/course/index.js
  4. 10 0
      src/api/app/cpsc/index.js
  5. 48 0
      src/api/app/customerBind/index.js
  6. 52 0
      src/api/app/customerMember/index.js
  7. 37 0
      src/api/app/customerRole/index.js
  8. 63 0
      src/api/app/groupChat/index.js
  9. 32 0
      src/api/app/invitationCode/index.js
  10. 19 0
      src/api/app/live/index.js
  11. 9 0
      src/api/app/medicines/index.js
  12. 9 0
      src/api/app/openClassVideo/index.js
  13. 9 0
      src/api/app/pkg/index.js
  14. 9 0
      src/api/app/product/index.js
  15. 9 0
      src/api/app/shortVideo/index.js
  16. 156 0
      src/api/app/sop/info/index.js
  17. 150 0
      src/api/app/sop/template/index.js
  18. 74 0
      src/api/app/sopLogs/index.js
  19. 34 0
      src/api/app/tag/bindTag.js
  20. 69 0
      src/api/app/tag/tagGroup.js
  21. 38 0
      src/api/app/urgentClass/index.js
  22. 64 0
      src/api/app/user/index.js
  23. 37 0
      src/api/app/user/portrait.js
  24. 26 0
      src/api/app/user/userInfo.js
  25. 26 0
      src/api/app/user/userList.js
  26. 40 0
      src/api/app/userChatLogs/index.js
  27. 126 0
      src/api/app/userLogs/index.js
  28. 79 0
      src/api/app/userLogsInfo/index.js
  29. 64 0
      src/api/app/welcome/index.js
  30. 34 0
      src/api/course/courseWatchLog.js
  31. 77 0
      src/api/system/appDept.js
  32. 52 0
      src/router/index.js
  33. 37 0
      src/utils/openIM.js
  34. 2 1
      src/utils/request.js
  35. 319 0
      src/views/app/appDept/index.vue
  36. 1206 0
      src/views/app/courseFinishTemp/index.vue
  37. 356 0
      src/views/app/customerBind/index.vue
  38. 561 0
      src/views/app/customerMember/index.vue
  39. 215 0
      src/views/app/customerRole/index.vue
  40. 908 0
      src/views/app/invitationCode/index.vue
  41. 309 0
      src/views/app/sop/info/GroupChatSelector.vue
  42. 205 0
      src/views/app/sop/info/ImageUpload.vue
  43. 542 0
      src/views/app/sop/info/TimeRangePicker.vue
  44. 969 0
      src/views/app/sop/info/addSop.vue
  45. 1603 0
      src/views/app/sop/info/index.vue
  46. 624 0
      src/views/app/sop/logs/index.vue
  47. 579 0
      src/views/app/sop/template/index.vue
  48. 171 0
      src/views/app/sop/template/sopTemp.vue
  49. 3094 0
      src/views/app/sop/template/updateSopTemp.vue
  50. 1207 0
      src/views/app/sop/userLogs/sopLogsList.vue
  51. 613 0
      src/views/app/sop/userLogs/sopUserLogsSchedule.vue
  52. 1837 0
      src/views/app/sop/userLogsInfo/groupSendMessage.vue
  53. 1269 0
      src/views/app/sop/userLogsInfo/sendMsgOpenTool.vue
  54. 689 0
      src/views/app/sop/userLogsInfo/sopUserLogsInfoDetails.vue
  55. 269 0
      src/views/app/tagGroup/index.vue
  56. 163 0
      src/views/app/tagGroup/selectTag.vue
  57. 2068 0
      src/views/app/urgentClass/CustomerDetail.vue
  58. 1245 0
      src/views/app/urgentClass/index.vue
  59. 274 0
      src/views/app/user/CusRoleList.vue
  60. 150 0
      src/views/app/user/UserList.vue
  61. 1362 0
      src/views/app/user/index.vue
  62. 248 0
      src/views/app/userChatLogs/index.vue
  63. 545 0
      src/views/app/userPortrait/index.vue
  64. 1079 0
      src/views/app/welcome/index.vue
  65. 864 0
      src/views/course/courseWatchLog/indexApp.vue
  66. 8 8
      src/views/index.vue

+ 9 - 0
src/api/app/article/index.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+export function getArticleOptions(data) {
+  return request({
+    url: '/app/article/findOptions',
+    method: 'get',
+    params: data,
+  })
+}

+ 47 - 0
src/api/app/course/courseFinishTemp.js

@@ -0,0 +1,47 @@
+import request from '@/utils/request';
+
+export function findList(data) {
+  return request({
+    url: '/app/courseFinishTemp/findList',
+    method: 'get',
+    params: data
+  })
+}
+
+export function exportList(data) {
+  return request({
+    url: '/app/courseFinishTemp/export',
+    method: 'get',
+    params: data
+  })
+}
+
+export function getById(id) {
+  return request({
+    url: '/app/courseFinishTemp/getById/' + id,
+    method: 'get',
+  })
+}
+
+export function add(data) {
+  return request({
+    url: '/app/courseFinishTemp/add',
+    method: 'post',
+    data
+  })
+}
+
+export function edit(data) {
+  return request({
+    url: '/app/courseFinishTemp/edit',
+    method: 'post',
+    data
+  })
+}
+
+export function deleteByIds(ids) {
+  return request({
+    url: '/app/courseFinishTemp/deleteByIds/' + ids,
+    method: 'delete'
+  })
+}

+ 17 - 0
src/api/app/course/index.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request';
+
+export function courseList(data) {
+  return request({
+    url: '/app/course/courseList',
+    method: 'get',
+    params: data
+  })
+}
+
+export function videoList(data) {
+  return request({
+    url: '/app/course/videoList',
+    method: 'get',
+    params: data
+  })
+}

+ 10 - 0
src/api/app/cpsc/index.js

@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+export function getList() {
+  return request({
+    url: '/app/cpsc/getList',
+    method: 'get'
+  })
+}
+
+

+ 48 - 0
src/api/app/customerBind/index.js

@@ -0,0 +1,48 @@
+import request from '@/utils/request'
+
+// 查询绑定列表
+export function findList(query) {
+  return request({
+    url: '/app/comuserdept/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getById(id) {
+  return request({
+    url: '/app/comuserdept/getById/' + id,
+    method: 'get',
+  })
+}
+
+export function deleteByIds(ids) {
+  return request({
+    url: '/app/comuserdept/deleteByIds/' + ids,
+    method: 'delete',
+  })
+}
+
+export function add(data) {
+  return request({
+    url: '/app/comuserdept/add',
+    method: 'post',
+    data: data
+  })
+}
+
+export function edit(data) {
+  return request({
+    url: '/app/comuserdept/edit',
+    method: 'post',
+    data: data
+  })
+}
+
+export function findCompanyUserOptions(data) {
+  return request({
+    url: '/company/user/findOptions',
+    method: 'get',
+    params: data,
+  })
+}

+ 52 - 0
src/api/app/customerMember/index.js

@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 查询客服分页列表
+export function listCustomerMember(query) {
+  return request({
+    url: '/app/cusrole/member/pageList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 新增客服
+export function addCustomerMember(data) {
+  return request({
+    url: '/app/cusrole/member',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改客服
+export function updateCustomerMember(data) {
+  return request({
+    url: '/app/cusrole/member',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除客服
+export function delCustomerMember(ids) {
+  return request({
+    url: '/app/cusrole/member/' + ids,
+    method: 'delete'
+  })
+}
+
+// 同步销售(异步)
+export function syncSalesCustomerMember() {
+  return request({
+    url: '/app/cusrole/member/syncSales',
+    method: 'post'
+  })
+}
+
+// 查询同步销售任务状态
+export function getSyncSalesCustomerMemberStatus() {
+  return request({
+    url: '/app/cusrole/member/syncSales/status',
+    method: 'get'
+  })
+}

+ 37 - 0
src/api/app/customerRole/index.js

@@ -0,0 +1,37 @@
+import request from '@/utils/request'
+
+// 查询客服组分页列表
+export function listCustomerRole(query) {
+  return request({
+    url: '/app/cusrole/pageList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询客服组下拉选项
+export function listCustomerRoleOptions() {
+  return request({
+    url: '/app/cusrole/pageList',
+    method: 'get',
+    params: { pageNum: 1, pageSize: 999 }
+  })
+}
+
+// 新增客服组
+export function addCustomerRole(data) {
+  return request({
+    url: '/app/cusrole',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改客服组
+export function updateCustomerRole(data) {
+  return request({
+    url: '/app/cusrole',
+    method: 'put',
+    data: data
+  })
+}

+ 63 - 0
src/api/app/groupChat/index.js

@@ -0,0 +1,63 @@
+import request from '@/utils/request'
+
+// 查询客户群详情列表
+export function listGroupChat(query) {
+  return request({
+    url: '/qw/groupChat/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function listGroupChatDeptList(query) {
+  return request({
+    url: '/qw/groupChat/deptList',
+    method: 'get',
+    params: query
+  })
+}
+export function listGroupChatMyList(query) {
+  return request({
+    url: '/qw/groupChat/myList',
+    method: 'get',
+    params: query
+  })
+}
+// 查询客户群详情详细
+export function getGroupChat(chatId) {
+  return request({
+    url: '/qw/groupChat/' + chatId,
+    method: 'get'
+  })
+}
+export function allList(corpId) {
+  return request({
+    url: '/qw/groupChat/allList/' + corpId,
+    method: 'get'
+  })
+}
+
+// 同步客户群详情
+export function cogradientGroupChat(corpId) {
+  return request({
+    url: '/app/groupChat/cogradientGroupChat/'+corpId,
+    method: 'get',
+  })
+}
+
+export function cogradientMyGroupChat(corpId) {
+  return request({
+    url: '/qw/groupChat/cogradientMyGroupChat/'+corpId,
+    method: 'get',
+  })
+}
+
+
+
+export function listAll(qwUserIds, corpId, sopId, searchMsg) {
+  return request({
+    url: '/app/groupChat/listAll',
+    method: 'get',
+    params:{qwUserIds, corpId, sopId, searchMsg}
+  })
+}

+ 32 - 0
src/api/app/invitationCode/index.js

@@ -0,0 +1,32 @@
+import request from '@/utils/request'
+
+export function findList(data) {
+  return request({
+    url: '/app/invCode/findList',
+    method: 'get',
+    params: data
+  })
+}
+
+export function save(data) {
+  return request({
+    url: '/app/invCode/save',
+    method: 'post',
+    data: data
+  })
+} 
+
+export function deleteByIds(ids) {
+  return request({
+    url: '/app/invCode/deleteByIds/' + ids,
+    method: 'delete'
+  })
+} 
+
+export function getById(data) {
+  return request({
+    url: '/app/invCode/getById',
+    method: 'get',
+    params: data
+  })
+} 

+ 19 - 0
src/api/app/live/index.js

@@ -0,0 +1,19 @@
+import request from '@/utils/request'
+
+/** 直播分组下拉 */
+export function getLiveGroupTypeList(query) {
+  return request({
+    url: '/live/liveGroupType/list',
+    method: 'get',
+    params: query || { pageNum: 1, pageSize: 999 }
+  })
+}
+
+/** 直播间下拉(可按分组筛选) */
+export function getLiveOptions(params) {
+  return request({
+    url: '/live/live/getOptions',
+    method: 'get',
+    params
+  })
+}

+ 9 - 0
src/api/app/medicines/index.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+export function getMedicinesOptions(data) {
+  return request({
+    url: '/app/medicines/findOptions',
+    method: 'get',
+    params: data,
+  })
+}

+ 9 - 0
src/api/app/openClassVideo/index.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+export function getOpenClassVideoOptions(data) {
+  return request({
+    url: '/app/openClassVideo/findOptions',
+    method: 'get',
+    params: data,
+  })
+}

+ 9 - 0
src/api/app/pkg/index.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+export function getPackageOptions(data) {
+  return request({
+    url: '/store/package/getOptions',
+    method: 'get',
+    params: data
+  })
+}

+ 9 - 0
src/api/app/product/index.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+export function getProductOptions(data) {
+  return request({
+    url: '/app/civilianGoods/findOptions',
+    method: 'get',
+    params: data,
+  })
+}

+ 9 - 0
src/api/app/shortVideo/index.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+export function getShortVideoOptions(data) {
+  return request({
+    url: '/app/shortVideo/findOptions',
+    method: 'get',
+    params: data,
+  })
+}

+ 156 - 0
src/api/app/sop/info/index.js

@@ -0,0 +1,156 @@
+import request from '@/utils/request'
+
+// 查询企微sop列表
+export function listSop(query) {
+  return request({
+    url: '/app/sop/list',
+    method: 'get',
+    params: query
+  })
+}
+export function listMySop(query) {
+  return request({
+    url: '/app/sop/myList',
+    method: 'get',
+    params: query
+  })
+}
+export function getTempUserList(id) {
+  return request({
+    url: '/app/sop/getTempUserList',
+    method: 'get',
+    params: {id}
+  })
+}
+export function listDeptSop(query) {
+  return request({
+    url: '/app/sop/deptList',
+    method: 'get',
+    params: query
+  })
+}
+// 查询企微sop模板列表
+export function listAiChatSop(query) {
+  return request({
+    url: '/app/sop/listAiChatSop',
+    method: 'get',
+    params: query
+  })
+}
+
+export function courseList(data) {
+  return request({
+    url: '/app/course/courseList',
+    method: 'get',
+    params: data
+  })
+}
+export function videoList(id,query) {
+
+  return request({
+    url: '/app/sop/videoList/' + id,
+    params: query,
+    method: 'get'
+  })
+}
+// 查询企微sop详细
+export function getSop(params) {
+  return request({
+    url: '/app/sop/getById',
+    method: 'get',
+    params: params
+  })
+}
+
+// 新增sop
+export function addSop(data) {
+  return request({
+    url: '/app/sop/add',
+    method: 'post',
+    data: data
+  })
+}
+
+// 新增企微sop
+export function updateTemp(data) {
+  return request({
+    url: '/app/sop/updateTemp',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改企微sop
+export function updateSop(data) {
+  return request({
+    url: '/app/sop/edit',
+    method: 'post',
+    data: data
+  })
+}
+//修改sop自动创建时间
+export function updateAutoSopTime(data) {
+  return request({
+    url: '/app/sop/updateAutoSopTime',
+    method: 'post',
+    data: data
+  })
+}
+
+//修改状态
+export function updateSopStatus(data) {
+  return request({
+    url: '/app/sop/updateSopStatus',
+    method: 'post',
+    data: data
+  })
+}
+// 删除sop
+export function delSop(id) {
+  return request({
+    url: '/app/sop/delete/' + id,
+    method: 'delete'
+  })
+}
+
+//批量执行sop
+export function executeSop(ids) {
+  return request({
+    url: '/app/sop/executeSop/' + ids,
+    method: 'put'
+  })
+}
+//批量执行企微sop先改状态
+export function updateStatus(ids) {
+  return request({
+    url: '/app/sop/updateStatus/' + ids,
+    method: 'get'
+  })
+}
+// 导出企微sop
+export function exportSop(query) {
+  return request({
+    url: '/app/sop/export',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出企微sop
+export function getSopVoiceList(params) {
+  return request({
+    url: '/app/sop/getSopVoiceList',
+    method: 'get',
+    params
+  })
+}
+
+
+// 修改企微sop
+export function updateSopQwUser(data) {
+  return request({
+    url: '/app/sop/updateSopQwUser',
+    method: 'post',
+    data: data
+  })
+}

+ 150 - 0
src/api/app/sop/template/index.js

@@ -0,0 +1,150 @@
+import request from '@/utils/request'
+
+// 查询sop模板列表
+export function listSopTemp(query) {
+  return request({
+    url: '/app/sopTemp/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function listMySopTemp(query) {
+  return request({
+    url: '/app/sopTemp/myList',
+    method: 'get',
+    params: query
+  })
+}
+
+export function listDeptSopTemp(query) {
+  return request({
+    url: '/app/sopTemp/deptList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询sop模板详细
+export function getSopTemp(id) {
+  return request({
+    url: '/app/sopTemp/' + id,
+    method: 'get'
+  })
+}
+export function dayListFun(id) {
+  return request({
+    url: '/app/sopTemp/dayList',
+    method: 'get',
+    params: {id}
+  })
+}
+// 查询sop模板详细
+export function sortDay(list) {
+  return request({
+    url: '/app/sopTemp/sortDay',
+    method: 'post',
+    data: list
+  })
+}
+// 查询sop模板详细
+export function selectRulesInfo(id, name, dayNum) {
+  return request({
+    url: '/app/sopTemp/selectRulesInfo',
+    method: 'get',
+    params: {id, name, dayNum}
+  })
+}
+// 查询sop模板详细
+export function delRules(id, name, dayNum) {
+  return request({
+    url: '/app/sopTemp/delRules',
+    method: 'get',
+    params: {id, name, dayNum}
+  })
+}
+
+// 新增sop模板
+export function addSopTemp(data) {
+  return request({
+    url: '/app/sopTemp/sopTemp',
+    method: 'post',
+    data: data
+  })
+}
+// 新增sop模板
+export function addTemp(data) {
+  return request({
+    url: '/app/sopTemp/add',
+    method: 'post',
+    data: data
+  })
+}
+
+// 新增sop模板
+export function copyTemplate(data) {
+  return request({
+    url: '/app/sopTemp/copy',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改sop模板
+export function updateTemp(data) {
+  return request({
+    url: '/app/sopTemp/edit',
+    method: 'post',
+    data: data
+  })
+}
+// 修改sop模板
+export function addOrUpdateSetting(data) {
+  return request({
+    url: '/app/sopTemp/addOrUpdateSetting',
+    method: 'post',
+    data: data
+  })
+}
+// 修改sop模板
+export function updateSopTemp(data) {
+  return request({
+    url: '/app/sopTemp/sopTemp',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除sop模板
+export function delSopTemp(id) {
+  return request({
+    url: '/app/sopTemp/' + id,
+    method: 'delete'
+  })
+}
+// 删除sop模板
+export function updateImage(id) {
+  return request({
+    url: '/app/sopTemp/updateImage',
+    method: 'get',
+    params: {id}
+  })
+}
+
+
+export function shareSopTemp(data) {
+  return request({
+    url: '/app/sopTemp/shareTemp',
+    method: 'post',
+    data: data
+  })
+}
+
+// 导出sop模板
+export function exportSopTemp(query) {
+  return request({
+    url: '/app/sopTemp/export',
+    method: 'get',
+    params: query
+  })
+}

+ 74 - 0
src/api/app/sopLogs/index.js

@@ -0,0 +1,74 @@
+import request from '@/utils/request'
+
+// 查询SOP  定时任务列表
+export function listQwSopLogs(query) {
+  return request({
+    url: '/qw/sopLogs/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询SOP  定时任务列表
+export function listSopLogsList(query) {
+  return request({
+    url: '/app/sopLogs/findList',
+    method: 'get',
+    params: query
+  })
+}
+
+
+
+// 查询SOP  定时任务详细
+export function getQwSopLogs(id) {
+  return request({
+    url: '/qw/sopLogs/' + id,
+    method: 'get'
+  })
+}
+
+// 新增SOP  定时任务
+export function addQwSopLogs(data) {
+  return request({
+    url: '/qw/sopLogs',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改SOP  定时任务
+export function updateQwSopLogs(data) {
+  return request({
+    url: '/qw/sopLogs',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除SOP  定时任务
+export function delSopLogs(ids) {
+  return request({
+    url: '/app/sopLogs/deleteByIds/' + ids,
+    method: 'delete'
+  })
+}
+
+//修改 执行记录为待发送
+export function editCourseSopLogs(id) {
+  return request({
+    url: '/app/sopLogs/editCourseSopLogs/' + id,
+    method: 'put'
+  })
+}
+
+
+
+// 导出SOP  定时任务
+export function exportSopLogs(query) {
+  return request({
+    url: '/app/sopLogs/export',
+    method: 'get',
+    params: query
+  })
+}

+ 34 - 0
src/api/app/tag/bindTag.js

@@ -0,0 +1,34 @@
+import request from '@/utils/request'
+
+//
+export function bindTag(param) {
+  return request({
+    url: '/vip/userTag/bind',
+    method: 'post',
+    data: param
+  })
+}
+
+export function unbindTag(param) {
+  return request({
+    url: '/vip/userTag/unbind',
+    method: 'post',
+    data: param
+  })
+}
+
+export function bindTagV2(param) {
+  return request({
+    url: '/vip/userTag/bind/v2',
+    method: 'post',
+    data: param
+  })
+}
+
+export function unbindTagV2(param) {
+  return request({
+    url: '/vip/userTag/unbind/v2',
+    method: 'post',
+    data: param
+  })
+}

+ 69 - 0
src/api/app/tag/tagGroup.js

@@ -0,0 +1,69 @@
+import request from '@/utils/request'
+
+// 查询会员标签组列表
+export function listTagGroup(query) {
+  return request({
+    url: '/vip/tagGroup/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function listTag(query) {
+  return request({
+    url: '/vip/tag/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询会员标签组列表
+export function listTagGroupForUserBindTag(query) {
+  return request({
+    url: '/vip/tagGroup/listForUserBindTag',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询会员标签组详细
+export function getTagGroup(id) {
+  return request({
+    url: '/vip/tagGroup/getById/' + id,
+    method: 'get'
+  })
+}
+
+// 批量查询会员标签组详细
+export function getTagByIds(ids) {
+  return request({
+    url: '/vip/tagGroup/getTagByIds/' + ids,
+    method: 'get'
+  })
+}
+
+// 新增会员标签组
+export function addTagGroup(data) {
+  return request({
+    url: '/vip/tagGroup/add',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改会员标签组
+export function updateTagGroup(data) {
+  return request({
+    url: '/vip/tagGroup/edit',
+    method: 'post',
+    data: data
+  })
+}
+
+export function searchTags(query) {
+  return request({
+    url: '/vip/tagGroup/listForUserBindTag',
+    method: 'get',
+    params: query
+  })
+}

+ 38 - 0
src/api/app/urgentClass/index.js

@@ -0,0 +1,38 @@
+import request from '@/utils/request'
+
+/**
+ * 查询催课任务看板列表
+ * @param {*} query
+ * @returns
+ */
+export function listUrgentClassTask(query) {
+  return request({
+    url: '/app/urgentClass/list',
+    method: 'get',
+    params: query
+  })
+}
+
+/**
+ * 设置状态
+ * @param {*} query
+ * @returns
+ */
+export function setStatus(data) {
+  return request({
+    url: '/app/urgentClass/setStatus',
+    method: 'post',
+    data: data
+  })
+}
+/**
+ * 查询app-任务看板用户电话
+ * @param {*} fsUserId
+ * @returns
+ */
+export function getFsUserPhone(fsUserId) {
+  return request({
+    url: '/app/urgentClass/getFsUserPhone/' + fsUserId,
+    method: 'get'
+  })
+}

+ 64 - 0
src/api/app/user/index.js

@@ -0,0 +1,64 @@
+import request from '@/utils/request'
+
+// 根据用户id列表获取用户信息
+export function getUserByIds(ids) {
+  return request({
+    url: '/app/user/getByIds/' + ids,
+    method: 'get',
+  })
+}
+
+// 给用户绑定客服
+export function bindUserCusByIds(data) {
+  return request({
+    url: '/vip/userCusRole/bind',
+    method: 'post',
+    data: data
+  })
+}
+
+// 给用户解绑客服
+export function unbindUserCusByIds(data) {
+  return request({
+    url: '/vip/userCusRole/unbind',
+    method: 'post',
+    data: data
+  })
+}
+
+// 给用户绑定客服v2
+export function bindUserCusByIdsV2(data) {
+  return request({
+    url: '/vip/userCusRole/bind/v2',
+    method: 'post',
+    data: data
+  })
+}
+
+// 给用户解绑客服v2
+export function unbindUserCusByIdsV2(data) {
+  return request({
+    url: '/vip/userCusRole/unbind/v2',
+    method: 'post',
+    data: data
+  })
+}
+
+// 用户获取列表
+export function listAppUser(data) {
+  return request({
+    url: '/app/user/findList',
+    method: 'get',
+    params: data
+  })
+}
+
+// 用户导出列表
+export function exportAppUser(data) {
+  return request({
+    url: '/app/user/export',
+    method: 'get',
+    params: data
+  })
+}
+

+ 37 - 0
src/api/app/user/portrait.js

@@ -0,0 +1,37 @@
+import request from '@/utils/request'
+
+// 用户画像列表
+export function listUserPortrait(params) {
+  return request({
+    url: '/app/user/portrait/findList',
+    method: 'get',
+    params
+  })
+}
+
+// 获取用户疗法周期详情(cycleName: 具体周期名称)
+export function getUserTreatmentCycle(userId, cycleName) {
+  return request({
+    url: '/app/user/portrait/treatmentCycle/' + userId,
+    method: 'get',
+    params: cycleName ? { cycleName } : {}
+  })
+}
+
+// 批量绑定/撤销标签(contentId: 视频id集合, tagId: 标签id集合, contentType: 固定为1)
+export function bindUserTag(data) {
+  return request({
+    url: '/app/user/portrait/bind/tag',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改信息
+export function saveEdit(data) {
+  return request({
+    url: '/app/user/portrait/saveEdit',
+    method: 'post',
+    data: data
+  })
+}

+ 26 - 0
src/api/app/user/userInfo.js

@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// 根据用户id列表获取用户信息
+export function getAIUserInfoByUserId(id) {
+  return request({
+    url: '/app/fsUserInfo/getByUserId/' + id,
+    method: 'get',
+  })
+}
+
+// 保存
+export function saveAiUserInfo(data) {
+  return request({
+    url: '/app/fsUserInfo/saveUserInfo',
+    method: 'post',
+    data: data,
+  })
+}
+
+// 获取用户兴趣标签
+export function getUserTags(userId) {
+  return request({
+    url: '/app/fsUserInfo/getUserTags/' + userId,
+    method: 'get',
+  })
+}

+ 26 - 0
src/api/app/user/userList.js

@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// 查询app用户列表
+export function listUser(query) {
+  return request({
+    url: '/app/user/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function listRole(query) {
+  return request({
+    url: '/app/cusrole/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function accountCheck(userId) {
+  return request({
+    url: '/app/cusrole/accountCheck',
+    method: 'post',
+    data: { userId: userId }
+  })
+}

+ 40 - 0
src/api/app/userChatLogs/index.js

@@ -0,0 +1,40 @@
+import request from '@/utils/request'
+
+//获取对话列表
+export function findList(data) {
+  return request({
+    url: '/app/userChatLogs/findList',
+    method: 'get',
+    params: data,
+  })
+}
+
+/**
+ * 
+ * 
+ * 获取对话聊天记录
+ * @param {*} data 
+ * @returns 
+ */
+export function findChatLogsByUserIdAndCustomerId(data) {
+  return request({
+    url: '/app/userChatLogs/findChatLogsByUserIdAndCustomerId',
+    method: 'get',
+    params: data,
+  })
+}
+
+/**
+ * 
+ * 
+ * 修改对话内容
+ * @param {*} data 
+ * @returns 
+ */
+export function editUserChatLogs(data) {
+  return request({
+    url: '/app/userChatLogs/edit',
+    method: 'post',
+    data: data,
+  })
+}

+ 126 - 0
src/api/app/userLogs/index.js

@@ -0,0 +1,126 @@
+import request from '@/utils/request'
+
+// 查询sopUserLogs列表
+export function listSopUserLogs(query) {
+  return request({
+    url: '/app/userLogs/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询sopUserLogs详细
+export function getSopUserLogs(id) {
+  return request({
+    url: '/qwSop/sopUserLogs/' + id,
+    method: 'get'
+  })
+}
+
+// 新增sopUserLogs
+export function addSopUserLogs(data) {
+  return request({
+    url: '/qwSop/sopUserLogs',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改sopUserLogs
+export function updateSopUserLogs(data) {
+  return request({
+    url: '/qwSop/sopUserLogs',
+    method: 'put',
+    data: data
+  })
+}
+// 修改sopUserLogs
+export function updateLogDate(data) {
+  return request({
+    url: '/qwSop/sopUserLogs/updateLogDate',
+    method: 'post',
+    data: data
+  })
+}
+
+export function replaceUser(data) {
+  return request({
+    url: '/qwSop/sopUserLogs/replaceUser',
+    method: 'post',
+    data: data
+  })
+}
+// 修改sopUserLogs
+export function addGroupChat(data) {
+  return request({
+    url: '/qwSop/sopUserLogs/addGroupChat',
+    method: 'post',
+    data: data
+  })
+}
+
+// 删除sopUserLogs
+export function delSopUserLogs(id) {
+  return request({
+    url: '/app/userLogs/' + id,
+    method: 'delete'
+  })
+}
+
+export function updateTimeSopUserLogs(data) {
+  return request({
+    url: '/app/userLogs/batchUpdateSopUserLogsToTime',
+    method: 'post',
+    data: data
+  })
+}
+
+// 导出sopUserLogs
+export function exportSopUserLogs(query) {
+  return request({
+    url: '/qwSop/sopUserLogs/export',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出sopUserLogs
+export function getSelectChat(id) {
+  return request({
+    url: '/qwSop/sopUserLogs/getSelectChat',
+    method: 'get',
+    params: {id}
+  })
+}
+
+//修复营期
+export function repairSopUserLogs(data) {
+  return request({
+    url: '/qwSop/sopUserLogs/repairSopUserLogs',
+    method: 'post',
+    data: data
+  })
+}
+
+export function getCompanyMiniAppList() {
+  return request({
+    url: '/company/companyConfig/getCompanyMiniAppList',
+    method: 'get'
+  })
+}
+export function getShortLink(data) {
+  return request({
+    url: '/qwSop/sopUserLogs/getShortLink',
+    method: 'get',
+    params: data
+  })
+}
+
+
+export function groupSendMessage(data) {
+  return request({
+    url: '/app/userLogs/groupSendMessage',
+    method: 'post',
+    data: data
+  })
+}

+ 79 - 0
src/api/app/userLogsInfo/index.js

@@ -0,0 +1,79 @@
+import request from '@/utils/request'
+
+// 查询sopUserLogsInfo列表
+export function listSopUserLogsInfo(query) {
+  return request({
+    url: '/app/userLogsInfo/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询sopUserLogsInfo详细
+export function getSopUserLogsInfo(id) {
+  return request({
+    url: '/qwSop/sopUserLogsInfo/' + id,
+    method: 'get'
+  })
+}
+
+// 新增sopUserLogsInfo
+export function addSopUserLogsInfo(data) {
+  return request({
+    url: '/qwSop/sopUserLogsInfo',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改sopUserLogsInfo
+export function updateSopUserLogsInfo(data) {
+  return request({
+    url: '/qwSop/sopUserLogsInfo',
+    method: 'put',
+    data: data
+  })
+}
+
+export function batchUpdateSopUserLogsInfoToTime(data) {
+  return request({
+    url: '/app/userLogsInfo/batchUpdateSopUserLogsInfoToTime',
+    method: 'post',
+    data: data
+  })
+}
+
+// 删除sopUserLogsInfo
+export function delSopUserLogsInfo(id) {
+  return request({
+    url: '/app/userLogsInfo/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出sopUserLogsInfo
+export function exportSopUserLogsInfo(query) {
+  return request({
+    url: '/qwSop/sopUserLogsInfo/export',
+    method: 'get',
+    params: query
+  })
+}
+
+//一键群发
+export function sendMsgSop(data) {
+  return request({
+    url: '/qwSop/sopUserLogsInfo/sendUserLogsInfoMsg',
+    method: 'post',
+    data: data
+  })
+}
+
+//一键群发新版
+export function sendMsgSopType(data) {
+  return request({
+    url: '/qwSop/sopUserLogsInfo/sendUserLogsInfoMsgType',
+    method: 'post',
+    data: data
+  })
+}

+ 64 - 0
src/api/app/welcome/index.js

@@ -0,0 +1,64 @@
+import request from '@/utils/request'
+
+/**
+ * 查询欢迎语列表
+ * @param {*} query 
+ * @returns 
+ */
+export function list(query) {
+    return request({
+        url: '/app/welcome/list',
+        method: 'get',
+        params: query
+    })
+}
+
+/**
+ * 保存欢迎语
+ * @param {*} data 
+ * @returns 
+ */
+export function save(data) {
+    return request({
+        url: '/app/welcome/save',
+        method: 'post',
+        data: data
+    })
+}
+
+/**
+ * 删除欢迎语
+ * @param {*} data 
+ * @returns 
+ */
+export function deleteByIds(ids) {
+    return request({
+        url: '/app/welcome/deleteByIds/' + ids,
+        method: 'delete'
+    })
+}
+
+/**
+ * 根据id获取欢迎语详情
+ * @param {*} data 
+ * @returns 
+ */
+export function getById(id) {
+    return request({
+        url: '/app/welcome/getById/' + id,
+        method: 'get'
+    })
+}
+
+/**
+ * 设置发送状态
+ * @param {*} data 
+ * @returns 
+ */
+export function setSendStatus(data) {
+    return request({
+        url: '/app/welcome/setSendStatus',
+        method: 'post',
+        data: data
+    })
+}

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

@@ -129,3 +129,37 @@ export function watchLogStatisticsExport(query) {
     params: query
   })
 }
+
+// 根据名称模糊查询用户列表app看课
+export function getCustomerListLikeName(params) {
+  return request({
+    url: '/course/appCourseWatchLog/getCustomerListLikeName',
+    method: 'get',
+    params: params
+  })
+}
+// 查询短链课程看课记录列表app看课
+export function listAppCourseWatchLog(query) {
+  return request({
+    url: '/course/appCourseWatchLog/list',
+    method: 'get',
+    params: query
+  })
+}
+// 导出短链课程看课记录app看课
+export function exportAppCourseWatchLog(query) {
+  return request({
+    url: '/course/appCourseWatchLog/export',
+    method: 'get',
+    params: query
+  })
+}
+
+// 批量修改好友备注
+export function batchUpdateFriendRemark(data) {
+  return request({
+    url: '/course/appCourseWatchLog/batchUpdateFriendRemark',
+    method: 'post',
+    data: data
+  })
+}

+ 77 - 0
src/api/system/appDept.js

@@ -0,0 +1,77 @@
+import request from '@/utils/request'
+
+// 查询部门列表
+export function listDept(query) {
+  return request({
+    url: '/app/dept/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询部门列表(排除节点)
+export function listDeptExcludeChild(deptId) {
+  return request({
+    url: '/app/dept/list/exclude/' + deptId,
+    method: 'get'
+  })
+}
+
+// 查询部门详细
+export function getDept(deptId) {
+  return request({
+    url: '/app/dept/' + deptId,
+    method: 'get'
+  })
+}
+
+// 查询部门下拉树结构
+export function treeselect() {
+  return request({
+    url: '/app/dept/treeselect',
+    method: 'get'
+  })
+}
+
+// 根据角色ID查询部门树结构
+export function roleDeptTreeselect(roleId) {
+  return request({
+    url: '/app/dept/roleDeptTreeselect/' + roleId,
+    method: 'get'
+  })
+}
+
+// 新增部门
+export function addDept(data) {
+  return request({
+    url: '/app/dept',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改部门
+export function updateDept(data) {
+  return request({
+    url: '/app/dept',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除部门
+export function delDept(deptId) {
+  return request({
+    url: '/app/dept/' + deptId,
+    method: 'delete'
+  })
+}
+
+
+export function companyOption(query) {
+  return request({
+    url: '/company/company/getCompanyList',
+    method: 'get',
+    params: query
+  })
+}

+ 52 - 0
src/router/index.js

@@ -92,6 +92,58 @@ export const constantRoutes = [
       }
     ]
   },
+  // APP SOP 模板编辑页(隐藏路由,页面内跳转用)
+  {
+    path: '/user/sopTemp',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: 'updateSopTemp/:id/:type(\\d+)',
+        component: () => import('@/views/app/sop/template/updateSopTemp'),
+        name: 'appUpdateSopTemp',
+        meta: { title: '改动SOP模板', activeMenu: '/app/sop/template' }
+      }
+    ]
+  },
+  // APP SOP 任务相关隐藏路由
+  {
+    path: '/app',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: 'sop',
+        component: (resolve) => require(['@/views/app/sop/info'], resolve),
+        name: 'appSopInfo',
+        meta: { title: 'SOP任务', activeMenu: '/app/sop/info' }
+      },
+      {
+        path: 'sop/addSop',
+        component: (resolve) => require(['@/views/app/sop/info/addSop'], resolve),
+        name: 'appAddSop',
+        meta: { title: '新增SOP任务', activeMenu: '/app/sop/info' }
+      },
+      {
+        path: 'sop/updateSop/:id/:type',
+        component: (resolve) => require(['@/views/app/sop/info/addSop.vue'], resolve),
+        name: 'appUpdateSop',
+        meta: { title: '修改/查看SOP任务', activeMenu: '/app/sop/info' }
+      },
+      {
+        path: 'sop/sopUserLogsSchedule/:id',
+        component: (resolve) => require(['@/views/app/sop/userLogs/sopUserLogsSchedule.vue'], resolve),
+        name: 'appSopUserLogsSchedule',
+        meta: { title: '查看营期', activeMenu: '/app/sop/info' }
+      },
+      {
+        path: 'invitationCode',
+        component: (resolve) => require(['@/views/app/invitationCode'], resolve),
+        name: 'appInvitationCode',
+        meta: { title: '邀请码', activeMenu: '/app/invitationCode' }
+      },
+    ]
+  },
   {
     path: '/fastGpt/fastGptDataset/Collection',
     component: Layout,

+ 37 - 0
src/utils/openIM.js

@@ -0,0 +1,37 @@
+import { getSDK, CbEvents } from '@openim/wasm-client-sdk';
+
+let sdkInstance = null;
+let cbEvents = null;
+export function getOpenIM() {
+  if (!sdkInstance) {
+    sdkInstance = getSDK({ debug: true })
+  }
+  return sdkInstance
+}
+export const getCbEvents = () => {
+  if (!cbEvents) {
+    cbEvents = CbEvents;
+  }
+  return cbEvents;
+};
+
+// 设置全局监听(只需一次)
+/*export const setupListeners = (store) => {
+  if (isListenersSetup) return;
+
+  const im = getOpenIM();
+
+  im.on(CbEvents.OnConnectSuccess, () => {
+    store.commit('im/setReadyState', true);
+    console.log('[OpenIM] 连接成功');
+  });
+
+  im.on(CbEvents.OnConnectFailed, (error) => {
+    store.commit('im/setError', error.message);
+    console.error('[OpenIM] 连接失败:', error);
+  });
+
+  // 其他必要的事件监听...
+  isListenersSetup = true;
+  console.log('[OpenIM] 全局监听器已设置');
+};*/

+ 2 - 1
src/utils/request.js

@@ -71,7 +71,8 @@ service.interceptors.response.use(res => {
         store.dispatch('LogOut').then(() => {
           location.href = '/index';
         })
-      })
+      }).catch(() => {});
+      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
     } else if (code === 500) {
       Notification.error({
         title: msg

+ 319 - 0
src/views/app/appDept/index.vue

@@ -0,0 +1,319 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索区域 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+      <el-form-item label="部门名称" prop="deptName">
+        <el-input
+          v-model="queryParams.deptName"
+          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="['system:dept:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          icon="el-icon-sort"
+          size="mini"
+          @click="toggleExpandAll"
+        >展开/折叠</el-button>
+      </el-col>
+    </el-row>
+
+    <!-- 部门树形表格 -->
+    <el-table
+      v-if="refreshTable"
+      v-loading="loading"
+      :data="deptList"
+      row-key="deptId"
+      :default-expand-all="isExpandAll"
+      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+    >
+      <el-table-column prop="deptName" label="部门名称" width="260"></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="['system:dept:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-plus"
+            @click="handleAdd(scope.row)"
+            v-hasPermi="['system:dept:add']"
+          >新增子部门</el-button>
+          <el-button
+            v-if="scope.row.parentId != 0"
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['system:dept:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 新增/修改部门弹窗 -->
+    <el-dialog :title="title" :visible.sync="open" width="400px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="上级部门" prop="parentId">
+          <treeselect
+            v-model="form.parentId"
+            :options="deptOptions"
+            :normalizer="normalizer"
+            placeholder="选择上级部门"
+            :disable-nodes="form.deptId ? [form.deptId] : []"
+          />
+        </el-form-item>
+        <el-form-item label="部门名称" prop="deptName">
+          <el-input v-model="form.deptName" placeholder="请输入部门名称" />
+        </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 { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/appDept";
+// 树形选择器组件
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+
+export default {
+  name: "SimpleDept",
+  components: { Treeselect },
+  data() {
+    return {
+      // 加载状态
+      loading: true,
+      // 是否显示搜索
+      showSearch: true,
+      // 部门树形列表数据
+      deptList: [],
+      // 部门树形选择器选项
+      deptOptions: [],
+      // 弹窗标题
+      title: "",
+      // 弹窗显示状态
+      open: false,
+      // 是否全部展开树形表格
+      isExpandAll: true,
+      // 表格重新渲染标识(用于展开/折叠)
+      refreshTable: true,
+      // 查询参数(仅保留部门名称)
+      queryParams: {
+        deptName: undefined
+      },
+      // 表单数据
+      form: {
+        deptId: undefined,    // 部门ID(修改时用)
+        parentId: 0,          // 上级部门ID(默认顶级)
+        deptName: undefined   // 部门名称
+      },
+      // 表单校验规则(仅保留必填项)
+      rules: {
+        parentId: [
+          { required: true, message: "上级部门不能为空", trigger: "blur" }
+        ],
+        deptName: [
+          { required: true, message: "部门名称不能为空", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  created() {
+    // 初始化加载部门列表
+    this.getList();
+  },
+  methods: {
+    /**
+     * 查询部门列表
+     */
+    getList() {
+      this.loading = true;
+      listDept(this.queryParams).then(response => {
+        // 转换为树形结构(handleTree是全局方法,和原代码一致)
+        this.deptList = this.handleTree(response.data, "deptId");
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+
+    /**
+     * 树形选择器数据格式化
+     */
+    normalizer(node) {
+      if (node.children && !node.children.length) {
+        delete node.children;
+      }
+      return {
+        id: node.deptId,
+        label: node.deptName,
+        children: node.children
+      };
+    },
+
+    /**
+     * 取消弹窗
+     */
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+
+    /**
+     * 重置表单
+     */
+    reset() {
+      this.form = {
+        deptId: undefined,
+        parentId: 0,
+        deptName: undefined
+      };
+      this.resetForm("form");
+    },
+
+    /**
+     * 搜索
+     */
+    handleQuery() {
+      this.getList();
+    },
+
+    /**
+     * 重置搜索
+     */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams = { deptName: undefined };
+      this.handleQuery();
+    },
+
+    /**
+     * 新增部门
+     */
+    handleAdd(row) {
+      this.reset();
+      // 如果是新增子部门,默认选中当前部门作为上级
+      if (row) {
+        this.form.parentId = row.deptId;
+      }
+      this.open = true;
+      this.title = "添加部门";
+      // 加载部门树形选择器数据
+      listDept().then(response => {
+        this.deptOptions = this.handleTree(response.data, "deptId");
+      });
+    },
+
+    /**
+     * 展开/折叠全部
+     */
+    toggleExpandAll() {
+      this.refreshTable = false;
+      this.isExpandAll = !this.isExpandAll;
+      this.$nextTick(() => {
+        this.refreshTable = true;
+      });
+    },
+
+    /**
+     * 修改部门
+     */
+    handleUpdate(row) {
+      this.reset();
+      // 获取部门详情
+      getDept(row.deptId).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改部门";
+      });
+      // 加载排除当前部门的树形数据(防止修改时选择自己作为上级)
+      listDeptExcludeChild(row.deptId).then(response => {
+        this.deptOptions = this.handleTree(response.data, "deptId");
+      });
+    },
+
+    /**
+     * 提交表单(新增/修改)
+     */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          // 修改操作
+          if (this.form.deptId) {
+            updateDept(this.form).then(() => {
+              this.$message.success("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            // 新增操作
+            addDept(this.form).then(() => {
+              this.$message.success("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+
+    /**
+     * 删除部门
+     */
+    handleDelete(row) {
+      this.$confirm(`是否确认删除【${row.deptName}】部门?`, "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return delDept(row.deptId);
+      }).then(() => {
+        this.getList();
+        this.$message.success("删除成功");
+      }).catch(() => {});
+    }
+  }
+};
+</script>
+
+<style scoped>
+.mb8 {
+  margin-bottom: 8px;
+}
+.dialog-footer {
+  text-align: center;
+}
+</style>

+ 1206 - 0
src/views/app/courseFinishTemp/index.vue

@@ -0,0 +1,1206 @@
+<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 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="courseId">
+                <el-select
+                    v-model="queryParams.courseId"
+                    placeholder="默认50条,可输入关键字检索"
+                    style=" margin-right: 10px;"
+                    size="mini"
+                    filterable
+                    clearable
+                    :loading="courseLoading"
+                    remote
+                    :remote-method="(keyword) => handleCourseRemoteSearch(keyword, 0)"
+                    @change="courseChangeUpdate(0)"
+                    @keyup.enter.native="handleQuery"
+                >
+                    <el-option
+                        v-for="dict in queryCourseList"
+                        :key="dict.dictValue"
+                        :label="dict.dictLabel"
+                        :value="parseInt(dict.dictValue)"
+                    />
+                </el-select>
+            </el-form-item>
+
+            <el-form-item label="小节" prop="videoId">
+                <el-select
+                    v-model="queryParams.videoId"
+                    placeholder="请选择小节"
+                    size="mini"
+                    style=" margin-right: 10px;"
+                    filterable
+                    clearable
+                    :disabled="!queryParams.courseId"
+                    @change="videoIdChange(0)"
+                    @keyup.enter.native="handleQuery"
+                >
+                    <el-option
+                        v-for="dict in queryVideoList"
+                        :key="dict.dictValue"
+                        :label="dict.dictLabel"
+                        :value="parseInt(dict.dictValue)"
+                    >
+                        <div :style="{ color: dict.isPause == 1 ? '#f56c6c' : 'inherit', display: 'flex', alignItems: 'center' }">
+                            <span>{{ dict.dictLabel }}</span>
+                            <span v-if="dict.isPause == 1" style="font-size: 12px; margin-left: 8px; color: #f56c6c;">
+                                (该科普已被关闭)
+                            </span>
+                        </div>
+                    </el-option>
+                </el-select>
+            </el-form-item>
+
+            <el-form-item>
+                <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+                <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-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="['app:finishTemp:add']"
+                >
+                    新增
+                </el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    type="danger"
+                    plain
+                    icon="el-icon-delete"
+                    size="mini"
+                    @click="handleDelete"
+                    :disabled="multiple"
+                    v-hasPermi="['app:finishTemp:delete']"
+                >
+                    删除
+                </el-button>
+            </el-col>
+            <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+
+        <el-table border v-loading="loading" :data="courseFinishTempList" @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" align="center" />
+            <el-table-column label="模板ID" align="center" prop="id" />
+            <el-table-column label="模板名称" align="center" prop="name" />
+            <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="courseName" />
+            <el-table-column label="小节名称" align="center" prop="videoName" />
+            <el-table-column label="创建时间" align="center" prop="createTime" />
+            <el-table-column label="修改时间" align="center" prop="updateTime" />
+            <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, true)"
+                        v-hasPermi="['app:finishTemp:edit']"
+                    >
+                        修改
+                    </el-button>
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-s-promotion"
+                        @click="handleUpdate(scope.row, false)"
+                    >
+                        详情
+                    </el-button>
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-delete"
+                        @click="handleDelete(scope.row)"
+                        v-hasPermi="['app:finishTemp:delete']"
+                    >
+                        删除
+                    </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="addForm.title"
+            :visible.sync="addForm.visible"
+            width="1000px"
+            v-loading="addForm.loading"
+            append-to-body
+            :close-on-click-modal="false"
+        >
+            <el-form ref="addForm" :model="addForm" :rules="rules" label-width="80px">
+
+                <el-form-item label="模板名称" prop="name">
+                    <el-input v-model="addForm.name" :disabled="!addForm.canOp" placeholder="请输入模板名称" />
+                </el-form-item>
+
+                <el-form-item label="状态">
+                    <el-radio-group v-model="addForm.status">
+                        <el-radio v-for="dict in statusOptions"
+                            :key="dict.dictValue"
+                            :disabled="!addForm.canOp"
+                            :label="parseInt(dict.dictValue)"
+                        >
+                            {{ dict.dictLabel }}
+                        </el-radio>
+                    </el-radio-group>
+                </el-form-item>
+
+                <el-form-item label="科普"
+                    style="margin-bottom: 15px;"
+                    prop="videoId"
+                >
+                
+                    <el-select
+                        v-model="addForm.courseId"
+                        placeholder="默认50条,可输入关键字检索"
+                        style=" margin-right: 10px;"
+                        size="mini"
+                        filterable
+                        clearable
+                        :loading="courseLoading"
+                        remote
+                        :disabled="!addForm.canOp"
+                        :remote-method="(keyword) => handleCourseRemoteSearch(keyword, 1)"
+                        @change="courseChangeUpdate(1)"
+                    >
+                        <el-option
+                            v-for="dict in courseList"
+                            :key="dict.dictValue"
+                            :label="dict.dictLabel"
+                            :value="parseInt(dict.dictValue)"
+                        />
+                    </el-select>
+
+                    <el-select
+                        v-model="addForm.videoId"
+                        placeholder="请选择小节"
+                        size="mini"
+                        style=" margin-right: 10px;"
+                        filterable
+                        clearable
+                        :disabled="!addForm.canOp"
+                        @change="videoIdChange(1)"
+                    >
+                        <el-option
+                            v-for="dict in videoList"
+                            :key="dict.dictValue"
+                            :label="dict.dictLabel"
+                            :value="parseInt(dict.dictValue)"
+                        >
+                            <div :style="{ color: dict.isPause == 1 ? '#f56c6c' : 'inherit', display: 'flex', alignItems: 'center' }">
+                                <span>{{ dict.dictLabel }}</span>
+                                <span v-if="dict.isPause == 1" style="font-size: 12px; margin-left: 8px; color: #f56c6c;">
+                                    (该科普已被关闭,无法正常发送)
+                                </span>
+                            </div>
+                        </el-option>
+                    </el-select>
+
+                    <!-- 默认完课消息 -->
+                    <el-select
+                        v-model="addForm.courseType"
+                        placeholder="请选择消息类型"
+                        size="mini"
+                        style=" margin-right: 10px;"
+                        disabled
+                    >
+                        <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="规则" style="height: 500px; overflow: auto;">
+                    <!-- 遍历规则 所有内容【存在多个】 -->
+                    <div
+                        v-for="(content, contentIndex) in addForm.setting"
+                        :key="contentIndex"
+                        style="
+                            background-color: #fdfdfd;
+                            border: 1px solid #e6e6e6;
+                            margin-bottom: 20px;
+                        "
+                    >
+                        <el-row style="padding-bottom: 20px">
+                            <el-col :span="22">
+                                <el-form 
+                                    :model="content"
+                                    label-width="70px"
+                                >
+                                    <el-form-item label="内容类别"
+                                        style="margin: 2%"
+                                    >
+                                        <div>
+                                            <el-radio-group 
+                                                v-model="content.contentType"
+                                                @change="handleContentTypeChange()"
+                                            >
+                                                <el-radio
+                                                    :disabled="!addForm.canOp || !['11', '20'].includes(item.dictValue)"
+                                                    :key="item.dictValue"
+                                                    :label="item.dictValue"
+                                                    v-for="item in sysQwSopAiContentType"
+                                                >{{ item.dictLabel }}
+                                                </el-radio>
+                                            </el-radio-group>
+                                        </div>
+
+                                    </el-form-item>
+
+                                    <!-- 科普 -->
+                                    <el-form-item label="内容" v-if="[9].includes(Number(content.contentType))">
+                                        
+                                        <div>
+                                            <el-card class="box-card">
+                                                <el-form-item 
+                                                    label="链接标题:"
+                                                    label-width="100px"
+                                                    required
+                                                >
+                                                    <el-input
+                                                        v-model="content.linkTitle"
+                                                        placeholder="请输入链接标题"
+                                                        style="width: 90%;"
+                                                        :disabled="!addForm.canOp"
+                                                    />
+                                                </el-form-item>
+                                                <el-form-item 
+                                                    label="链接描述:"
+                                                    label-width="100px"
+                                                    required
+                                                >
+                                                    <el-input
+                                                        type="textarea"
+                                                        :rows="3"
+                                                        :disabled="!addForm.canOp"
+                                                        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"
+                                                        :file-size="1"
+                                                        :width="150"
+                                                        :height="150"
+                                                        style="margin-top: 1%;"
+                                                    />
+                                                </el-form-item>
+                                                <div>
+                                                    <el-form-item 
+                                                        label="链接地址:"
+                                                        label-width="100px"
+                                                    >
+                                                        <el-tag 
+                                                            type="warning"
+                                                            v-model="content.isBindUrl"
+                                                        >选择的科普小节
+                                                            即为卡片链接地址
+                                                        </el-tag>
+                                                    </el-form-item>
+                                                </div>
+                                                <el-form-item 
+                                                    label="课节过期时间"
+                                                >
+                                                    <el-row>
+                                                        <el-input 
+                                                        type="number"
+                                                        v-model="content.expiresDays"
+                                                        style="width: 200px"
+                                                        >
+                                                        <template slot="append">天</template>
+                                                        </el-input>
+                                                    </el-row>
+                                                    <el-row>
+                                                        <span class="tip">填写0或不填时,默认为系统配置的默认时间</span>
+                                                    </el-row>
+                                                </el-form-item>
+                                            </el-card>
+                                        </div>
+
+                                    </el-form-item>
+
+                                    <!-- 文本 -->
+                                    <el-form-item label="内容" v-else-if="[11].includes(Number(content.contentType))">
+                                        <el-input
+                                            v-model="content.value"
+                                            type="textarea"
+                                            :rows="3"
+                                            placeholder="内容"
+                                            :disabled="!addForm.canOp"
+                                            style="width: 90%; margin-top: 10px;"
+                                            @keydown.native="handleKeydown($event, contentIndex)"
+                                            :ref="`textarea-${contentIndex}`"
+                                        />
+
+                                    </el-form-item>
+
+                                    <!-- 语音 -->
+                                    <el-form-item label="内容" v-else-if="[12].includes(Number(content.contentType))">
+
+                                        <div >
+                                            <el-input
+                                                v-model="content.value"
+                                                type="textarea"
+                                                :rows="3"
+                                                :disabled="!addForm.canOp"
+                                                maxlength="66"
+                                                show-word-limit
+                                                placeholder="输入要转为语音的内容"
+                                                style="width: 90%;margin-top: 10px;"
+                                                @input="handleInputVoiceText(content.value, content)"
+                                            />
+
+                                        </div>
+
+                                    </el-form-item>
+
+                                    <!-- 图片 -->
+                                    <el-form-item label="内容" v-else-if="[2].includes(Number(content.contentType))">
+
+                                        <ImageUpload
+                                            v-model="content.imgUrl"
+                                            type="image"
+                                            :num="1"
+                                            :width="150"
+                                            :height="150"
+                                        />
+
+                                    </el-form-item>
+
+                                    <!-- 视频 -->
+                                    <el-form-item label="内容" v-else-if="[6].includes(Number(content.contentType))">
+
+                                        <div>
+                                            <el-form-item 
+                                                label="上传视频:"
+                                                prop="videoUrl"
+                                                label-width="100px"
+                                            >
+                                            <el-upload
+                                                v-model="content.videoUrl"
+                                                class="avatar-uploader"
+                                                :action="uploadUrl"
+                                                :show-file-list="false"
+                                                :disabled="!addForm.canOp"
+                                                :on-success="(res, file) => handleSuccessVideo(res, file, content)"
+                                                :before-upload="beforeUploadVideo"
+                                            >
+                                                <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>
+
+                                    </el-form-item>
+
+                                    <!-- 疗法 -->
+                                    <el-form-item label="内容" v-else-if="[20].includes(Number(content.contentType))">
+                                        
+                                        <el-card class="box-card">
+                                            <el-form-item 
+                                                label="选择疗法:"
+                                                label-width="100px"
+                                                required
+                                            >
+                                                <el-select
+                                                    :disabled="!addForm.canOp"
+                                                    v-model="content.packageId"
+                                                    placeholder="默认50条,可输入关键字检索"
+                                                    style=" margin-right: 10px; width: 230px;"
+                                                    size="mini"
+                                                    filterable
+                                                    clearable
+                                                    :loading="content.pkgLoading"
+                                                    remote
+                                                    :remote-method="(keyword) => handlePkgRemoteSearch(keyword, content)"
+                                                    @change="pkgChangeUpdate(content)"
+                                                >
+                                                <el-option
+                                                    v-for="dict in (content.meta && content.meta.pkgList ? content.meta.pkgList : [])"
+                                                    :key="dict.packageId"
+                                                    :label="`${dict.packageName}(${dict.packageId})`"
+                                                    :value="parseInt(dict.packageId)"
+                                                />
+                                                </el-select>
+                                            </el-form-item>
+                                            <el-form-item 
+                                                label="疗法名称:"
+                                                label-width="100px"
+                                            >
+                                                <el-input
+                                                    disabled
+                                                    :rows="3"
+                                                    v-model="content.packageName"
+                                                    style="width: 90%;margin-top: 1%;"
+                                                />
+                                            </el-form-item>
+                                            <el-form-item 
+                                                label="疗法封面:"
+                                                label-width="100px"
+                                            >
+                                                <ImageUpload 
+                                                    :disabled="true"
+                                                    v-model="content.packageImgUrl"
+                                                    type="image"
+                                                    :num="1"
+                                                    :file-size="1"
+                                                    :width="150"
+                                                    :height="150"
+                                                    style="margin-top: 1%;"
+                                                />
+                                            </el-form-item>
+                                        </el-card>
+
+                                    </el-form-item>
+
+                                </el-form>
+                            </el-col>
+
+                            <el-col 
+                                :span="1"
+                                :offset="1"
+                            >
+                                <!-- 删除内容节点 -->
+                                <i 
+                                    class="el-icon-delete"
+                                    @click="delContentNode(contentIndex)"
+                                    style="margin-top: 20px;"
+                                    v-if="addForm.setting.length > 1 && addForm.canOp"
+                                ></i>
+                            </el-col>
+                        </el-row>
+
+                    </div>
+                    <el-link type="primary" class="el-icon-plus" :disabled="!addForm.canOp" :underline="false" @click='addContent()'>添加内容</el-link>
+                </el-form-item>
+
+                <el-form-item label="所属客服" prop="appCustomerIds" label-width="100px">
+                    <div>
+                        <el-button size="medium" icon="el-icon-circle-plus-outline" plain :disabled="!addForm.canOp"
+                            @click="openSelectCustomerPage">请选择客服</el-button>
+                    </div>
+                    <div>
+                        <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in customerConfig.selected" closable
+                            :disable-transitions="false" @close="handleCloseCustomer(s)">
+                            <span>{{ s.memberName }}</span>
+                        </el-tag>
+                    </div>
+                    <div style="
+                        color: #999;
+                        font-size: 14px;
+                        display: flex;
+                        align-items: center;
+                    ">
+                        <i class="el-icon-info"></i>
+                        选择了客服之后,此科普小节的完课 消息
+                        只作用于此客服
+                    </div>
+                </el-form-item>
+            
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" v-if="addForm.canOp" @click="submitForm">确 定</el-button>
+                <el-button @click="cancel">取 消</el-button>
+            </div>
+        </el-dialog>
+
+        <!-- 选择app客服角色 -->
+        <el-dialog title="选择客服" :visible.sync="customerConfig.visible" width="1300px" append-to-body>
+            <CusRoleList
+                :selected="customerConfig.selected"
+                @confirm="customerSelectedConfirmCallForBind"
+                @selectionChange="customerSelectionChangeCallForBind"
+            />
+        </el-dialog>
+
+    </div>
+</template>
+
+<script>
+import {
+    findList,
+    // exportList,
+    add,
+    edit,
+    getById,
+    deleteByIds,
+} from "@/api/app/course/courseFinishTemp";
+import { courseList, videoList } from "@/api/app/course";
+import { getPackageOptions,} from "@/api/app/pkg";
+import ImageUpload from "@/views/qw/sop/ImageUpload.vue";
+import CusRoleList from '@/views/app/user/CusRoleList.vue';
+import {listRole} from '@/api/app/user/userList';
+
+export default {
+    name: "CourseFinishTemp",
+    components: { ImageUpload, CusRoleList, },
+    data() {
+        return {
+            //上传语音的遮罩层
+            voiceLoading: false,
+            uploadUrl: process.env.VUE_APP_BASE_API + "/app/common/uploadOSS2",
+            uploadUrlByVoice: process.env.VUE_APP_BASE_API + "/app/common/uploadOSSByHOOKVoice",
+            videoLoading: false,
+            videoOptionsLoading: false,
+            courseLoading: false,
+            videoOptions: [],
+            sysFsSopWatchStatus: [],
+            // 状态字典
+            statusOptions: [],
+            courseList: [],
+            videoList: [],
+            pkgList: [],
+            queryCourseList: [],
+            queryVideoList: [],
+            // 遮罩层
+            loading: true,
+            // 导出遮罩层
+            exportLoading: false,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            formType: 1,
+            // 总条数
+            total: 0,
+            // 完课模板表格数据
+            courseFinishTempList: [],
+            //插件版
+            sysQwSopAiContentType: [],
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                name: null,
+                status: null,
+                courseId: null,
+                videoId: null
+            },
+            // 表单校验
+            rules: {
+                name: [
+                    { required: true, message: '模板名称不能为空', trigger: 'submit' }
+                ],
+                status: [
+                    { required: true, message: '状态不能为空', trigger: 'submit' }
+                ],
+                courseId: [
+                    { required: true, message: '科普/课节/类型不能为空', trigger: 'submit' }
+                ],
+                videoId: [
+                    { required: true, message: '科普/课节/类型不能为空', trigger: 'submit' }
+                ],
+                courseType: [
+                    { required: true, message: '科普/课节/类型不能为空', trigger: 'submit' }
+                ],
+            },
+            finishTempList: [],
+            //用来记录上次选择部门的数组
+            addForm: {
+                visible: false,
+                title: '',
+                canOp: true,
+                id: null,
+                name: null,
+                status: 1,
+                courseId: null,
+                videoId: null,
+                courseType: 3,//指定是完课消息
+                setting: [],
+                loading: false,
+            },
+            //选择客服对话框
+            customerConfig: {
+                visible: false,
+                selected: [],
+            },
+        };
+    },
+    async created() {
+        this.getList();
+        this.getDicts("app_normal_disabled_status").then((response) => {
+            this.statusOptions = response.data;
+        });
+        this.getDicts("app_sop_plugin_settingType").then(response => {
+            this.sysQwSopAiContentType = response.data;
+        });
+        this.getDicts("app_sop_watch_status").then(response => {
+            this.sysFsSopWatchStatus = response.data;
+        });
+        let courseList = await this.getCourseList(null, null);
+        this.queryCourseList = courseList.list;
+        let defaultPkg = await this.getPkgList(null, null);
+        this.pkgList = defaultPkg.data;
+    },
+    methods: {
+        //--------------------------------------------------------------列表 begin---------------------------------------------------------
+        /**
+         * 查询完课模板列表
+         */
+        getList() {
+            this.loading = true;
+            findList(this.queryParams).then((response) => {
+                this.courseFinishTempList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        /**
+         * 搜索按钮操作
+         */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /**
+         * 重置按钮操作
+         */
+        resetQuery() {
+            this.resetForm("queryForm");
+            this.handleQuery();
+        },
+        /**
+         * 多选框选中数据
+         * @param selection 
+         */
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.id);
+            this.single = selection.length !== 1;
+            this.multiple = !selection.length;
+        },
+        /**
+         * 新增按钮操作
+         */
+        async handleAdd() {
+            this.reset();
+            this.addContent();
+            this.addForm.title = "添加完课模板";
+            this.addForm.loading = true;
+            this.addForm.visible = true;
+            let courseList = await this.getCourseList(null, null);
+            this.courseList = courseList.list;
+            this.addForm.loading = false;
+        },
+        /**
+         * 修改按钮操作
+         * @param row 
+         */
+        async handleUpdate(row, canOp) {
+            this.reset();
+            const id = row.id;
+            this.addForm.title = canOp ? "修改完课模板" : "完课模板详情";
+            this.addForm.canOp = canOp;
+            this.addForm.loading = true;
+            this.addForm.visible = true;
+            let info = await getById(id);
+            for (let key in this.addForm) {
+                if (info.data.hasOwnProperty(key) && key !== 'setting') {
+                    this.addForm[key] = (key === 'status' ? parseInt(info.data.status) : info.data[key]);
+                }
+            }
+            this.addForm.setting = JSON.parse(info.data.setting);
+            this.addForm.setting.forEach(async st => {
+                if (st.contentType == 20) {
+                    if (!st.packageId) {
+                        this.$set(st, 'meta', {
+                            pkgLoading: false,
+                            pkgList: []
+                        });
+                    } else {
+                        let pkgList = await this.getPkgList(null, st.packageId);
+                        this.$set(st, 'meta', {
+                            pkgLoading: false,
+                            pkgList: pkgList.data,
+                        });
+                    }
+                }
+                
+            })
+            if (info.data.appCustomerIds) {
+                let userInfo = await listRole({ids: info.data.appCustomerIds});
+                this.customerConfig.selected = userInfo.rows;
+            }
+            if (this.addForm.courseId) {
+                let courseList = await this.getCourseList(null, this.addForm.courseId);
+                this.courseList = courseList.list;
+                let videoList = await this.getVideoList(this.addForm.courseId, null);
+                this.videoList = videoList.list;
+            }
+            this.addForm.loading = false;
+        },
+        /**
+         * 删除按钮操作
+         * @param row 
+         */
+        handleDelete(row) {
+            const ids = row.id || this.ids;
+            this.$confirm(
+                '是否确认删除完课模板ID为"' + ids + '"的数据项?',
+                "警告",
+                {
+                    confirmButtonText: "确定",
+                    cancelButtonText: "取消",
+                    type: "warning",
+                }
+            )
+                .then(async () => {
+                    await deleteByIds(ids);
+                    this.getList();
+                    this.msgSuccess("删除成功");
+                })
+                .catch(() => { });
+        },
+        //--------------------------------------------------------------新增/编辑 begin---------------------------------------------------------
+        customerSelectedConfirmCallForBind() {
+            this.customerConfig.visible = false;
+        },
+        /**
+         * 选中的用户列表
+         * @param list 
+         */
+        customerSelectionChangeCallForBind(config) {
+            let selected = config.selected;
+            //单行操作
+            if (config.mode === 0) {
+                let row = config.row;
+                if (selected) {
+                    if (!this.customerConfig.selected.some(s => s.id == row.id)) {
+                        this.customerConfig.selected.push(row)
+                    }
+                } else {
+                    this.customerConfig.selected = this.customerConfig.selected.filter(m => m.id != row.id);
+                }
+            }
+            //操作当前页所有行
+            else {
+                let list = config.rows;
+                if (selected) {
+                    list.map(l => {
+                        if (!this.customerConfig.selected.some(s => s.id == l.id)) {
+                            this.customerConfig.selected.push(l)
+                        }
+                    });
+                } else {
+                    list.map(l => {
+                        for (let i = 0; i < this.customerConfig.selected.length; i++) {
+                            if (l.id == this.customerConfig.selected[i].id) {
+                                this.customerConfig.selected.splice(i, 1);
+                            }
+                        }
+                    })
+                }
+            }
+        },
+        /**
+         * 提交按钮
+         */
+        submitForm() {
+            this.$refs["addForm"].validate(async (valid) => {
+                if (valid) {
+                    if (this.checkData(this.addForm)) {
+                        let data = JSON.parse(JSON.stringify(this.addForm));
+                        data.setting.forEach(ele => {
+                            delete ele.meta;
+                        });
+                        data['appCustomerIds'] = this.customerConfig.selected.map(s => s.id).join(',');
+                        data['contentType'] = data.type;
+                        data['setting'] = JSON.stringify(data.setting);
+                        if (data.id != null) {
+                            await edit(data);
+                            this.msgSuccess("修改成功");
+                            this.reset();
+                            this.getList();
+                        } else {
+                            await add(data);
+                            this.msgSuccess("新增成功");
+                            this.reset();
+                            this.getList();
+                        }
+                    }
+                }
+            });
+        },
+        /**
+         * 校验数据
+         * @param data 
+         */
+        checkData(data) {
+
+            if (this.isEmpty(data.name)) {
+                this.$message.error("模板名称不能为空")
+                return false;
+            }
+            if (this.isEmpty(data.status)) {
+                this.$message.error("状态不能为空")
+                return false;
+            }
+            if (this.isEmpty(data.courseId)) {
+                this.$message.error("请选择科普")
+                return false;
+            }
+            if (this.isEmpty(data.videoId)) {
+                this.$message.error("请选择课节")
+                return false;
+            }
+            if (this.isEmpty(data.courseType)) {
+                this.$message.error("消息类型不能为空")
+                return false;
+            }
+
+            if (data.setting.length == 0) {
+                this.$message.error("请添加规则!")
+                return false;
+            }
+
+            for (let i = 0; i < data.setting.length; i++) {
+                let setting = data.setting[i];
+                if (setting.contentType == 11 && this.isEmpty(setting.value)) {
+                    this.$message.error("内容不能为空")
+                    return false;
+                }
+                if (setting.contentType == 12 && this.isEmpty(setting.value)) {
+                    this.$message.error("语音文本不能为空")
+                    return false;
+                }
+                if (setting.contentType == 2 && this.isEmpty(setting.imgUrl)) {
+                    this.$message.error("图片不能为空")
+                    return false;
+                }
+                if (9 == setting.contentType && this.isEmpty(setting.linkTitle)) {
+                    this.$message.error("链接标题不能为空")
+                    return false;
+                }
+                if (9 == setting.contentType && this.isEmpty(setting.linkDescribe)) {
+                    this.$message.error("链接描述不能为空")
+                    return false;
+                }
+                if (9 == setting.contentType && this.isEmpty(setting.linkImageUrl)) {
+                    this.$message.error("链接图片不能为空")
+                    return false;
+                }
+                if (20 == setting.contentType && this.isEmpty(setting.packageId)) {
+                    this.$message.error("疗法不能为空")
+                    return false;
+                }
+            }
+    
+            return true;
+        },
+        /**
+         * 取消按钮
+         */
+        cancel() {
+            this.reset();
+        },
+        /**
+         * 表单重置
+         */
+        reset() {
+            this.addForm = {
+                visible: false,
+                title: '',
+                canOp: true,
+                id: null,
+                name: null,
+                status: 1,
+                courseId: null,
+                videoId: null,
+                courseType: 3,
+                setting: [],
+                loading: false,
+            };
+            this.customerConfig.selected = [];
+            this.resetForm("addForm");
+        },
+        /**
+         * 科普小节变化
+         * @param mode 模式,0-查询列表,1-新增编辑页 
+         */
+        videoIdChange(mode) {
+            if (mode == 1) {
+                //选择了科普小节则 默认绑上
+                let selectedVideo;
+                // 查找选中的科普对应的 label
+                if (this.addForm.videoId) {
+                    selectedVideo = this.videoList.find(course => parseInt(course.dictValue) == this.addForm.videoId);
+                }
+                for (let i = 0; i < this.addForm.setting.length; i++) {
+                    let contentItem = this.addForm.setting[i];
+                    //科普消息-文本内容
+                    if (contentItem == 11) {
+                        this.$set(contentItem, 'isBindUrl', '2');
+                    }
+                    //如果是科普的才上
+                    if (selectedVideo && this.addForm.videoId != null) {
+
+                        if (contentItem.contentType == 9) {
+                            this.$set(contentItem, 'linkDescribe', selectedVideo.dictLabel);
+                        }
+                    }
+                }
+            }
+        },
+        /**
+         * 疗法下拉搜索
+         * @param keyword 
+         * @param content 
+         */
+        async handlePkgRemoteSearch(keyword, content) {
+            content.meta.pkgLoading = true;
+            // 模拟远程接口请求(替换为你的真实接口)
+            let cl = await this.getPkgList(this.isEmpty(keyword) ? null : keyword, null);
+            content.meta.pkgLoading = false;
+            content.meta.pkgList = cl.data;
+        },
+        /**
+         * 删除内容节点
+         * @param index 
+         */
+        delContentNode(index){
+            this.addForm.setting.splice(index, 1)
+        },
+        /**
+         * 添加规则内容
+         */
+        addContent() {
+            let content = {
+                contentType: '11',//默认文本类型,其余类型暂时禁用
+                value: '',
+            };
+            this.addForm.setting.push(content);
+            this.handleContentTypeChange();
+        },
+        /**
+         * 内容类别切换
+         */
+        handleContentTypeChange() {
+            //获取选中的科普
+            const selectedCourse = this.addForm.courseId != null
+                ? this.courseList.find(course => parseInt(course.dictValue) == this.addForm.courseId)
+                : null;
+                //获取选中的视频
+            const selectedVideo = this.addForm.videoId != null
+                ? this.videoList.find(course => parseInt(course.dictValue) === this.addForm.videoId)
+                : null;
+            for (let i = 0; i < this.addForm.setting.length; i++) {
+                let contentItem = this.addForm.setting[i];
+                
+                // 科普
+                if (Number(contentItem.contentType) === 9) {
+                    // 处理科普链接标题/封面
+                    if (selectedCourse) {
+                        this.$set(contentItem, 'linkTitle', selectedCourse.dictLabel);
+                        this.$set(contentItem, 'linkImageUrl', selectedCourse.dictImgUrl);
+                    }
+                    // 处理视频链接描述
+                    if (selectedVideo) {
+                        this.$set(contentItem, 'linkDescribe', selectedVideo.dictLabel);
+                    }
+                }
+                // 疗法
+                else if (Number(contentItem.contentType) === 20) {
+                    if (!contentItem.packageId) {
+                        this.$set(contentItem, 'meta', {
+                            pkgLoading: false,
+                            pkgList: this.pkgList,
+                        });
+                    } else {
+                        let pkgList = this.getPkgList(null, contentItem.packageId);
+                        this.$set(contentItem, 'meta', {
+                            pkgLoading: false,
+                            pkgList: pkgList.data,
+                        });
+                    }
+                }
+            }
+        },
+        /**
+         * 疗法变动处理
+         * @param content 
+         */
+        async pkgChangeUpdate(content) {
+            //将当前规则已选中的视频小节清空
+            this.$set(content, 'packageName', null);
+            this.$set(content, 'packageImgUrl', null);
+            if (content.packageId) {
+                // 查找选中疗法的信息
+                const selectedPkg = content.meta.pkgList.find(pkg => parseInt(pkg.packageId) === content.packageId);
+                this.$set(content, 'packageName', selectedPkg.packageName);
+                this.$set(content, 'packageImgUrl', selectedPkg.imgUrl);
+            }
+        },
+        /**
+         * 打开选择客服的对话框
+         */
+        openSelectCustomerPage() {
+            this.customerConfig.visible = true;
+        },
+        /**
+         * 删除客服
+         * @param list 
+         */
+        handleCloseCustomer(node) {
+            const index = this.customerConfig.selected.findIndex(t => t == node);
+            if (index !== -1) {
+                this.customerConfig.selected.splice(index, 1);
+            }
+        },
+        //--------------------------------------------------------------公共 begin---------------------------------------------------------
+        /**
+         * 获取科普信息
+         */
+        async getCourseList(keyword, selectedId) {
+            let data = {
+                keyword: keyword,
+                selectedId: selectedId,
+                pageNum: 1,
+                pageSize: 50,
+            };
+            return await courseList(data);
+        },
+        /**
+         * 科普变动处理
+         * @param mode 模式,0-查询列表,1-新增编辑页
+         */
+        async courseChangeUpdate(mode) {
+            if (mode == 1) {
+                //将当前规则已选中的视频小节清空
+                this.addForm.videoId = null;
+                this.videoList = [];
+                // 查找选中的科普的信息
+                const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) == this.addForm.courseId);
+                for (let i = 0; i < this.addForm.setting.length; i++) {
+                    let contentItem = this.addForm.setting[i];
+                    this.$set(contentItem, 'linkTitle', null);
+                    this.$set(contentItem, 'linkDescribe', null);
+                    this.$set(contentItem, 'linkImageUrl', null);
+
+                    //如果是科普才上
+                    if (selectedCourse && this.addForm.courseId) {
+                        //响应式直接给链接的标题/封面上值
+                        if (contentItem.contentType == 9) {
+                            this.$set(contentItem, 'linkTitle', selectedCourse.dictLabel);
+                            this.$set(contentItem, 'linkImageUrl', selectedCourse.dictImgUrl);
+                        }
+                    }
+                }
+                if (this.addForm.courseId) {
+                    let vl = await this.getVideoList(this.addForm.courseId, null);
+                    this.videoList = vl.list;
+                } else {
+                    let cl = await this.getCourseList(null, null);
+                    this.courseList = cl.list;
+                }
+            } else {
+                if (this.queryParams.courseId) {
+                    let vl = await this.getVideoList(this.queryParams.courseId, null);
+                    this.queryVideoList = vl.list;
+                } else {
+                    this.queryParams.videoId = null;
+                    let cl = await this.getCourseList(null, null);
+                    this.queryCourseList = cl.list;
+                    let vl = await this.getVideoList(null, null);
+                    this.queryVideoList = vl.list;
+                }
+            }
+        },
+        /**
+         * 科普下拉远程搜索
+         * @param {*} keyword 搜索关键字
+         * @param {*} mode    模式,0-查询列表,1-新增编辑页
+         */
+        async handleCourseRemoteSearch(keyword, mode) {
+            this.courseLoading = true;
+            // 模拟远程接口请求(替换为你的真实接口)
+            let cl = await this.getCourseList(this.isEmpty(keyword) ? null : keyword, null);
+            this.courseLoading = false;
+            if (1 == mode) {
+                this.courseList = cl.list;
+            } else {
+                this.queryCourseList = cl.list;
+            }
+        },
+        /**
+         * 根据科普获取视频小节
+         * @param courseId 
+         * @param title 
+         */
+        async getVideoList(courseId, title) {
+            return await videoList({
+                courseId: courseId,
+                title: title
+            });
+        },
+        /**
+         * 获取疗信息
+         */
+        async getPkgList(keyword, selectedId) {
+            let data = {
+                keyword: keyword,
+                metaId: selectedId,
+                limit: 50
+            };
+            return await getPackageOptions(data);
+        },
+        /**
+         * 空校验
+         * @param obj 
+         */
+        isEmpty(obj) {
+            return obj === null || obj === undefined || obj.length === 0;
+        },
+    },
+};
+</script>

+ 356 - 0
src/views/app/customerBind/index.vue

@@ -0,0 +1,356 @@
+<template>
+    <div class="app-container">
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+            <el-form-item label="销售" prop="companyUserId">
+                <el-select
+                    v-model="queryParams.companyUserId"
+                    placeholder="请输入销售id或名称筛选,默认100条"
+                    clearable
+                    filterable
+                    remote
+                    :remote-method="(keyword) => companyUserRemoteSearch(keyword, queryParams.companyUserId)"
+                    style="width: 290px;"
+                >
+                    <el-option
+                        v-for="item in companyUserOptions"
+                        :key="item.dictValue"
+                        :label="`${item.dictLabel}(${item.dictValue})`"
+                        :value="item.dictValue"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item label="部门" prop="deptId">
+                <treeselect
+                    v-model="queryParams.deptId"
+                    :options="deptOptions"
+                    :normalizer="normalizer"
+                    placeholder="选择部门"
+                    style="width: 220px"
+                />
+            </el-form-item>
+            <el-form-item label="含子部门" prop="includeChild">
+                <el-switch v-model="queryParams.includeChild"/>
+            </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"
+                    v-hasPermi="['app:comuserdept:add']"
+                    @click="handleAdd"
+                >
+                    新增
+                </el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    type="danger"
+                    plain
+                    icon="el-icon-delete"
+                    size="mini"
+                    :disabled="ids.length === 0"
+                    v-hasPermi="['app:comuserdept:delete']"
+                    @click="handleDelete"
+                >
+                    删除
+                </el-button>
+            </el-col>
+            <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+
+        <el-table v-loading="loading" :data="customerBindList" @selection-change="handleSelectionChange" border>
+            <el-table-column type="selection" width="55" align="center"/>
+            <el-table-column label="销售ID" align="center" prop="companyUserId" min-width="100"/>
+            <el-table-column label="销售名称" align="center" prop="companyUserName" min-width="150"/>
+            <el-table-column label="销售账号" align="center" prop="companyUserAccount" min-width="150"/>
+            <el-table-column label="部门" align="center" prop="deptName" min-width="120"/>
+            <el-table-column label="创建时间" align="center" prop="createTime" min-width="120"/>
+            <el-table-column label="修改时间" align="center" prop="updateTime" min-width="120"/>
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" min-width="120">
+                <template slot-scope="scope">
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-edit"
+                        v-hasPermi="['app:comuserdept:edit']"
+                        @click="handleUpdate(scope.row)"
+                    >修改</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <pagination
+            v-show="total>0"
+            :total="total"
+            :page.sync="queryParams.pageNum"
+            :limit.sync="queryParams.pageSize"
+            @pagination="getList"
+        />
+
+        <!-- 添加或修改客服绑定关系 -->
+        <el-dialog
+            :title="bindDialog.title"
+            :visible.sync="bindDialog.visible"
+            width="500px"
+            append-to-body
+            :close-on-click-modal="false"
+        >
+            <el-form ref="bindForm" :model="bindForm" :rules="rules" label-width="80px">
+                <el-form-item label="销售" prop="companyUserId">
+                    <el-select
+                        v-model="bindForm.companyUserId"
+                        placeholder="请输入销售id或名称筛选,默认100条"
+                        clearable
+                        filterable
+                        remote
+                        style="width: 290px;"
+                        :remote-method="(keyword) => companyUserRemoteSearch(keyword, bindForm.companyUserId)"
+                    >
+                        <el-option
+                            v-for="item in companyUserOptions"
+                            :key="item.dictValue"
+                            :label="`${item.dictLabel}(${item.dictValue})`"
+                            :value="item.dictValue"
+                        />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="部门" prop="deptId">
+                    <treeselect
+                        v-model="bindForm.deptId"
+                        :options="deptOptions"
+                        :normalizer="normalizer"
+                        placeholder="选择部门"
+                        style="width: 290px;"
+                    />
+                </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 { findList, getById, deleteByIds, add, edit, findCompanyUserOptions, } from "@/api/app/customerBind";
+import { listDept } from '@/api/system/appDept';
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+export default {
+    name: "CustomerBind",
+    components: { Treeselect },
+    data() {
+        return {
+            // 部门树形选择器选项
+            deptOptions: [],
+            companyUserOptions: [],
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 客服/销售绑定部门表格数据
+            customerBindList: [],
+            bindDialog: {
+                visible: false,
+                loading: false,
+                title: null,
+            },
+            // 表单参数
+            bindForm: {
+                id: null,
+                companyUserId: null,
+                deptId: null
+            },
+            // 表单校验
+            rules: {
+                companyUserId: [
+                    { required: true, message: "销售不可为空", trigger: "blur" },
+                ],
+                deptId: [
+                    { required: true, message: "部门不可为空", trigger: "blur" },
+                ],
+            },
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                companyUserId: null,
+                deptId: null,
+                includeChild: false,
+            },
+        };
+    },
+    created() {
+        this.getList();
+        this.getDeptList();
+        this.getCompanyUserOptions(null);
+    },
+    methods: {
+        /** 列表查询 */
+        getList() {
+            this.loading = true;
+            findList(this.queryParams).then(response => {
+                this.customerBindList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        /**
+         * 获取部门树形数据
+         */
+        getDeptList() {
+            listDept().then(response => {
+                if (response.code === 200) {
+                    // 转换为树形结构
+                    this.deptOptions = this.handleTree(response.data, "deptId");
+                } else {
+                    this.deptOptions = [];
+                }
+            }).catch(() => {
+                this.deptOptions = [];
+            });
+        },
+        /**
+         * 获取销售列表
+         */
+        async getCompanyUserOptions(keyword, selectedId) {
+            let res = await findCompanyUserOptions({keyword: keyword, selectedId: selectedId, pageNumber: 1, pageSize: 100});
+            this.companyUserOptions = res.data;
+        },
+        companyUserRemoteSearch(keyword, selectedId) {
+            this.getCompanyUserOptions(keyword, selectedId);
+        },
+        /**
+         * 树形结构转换
+         */
+        handleTree(data, id, parentId, children) {
+            id = id || 'id';
+            parentId = parentId || 'parentId';
+            children = children || 'children';
+            var tree = [];
+            var map = {};
+            data.forEach(function(item) {
+                map[item[id]] = item;
+            });
+            data.forEach(function(item) {
+                var parent = map[item[parentId]];
+                if (parent) {
+                    (parent[children] || (parent[children] = [])).push(item);
+                } else {
+                    tree.push(item);
+                }
+            });
+            return tree;
+        },
+         /**
+         * 树形选择器数据格式化
+         */
+        normalizer(node) {
+            if (node.children && !node.children.length) {
+                delete node.children;
+            }
+            return {
+                id: node.deptId,
+                label: node.deptName,
+                children: node.children
+            };
+        },
+        // 取消按钮
+        cancel() {
+            this.bindDialog.visible = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.bindForm = {
+                id: null,
+                companyUserId: null,
+                deptId: null,
+            };
+            this.resetForm("bindForm");
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.queryParams = {
+                pageNum: 1,
+                pageSize: 10,
+                companyUserId: null,
+                deptId: null,
+                includeChild: false,
+            };
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map(item => item.id);
+        },
+        /** 新增按钮操作 */
+        handleAdd() {
+            this.reset();
+            this.bindDialog.title = "添加绑定关系";
+            this.bindDialog.visible = true;
+        },
+        /** 修改按钮操作 */
+        handleUpdate(row) {
+            this.reset();
+            getById(row.id ).then(response => {
+                for (let key in this.bindForm) {
+                    this.bindForm[key] = response.data[key] || null;
+                }
+                this.bindDialog.title = "修改绑定关系";
+                this.bindDialog.visible = true;
+            });
+        },
+        handleDelete() {
+            this.$confirm('是否确认删除?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(async () => {
+                await deleteByIds(this.ids);
+                this.getList();
+                this.msgSuccess("删除成功");
+            });
+        },
+        /** 提交按钮 */
+        submitForm() {
+            this.$refs["bindForm"].validate(valid => {
+                if (valid) {
+                    if (this.bindForm.id != null) {
+                        edit(this.bindForm).then(response => {
+                            this.msgSuccess("修改成功");
+                            this.bindDialog.visible = false;
+                            this.getList();
+                        });
+                    } else {
+                        add(this.bindForm).then(response => {
+                            this.msgSuccess("新增成功");
+                            this.bindDialog.visible = false;
+                            this.getList();
+                        });
+                    }
+                }
+            });
+        },
+    }
+};
+</script>

+ 561 - 0
src/views/app/customerMember/index.vue

@@ -0,0 +1,561 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="90px">
+      <el-form-item label="成员名称" prop="keyword">
+        <el-input
+          v-model="queryParams.keyword"
+          placeholder="请输入成员名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="所属客服组" prop="appCustomerId">
+        <el-select
+          v-model="queryParams.appCustomerId"
+          placeholder="请选择客服组"
+          clearable
+          filterable
+          size="small"
+        >
+          <el-option
+            v-for="item in roleOptions"
+            :key="item.id"
+            :label="item.roleName"
+            :value="parseRoleId(item.id)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="primary"-->
+<!--          plain-->
+<!--          icon="el-icon-plus"-->
+<!--          size="mini"-->
+<!--          v-hasPermi="['app:customerMember:add']"-->
+<!--          @click="handleAdd"-->
+<!--        >新增</el-button>-->
+<!--      </el-col>-->
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="success"-->
+<!--          plain-->
+<!--          icon="el-icon-edit"-->
+<!--          size="mini"-->
+<!--          :disabled="single"-->
+<!--          v-hasPermi="['app:customerMember:edit']"-->
+<!--          @click="handleUpdate()"-->
+<!--        >修改</el-button>-->
+<!--      </el-col>-->
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          v-hasPermi="['app:customerMember:remove']"
+          @click="handleDelete()"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-refresh"
+          size="mini"
+          :loading="syncLoading"
+          v-hasPermi="['app:customerMember:add']"
+          @click="handleSyncSales"
+        >同步销售</el-button>
+        <el-tooltip
+          effect="dark"
+          content="该按钮初次同步可能较慢,后续再次同步只会新增新的销售数据"
+          placement="top"
+        >
+          <i class="el-icon-question sync-sales-tip"></i>
+        </el-tooltip>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="memberList" row-key="roleMemberId" @selection-change="handleSelectionChange" border>
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="所属客服组" align="center" prop="roleName" min-width="140" />
+<!--      <el-table-column label="成员名称" align="center" prop="memberName" min-width="120" />-->
+      <el-table-column label="绑定销售" align="center" prop="companyUserName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="头像" align="center" prop="avatar" width="100">
+        <template slot-scope="scope">
+          <el-image
+            v-if="scope.row.avatar"
+            style="width: 50px; height: 50px; border-radius: 4px;"
+            :src="scope.row.avatar"
+            :preview-src-list="[scope.row.avatar]"
+            fit="cover"
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="描述" align="center" prop="remark" min-width="160" show-overflow-tooltip />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="140">
+        <template slot-scope="scope">
+<!--          <el-button-->
+<!--            size="mini"-->
+<!--            type="text"-->
+<!--            icon="el-icon-edit"-->
+<!--            v-hasPermi="['app:customerMember:edit']"-->
+<!--            @click="handleUpdate(scope.row)"-->
+<!--          >修改</el-button>-->
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            v-hasPermi="['app:customerMember:remove']"
+            @click="handleDelete(scope.row)"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-dialog :title="title" :visible.sync="open" width="560px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="所属客服组" prop="appCustomerId">
+          <el-select
+            v-model="form.appCustomerId"
+            placeholder="请选择所属客服组"
+            filterable
+            style="width: 100%;"
+          >
+            <el-option
+              v-for="item in roleOptions"
+              :key="item.id"
+              :label="item.roleName"
+              :value="parseRoleId(item.id)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="绑定销售" prop="companyUserId">
+          <el-input
+            v-model="form.companyUserName"
+            placeholder="请选择绑定销售"
+            readonly
+            @click.native="openBindCompanyUserDialog"
+          >
+            <i slot="suffix" class="el-input__icon el-icon-search" @click.stop="openBindCompanyUserDialog"></i>
+          </el-input>
+        </el-form-item>
+<!--        <el-form-item label="成员名称" prop="memberName">-->
+<!--          <el-input v-model="form.memberName" placeholder="请输入成员名称" />-->
+<!--        </el-form-item>-->
+        <el-form-item label="头像" prop="avatar">
+          <ImageUpload
+            v-model="form.avatar"
+            type="image"
+            :limit="1"
+            :file-size="1"
+            :width="150"
+            :height="150"
+          />
+        </el-form-item>
+        <el-form-item label="描述" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入描述" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="选择绑定销售" :visible.sync="bindCompanyUserOpen" width="500px" append-to-body>
+      <el-form ref="bindCompanyUserForm" :model="bindCompanyUserForm" :rules="bindCompanyUserRules" label-width="100px">
+        <el-form-item label="选择公司" prop="companyId">
+          <el-select
+            v-model="bindCompanyUserForm.companyId"
+            placeholder="请选择公司"
+            filterable
+            style="width: 100%;"
+            @change="handleBindCompanyChange"
+          >
+            <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="companyUserId">
+          <el-select
+            v-model="bindCompanyUserForm.companyUserId"
+            placeholder="请选择销售"
+            filterable
+            style="width: 100%;"
+          >
+            <el-option
+              v-for="item in companyUserOptions"
+              :key="item.userId"
+              :label="item.nickName + '_' + item.userName"
+              :value="item.userId"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitBindCompanyUserForm">确 定</el-button>
+        <el-button @click="cancelBindCompanyUser">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listCustomerMember,
+  addCustomerMember,
+  updateCustomerMember,
+  delCustomerMember,
+  syncSalesCustomerMember,
+  getSyncSalesCustomerMemberStatus
+} from '@/api/app/customerMember'
+import { listCustomerRoleOptions } from '@/api/app/customerRole'
+import { getCompanyList, getCompanyUserList } from '@/api/company/companyUser'
+import ImageUpload from '@/views/qw/sop/ImageUpload'
+
+export default {
+  name: 'CustomerMember',
+  components: { ImageUpload },
+  data() {
+    return {
+      loading: true,
+      syncLoading: false,
+      syncTimer: null,
+      showSearch: true,
+      total: 0,
+      ids: [],
+      single: true,
+      multiple: true,
+      memberList: [],
+      roleOptions: [],
+      companyOptions: [],
+      companyUserOptions: [],
+      bindCompanyUserOpen: false,
+      bindCompanyUserForm: {
+        companyId: null,
+        companyUserId: null
+      },
+      bindCompanyUserRules: {
+        companyId: [
+          { required: true, message: '请选择公司', trigger: 'change' }
+        ],
+        companyUserId: [
+          { required: true, message: '请选择销售', trigger: 'change' }
+        ]
+      },
+      title: '',
+      open: false,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        keyword: null,
+        appCustomerId: null
+      },
+      form: {},
+      rules: {
+        appCustomerId: [
+          { required: true, message: '请选择所属客服组', trigger: 'change' }
+        ],
+        companyUserId: [
+          { required: true, message: '请选择绑定销售', trigger: 'change' }
+        ],
+        // memberName: [
+        //   { required: true, message: '成员名称不能为空', trigger: 'blur' }
+        // ]
+      }
+    }
+  },
+  created() {
+    this.loadRoleOptions()
+    this.getList()
+  },
+  beforeDestroy() {
+    this.clearSyncTimer()
+  },
+  methods: {
+    clearSyncTimer() {
+      if (this.syncTimer) {
+        clearInterval(this.syncTimer)
+        this.syncTimer = null
+      }
+    },
+    handleSyncSales() {
+      this.$confirm('是否确认同步销售数据', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.syncLoading = true
+        return syncSalesCustomerMember()
+      }).then(response => {
+        if (response.code !== 200) {
+          this.syncLoading = false
+          this.msgError(response.msg || '提交同步任务失败')
+          return
+        }
+        this.msgSuccess('同步任务已提交,同步中。。。')
+        this.clearSyncTimer()
+        this.syncTimer = setInterval(() => {
+          this.pollSyncSalesStatus()
+        }, 2000)
+        this.pollSyncSalesStatus()
+      }).catch(() => {
+        this.syncLoading = false
+      })
+    },
+    pollSyncSalesStatus() {
+      getSyncSalesCustomerMemberStatus().then(response => {
+        if (response.code !== 200) {
+          return
+        }
+        const data = response.data || {}
+        if (data.status === 'running') {
+          return
+        }
+        this.clearSyncTimer()
+        this.syncLoading = false
+        if (data.status === 'success') {
+          this.msgSuccess(data.message || '同步完成')
+          this.getList()
+          this.loadRoleOptions()
+        } else if (data.status === 'fail') {
+          this.msgError(data.message || '同步失败')
+        }
+      }).catch(() => {
+        this.clearSyncTimer()
+        this.syncLoading = false
+        this.msgError('查询同步状态失败')
+      })
+    },
+    parseRoleId(value) {
+      if (value === null || value === undefined || value === '') {
+        return null
+      }
+      const num = Number(value)
+      return Number.isNaN(num) ? value : num
+    },
+    loadRoleOptions() {
+      listCustomerRoleOptions().then(response => {
+        this.roleOptions = (response.rows || []).map(item => ({
+          ...item,
+          id: this.parseRoleId(item.id)
+        }))
+      })
+    },
+    getList() {
+      this.loading = true
+      listCustomerMember(this.queryParams).then(response => {
+        this.memberList = response.rows
+        this.total = response.total
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    resetBindCompanyUserForm() {
+      this.bindCompanyUserForm = {
+        companyId: null,
+        companyUserId: null
+      }
+      this.companyUserOptions = []
+      this.$nextTick(() => {
+        this.$refs.bindCompanyUserForm && this.$refs.bindCompanyUserForm.clearValidate()
+      })
+    },
+    openBindCompanyUserDialog() {
+      getCompanyList().then(response => {
+        if (response.code === 200) {
+          this.companyOptions = response.data || []
+          this.resetBindCompanyUserForm()
+          this.bindCompanyUserOpen = true
+        } else {
+          this.msgError(response.msg || '获取公司列表失败')
+        }
+      }).catch(() => {
+        this.msgError('获取公司列表失败')
+      })
+    },
+    handleBindCompanyChange(companyId) {
+      this.bindCompanyUserForm.companyUserId = null
+      if (!companyId) {
+        this.companyUserOptions = []
+        return
+      }
+      getCompanyUserList({ companyId }).then(response => {
+        if (response.code === 200) {
+          this.companyUserOptions = response.data || []
+        } else {
+          this.msgError(response.msg || '获取销售列表失败')
+          this.companyUserOptions = []
+        }
+      }).catch(() => {
+        this.msgError('获取销售列表失败')
+        this.companyUserOptions = []
+      })
+    },
+    submitBindCompanyUserForm() {
+      this.$refs.bindCompanyUserForm.validate(valid => {
+        if (!valid) {
+          return
+        }
+        const selected = this.companyUserOptions.find(
+          item => item.userId === this.bindCompanyUserForm.companyUserId
+        )
+        this.form.companyUserId = this.bindCompanyUserForm.companyUserId
+        this.form.companyUserName = selected
+          ? `${selected.nickName}_${selected.userName}`
+          : ''
+        this.bindCompanyUserOpen = false
+        this.$refs.form && this.$refs.form.validateField('companyUserId')
+      })
+    },
+    cancelBindCompanyUser() {
+      this.bindCompanyUserOpen = false
+      this.resetBindCompanyUserForm()
+    },
+    cancel() {
+      this.open = false
+      this.reset()
+    },
+    reset() {
+      this.form = {
+        roleMemberId: null,
+        appCustomerId: null,
+        companyUserId: null,
+        companyUserName: null,
+        avatar: null,
+        remark: null
+      }
+      this.resetForm('form')
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.queryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        keyword: null,
+        appCustomerId: null
+      }
+      this.handleQuery()
+    },
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.roleMemberId)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    handleAdd() {
+      this.reset()
+      this.open = true
+      this.title = '新增客服'
+    },
+    handleUpdate(row) {
+      const target = row || this.memberList.find(item => item.roleMemberId === this.ids[0])
+      if (!target) {
+        return
+      }
+      this.form = {
+        roleMemberId: target.roleMemberId,
+        appCustomerId: target.appCustomerId != null && target.appCustomerId !== ''
+          ? this.parseRoleId(target.appCustomerId)
+          : null,
+        companyUserId: target.companyUserId,
+        companyUserName: target.companyUserName,
+        avatar: target.avatar,
+        remark: target.remark
+      }
+      this.open = true
+      this.title = '修改客服'
+      this.$nextTick(() => {
+        this.$refs.form && this.$refs.form.clearValidate()
+      })
+    },
+    handleDelete(row) {
+      const ids = row ? row.roleMemberId : this.ids.join(',')
+      this.$confirm('是否确认删除选中的客服数据?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return delCustomerMember(ids)
+      }).then(() => {
+        this.getList()
+        this.msgSuccess('删除成功')
+      }).catch(() => {})
+    },
+    buildSubmitData() {
+      const data = {
+        appCustomerId: this.parseRoleId(this.form.appCustomerId),
+        avatar: this.form.avatar,
+        remark: this.form.remark,
+        companyUserId: this.form.companyUserId,
+        companyUserName: this.form.companyUserName
+      }
+      if (this.form.roleMemberId != null) {
+        data.roleMemberId = this.form.roleMemberId
+      }
+      return data
+    },
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (!valid) {
+          return
+        }
+        const data = this.buildSubmitData()
+        const request = this.form.roleMemberId != null
+          ? updateCustomerMember(data)
+          : addCustomerMember(data)
+        request.then(response => {
+          if (response.code === 200) {
+            this.msgSuccess(this.form.roleMemberId != null ? '修改成功' : '新增成功')
+            this.open = false
+            this.getList()
+          } else {
+            this.msgError(response.msg || '保存失败')
+          }
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.el-input__icon {
+  cursor: pointer;
+}
+.sync-sales-tip {
+  margin-left: 6px;
+  color: #909399;
+  cursor: pointer;
+  font-size: 14px;
+  vertical-align: middle;
+}
+</style>

+ 215 - 0
src/views/app/customerRole/index.vue

@@ -0,0 +1,215 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="90px">
+      <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"-->
+<!--          v-hasPermi="['app:customerRole:add']"-->
+<!--          @click="handleAdd"-->
+<!--        >新增</el-button>-->
+<!--      </el-col>-->
+<!--      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>-->
+<!--    </el-row>-->
+
+    <el-table v-loading="loading" :data="roleList" border>
+      <el-table-column label="客服组名称" align="center" prop="roleName" min-width="140" />
+      <el-table-column label="头像" align="center" prop="avatar" width="100">
+        <template slot-scope="scope">
+          <el-image
+            v-if="scope.row.avatar"
+            style="width: 50px; height: 50px; border-radius: 4px;"
+            :src="scope.row.avatar"
+            :preview-src-list="[scope.row.avatar]"
+            fit="cover"
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" min-width="160" show-overflow-tooltip />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160" />
+      <el-table-column label="添加客户最大数量" align="center" prop="memberMaxFriendCount" width="150" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            v-hasPermi="['app:customerRole:edit']"
+            @click="handleUpdate(scope.row)"
+          >修改</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-dialog :title="title" :visible.sync="open" width="560px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="140px">
+        <el-form-item label="客服组名称" prop="roleName">
+          <el-input v-model="form.roleName" placeholder="请输入客服组名称" />
+        </el-form-item>
+        <el-form-item label="头像" prop="avatar">
+          <ImageUpload
+            v-model="form.avatar"
+            type="image"
+            :limit="1"
+            :file-size="1"
+            :width="150"
+            :height="150"
+          />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+        <el-form-item label="添加客户最大数量" prop="memberMaxFriendCount">
+          <el-input-number
+            v-model="form.memberMaxFriendCount"
+            :min="0"
+            :max="999999"
+            controls-position="right"
+            placeholder="请输入数量"
+          />
+        </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 {
+  listCustomerRole,
+  addCustomerRole,
+  updateCustomerRole
+} from '@/api/app/customerRole'
+import ImageUpload from '@/views/qw/sop/ImageUpload'
+
+export default {
+  name: 'CustomerRole',
+  components: { ImageUpload },
+  data() {
+    return {
+      loading: true,
+      showSearch: true,
+      total: 0,
+      roleList: [],
+      title: '',
+      open: false,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        roleName: null
+      },
+      form: {},
+      rules: {
+        roleName: [
+          { required: true, message: '客服组名称不能为空', trigger: 'blur' }
+        ],
+        memberMaxFriendCount: [
+          { required: true, message: '添加客户最大数量不能为空', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listCustomerRole(this.queryParams).then(response => {
+        this.roleList = response.rows
+        this.total = response.total
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    cancel() {
+      this.open = false
+      this.reset()
+    },
+    reset() {
+      this.form = {
+        id: null,
+        roleName: null,
+        avatar: null,
+        remark: null,
+        memberMaxFriendCount: 0
+      }
+      this.resetForm('form')
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.queryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        roleName: null
+      }
+      this.handleQuery()
+    },
+    handleAdd() {
+      this.reset()
+      this.open = true
+      this.title = '新增客服组'
+    },
+    handleUpdate(row) {
+      this.reset()
+      this.form = { ...row }
+      this.open = true
+      this.title = '修改客服组'
+    },
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (!valid) {
+          return
+        }
+        if (this.form.id != null) {
+          updateCustomerRole(this.form).then(() => {
+            this.msgSuccess('修改成功')
+            this.open = false
+            this.getList()
+          })
+        } else {
+          addCustomerRole(this.form).then(() => {
+            this.msgSuccess('新增成功')
+            this.open = false
+            this.getList()
+          })
+        }
+      })
+    }
+  }
+}
+</script>

+ 908 - 0
src/views/app/invitationCode/index.vue

@@ -0,0 +1,908 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索区域 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="邀请码" prop="invitationCode">
+        <el-input
+          v-model="queryParams.invitationCode"
+          placeholder="请输入邀请码"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="标签" prop="tagIds">
+        <div
+          @click="openTagSearchPage(false)"
+          style="
+            cursor: pointer;
+            border: 1px solid #e6e6e6;
+            background-color: white;
+            overflow: hidden;
+            flex-grow: 1;
+            width: 250px;
+          "
+          >
+          <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+            <el-tag type="success"
+              closable
+              :disable-transitions="false"
+              v-for="list in this.dialog.selectTagForQuery.selected"
+              :key="list.id"
+              @close="handleCloseTagsForQuery(list)"
+              style="margin: 3px;"
+            >
+              {{ list.name }}
+            </el-tag>
+          </div>
+        </div>
+      </el-form-item>
+      <el-form-item label="客服" prop="customerIds">
+        <div
+          @click="openCusSearchPage(false)"
+          style="
+            cursor: pointer;
+            border: 1px solid #e6e6e6;
+            background-color: white;
+            overflow: hidden;
+            flex-grow: 1;
+            width: 250px;
+          "
+        >
+          <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+            <el-tag type="success"
+              closable
+              :disable-transitions="false"
+              v-for="list in this.dialog.selectCustomerForQuery.selected"
+              :key="list.id"
+              @close="handleCloseCusForQuery(list)"
+              style="margin: 3px;"
+            >
+              {{ list.memberName }}
+            </el-tag>
+          </div>
+        </div>
+      </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">
+      <div v-if="activeName === 'person'">
+        <el-col :span="1.5">
+          <el-button
+            type="primary"
+            plain
+            icon="el-icon-plus"
+            size="mini"
+            @click="handleAdd"
+            v-hasPermi="['app:invite:add']"
+          >新增</el-button>
+        </el-col>
+        <el-col :span="1.5">
+          <el-button
+            type="error"
+            plain
+            icon="el-icon-delete"
+            size="mini"
+            @click="handleDelete"
+            v-hasPermi="['app:invite:delete']"
+          >删除</el-button>
+        </el-col>
+      </div>
+      <div v-else style="height: 28px;">
+
+      </div>
+    </el-row>
+
+    <el-tabs v-model="activeName" type="card" @tab-click="handleClick">
+<!--      <el-tab-pane label="个人码" name="person">-->
+<!--        &lt;!&ndash; 个人邀请码表格 &ndash;&gt;-->
+<!--        <el-table-->
+<!--          v-loading="loading"-->
+<!--          :data="tableInfo.list"-->
+<!--          @selection-change="handleSelectionChange"-->
+<!--          border-->
+<!--        >-->
+<!--          <el-table-column type="selection" width="55" align="center" />-->
+<!--          <el-table-column label="邀请码" align="center" prop="invitationCode" min-width="150"/>-->
+<!--          <el-table-column label="部门" align="left" prop="deptName" min-width="150"/>-->
+<!--          <el-table-column label="备注" align="center" prop="remark"/>-->
+<!--          <el-table-column label="创建时间" align="center" prop="createTime" min-width="150" />-->
+<!--          <el-table-column label="修改时间" align="center" prop="createTime" min-width="150" />-->
+<!--          <el-table-column label="客服" align="center" min-width="200px" prop="customerNames">-->
+<!--            <template slot-scope="scope">-->
+<!--              <div v-for="i in (scope.row.customerNames ? scope.row.customerNames.split(',') : [])" :key="i" style="display: inline;">-->
+<!--                <el-tag type="primary" style="margin: 3px;">{{i}}</el-tag>-->
+<!--              </div>-->
+<!--            </template>-->
+<!--          </el-table-column>-->
+<!--          <el-table-column label="标签" align="center" min-width="200" prop="tagsName">-->
+<!--            <template slot-scope="scope">-->
+<!--              <div v-for="i in (scope.row.tagNames ? scope.row.tagNames.split(',') : [])" :key="i" style="display: inline;">-->
+<!--                <el-tag type="success" style="margin: 3px;">{{i}}</el-tag>-->
+<!--              </div>-->
+<!--            </template>-->
+<!--          </el-table-column>-->
+<!--          <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="190">-->
+<!--            <template slot-scope="scope">-->
+<!--              <el-button-->
+<!--                size="mini"-->
+<!--                type="text"-->
+<!--                icon="el-icon-edit"-->
+<!--                @click="handleUpdate(scope.row)"-->
+<!--                v-hasPermi="['app:invite:edit']"-->
+<!--              >修改</el-button>-->
+<!--              <el-button-->
+<!--                size="mini"-->
+<!--                type="text"-->
+<!--                icon="el-icon-delete"-->
+<!--                @click="handleDelete(scope.row)"-->
+<!--                v-hasPermi="['app:invite:delete']"-->
+<!--                style="color: red;"-->
+<!--              >删除</el-button>-->
+<!--            </template>-->
+<!--          </el-table-column>-->
+<!--        </el-table>-->
+<!--      </el-tab-pane>-->
+      <el-tab-pane label="公司码" name="company">
+        <!-- 公司邀请码表格 -->
+        <el-table
+          v-loading="loading"
+          :data="tableInfo.list"
+          @selection-change="handleSelectionChange"
+          border
+        >
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column label="公司码(公司ID)" align="center" prop="invitationCode" min-width="150"/>
+          <el-table-column label="公司" align="left" prop="companyName" min-width="150"/>
+          <el-table-column label="二维码" align="center" prop="qrCode" width="120">
+            <template slot-scope="scope">
+              <div v-if="getQrCodeUrl(scope.row)" class="qrcode-cell">
+                <el-popover placement="right" trigger="hover" width="220">
+                  <el-image
+                    :src="getQrCodeUrl(scope.row)"
+                    style="width: 200px; height: 200px;"
+                    fit="contain"
+                  />
+                  <el-image
+                    slot="reference"
+                    style="width: 60px; height: 60px; cursor: pointer;"
+                    :src="getQrCodeUrl(scope.row)"
+                    :preview-src-list="[getQrCodeUrl(scope.row)]"
+                    fit="contain"
+                  >
+                    <div slot="error" class="image-slot">
+                      <i class="el-icon-picture-outline"></i>
+                    </div>
+                  </el-image>
+                </el-popover>
+              </div>
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="备注" align="center" prop="remark"/>
+          <el-table-column label="创建时间" align="center" prop="createTime" min-width="150" />
+          <el-table-column label="修改时间" align="center" prop="createTime" min-width="150" />
+          <el-table-column label="客服" align="center" min-width="200px" prop="customerNames">
+            <template slot-scope="scope">
+              <div v-for="i in (scope.row.customerNames ? scope.row.customerNames.split(',') : [])" :key="i" style="display: inline;">
+                <el-tag type="primary" style="margin: 3px;">{{i}}</el-tag>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="标签" align="center" min-width="200" prop="tagsName">
+            <template slot-scope="scope">
+              <div v-for="i in (scope.row.tagNames ? scope.row.tagNames.split(',') : [])" :key="i" style="display: inline;">
+                <el-tag type="success" style="margin: 3px;">{{i}}</el-tag>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="190">
+            <template slot-scope="scope">
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-edit"
+                @click="handleUpdate(scope.row)"
+                v-hasPermi="['app:invite:edit']"
+              >
+                修改
+              </el-button>
+              <!-- 公司码不需要删除 -->
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-tab-pane>
+    </el-tabs>
+
+    <!-- 分页 -->
+    <pagination
+      v-show="tableInfo.total > 0"
+      :total="tableInfo.total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="handleQuery"
+    />
+
+    <!-- 新增/修改邀请码弹窗 -->
+    <el-dialog :title="dialog.title" :visible.sync="dialog.open" v-loading="dialog.loading" width="600px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="110px">
+        <el-form-item label="所属部门" prop="deptId" v-if="activeName === 'person'">
+          <treeselect
+            v-model="form.deptId"
+            :options="deptOptions"
+            :normalizer="normalizer"
+            placeholder="选择部门"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="邀请码" prop="invitationCode">
+          <el-input v-model="form.invitationCode" placeholder="请输入邀请码" :disabled="activeName === 'company'"/>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark" >
+          <el-input v-model="form.remark" placeholder="请输入备注" type="textarea"/>
+        </el-form-item>
+        <el-form-item label="选择的标签" prop="tags">
+          <div @click="openTagSearchPage(true)"
+               style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 351px">
+            <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+              <el-tag type="success" closable :disable-transitions="false"
+                v-for="list in this.dialog.selectTagForEdit.selected" :key="list.id" @close="handleCloseTagsForEdit(list)"
+                style="margin: 3px;">{{ list.name }}
+              </el-tag>
+            </div>
+          </div>
+        </el-form-item>
+        <el-form-item label="选择客服" prop="customerIds" style="margin-top: 2%">
+          <div>
+            <el-button size="medium" icon="el-icon-circle-plus-outline" plain
+                       @click="openCusSearchPage(true)">请选择客服</el-button>
+          </div>
+          <div>
+            <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in dialog.selectCustomerForEdit.selected" closable
+                   :disable-transitions="false" @close="handleCloseCusForEdit(s)">
+              <span>{{ s.memberName }}</span>
+            </el-tag>
+          </div>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 标签搜索弹窗 -->
+    <el-dialog title="标签搜索"
+      :visible.sync="dialog.selectTagForQuery.visible"
+      width="1000px"
+      custom-class="search-tag-dialog"
+      append-to-body
+    >
+      <select-tag
+        :tagSelected="dialog.selectTagForQuery.selected"
+        @tagSubmit="tagSubmitForm(false)"
+        @tagCancel="tagCancel(false)"
+        @tagSelection="tagSelectionForQuery"
+      />
+    </el-dialog>
+
+    <!-- 新增/编辑页:搜索标签 -->
+    <el-dialog title="标签搜索"
+      :visible.sync="dialog.selectTagForEdit.visible"
+      width="1000px"
+      custom-class="search-tag-dialog"
+      append-to-body
+    >
+      <select-tag
+        :tagSelected="dialog.selectTagForEdit.selected"
+        @tagSubmit="tagSubmitForm(true)"
+        @tagCancel="tagCancel(true)"
+        @tagSelection="tagSelectionForEdit"
+      />
+    </el-dialog>
+
+    <!-- 列表页:搜索客服 -->
+    <el-dialog title="客服搜索"
+      :visible.sync="dialog.selectCustomerForQuery.visible"
+      width="1000px"
+      append-to-body
+    >
+      <CusRoleList
+        @confirm="customerSubmitCallForQuery"
+        @selectionChange="customerSelectionChangeCallForQuery"
+        :selected="dialog.selectCustomerForQuery.selected"
+      />
+    </el-dialog>
+
+    <!-- 新增/编辑页:搜索客服 -->
+    <el-dialog title="客服搜索"
+      :visible.sync="dialog.selectCustomerForEdit.visible"
+      width="1000px"
+      append-to-body
+    >
+      <CusRoleList
+        @confirm="customerSubmitCallForEdit"
+        @selectionChange="customerSelectionChangeCallForEdit"
+        :selected="dialog.selectCustomerForEdit.selected"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { findList, save, deleteByIds, getById } from "@/api/app/invitationCode";
+import { listRole } from '@/api/app/user/userList';
+import { getTagByIds } from "@/api/app/tag/tagGroup";
+import { listDept } from '@/api/system/appDept';
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import CusRoleList from '@/views/app/user/CusRoleList.vue';
+import selectTag from "@/views/app/tagGroup/selectTag.vue";
+
+export default {
+  name: "invitationCode",
+  components: { CusRoleList, selectTag, Treeselect },
+  data() {
+    return {
+      // 加载状态
+      loading: false,
+      // 是否显示搜索
+      showSearch: true,
+      // 部门树形选择器选项
+      deptOptions: [],
+      // 表格数据
+      tableInfo: {
+        ids: [],
+        list: [],
+        total: 0,
+      },
+      activeName: 'company',
+      // 查询参数
+      queryParams: {
+        pageSize: 10,
+        pageNum: 1,
+        id: null,
+        invitationCode: null,
+        tagIds: null,
+        customerIds: null,
+      },
+      // 表单数据
+      form: {
+        id: null,
+        invitationCode: null,
+        remark: null,
+        deptId: null,
+        tagIds: null,
+        customerIds: null,
+      },
+      // 表单校验规则
+      rules: {
+        invitationCode: [
+          {
+            trigger: 'blur',
+            required: true,
+            message: '邀请码不可为空!'
+          }
+        ],
+        deptId: [
+          {
+            trigger: 'blur',
+            required: true,
+            message: '所属部门不可为空!'
+          }
+        ],
+      },
+      // 弹窗配置
+      dialog: {
+        title: '',
+        open: false,
+        loading: false,
+        selectTagForQuery: {
+          visible: false,
+          selected: [],
+        },
+        selectTagForEdit: {
+          visible: false,
+          selected: [],
+        },
+        selectCustomerForQuery: {
+          visible: false,
+          selected: [],
+        },
+        selectCustomerForEdit: {
+          visible: false,
+          selected: [],
+        },
+      },
+    };
+  },
+  created() {
+    this.getList();
+    this.getDeptList();
+  },
+  methods: {
+    /**
+     * 获取二维码地址
+     */
+    getQrCodeUrl(row) {
+      return row.qrCode || row.qrCodeUrl || row.codeUrl || null
+    },
+
+    /**
+     * 查询邀请码列表
+     */
+    getList() {
+      this.loading = true;
+      let qp = {...this.queryParams};
+      qp['tagIds'] = this.dialog.selectTagForQuery.selected.map(s => s.id);
+      qp['customerIds'] = this.dialog.selectCustomerForQuery.selected.map(s => s.id);
+      qp['activeName'] = this.activeName;
+      findList(qp).then(response => {
+        this.tableInfo.list = response.rows;
+        this.tableInfo.total = response.total;
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+
+    /**
+     * 获取部门树形数据
+     */
+    getDeptList() {
+      listDept().then(response => {
+        if (response.code === 200) {
+          // 转换为树形结构
+          this.deptOptions = this.handleTree(response.data, "deptId");
+        } else {
+          this.deptOptions = [];
+        }
+      }).catch(() => {
+        this.deptOptions = [];
+      });
+    },
+
+    /**
+     * 树形选择器数据格式化
+     */
+    normalizer(node) {
+      if (node.children && !node.children.length) {
+        delete node.children;
+      }
+      return {
+        id: node.deptId,
+        label: node.deptName,
+        children: node.children
+      };
+    },
+
+    /**
+     * 搜索
+     */
+    handleQuery() {
+      this.getList();
+    },
+
+    /**
+     * 重置搜索
+     */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams = {
+        pageSize: 10,
+        pageNum: 1,
+        id: null,
+        invitationCode: null,
+        tagIds: null,
+        customerIds: null,
+      };
+      this.dialog.selectTagForQuery.selected = [];
+      this.dialog.selectCustomerForQuery.selected = [];
+      this.handleQuery();
+    },
+
+    /**
+     * 多选框选中数据
+     */
+    handleSelectionChange(selection) {
+      this.tableInfo.ids = selection.map(item => item.id);
+    },
+
+    /**
+     * 新增邀请码
+     */
+    handleAdd() {
+      this.reset();
+      this.dialog.title = "新增邀请码";
+      this.dialog.open = true;
+    },
+
+    /**
+     * 修改邀请码
+     */
+    async handleUpdate(row) {
+      this.reset();
+      this.dialog.title = "修改邀请码";
+      this.dialog.open = true;
+      this.dialog.loading = true;
+      // 先加载部门数据,确保下拉框有数据
+      this.getDeptList();
+      let response = await getById({id: row.id, activeName: this.activeName});
+      this.form = response.data;
+      // 加载标签数据
+      if (response.data.tagIds && response.data.tagIds.length > 0) {
+        getTagByIds(response.data.tagIds.split(',')).then(tagsRes => {
+          this.dialog.selectTagForEdit.selected = tagsRes.data;
+        });
+      }
+      // 加载客服数据
+      if (response.data.customerIds && response.data.customerIds.length > 0) {
+        listRole({ids: response.data.customerIds.split(',')}).then(customerRes => {
+          this.dialog.selectCustomerForEdit.selected = customerRes.rows;
+        });
+      }
+      this.dialog.loading = false;
+    },
+
+    /**
+     * 删除邀请码
+     */
+    handleDelete(row) {
+      let ids = [];
+      if (row) {
+        ids = [row.id];
+      } else {
+        ids = this.tableInfo.ids;
+      }
+      if (!ids || ids.length === 0) {
+        return this.$message.error('请选中需要删除的记录!');
+      }
+      this.$confirm('是否确认删除?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        deleteByIds(ids).then(() => {
+          this.getList();
+          this.$message.success('删除成功');
+        });
+      });
+    },
+    /**
+     * tab切换
+     * @param tab
+     * @param event
+     */
+    handleClick(tab, event) {
+      this.handleQuery();
+    },
+
+    /**
+     * 提交表单(新增/修改)
+     */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          let submitData = {...this.form};
+          // 组装标签数据
+          if (this.dialog.selectTagForEdit.selected.length > 0) {
+            submitData['tagIds'] = this.dialog.selectTagForEdit.selected.map(s => s.id);
+          }
+          // 组装客服数据
+          if (this.dialog.selectCustomerForEdit.selected.length > 0) {
+            submitData['customerIds'] = this.dialog.selectCustomerForEdit.selected.map(s => s.id);
+          }
+          submitData['activeName'] = this.activeName;
+          save(submitData).then(response => {
+            if (response.code === 200) {
+              this.$message.success('保存成功');
+              this.dialog.open = false;
+              this.getList();
+            } else {
+              this.$message.error(response.msg);
+            }
+          });
+        }
+      });
+    },
+
+    /**
+     * 取消弹窗
+     */
+    cancel() {
+      this.dialog.open = false;
+      this.reset();
+    },
+
+    /**
+     * 重置表单
+     */
+    reset() {
+      this.form = {
+        id: null,
+        invitationCode: null,
+        remark: null,
+        deptId: null,
+        tagIds: null,
+        customerIds: null,
+      };
+      this.dialog.selectCustomerForEdit.selected = [];
+      this.dialog.selectTagForEdit.selected = [];
+      this.resetForm("form");
+    },
+
+    /**
+     * 打开标签搜索页面
+     */
+    openTagSearchPage(isEdit) {
+      if (isEdit) {
+        this.dialog.selectTagForEdit.visible = true;
+      } else {
+        this.dialog.selectTagForQuery.visible = true;
+      }
+    },
+
+    /**
+     * 确定选择标签
+     */
+    tagSubmitForm(isEdit) {
+      if (isEdit) {
+        this.dialog.selectTagForEdit.visible = false;
+      } else {
+        this.dialog.selectTagForQuery.visible = false;
+      }
+    },
+
+    /**
+     * 取消选择标签
+     */
+    tagCancel(isEdit) {
+      if (isEdit) {
+        this.dialog.selectTagForEdit.visible = false;
+      } else {
+        this.dialog.selectTagForQuery.visible = false;
+      }
+    },
+
+    /**
+     * 列表查询-标签选中切换回调
+     */
+    tagSelectionForQuery(group, tag) {
+      if (!tag.isSelected) {
+        this.dialog.selectTagForQuery.selected = this.dialog.selectTagForQuery.selected.filter(item => tag.id != item.id);
+      } else {
+        if (!this.dialog.selectTagForQuery.selected.some(st => st.id == tag.id)) {
+          this.dialog.selectTagForQuery.selected.push(tag);
+        }
+      }
+      this.queryParams.tagIds = this.dialog.selectTagForQuery.selected.map(st => st.id);
+    },
+
+    /**
+     * 编辑页-标签选中切换回调
+     */
+    tagSelectionForEdit(group, tag) {
+      if (!tag.isSelected) {
+        this.dialog.selectTagForEdit.selected = this.dialog.selectTagForEdit.selected.filter(item => tag.id != item.id);
+      } else {
+        if (!this.dialog.selectTagForEdit.selected.some(st => st.id == tag.id)) {
+          this.dialog.selectTagForEdit.selected.push(tag);
+        }
+      }
+    },
+
+    /**
+     * 列表查询-删除一些选择的标签回调
+     */
+    handleCloseTagsForQuery(tagItem) {
+      this.dialog.selectTagForQuery.selected = this.dialog.selectTagForQuery.selected.filter(t => t.id != tagItem.id);
+    },
+
+    /**
+     * 编辑页-删除一些选择的标签回调
+     */
+    handleCloseTagsForEdit(tagItem) {
+      this.dialog.selectTagForEdit.selected = this.dialog.selectTagForEdit.selected.filter(t => t.id != tagItem.id);
+    },
+
+    /**
+     * 打开客服搜索页面
+     */
+    openCusSearchPage(isEdit) {
+      if (isEdit) {
+        this.dialog.selectCustomerForEdit.visible = true;
+      } else {
+        this.dialog.selectCustomerForQuery.visible = true;
+      }
+    },
+
+    /**
+     * 查询选中客服
+     */
+    customerSubmitCallForQuery(selected) {
+      this.dialog.selectCustomerForQuery.visible = false;
+    },
+
+    /**
+     * 查询选中客服
+     */
+    customerSelectionChangeCallForQuery(config) {
+      let selected = config.selected;
+      if (config.mode === 0) {
+        let row = config.row;
+        if (selected) {
+          if (!this.dialog.selectCustomerForQuery.selected.some(s => s.id == row.id)) {
+            this.dialog.selectCustomerForQuery.selected.push(row);
+          }
+        } else {
+          this.dialog.selectCustomerForQuery.selected = this.dialog.selectCustomerForQuery.selected.filter(m => m.id != row.id);
+        }
+      } else {
+        let list = config.rows;
+        if (selected) {
+          list.map(l => {
+            if (!this.dialog.selectCustomerForQuery.selected.some(s => s.id == l.id)) {
+              this.dialog.selectCustomerForQuery.selected.push(l);
+            }
+          });
+        } else {
+          list.map(l => {
+            for (let i = 0; i < this.dialog.selectCustomerForQuery.selected.length; i++) {
+              if (l.id == this.dialog.selectCustomerForQuery.selected[i].id) {
+                this.dialog.selectCustomerForQuery.selected.splice(i, 1);
+              }
+            }
+          });
+        }
+      }
+    },
+
+    /**
+     * 编辑页选中客服
+     */
+    customerSubmitCallForEdit() {
+      this.dialog.selectCustomerForEdit.visible = false;
+    },
+
+    /**
+     * 编辑页选中客服回调
+     */
+    customerSelectionChangeCallForEdit(config) {
+      let selected = config.selected;
+      if (config.mode === 0) {
+        let row = config.row;
+        if (selected) {
+          if (!this.dialog.selectCustomerForEdit.selected.some(s => s.id == row.id)) {
+            this.dialog.selectCustomerForEdit.selected.push(row);
+          }
+        } else {
+          this.dialog.selectCustomerForEdit.selected = this.dialog.selectCustomerForEdit.selected.filter(m => m.id != row.id);
+        }
+      } else {
+        let list = config.rows;
+        if (selected) {
+          list.map(l => {
+            if (!this.dialog.selectCustomerForEdit.selected.some(s => s.id == l.id)) {
+              this.dialog.selectCustomerForEdit.selected.push(l);
+            }
+          });
+        } else {
+          list.map(l => {
+            for (let i = 0; i < this.dialog.selectCustomerForEdit.selected.length; i++) {
+              if (l.id == this.dialog.selectCustomerForEdit.selected[i].id) {
+                this.dialog.selectCustomerForEdit.selected.splice(i, 1);
+              }
+            }
+          });
+        }
+      }
+    },
+
+    /**
+     * 列表查询-删除选择的客服回调
+     */
+    handleCloseCusForQuery(item) {
+      this.dialog.selectCustomerForQuery.selected = this.dialog.selectCustomerForQuery.selected.filter(m => m.id != item.id);
+    },
+
+    /**
+     * 编辑页-删除选择的客服回调
+     */
+    handleCloseCusForEdit(item) {
+      this.dialog.selectCustomerForEdit.selected = this.dialog.selectCustomerForEdit.selected.filter(m => m.id != item.id);
+    },
+
+    /**
+     * 树形结构转换
+     */
+    handleTree(data, id, parentId, children) {
+      id = id || 'id';
+      parentId = parentId || 'parentId';
+      children = children || 'children';
+      var tree = [];
+      var map = {};
+      data.forEach(function(item) {
+        map[item[id]] = item;
+      });
+      data.forEach(function(item) {
+        var parent = map[item[parentId]];
+        if (parent) {
+          (parent[children] || (parent[children] = [])).push(item);
+        } else {
+          tree.push(item);
+        }
+      });
+      return tree;
+    },
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.mb8 {
+  margin-bottom: 8px;
+}
+
+.dialog-footer {
+  text-align: center;
+}
+
+.dialog-body {
+  height: 400px;
+  overflow: auto;
+}
+
+.tag-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.name-background {
+  display: inline-block;
+  background-color: #abece6;
+  padding: 4px 8px;
+  border-radius: 4px;
+}
+
+.tag-box {
+  padding: 8px 12px;
+  border: 1px solid #989797;
+  border-radius: 4px;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.tag-selected {
+  background-color: #00bc98;
+  color: #fff;
+  border-color: #00bc98;
+}
+
+.el-tag + .el-tag {
+  margin-left: 10px;
+}
+
+.qrcode-cell {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.qrcode-cell .image-slot {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  background: #f5f7fa;
+  color: #909399;
+  font-size: 20px;
+}
+</style>

+ 309 - 0
src/views/app/sop/info/GroupChatSelector.vue

@@ -0,0 +1,309 @@
+<template>
+  <el-dialog :title="title" :visible.sync="open" width="900px" append-to-body>
+    <div class="group-chat-selector">
+      <!-- 左侧搜索选择区域 -->
+      <div class="left-panel">
+        <div class="search-box">
+          <el-input
+            v-model="searchMsg"
+            placeholder="请输入群聊名称搜索"
+            clearable
+            @input="handleSearch"
+          >
+            <el-button slot="append" icon="el-icon-search"></el-button>
+          </el-input>
+        </div>
+        <div class="group-list">
+          <el-checkbox-group v-model="selectedChatIds">
+            <div v-for="item in filteredGroupList" :key="item.chatId" class="group-item">
+              <el-checkbox :label="item.chatId" @change="handleCheckboxChange">
+                <div class="group-info">
+                  <span class="group-name">{{ item.name }}</span>
+                </div>
+              </el-checkbox>
+            </div>
+          </el-checkbox-group>
+        </div>
+      </div>
+      
+      <!-- 分隔线 -->
+      <div class="divider"></div>
+      
+      <!-- 右侧结果展示区域 -->
+      <div class="right-panel">
+        <div class="selected-header">
+          <span>已选择群聊 ({{ selectedChatIds.length }})</span>
+          <el-button type="text" @click="clearSelection">清空</el-button>
+        </div>
+        <div class="selected-list">
+          <div v-for="chatId in selectedChatIds" :key="chatId" class="selected-item">
+            <span class="selected-name">{{ getGroupNameById(chatId) }}</span>
+            <el-button 
+              type="text" 
+              icon="el-icon-close" 
+              @click="removeSelected(chatId)"
+              class="remove-btn"
+            ></el-button>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="cancel">取 消</el-button>
+      <el-button type="primary" @click="confirmSelection">确 定</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+// import { listAll as chatListAll } from '@/api/qw/groupChat'
+
+export default {
+  name: "GroupChatSelector",
+  props: {
+    title: {
+      type: String,
+      default: "选择群聊"
+    },
+    qwUserIds: {
+      type: Array,
+      default: () => []
+    },
+    corpId: {
+      type: String,
+      default: ""
+    },
+    sopId: {
+      type: String,
+      default: ""
+    }
+  },
+  data() {
+    return {
+      open: false,
+      searchMsg: "",
+      groupList: [],
+      selectedChatIds: [],
+      initialSelectedChatIds: [], // 用于存储初始选中的群聊ID
+      groupNameCache: {} // 用于缓存群聊名称
+    };
+  },
+  computed: {
+    filteredGroupList() {
+      if (!this.searchMsg) {
+        return this.groupList;
+      }
+      return this.groupList.filter(item => 
+        item.name.toLowerCase().includes(this.searchMsg.toLowerCase())
+      );
+    }
+  },
+  methods: {
+    // 获取群聊列表
+    getGroupChatList() {
+      const qwUserIdsStr = this.qwUserIds.join();
+      // chatListAll(qwUserIdsStr, this.corpId, this.sopId, this.searchMsg).then(res => {
+      //   this.groupList = res.data || [];
+      //   // 更新缓存
+      //   this.updateGroupNameCache();
+      // });
+    },
+    
+    // 更新群聊名称缓存
+    updateGroupNameCache() {
+      if (!this.groupNameCache) {
+        this.groupNameCache = {};
+      }
+      
+      // 将所有群聊信息添加到缓存中
+      this.groupList.forEach(group => {
+        if (group.chatId && group.name) {
+          this.groupNameCache[group.chatId] = group.name;
+        }
+      });
+    },
+    
+    // 处理搜索
+    handleSearch() {
+      this.getGroupChatList();
+    },
+    
+    // 根据ID获取群聊名称
+    getGroupNameById(chatId) {
+      // 先从缓存中查找
+      if (this.groupNameCache && this.groupNameCache[chatId]) {
+        return this.groupNameCache[chatId];
+      }
+      
+      // 如果缓存中没有,则从当前列表中查找
+      const group = this.groupList.find(item => item.chatId === chatId);
+      if (group && group.name) {
+        // 找到后保存到缓存
+        if (!this.groupNameCache) {
+          this.groupNameCache = {};
+        }
+        this.groupNameCache[chatId] = group.name;
+        return group.name;
+      }
+      
+      return chatId || '';
+    },
+    
+    // 处理复选框变化
+    handleCheckboxChange() {
+      // 复选框变化时立即通知父组件更新
+      this.$emit('selectGroupChats', this.selectedChatIds);
+    },
+    
+    // 从右侧移除选中项
+    removeSelected(chatId) {
+      const index = this.selectedChatIds.indexOf(chatId);
+      if (index !== -1) {
+        this.selectedChatIds.splice(index, 1);
+        // 移除选中项时立即通知父组件更新
+        this.$emit('selectGroupChats', this.selectedChatIds);
+      }
+    },
+    
+    // 清空选择
+    clearSelection() {
+      this.selectedChatIds = [];
+    },
+    
+    // 确认选择
+    confirmSelection() {
+      this.$emit('selectGroupChats', this.selectedChatIds);
+      this.cancel();
+    },
+    
+    // 取消选择
+    cancel() {
+      this.open = false;
+      this.searchMsg = "";
+      // 恢复到初始选中状态
+      this.selectedChatIds = [...this.initialSelectedChatIds];
+    },
+    
+    // 打开弹窗并初始化数据
+    show(selectedChatIds = []) {
+      this.open = true;
+      this.selectedChatIds = [...selectedChatIds];
+      this.initialSelectedChatIds = [...selectedChatIds];
+      this.getGroupChatList();
+    }
+  }
+};
+</script>
+
+<style scoped>
+.group-chat-selector {
+  display: flex;
+  height: 500px;
+}
+
+.left-panel {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  padding-right: 10px;
+}
+
+.search-box {
+  margin-bottom: 15px;
+}
+
+.group-list {
+  flex: 1;
+  overflow-y: auto;
+  border: 1px solid #e6e6e6;
+  border-radius: 4px;
+}
+
+.group-item {
+  padding: 10px;
+  transition: background-color 0.3s;
+}
+
+.group-item:hover {
+  background-color: #f5f7fa;
+}
+
+.group-info {
+  display: flex;
+  align-items: center;
+}
+
+.group-name {
+  margin-left: 5px;
+  font-size: 14px;
+}
+
+.divider {
+  width: 1px;
+  background-color: #e6e6e6;
+  margin: 0 15px;
+}
+
+.right-panel {
+  width: 300px;
+  display: flex;
+  flex-direction: column;
+}
+
+.selected-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+  padding: 0 10px;
+}
+
+.selected-list {
+  flex: 1;
+  overflow-y: auto;
+  border: 1px solid #e6e6e6;
+  border-radius: 4px;
+}
+
+.selected-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px;
+  border-bottom: 1px solid #f0f0f0;
+  transition: background-color 0.3s;
+}
+
+.selected-item:hover {
+  background-color: #f5f7fa;
+}
+
+.selected-item {
+  animation: fadeIn 0.3s ease-in-out;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.selected-name {
+  flex: 1;
+}
+
+.remove-btn {
+  padding: 2px;
+}
+
+.remove-btn:hover {
+  color: #f56c6c;
+  transform: scale(1.2);
+  transition: transform 0.2s;
+}
+</style>

+ 205 - 0
src/views/app/sop/info/ImageUpload.vue

@@ -0,0 +1,205 @@
+<template>
+  <div class="component-upload-image">
+    <el-upload
+      :action="uploadUrl"
+      list-type="picture-card"
+      :on-success="handleUploadSuccess"
+      :before-upload="handleBeforeUpload"
+      :limit="limit"
+      :on-error="handleUploadError"
+      :on-exceed="handleExceed"
+      name="file"
+      :on-remove="handleRemove"
+      :show-file-list="true"
+      :file-list="fileList"
+      :on-preview="handlePictureCardPreview"
+      :class="{hide: this.fileList.length >= this.limit}"
+    >
+      <i class="el-icon-plus"></i>
+    </el-upload>
+
+    <el-dialog
+      :visible.sync="dialogVisible"
+      title="预览"
+      width="800"
+      append-to-body
+    >
+      <img
+        :src="dialogImageUrl"
+        style="display: block; max-width: 100%; margin: 0 auto"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getToken } from "@/utils/auth";
+
+export default {
+  name: "ImageUpload",
+  props: {
+    value: [String, Object, Array],
+    // 图片数量限制
+    limit: {
+      type: Number,
+      default: 1,
+    },
+    // 大小限制(MB)
+    fileSize: {
+       type: Number,
+       default: 1,
+    },
+    // 文件类型, 例如['png', 'jpg', 'jpeg']
+    fileType: {
+      type: Array,
+      default: () => ["png", "jpg", "jpeg"],
+    },
+    // 是否显示提示
+    isShowTip: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      dialogImageUrl: "",
+      dialogVisible: false,
+      hideUpload: false,
+      baseUrl:"",
+      uploadUrl:process.env.VUE_APP_BASE_API+"/app/common/uploadOSSByHOOKImage",
+      headers: {
+        Authorization: "Bearer " + getToken(),
+      },
+      fileList: []
+    };
+  },
+  watch: {
+    value: {
+      handler(val) {
+        if (val) {
+          // 首先将值转为数组
+          const list = Array.isArray(val) ? val : this.value.split(',');
+          // 然后将数组转为对象数组
+          this.fileList = list.map(item => {
+            if (typeof item === "string") {
+              if (item.indexOf(this.baseUrl) === -1) {
+                  item = { name: item, url: item };
+              } else {
+                  item = { name: item, url: item };
+              }
+            }
+            return item;
+          });
+        } else {
+          this.fileList = [];
+          return [];
+        }
+      },
+      deep: true,
+      immediate: true
+    }
+  },
+  computed: {
+    // 是否显示提示
+    showTip() {
+      return this.isShowTip && (this.fileType || this.fileSize);
+    },
+  },
+  methods: {
+    // 删除图片
+    handleRemove(file, fileList) {
+      const findex = this.fileList.map(f => f.name).indexOf(file.name);
+      if(findex > -1) {
+        this.fileList.splice(findex, 1);
+        this.$emit("input", this.listToString(this.fileList));
+      }
+    },
+    // 上传成功回调
+    handleUploadSuccess(res,file) {
+      this.fileList.push({ name: res.url, url: res.url });
+      this.$emit("input", this.listToString(this.fileList));
+      this.loading.close();
+    },
+    // 上传前loading加载
+    handleBeforeUpload(file) {
+      let isImg = false;
+      if (this.fileType.length) {
+        let fileExtension = "";
+        if (file.name.lastIndexOf(".") > -1) {
+          fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
+        }
+        isImg = this.fileType.some(type => {
+          if (file.type.indexOf(type) > -1) return true;
+          if (fileExtension && fileExtension.indexOf(type) > -1) return true;
+          return false;
+        });
+      } else {
+        isImg = file.type.indexOf("image") > -1;
+      }
+
+      if (!isImg) {
+        this.$message.error(
+          `文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`
+        );
+        return false;
+      }
+      if (this.fileSize) {
+        const isLt = file.size / 1024 / 1024 < this.fileSize;
+        if (!isLt) {
+          this.$message.error(`上传头像图片大小不能超过 ${this.fileSize} MB!`);
+          return false;
+        }
+      }
+      this.loading = this.$loading({
+        lock: true,
+        text: "上传中",
+        background: "rgba(0, 0, 0, 0.7)",
+      });
+    },
+    // 文件个数超出
+    handleExceed() {
+      this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`);
+    },
+    // 上传失败
+    handleUploadError() {
+      this.$message({
+        type: "error",
+        message: "上传失败",
+      });
+      this.loading.close();
+    },
+    // 预览
+    handlePictureCardPreview(file) {
+      console.log(file)
+      this.dialogImageUrl = file.url;
+      this.dialogVisible = true;
+    },
+    // 对象转成指定字符串分隔
+    listToString(list, separator) {
+      let strs = "";
+      separator = separator || ",";
+      for (let i in list) {
+        strs += list[i].url.replace(this.baseUrl, "") + separator;
+      }
+      return strs != '' ? strs.substr(0, strs.length - 1) : '';
+    }
+  }
+};
+</script>
+<style scoped lang="scss">
+// .el-upload--picture-card 控制加号部分
+::v-deep.hide .el-upload--picture-card {
+    display: none;
+}
+// 去掉动画效果
+::v-deep .el-list-enter-active,
+::v-deep .el-list-leave-active {
+    transition: all 0s;
+}
+
+::v-deep .el-list-enter, .el-list-leave-active {
+    opacity: 0;
+    transform: translateY(0);
+}
+</style>
+

+ 542 - 0
src/views/app/sop/info/TimeRangePicker.vue

@@ -0,0 +1,542 @@
+<template>
+  <div>
+    <!-- 可选的标题区 -->
+    <template v-if="showHeader">
+      <h2>日期 → 弹窗精确到分钟的时间范围选择器</h2>
+    </template>
+
+    <div class="row">
+      <el-date-picker
+        v-model="selectedDate"
+        type="date"
+        placeholder="选择日期"
+        value-format="yyyy-MM-dd"
+      />
+      <el-button type="primary" @click="openPicker">
+        选择时间范围
+      </el-button>
+
+    </div>
+
+    <template v-if="showHeader">
+      <h3 style="margin-top:20px;">已保存的范围</h3>
+    </template>
+    <ul>
+      <li v-for="(r, i) in ranges" :key="i">
+        日期 <b>{{ r.date }}</b>:
+        <code>{{ fmt(r.startMs) }}</code> ~ <code>{{ fmt(r.endMs) }}</code>
+        <el-button type="text" size="mini" @click="removeRange(i)" style="color:red;">删除</el-button>
+      </li>
+      <li v-if="!ranges.length" class="hint">暂无数据</li>
+
+    </ul>
+
+    <!-- 弹窗 -->
+    <el-dialog
+      :title="'为 ' + selectedDate + ' 选择时间范围(分钟级禁用,秒补 00)'"
+      :visible.sync="show"
+      width="980px"
+      :close-on-click-modal="false"
+      append-to-body
+    >
+      <div class="modal-body">
+        <!-- 左侧主区域 -->
+        <div>
+          <div class="day-bar-wrap">
+            <div class="day-bar-header">
+              <div class="row">
+                <el-button @click="shiftDay(-1)" size="mini">← 上一天</el-button>
+                <el-date-picker
+                  v-model="viewDay"
+                  type="date"
+                  size="mini"
+                  value-format="yyyy-MM-dd"
+                />
+                <el-button @click="shiftDay(+1)" size="mini">下一天 →</el-button>
+              </div>
+              <div class="bar-legend">
+                <span><i class="dot dot-free"></i>可选</span>
+                <span><i class="dot dot-busy"></i>已占用</span>
+                <span><i class="dot dot-range"></i>当前选择</span>
+              </div>
+            </div>
+
+            <canvas ref="canvas"
+                    @mousemove="onHover"
+                    @mouseleave="hoverSec=null"
+                    @click="onClick"></canvas>
+            <div class="ticks">
+              <span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>24:00</span>
+            </div>
+          </div>
+
+          <div style="margin-top:12px;" class="kv">
+            <div>当前在选:<span class="chip">{{ selectingLabel }}</span>
+              <span v-if="hoverSec !== null" class="hint">光标:{{ secToHMS(hoverSec) }}</span>
+            </div>
+            <div>开始:<b v-if="draft.startMs">{{ fmt(draft.startMs) }}</b><span v-else class="hint">未设置</span></div>
+            <div>结束:<b v-if="draft.endMs">{{ fmt(draft.endMs) }}</b><span v-else class="hint">未设置</span></div>
+            <div v-if="conflictMsg" class="danger">{{ conflictMsg }}</div>
+            <div v-else-if="draft.startMs && draft.endMs" class="ok">选择有效 ✅</div>
+          </div>
+        </div>
+
+        <!-- 右侧辅助输入 -->
+        <div class="aside">
+          <div class="kv">
+            <div><b>辅助输入(分钟级)</b></div>
+            <div style="margin:6px 0 10px" class="hint">秒数会自动补 00</div>
+            <div class="row">
+              <el-date-picker
+                v-model="startStr"
+                type="datetime"
+                value-format="yyyy-MM-ddTHH:mm"
+                format="yyyy-MM-dd HH:mm"
+                placeholder="开始时间"
+                :picker-options="{ selectableRange: '00:00:00 - 23:59:00' }"
+                @change="applyStartStr"
+              />
+              <el-button size="mini" @click="selecting='end'">下一步:选结束</el-button>
+            </div>
+            <div class="row" style="margin-top:8px;">
+              <el-date-picker
+                v-model="endStr"
+                type="datetime"
+                value-format="yyyy-MM-ddTHH:mm"
+                format="yyyy-MM-dd HH:mm"
+                placeholder="结束时间"
+                :picker-options="{ selectableRange: '00:00:00 - 23:59:00' }"
+                @change="applyEndStr"
+              />
+              <el-button size="mini" @click="selecting='start'">切回:选开始</el-button>
+            </div>
+            <div class="hint" style="margin-top:8px;">已占用区间会被自动挡住或剪齐到边界。</div>
+
+            <div style="margin-top:18px;"><b>当天禁用切片(分钟)</b></div>
+            <ul style="max-height:140px; overflow:auto; border:1px dashed #e5e7eb; border-radius:8px; padding:8px;">
+              <li v-for="(seg, i) in daySegments" :key="i" class="hint">
+                {{ secToHMS(seg[0]) }} ~ {{ secToHMS(seg[1]) }}
+              </li>
+              <li v-if="!daySegments.length" class="hint">无</li>
+            </ul>
+          </div>
+        </div>
+      </div>
+
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="show=false">关闭</el-button>
+        <el-button type="primary" :disabled="!canSave" @click="save">保存</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "TimeRangePicker",
+  props: {
+    showHeader: { type: Boolean, default: false },
+    emitOnSave: { type: Boolean, default: true }, // 保存时向父组件通知
+    value: { type: Array, default: () => [] } // 新增,用来接收外部 ranges
+  },
+  data() {
+    return {
+      timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Local",
+      selectedDate: new Date().toISOString().split("T")[0],
+      show: false,
+
+      ranges: [], // [{date,startMs,endMs}]
+      draft: { startMs: null, endMs: null },
+
+      viewDay: "",
+      hoverSec: null,
+      daySegments: [],
+
+      selecting: "start",
+      startStr: "",
+      endStr: "",
+      conflictMsg: "",
+    };
+  },
+  computed: {
+    canSave() {
+      return (
+        this.draft.startMs &&
+        this.draft.endMs &&
+        this.draft.startMs < this.draft.endMs &&
+        !this.conflictMsg
+      );
+    },
+    selectingLabel() {
+      return this.selecting === "start"
+        ? "正在选择:开始时间"
+        : "正在选择:结束时间";
+    },
+  },
+  methods: {
+
+    removeRange(index) {
+      this.ranges.splice(index, 1);
+      // 同步更新到父组件
+      this.$emit("input", JSON.parse(JSON.stringify(this.ranges)));
+      this.$emit("saved", JSON.parse(JSON.stringify(this.ranges)));
+    },
+
+    pad(n){ return String(n).padStart(2,'0'); },
+    fmt(ms) {
+      const d = new Date(ms);
+      return `${d.getFullYear()}-${this.pad(d.getMonth()+1)}-${this.pad(d.getDate())} ${this.pad(d.getHours())}:${this.pad(d.getMinutes())}:00`;
+    },
+    toMs(str) {
+      const [d,t] = str.split(' ');
+      const [y,M,D] = d.split('-').map(Number);
+      const [h,m,s] = t.split(':').map(Number);
+      const dt = new Date(y, M-1, D, h, m, s);
+      dt.setSeconds(0,0);
+      return dt.getTime();
+    },
+    dayStartMs(dayStr){ const [y,M,D]=dayStr.split('-').map(Number); const dt=new Date(y,M-1,D,0,0,0); dt.setSeconds(0,0); return dt.getTime(); },
+    dayEndMs(dayStr){ const [y,M,D]=dayStr.split('-').map(Number); return new Date(y,M-1,D,23,59,59).getTime(); },
+    secToHMS(min){ const h=Math.floor(min/60),m=min%60; return `${this.pad(h)}:${this.pad(m)}:00`; },
+
+    refreshRanges() {
+      // 优先来源:props.value(父组件可能在任何时刻更新)
+      // 如果父组件没给,则保留已有 this.ranges
+      const source = (Array.isArray(this.value) && this.value.length) ? this.value : this.ranges;
+      // 规范化(使用你已有 normalizeRanges 实现)
+      this.ranges = this.normalizeRanges(source || []);
+      // 如果当前 viewDay 没有占用段,优先切到第一个有占用的日期
+      if (!this.viewDay && this.ranges.length) {
+        this.viewDay = this.ranges[0].date;
+      }
+      const hasOnView = this.viewDay && this.ranges.some(r => r.date === this.viewDay);
+      if (!hasOnView && this.ranges.length) {
+        // 切换到第一个有占用的日期,便于查看
+        this.viewDay = this.ranges[0].date;
+      }
+      // 强制重算并重绘
+      this.computeDaySegments();
+      this.$nextTick(this.drawCanvas);
+    },
+
+// 放在 methods: { ... } 里
+    normalizeRanges(arr) {
+      if (!Array.isArray(arr)) return [];
+      const pad = (n) => String(n).padStart(2,'0');
+
+      // 把各种可能的值解析为毫秒(返回 null 表示无法解析)
+      const parseToMs = (v, dateForHHmm) => {
+        if (v == null) return null;
+        if (typeof v === 'number') return v;
+        if (v instanceof Date) return v.getTime();
+        if (typeof v !== 'string') return null;
+
+        // 纯数字字符串(时间戳毫秒)
+        if (/^\d+$/.test(v)) return Number(v);
+
+        // ISO 或带 T 的 datetime
+        if (v.includes('T')) {
+          const dt = new Date(v);
+          return isNaN(dt.getTime()) ? null : dt.getTime();
+        }
+
+        // "yyyy-MM-dd HH:mm" 或 "yyyy-MM-dd HH:mm:ss"
+        if (/^\d{4}-\d{2}-\d{2} /.test(v)) {
+          const iso = v.replace(' ', 'T');
+          const dt = new Date(iso);
+          if (!isNaN(dt.getTime())) return dt.getTime();
+          // fallback 手动解析
+          const [d, t] = v.split(' ');
+          const [y, mo, da] = d.split('-').map(Number);
+          const [hh = 0, mm = 0] = (t || '00:00').split(':').map(Number);
+          const dt2 = new Date(y, mo - 1, da, hh, mm, 0);
+          return isNaN(dt2.getTime()) ? null : dt2.getTime();
+        }
+
+        // 只有 "HH:mm" 且同时提供 dateForHHmm('yyyy-MM-dd')
+        if (/^\d{1,2}:\d{2}$/.test(v) && dateForHHmm) {
+          const [y, mo, da] = dateForHHmm.split('-').map(Number);
+          const [hh, mm] = v.split(':').map(Number);
+          const dt = new Date(y, mo - 1, da, hh, mm, 0);
+          return isNaN(dt.getTime()) ? null : dt.getTime();
+        }
+
+        // 最后尝试 new Date()
+        const dtry = new Date(v);
+        return isNaN(dtry.getTime()) ? null : dtry.getTime();
+      };
+
+      const getDateStrFromMs = (ms) => {
+        const d = new Date(ms);
+        return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+      };
+      const getTimePartsFromMs = (ms) => {
+        const d = new Date(ms);
+        return { h: d.getHours(), m: d.getMinutes(), s: d.getSeconds() };
+      };
+
+      const out = [];
+      for (const raw of arr) {
+        if (!raw) continue;
+        const r = Object.assign({}, raw);
+
+        // 尝试从各种命名拿到 start/end
+        let startMs = parseToMs(r.startMs ?? r.start ?? r.startTime ?? r.startAt, r.date);
+        let endMs = parseToMs(r.endMs ?? r.end ?? r.endTime ?? r.endAt, r.date);
+
+        // 支持 timeRange 字段 "09:00-10:00"
+        if ((startMs == null || endMs == null) && typeof r.timeRange === 'string' && r.date) {
+          const parts = r.timeRange.split('-').map(s => s.trim());
+          if (parts.length === 2) {
+            const sMs = parseToMs(parts[0], r.date);
+            const eMs = parseToMs(parts[1], r.date);
+            if (sMs != null && eMs != null) {
+              startMs = startMs ?? sMs;
+              endMs = endMs ?? eMs;
+            }
+          }
+        }
+
+        // 仍解析失败则跳过并打印 warn(便于调试)
+        if (typeof startMs !== 'number' || typeof endMs !== 'number' || isNaN(startMs) || isNaN(endMs)) {
+          console.warn('[TimeRangePicker] normalizeRanges 跳过无法解析的 range:', raw);
+          continue;
+        }
+
+        // 若有 r.date,但 startMs/endMs 对应的本地日期与 r.date 不一致,
+        // 则把 startMs/endMs 的 时分秒“贴到” r.date 上进行修正(这是为了解决你传入 date 与 timestamp 不对应的问题)
+        if (r.date) {
+          const sDateStr = getDateStrFromMs(startMs);
+          const eDateStr = getDateStrFromMs(endMs);
+          if (sDateStr !== r.date) {
+            const tp = getTimePartsFromMs(startMs);
+            const [y, mo, da] = r.date.split('-').map(Number);
+            const fixed = new Date(y, mo - 1, da, tp.h, tp.m, tp.s || 0);
+            console.warn('[TimeRangePicker] 修正 startMs 日期不匹配:', sDateStr, '-> 贴到', r.date, '(new:', fixed.toString(), ')');
+            startMs = fixed.getTime();
+          }
+          if (eDateStr !== r.date) {
+            const tp = getTimePartsFromMs(endMs);
+            const [y, mo, da] = r.date.split('-').map(Number);
+            const fixed = new Date(y, mo - 1, da, tp.h, tp.m, tp.s || 0);
+            console.warn('[TimeRangePicker] 修正 endMs 日期不匹配:', eDateStr, '-> 贴到', r.date, '(new:', fixed.toString(), ')');
+            endMs = fixed.getTime();
+          }
+        }
+
+        // 保证 start <= end
+        if (startMs > endMs) {
+          const t = startMs; startMs = endMs; endMs = t;
+        }
+
+        r.startMs = Math.floor(startMs);
+        r.endMs = Math.floor(endMs);
+
+        // 如果没有 date 字段则根据 startMs 补上(本地日期)
+        if (!r.date) {
+          const d = new Date(r.startMs);
+          r.date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+        }
+
+        out.push(r);
+      }
+
+      return out;
+    },
+
+    // 自带按钮打开弹窗(父组件无需 $refs)
+    openPicker(){
+      if (!this.selectedDate) {
+        this.$message.warning("请先选择日期");
+        return;
+      }
+
+      // 规范并刷新 ranges(从 props.value 或内部 ranges)
+      this.refreshRanges();
+
+      // 选择视图日期:优先显示 selectedDate(如果有占用),否则显示 first busy date
+      const hasOnSelected = this.ranges.some(r => r.date === this.selectedDate);
+      if (hasOnSelected) this.viewDay = this.selectedDate;
+      else if (this.ranges.length) this.viewDay = this.ranges[0].date || this.selectedDate;
+      else this.viewDay = this.selectedDate;
+
+      this.show=true;
+      this.viewDay=this.selectedDate;
+      this.draft={startMs:null,endMs:null};
+      this.startStr='';this.endStr='';
+      this.selecting='start';
+      this.computeDaySegments();
+      this.$nextTick(this.drawCanvas);
+    },
+    shiftDay(delta){
+      const base=this.dayStartMs(this.viewDay);
+      const next=new Date(base+delta*24*3600*1000);
+      this.viewDay=`${next.getFullYear()}-${this.pad(next.getMonth()+1)}-${this.pad(next.getDate())}`;
+      this.computeDaySegments();
+      this.drawCanvas();
+    },
+    computeDaySegments(){
+      const ds=this.dayStartMs(this.viewDay),de=this.dayEndMs(this.viewDay);
+      let segs=[];
+      for(const r of this.ranges){
+        const s=Math.max(r.startMs,ds),e=Math.min(r.endMs,de);
+        if(s<=e){
+          const from=Math.floor((s-ds)/60000);
+          const to=Math.floor((e-ds)/60000);
+          segs.push([Math.max(0,from),Math.min(1439,to)]);
+        }
+      }
+      segs.sort((a,b)=>a[0]-b[0]);
+      const merged=[];
+      for(const seg of segs){
+        if(!merged.length||seg[0]>merged[merged.length-1][1]+1) merged.push(seg);
+        else merged[merged.length-1][1]=Math.max(merged[merged.length-1][1],seg[1]);
+      }
+      this.daySegments=merged;
+      this.validateConflict();
+    },
+    validateConflict(){
+      this.conflictMsg='';
+      const {startMs,endMs}=this.draft;
+      if(!startMs||!endMs||startMs>=endMs) return;
+      for(const r of this.ranges){
+        if(!(endMs<r.startMs||startMs>r.endMs)){
+          const touchStart=startMs===r.endMs+60000;
+          const touchEnd=endMs===r.startMs-60000;
+          if(touchStart||touchEnd) continue;
+          this.conflictMsg='所选范围与已占用区间发生重叠,请重新选择或使用辅助输入自动剪齐。';
+          this.$message.warning("所选范围与已占用区间发生重叠,请重新选择或使用辅助输入自动剪齐")
+          break;
+        }
+      }
+    },
+    dateOfMs(ms){ const d=new Date(ms); return `${d.getFullYear()}-${this.pad(d.getMonth()+1)}-${this.pad(d.getDate())}`; },
+    drawCanvas(){
+      const el=this.$refs.canvas;if(!el)return;
+      const dpr=window.devicePixelRatio||1;
+      const cssW=el.clientWidth,cssH=el.clientHeight;
+      el.width=Math.floor(cssW*dpr);el.height=Math.floor(cssH*dpr);
+      const ctx=el.getContext('2d');ctx.scale(dpr,dpr);
+      ctx.clearRect(0,0,cssW,cssH);
+      const W=cssW,H=cssH;
+      const minToX=(min)=>min/1439*(W-2)+1;
+      ctx.fillStyle='#e5e7eb';ctx.strokeStyle='#d1d5db';ctx.lineWidth=1;
+      for(const [a,b] of this.daySegments){const x=minToX(a),w=minToX(b)-x;ctx.fillRect(x,1,w,H-2);ctx.strokeRect(x,1,Math.max(1,w),H-2);}
+      ctx.strokeStyle='#f3f4f6';
+      for(let h=0;h<=24;h++){const min=h*60;const x=Math.floor(minToX(min));ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
+      const {startMs,endMs}=this.draft;
+      if(startMs){const s=Math.floor((startMs-this.dayStartMs(this.viewDay))/60000);if(this.dateOfMs(startMs)===this.viewDay){ctx.fillStyle='#93c5fd';ctx.fillRect(minToX(s)-1,0,2,H);}}
+      if(startMs&&endMs){const ds=this.dayStartMs(this.viewDay),de=this.dayEndMs(this.viewDay);const visS=Math.max(startMs,ds),visE=Math.min(endMs,de);if(visS<=visE){const a=Math.floor((visS-ds)/60000);const b=Math.floor((visE-ds)/60000);ctx.fillStyle='#60a5fa';const x=minToX(a),w=minToX(b)-x;ctx.fillRect(x,6,Math.max(2,w),H-12);}}
+      if(this.hoverSec!==null){ctx.fillStyle='#111827';const x=Math.floor(minToX(this.hoverSec));ctx.fillRect(x,0,1,H);}
+    },
+    pxToSec(evt){const el=this.$refs.canvas;const rect=el.getBoundingClientRect();const x=evt.clientX-rect.left;const w=el.clientWidth;let min=Math.round(x/w*1439);return Math.max(0,Math.min(1439,min));},
+    onHover(evt){this.hoverSec=this.pxToSec(evt);this.drawCanvas();},
+    onClick(evt){
+      const min=this.pxToSec(evt);
+      if(this.isSecDisabled(min)) return;
+      const ms=this.dayStartMs(this.viewDay)+min*60000;
+      if(this.selecting==='start'){this.draft.startMs=ms;if(this.draft.endMs&&this.draft.endMs<=ms)this.draft.endMs=null;this.startStr=this.msToLocalInput(ms);this.selecting='end';}
+      else{const fixed=this.clipEndAuto(ms);this.draft.endMs=fixed;this.endStr=this.msToLocalInput(fixed);}
+      this.computeDaySegments();this.validateConflict();this.drawCanvas();
+    },
+    isSecDisabled(min){
+      for(const [a,b] of this.daySegments){if(min>=a&&min<=b)return true;}
+      if(this.selecting==='end'&&this.draft.startMs){const minMs=this.draft.startMs+60000;const candidate=this.dayStartMs(this.viewDay)+min*60000;if(candidate<minMs)return true;}
+      return false;
+    },
+    clipEndAuto(endMs){
+      const startMs=this.draft.startMs;if(!startMs)return endMs;if(endMs<=startMs)return startMs+60000;
+      for(const r of this.ranges){if(!(endMs<=r.startMs||startMs>=r.endMs)){if(r.startMs>startMs){return Math.min(endMs,r.startMs-60000);}}}
+      return endMs;
+    },
+    msToLocalInput(ms){const d=new Date(ms);const Y=d.getFullYear(),M=this.pad(d.getMonth()+1),D=this.pad(d.getDate());const h=this.pad(d.getHours()),m=this.pad(d.getMinutes());return `${Y}-${M}-${D}T${h}:${m}`;},
+    parseLocalInput(str){if(!str)return null;const [date,time]=str.split('T');if(!time)return null;const [y,M,D]=date.split('-').map(Number);const parts=time.split(':').map(Number);const h=parts[0]||0;const m=parts[1]||0;return new Date(y,M-1,D,h,m,0).getTime();},
+    applyStartStr(){
+      const ms=this.parseLocalInput(this.startStr);if(!ms)return;
+      if(this.isMsInBusy(ms)){const next=this.snapOut(ms,+1);this.draft.startMs=next;this.startStr=this.msToLocalInput(next);}
+      else{this.draft.startMs=ms;}
+      if(this.draft.endMs&&this.draft.endMs<=this.draft.startMs)this.draft.endMs=null;
+      this.selecting='end';this.computeDaySegments();this.validateConflict();this.drawCanvas();
+    },
+    applyEndStr(){
+      let ms=this.parseLocalInput(this.endStr);if(!ms)return;
+      if(!this.draft.startMs){this.$message.warning('请先设置开始时间');this.endStr='';return;}
+      if(ms<=this.draft.startMs) ms=this.draft.startMs+60000;
+      if(this.isMsRangeOverBusy(this.draft.startMs,ms)){ms=this.clipEndAuto(ms);}
+      this.draft.endMs=ms;this.selecting='start';this.computeDaySegments();this.validateConflict();this.drawCanvas();
+    },
+    isMsInBusy(ms){for(const r of this.ranges){if(!(ms<r.startMs||ms>r.endMs))return true;}return false;},
+    isMsRangeOverBusy(s,e){for(const r of this.ranges){if(!(e<=r.startMs||s>=r.endMs))return true;}return false;},
+    snapOut(ms,dir){if(dir===0)return ms;let cur=ms;const step=60000*Math.sign(dir);for(let i=0;i<1440;i++){let inside=false;for(const r of this.ranges){if(!(cur<r.startMs||cur>r.endMs)){inside=true;break;}}if(!inside)return cur;cur+=step;}return ms;},
+    save() {
+      if (!this.canSave) return;
+
+      const start = new Date(this.draft.startMs);
+      start.setSeconds(0, 0);
+      const end = new Date(this.draft.endMs);
+      end.setSeconds(0, 0);
+
+      const pad = (n) => String(n).padStart(2, '0');
+      const dateStr = `${start.getFullYear()}-${pad(start.getMonth() + 1)}-${pad(start.getDate())}`;
+
+      const rec = {
+        date: dateStr,
+        startMs: start.getTime(),
+        endMs: end.getTime()
+      };
+
+      // 直接 push
+      this.ranges.push(rec);
+
+      // emit 给父组件,让它来驱动
+      const out = JSON.parse(JSON.stringify(this.ranges));
+      if (this.emitOnSave) {
+        this.$emit('saved', out);
+        this.$emit('input', out);
+      }
+
+      // 更新界面状态
+      this.selectedDate = dateStr;
+      this.viewDay = dateStr;
+      this.show = false;
+    }
+
+
+  },
+  watch:{
+    viewDay(){this.computeDaySegments();this.$nextTick(this.drawCanvas);},
+    daySegments(){this.$nextTick(this.drawCanvas);},
+    draft:{deep:true,handler(){this.validateConflict();this.$nextTick(this.drawCanvas);}},
+    value: {
+      deep: true,
+      handler(val) {
+        this.ranges = JSON.parse(JSON.stringify(val));
+      }
+    }
+  },
+  mounted() {
+    this.ranges = JSON.parse(JSON.stringify(this.value));
+  },
+};
+</script>
+
+<style scoped>
+/* 复用你的样式(略有精简) */
+.row { display:flex; gap:12px; align-items:center; flex-wrap: wrap; }
+.hint { font-size:12px; color:#6b7280; }
+.chip { display:inline-flex; align-items:center; padding:3px 8px; border-radius:999px; background:#eef2ff; color:#3730a3; font-size:12px; margin-left:6px; }
+.day-bar-wrap { border:1px solid #e5e7eb; border-radius:12px; padding:12px; }
+.day-bar-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
+.bar-legend { font-size:12px; color:#6b7280; display:flex; gap:12px; align-items:center; }
+.dot { width:10px; height:10px; border-radius:2px; display:inline-block; vertical-align:middle; margin-right:6px; }
+.dot-free { background:#10b981; }
+.dot-busy { background:#e5e7eb; border:1px solid #d1d5db; }
+.dot-range { background:#60a5fa; }
+canvas { width:100%; height:52px; border-radius:8px; display:block; background:linear-gradient(180deg, #fafafa, #fff); border:1px solid #e5e7eb; }
+.ticks { font-size:12px; color:#9ca3af; display:flex; justify-content:space-between; margin-top:6px; }
+.kv { font-size:14px; line-height:1.8; }
+.kv b { color:#111827; }
+.danger { color:#b91c1c; font-size: 20px;background-color: wheat }
+.ok { color:#065f46; }
+.aside { border-left:1px dashed #e5e7eb; padding-left:16px; }
+</style>

+ 969 - 0
src/views/app/sop/info/addSop.vue

@@ -0,0 +1,969 @@
+<template>
+ <div class="app-container">
+  
+    <div v-if="opType == 0" style="margin: 30px;"> 添加自动化任务</div>
+    <div v-else-if="opType == 1" style="margin: 30px;"> 修改自动化任务</div>
+    <div v-else style="margin: 30px;"> 查看自动化任务</div>
+
+    <div style="margin-top: 10px;margin-left: 50px;margin-right: 100px;margin-bottom: 30px;">
+
+      <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="状态" prop="status">
+          <dict-tag :options="statusOptions" :value="form.status"></dict-tag>
+        </el-form-item>
+
+        <el-form-item label="筛选方式" prop="filterMode">
+          <el-radio-group v-model="form.filterMode">
+            <el-radio :label="1">标签
+            </el-radio>
+            <el-radio :label="2" v-show="false">群聊
+            </el-radio>
+          </el-radio-group>
+          <div style="color: #999;font-size: 14px;display: flex;align-items: center;" v-if="form.filterMode == 1">
+            <i class="el-icon-info"></i>
+            标签:根据客户的标签筛选客户进入自动化
+          </div>
+          <div style="color: #999;font-size: 14px;display: flex;align-items: center;" v-if="form.filterMode == 2">
+            <i class="el-icon-info"></i>
+            群聊:选择用户所属的群聊,并且只能选择科普模板,科普模板里面第一条规则是发送到群聊,其他规则提醒会发送到群里面的个人
+          </div>
+        </el-form-item>
+  
+
+        <el-form-item label="次要节点天数" prop="minConversionDay" v-if="/*form.type != 3 && */form.filterMode == 1">
+          <!-- <el-input-number v-model="form.minConversionDay" :min="1" :max="100"></el-input-number> -->
+          <el-input style="width: 220px;" v-model="form.minConversionDay" placeholder="请输入次要节点天数" />
+          <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+            <i class="el-icon-info"></i>
+            第一次提醒客服,去联系客户,在【提醒看板】处显示,哪些需要联系的客户,多个使用英文逗号隔开
+          </div>
+        </el-form-item>
+
+        <el-form-item label="重要节点天数" prop="maxConversionDay" v-if="/*form.type != 3 && */form.filterMode == 1">
+          <el-input style="width: 220px;" v-model="form.maxConversionDay" placeholder="请输入重要节点天数" />
+          <!-- <el-input-number v-model="form.maxConversionDay" :min="1" :max="100"></el-input-number> -->
+          <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+            <i class="el-icon-info"></i>
+            第二次提醒客服,去联系客户,在【提醒看板】处显示,哪些需要联系的客户,多个使用英文逗号隔开
+          </div>
+        </el-form-item>
+
+        <div v-if="form.filterMode == 1">
+
+          <el-form-item label="推送方式 ">
+            <el-radio-group v-model="form.sendType" @input="handleSendTypeChange">
+              <el-radio v-for="dict in sopSendTypeOptions" :key="dict.dictValue"
+                :label="parseInt(dict.dictValue)">{{ dict.dictLabel }}</el-radio>
+            </el-radio-group>
+            <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+              <i class="el-icon-info"></i>
+              群发助手 则是正常的自动化任务,新客对话 是客户进线当天,根据【新客对话模板】的内容,给客户发送相应的消息
+            </div>
+          </el-form-item>
+          <el-form-item label="选择客服" prop="appCustomerId" style="margin-top: 2%">
+            <div>
+              <el-button size="medium" icon="el-icon-circle-plus-outline" plain
+                @click="openSelectCustomerPage(form.type, form.sendType)">请选择客服</el-button>
+            </div>
+            <div>
+              <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in selected" closable
+                :disable-transitions="false" @close="handleCloseCustomer(s)">
+                <span>{{ s.memberName }}</span>
+              </el-tag>
+            </div>
+          </el-form-item>
+
+          <el-form-item label="标签规则" prop="filterType">
+            <el-radio-group v-model="form.filterType">
+              <el-radio :label="1">含全部标签</el-radio>
+              <el-radio :label="2">含任意标签</el-radio>
+            </el-radio-group>
+            <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+              <i class="el-icon-info"></i>
+              【含全部标签】:客户要完全一对一匹配上选择标签,ps:多一个少一个都不行
+              【含任意标签】:客户只要有一个标签匹配上即可
+            </div>
+          </el-form-item>
+
+          <el-form-item label="选择的标签" prop="tags">
+            <div @click="hangleChangeTags()"
+              style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 390px">
+              <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+                <el-tag type="success" closable :disable-transitions="false"
+                  v-for="list in selectTagDialog.selected" :key="list.id" @close="handleCloseTags(list)"
+                  style="margin: 3px;">{{ list.name }}
+                </el-tag>
+              </div>
+            </div>
+            <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+              <i class="el-icon-info"></i>
+              选择创建的自动化任务的标签
+            </div>
+          </el-form-item>
+        </div>
+
+        <el-form-item label="开始时间" prop="startTime">
+          <el-date-picker clearable size="small" v-model="form.startTime" type="date" value-format="yyyy-MM-dd"
+            placeholder="选择开始时间" @change="clearAutoDatesIfNeeded">
+          </el-date-picker>
+          <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+            <i class="el-icon-info"></i>
+            任务的开始时间,客户进线在开始时间之前就符合此自动化任务的,都会进入到开始时间的营期里
+          </div>
+        </el-form-item>
+
+        <el-form-item label="任务过期时间" prop="expiryTime">
+          <el-row>
+            <el-input-number v-model="form.expiryTime" :min="3" :max="100"></el-input-number>
+            (小时)
+          </el-row>
+          <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+            <i class="el-icon-info"></i>
+            发送自动化任务消息的过期时间,超过这个时间,消息将不再发送(作废),比如:9点的消息,设置3个小时过期,12点之后还未发送 则这条消息将过期且不再发送
+          </div>
+        </el-form-item>
+
+        <el-form-item v-if="form.sendType == 2 && form.sendType != 4 && form.type != 3 && form.filterMode == 1"
+          label="自动添加自动化" prop="autoSopTime.autoSopType">
+          <el-radio-group v-model="form.autoSopTime.autoSopType">
+            <el-radio :label="Number(dict.dictValue)" v-for="dict in autoSopTypeOptions">{{ dict.dictLabel }}</el-radio>
+          </el-radio-group>
+          <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+            <i class="el-icon-info"></i>
+            这个选项仅作用于【新客户】进线时或【给客户打标签时】,是进入当日的营期 还是 次日的营期或者分营期
+          </div>
+        </el-form-item>
+
+        <div style="display: flex; align-items: center; flex-wrap: nowrap;">
+          <div v-if="form.autoSopTime.autoSopType == 1 && form.filterMode == 1"
+            style="display: flex; align-items: center">
+            <el-form-item label="起始时间" prop="autoStartTime" label-width="100px"
+              style="margin: 2% 0;align-items: center;">
+              <el-time-select style="width: 120px;" placeholder="起始时间" v-model="form.autoSopTime.autoStartTime"
+                :picker-options="{
+                  start: '00:00',
+                  step: '00:15',
+                  end: '24:00'
+                }">
+              </el-time-select>
+            </el-form-item>
+            <el-form-item label="结束时间" prop="autoEndTime" label-width="100px"
+              style="margin: 2% 0; align-items: center; ">
+              <el-time-select style="width: 120px;" placeholder="结束时间" v-model="form.autoSopTime.autoEndTime"
+                :picker-options="{
+                  start: '00:00',
+                  step: '00:15',
+                  end: '24:00',
+                  minTime: form.autoSopTime.autoEndTime
+                }">
+              </el-time-select>
+            </el-form-item>
+          </div>
+        </div>
+        
+        <el-form-item v-if="form.autoSopTime.autoSopType == 1" label="过期消息是否发送" prop="autoSopSend" label-width="130px">
+          <el-radio-group v-model="form.autoSopTime.autoSopSend">
+            <el-radio :label="1">是</el-radio>
+            <el-radio :label="2">否</el-radio>
+          </el-radio-group>
+          <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+            <i class="el-icon-info"></i>
+            起始时间-结束时间之内的进线客户,进入【当日营期】,时间之外的,进入【次日营期】
+          </div>
+        </el-form-item>
+
+        <div style="display: flex; align-items: center; flex-wrap: nowrap;"
+          v-if="form.autoSopTime.autoSopType == 3 || form.autoSopTime.autoSopType == 4">
+          <el-form-item label="选择一个或多个日期" prop="autoDateTime" label-width="150px">
+            <el-date-picker size="large" type="dates" style="width: 600px" v-model="form.autoSopTime.autoDateTime"
+              value-format="yyyy-MM-dd" placeholder="选择一个或多个日期【ps:请先选择开始时间,不得小于任务开始时间】"
+              @change="sortSelectedDates(form.autoSopTime.autoDateTime)" :disabled="!form.startTime">
+            </el-date-picker>
+          </el-form-item>
+        </div>
+
+        <el-form-item label="模板" prop="tempId">
+          <div @click="selectListSopTemp(form.sendType)"
+            style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;">
+            <el-tag v-if="form.tempId" type="success" style="margin: 3px;">
+              {{ form.tempName }}
+            </el-tag>
+            <span v-else style="margin: 3px; color: #999;">请点我选择模板</span>
+          </div>
+          <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+            <i class="el-icon-info"></i>
+            在左边的菜单【任务模板】板块设置好每天要发送的内容
+          </div>
+        </el-form-item>
+        <el-alert v-if="form.autoSopTime.autoSopType == 3" type="warning" :closable="false" show-icon>
+          <template #title>
+            <span style="font-size: 23px; line-height: 1.5;">
+              此功能用于正课区分营期,可以选择你想要开始营期的具体时间,选择后在此期间添加的用户将会自动进入对应营期。
+              例如今天1月1日,选择时间是1月1日,1月5日,1月10日。
+              用户1月1日-1月4日添加(添加上对应标签)则会进入1月5日的营期,1月5日-1月9日添加则会进入1月10日营期,1月5日自动给用户发送第一节科普
+            </span>
+          </template>
+        </el-alert>
+        <el-alert v-if="form.autoSopTime.autoSopType == 4" title="" type="warning" :closable="false" show-icon>
+          <template #title>
+            <span style="font-size: 23px; line-height: 1.5;">
+              此功能用于先导课区分营期,可以选择先导课开始营期的具体时间,选择后在此期间添加的用户将会自动进入对应营期。
+              例如今天1月1日,选择时间是1月1日,1月5日,1月10日。
+              用户1月1日-1月4日添加(添加上对应标签)则会进入1月1日的营期,1月5日-1月9日添加则会进入1月5日营期,自动给用户匹配上对应应该听到天数的时间节点
+            </span>
+          </template>
+        </el-alert>
+
+      </el-form>
+      <div slot="footer" class="dialog-footer" style="display: flex;justify-content: flex-end;">
+        <el-button type="primary" v-if="opType != 2" @click="submitForm">确 定</el-button>
+        <el-button v-if="opType != 2" @click="cancel">取 消</el-button>
+      </div>
+
+      <!-- 选择app客服角色 -->
+      <el-dialog :title="customerConfig.title" :visible.sync="customerConfig.visible" width="1300px" append-to-body>
+        <CusRoleList
+          ref="QwUserList"
+          :selected="selected"
+          @confirm="customerSelectedConfirmCallForBind"
+          @selectionChange="customerSelectionChangeCallForBind"
+        />
+      </el-dialog>
+
+      <!-- 选择模板 -->
+      <el-dialog title="选择模板" :visible.sync="tempOpen" append-to-body>
+        <sop-temp ref="SopTempComments" @sopTemp="sopTemp"></sop-temp>
+      </el-dialog>
+
+      <!-- 标签 -->
+      <el-dialog title="选择标签"
+        :visible.sync="selectTagDialog.visible"
+        width="1000px"
+        append-to-body
+      >
+        <select-tag
+          :tagSelected="selectTagDialog.selected"
+          @tagSubmit="tagSubmitForBind"
+          @tagCancel="tagCancelForBind"
+          @tagSelection="tagSelectionChangeCallForBind"
+        />
+      </el-dialog>
+
+    </div>
+  </div>
+</template>
+
+<script>
+import {addSop, courseList, updateSop, getSop} from "@/api/app/sop/info";
+import {getList} from "@/api/app/cpsc";
+import {getSopTemp} from '@/api/app/sop/template';
+import CusRoleList from '@/views/app/user/CusRoleList.vue';
+import {listRole} from '@/api/app/user/userList';
+import ImageUpload from "@/views/app/sop/info/ImageUpload";
+import selectTag from "@/views/app/tagGroup/selectTag.vue";
+import { getTagByIds,  listTagGroupForUserBindTag as searchTags, } from "@/api/app/tag/tagGroup";
+import SopTemp from "@/views/app/sop/template/sopTemp.vue";
+
+export default {
+  name: "addSop",
+  components: { SopTemp, CusRoleList, ImageUpload, selectTag },
+  data() {
+    return {
+      miniAppList: [],
+      changeDate: '2025-08-20',
+      changeRange: null,
+      changeBusy: [
+        { startMs: new Date('2025-08-20 12:00').getTime(), endMs: new Date('2025-08-20 13:30').getTime() }
+      ],
+      autoSopTypeOptions: [],
+      //模板遮罩
+      tempOpen: false,
+      // 选中数组
+      ids: [],
+      courseList: [],
+      tags: null,
+      setting: [],
+      //标签
+      changeTagDialog: {
+        title: "",
+        open: false,
+        type: null,
+      },
+      //选择的标签
+      tagsIdsChangeSelectList: null,
+      queryTagParams: {
+        pageNum: 1,
+        pageSize: 10,
+        total: 0,
+        name: null,
+      },
+      // 总条数
+      total: 0,
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 状态字典
+      statusOptions: [],
+      //SOP发送类型
+      sopSendTypeOptions: [],
+      // 表单参数
+      form: {
+        status: 1,
+        filterMode: 1,
+        sendType: 2,
+        type: 2,
+        filterType: 2,
+        expiryTime: 3,
+        autoGroup: 0,
+        isAutoSop: 1,
+        tempId: null,
+        tempName: null,
+        autoSopTime: { autoSopType: 2, autoStartTime: '00:00', autoEndTime: '24:00', autoSopSend: 2, autoDateTime: [] },
+      },
+      selected: [],
+      //选择客服对话框
+      customerConfig: {
+        title: "",
+        visible: false
+      },
+      // 表单校验
+      rules: {
+        name: [{ required: true, message: "名称不能为空", trigger: "submit" }],
+        type: [{ required: true, message: "不能为空", trigger: "submit" }],
+        sendType: [{ required: true, message: "不能为空", trigger: "submit" }],
+        startTime: [{ required: true, message: "开始时间不能为空", trigger: "submit" }],
+        tempId: [{ required: true, message: "模板不能为空", trigger: "submit" }],
+        tags: [
+          {
+            validator: this.validateGroupLevelOrTags,
+            trigger: ["blur", "change"]
+          }
+        ],
+      },
+      opType: 0,//操作类型,0-新增(默认),1-编辑,2-查看
+      formId: null,
+      selectTagDialog: {
+        visible: false,
+        selected: [],
+      },
+    };
+  },
+  created() {
+    this.getDicts("app_sop_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    this.getDicts("app_sop_type").then(response => {
+      this.sopSendTypeOptions = response.data;
+    });
+    this.getDicts("app_sop_autoType").then(response => {
+      this.autoSopTypeOptions = response.data;
+    });
+
+    //操作类型
+    let opType = this.$route.params && this.$route.params.type;
+    if (opType) {
+      this.opType = opType;
+    }
+
+    //编辑/查看时有效
+    let id = this.$route.params && this.$route.params.id;
+    if (id) {
+      this.formId = id;
+      this.handleUpdate(id);
+    }
+
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+
+    getList().then(response => {
+    });
+  },
+  watch: {
+    selected(newList) {
+      this.form.appCustomerId = newList.map(item => item.id);
+    }
+  },
+  methods: {
+
+    validateGroupLevelOrTags(rule, value, callback) {
+      // 检查是否是群聊筛选方式且自动创建群为1
+      if (this.form.filterMode == 2 && this.form.autoGroup == 1) {
+        // 检查评级数组和标签数组是否都为空
+        const hasLevel = this.form.autoGroupLevelArray && this.form.autoGroupLevelArray.length > 0;
+        const hasTags = this.tagsIdsChangeSelectList && this.tagsIdsChangeSelectList.length > 0;
+
+        if (!hasLevel && !hasTags) {
+          callback(new Error('创建群聊评级和企微标签必须至少选择一项'));
+        } else {
+          callback();
+        }
+      } else {
+        // 如果不满足条件,验证通过
+        callback();
+      }
+    },
+
+    /** 打开子组件的选择器 */
+    openPicker() {
+      this.$refs.timePicker.openPicker();
+    },
+    fmt(ms) {
+      if (!ms) return "-";
+      const d = new Date(ms);
+      return d.toLocaleTimeString("zh-CN", { hour12: false });
+    },
+    /**
+     * 取消选择标签
+     */
+    tagCancel() {
+      this.resetSearchQueryTag();
+      this.changeTagDialog.open = false;
+    },
+    /**
+     * 选择标签
+     */
+    hangleChangeTags() {
+      this.selectTagDialog.visible = true;
+    },
+    /**
+     * 绑定标签取消
+     */
+    tagCancelForBind() {
+      this.selectTagDialog.visible = false;
+    },
+    /**
+     * 绑定标签确认
+     */
+    tagSubmitForBind() {
+      this.selectTagDialog.visible = false;
+    },
+    /**
+     * 选中的绑定标签列表
+     * @param group 标签所属组
+     * @param tag 具体操作的标签
+     */
+    tagSelectionChangeCallForBind(group, tag) {
+      if (!tag.isSelected) {
+        this.selectTagDialog.selected = this.selectTagDialog.selected.filter(item => tag.id != item.id);
+      } else {
+        if (!this.selectTagDialog.selected.some(st => st.id == tag.id)) {
+          this.selectTagDialog.selected.push(tag);
+        }
+      }
+    },
+    /**
+     * 删除一些选择的标签
+     * @param list 
+     */
+    handleCloseTags(ruleItem) {
+      const index = this.selectTagDialog.selected.findIndex(t => t === ruleItem);
+        if (index !== -1) {
+          this.selectTagDialog.selected.splice(index, 1);
+        }
+    },
+    /**
+     * 开始时间发生变化
+     */
+    clearAutoDatesIfNeeded() {
+      if (this.form.autoSopTime.autoDateTime.length > 0 && (this.form.autoSopTime.autoSopType == 3 || this.form.autoSopTime.autoSopType == 4)) {
+        // 1. 过滤掉 < startTime 的日期
+        const validDates = this.form.autoSopTime.autoDateTime.filter(date => new Date(date) >= new Date(this.form.startTime));
+        // 2. 如果有被过滤掉的日期,提示用户
+        if (validDates.length < this.form.autoSopTime.autoDateTime.length) {
+          this.$message.warning(`已自动移除小于 ${this.form.startTime} 的日期!`);
+        }
+        // 3. 升序排序
+        validDates.sort((a, b) => new Date(a) - new Date(b));
+        // 4. 更新数据
+        this.form.autoSopTime.autoDateTime = validDates;
+      }
+    },
+    /**
+     * 开始时间,任务开始时间排序
+     * @param dates 
+     */
+    sortSelectedDates(dates) {
+
+      if (!this.form.startTime) {
+        this.$message.error("请先选择开始时间!不得小于任务开始时间");
+        this.form.autoSopTime.autoDateTime = [];
+        return;
+      }
+      // 1. 过滤掉 < startTime 的日期
+      const validDates = dates.filter(date => new Date(date) >= new Date(this.form.startTime));
+      // 2. 如果有被过滤掉的日期,提示用户
+      if (validDates.length < dates.length) {
+        this.$message.warning(`已自动移除小于 ${this.form.startTime} 的日期!`);
+      }
+      // 3. 升序排序
+      validDates.sort((a, b) => new Date(a) - new Date(b));
+      // 4. 更新数据
+      this.form.autoSopTime.autoDateTime = validDates;
+    },
+    /**
+     * 查询模板
+     * @param type 
+     */
+    selectListSopTemp(type) {
+      this.tempOpen = true;
+      setTimeout(() => {
+        this.$refs.SopTempComments.getList(type);
+      }, 200);
+    },
+    sopTemp(val) {
+      this.$set(this.form, 'tempId', val.id);
+      this.$set(this.form, 'tempName', val.name);
+      this.tempOpen = false;
+    },
+    //选择app用户时
+    openSelectCustomerPage(type, sendType) {
+      this.customerConfig.title = "选择客服"
+      this.customerConfig.visible = true;
+    },
+    /**
+     * 切换类别时 ,清除选择的成员
+     */
+    clearUser() {
+      this.selected = [];
+      this.tags = null;
+    },
+    /**
+     * 编辑/查看获取任务信息
+     * @param sopId
+     */
+    handleUpdate(sopId) {
+      this.reset();
+      getSop({id: sopId}).then(async (response) => {
+        this.form = response.data;
+        this.form.autoSopTime = JSON.parse(response.data.autoSopTime)
+        if (this.form.filterMode != 1) {
+          this.form.sendType = 2;
+        }
+        if (!this.isEmpty(this.form.tags)) {
+          let tagInfo = await getTagByIds(this.form.tags);
+          this.selectTagDialog.selected = tagInfo.data || [];
+        }
+        this.form.status = response.data.status.toString();
+        if (this.form.setting != null) {
+          this.setting = JSON.parse(this.form.setting);
+        }
+        if (this.form.autoGroupLevel != null) {
+          this.form.autoGroupLevelArray = this.form.autoGroupLevel.split(",").map(Number)
+            .filter(item => item !== 0);
+        }
+        if (!this.isEmpty(this.form.appCustomerId)) {
+          let userInfo = await listRole({ids: this.form.appCustomerId});
+          this.selected = userInfo.rows;
+        }
+        let tempInfo = await getSopTemp(this.form.tempId);
+        this.$set(this.form, 'tempName', tempInfo.data.name);
+      });
+
+    },
+    isEmpty(obj) {
+        if (null === obj || undefined === obj || 0 === obj.length) {
+            return true;
+        }
+        return false;
+    },
+    customerSelectedConfirmCallForBind() {
+      this.customerConfig.visible = false;
+    },
+    /**
+     * 选中的用户列表
+     * @param list 
+     */
+    customerSelectionChangeCallForBind(config) {
+      let selected = config.selected;
+      //单行操作
+      if (config.mode === 0) {
+        let row = config.row;
+        if (selected) {
+          if (!this.selected.some(s => s.id == row.id)) {
+            this.selected.push(row)
+          }
+        } else {
+          this.selected = this.selected.filter(m => m.id != row.id);
+        }
+      }
+      //操作当前页所有行
+      else {
+        let list = config.rows;
+        if (selected) {
+          list.map(l => {
+            if (!this.selected.some(s => s.id == l.id)) {
+              this.selected.push(l)
+            }
+          });
+        } else {
+          list.map(l => {
+            for (let i = 0; i < this.selected.length; i++) {
+              if (l.id == this.selected[i].id) {
+                this.selected.splice(i, 1);
+              }
+            }
+          })
+        }
+      }
+    },
+
+    //选择变动时的变动
+    
+    /**
+     * 推送方式修改
+     * @param val 
+     */
+    handleSendTypeChange(val) {
+      this.selected = [];
+      this.form.tempId = null;
+      this.form.tempName = null;
+      if (val == 1) {
+        // 遍历 this.setting 数组并清空每个对象的 content 属性
+        this.setting.forEach(item => {
+          if (item.content.length > 9) {
+            item.content = item.content.slice(0, 9); // 保留前 9 个元素
+          }
+        });
+      }
+    },
+    handleCloseCustomer(list) {
+      const index = this.selected.findIndex(t => t === list);
+      if (index !== -1) {
+        this.selected.splice(index, 1);
+      }
+    },
+    /**
+     * 取消按钮
+     */
+    cancel() {
+      //删除当前打开的菜单tab
+      this.$store.dispatch("tagsView/delView", this.$route);
+      //替换浏览器路由
+      this.$router.replace('/app/sop')
+      this.reset();
+    },
+    /**
+     * 清空搜索标签条件
+     */
+    resetSearchQueryTag() {
+      this.queryTagParams = {
+        pageNum: 1,
+        pageSize: 10,
+        total: 0,
+        name: null,
+      };
+    },
+
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        filterMode: 1,
+        name: null,
+        status: 1,
+        sendType: 2,
+        filterType: 2,
+        expiryTime: 3,
+        appCustomerId: null,
+        setting: null,
+        createBy: null,
+        createTime: null,
+        autoSopTime: { autoSopType: 2, autoStartTime: '00:00', autoEndTime: '24:00', autoSopSend: 2, autoDateTime: [] },
+      };
+      this.resetForm("form");
+      this.tags = null;
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+
+    formatDateTo24HourString(date) {
+      let year = date.getFullYear();
+      let month = ('0' + (date.getMonth() + 1)).slice(-2); // 月份需要加 1 并补零
+      let day = ('0' + date.getDate()).slice(-2); // 日需要补零
+      let hours = ('0' + date.getHours()).slice(-2); // 小时需要补零
+      let minutes = ('0' + date.getMinutes()).slice(-2); // 分钟需要补零
+      let seconds = ('0' + date.getSeconds()).slice(-2); // 秒需要补零
+
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+    },
+
+    // 打开群聊选择器
+    openGroupChatSelector() {
+      this.$refs.groupChatSelector.show(this.form.chatIds);
+    },
+
+    // 处理群聊选择结果
+    handleSelectGroupChats(selectedChatIds) {
+      this.form.chatIds = selectedChatIds;
+      // 强制更新视图
+      this.$forceUpdate();
+    },
+    /**
+     * 保存sop任务
+     */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.selected.length <= 0) {
+            return this.$message.error("请选择客服")
+          }
+          this.form.appCustomerId = this.selected.map(s => s.id).join(",");
+          if (this.selectTagDialog.selected != null && this.selectTagDialog.selected.length > 0) {
+            this.form.tags = this.selectTagDialog.selected?.map(item => item.id).join(",");
+          } else {
+            return this.$message.error("选择的标签不能为空!!请选择筛选的标签")
+          }
+          if (this.form.autoSopTime.autoSopType == 3 || this.form.autoSopTime.autoSopType == 4) {
+            if (this.form.autoSopTime.autoDateTime == null || this.form.autoSopTime.autoDateTime == "") {
+              return this.$message.error("选择的日期不能为空!!请选择日期")
+            }
+          }
+          this.form.appCustomerId = this.selected.map(s => s.id).join(",");
+          this.form.setting = JSON.stringify(this.setting)
+          this.form.autoSopTime.createTime = this.formatDateTo24HourString(new Date());
+          this.form.autoSopTime = JSON.stringify(this.form.autoSopTime)
+          if (this.form.id != null) {
+            updateSop(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.$store.dispatch("tagsView/delView", this.$route);
+              window.location.replace('/app/sop')
+              this.reset();
+            });
+          } else {
+            addSop(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.$store.dispatch("tagsView/delView", this.$route);
+              window.location.replace('/app/sop')
+              this.reset();
+            });
+          }
+        }
+      });
+    },
+
+  }
+};
+</script>
+<style scoped lang="scss">
+.custom-input ::v-deep  .el-input__inner {
+  height: 20px;
+  padding: 0 4px;
+  text-align:center;
+  display: block;
+}
+.custom-input ::v-deep  .el-input__icon {
+  line-height: 20px;
+}
+.user-box {
+  width: 50px;           /* 盒子宽度 */
+  height: 30px;           /* 盒子高度 */
+  padding: 5px 10px;          /* 内边距 */
+  border: 1px solid #d5d1d1;/* 边框 */
+  margin: 3px;            /* 外边距 */
+  background-color: #f9f9f9;/* 背景颜色 */
+  color: #333;            /* 文本颜色 */
+  font-size: 14px;        /* 字体大小 */
+  text-align: center;     /* 文本居中 */
+}
+.elFormItemClass{
+  margin: 20px 0px
+
+}
+
+.message-stayle{
+  display: flex;
+  justify-content: normal;
+  align-items: center;
+  margin-top: 10px;
+}
+.message-stayle .el-link {
+  white-space: normal; /* 允许换行 */
+  word-break: break-all; /* 单词中间断行 */
+  overflow-wrap: break-word; /* 允许在单词内换行 */
+}
+.message-stayle span {
+  word-break: break-all;
+}
+.container {
+  position: relative; /* 使 phone div 的 absolute 定位基于这个容器 */
+  width: 100%;
+  height: 100%;
+}
+.phone {
+  position: absolute; /* 定位到容器内 */
+  top: 5%;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 100%;
+  max-width: 375px;
+  height: 100%;
+  max-height: 90%;
+  border: 8px solid #ccc;
+  border-radius: 36px;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+  background-color: #fff;
+  overflow: hidden;
+}
+
+.phone::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 50%;
+  width: 60px;
+  height: 5px;
+  background-color: #ccc;
+  border-radius: 10px;
+  transform: translateX(-50%);
+}
+
+.chat-interface {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  background-color: #fff;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  height: 50px;
+  padding: 0 15px;
+  background-color: #fff;
+  border-bottom: 1px solid #e0e0e0;
+}
+
+.header .back,
+.header .menu {
+  height: 25px;
+  width: 25px;
+}
+.header .title{
+  font-size: 16px;
+  margin: auto;
+}
+
+.message-area {
+  flex: 1;
+  background-color: rgb(240, 242, 245);
+  padding: 10px;
+  overflow-y: auto;
+}
+
+.footer {
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  background-color: #fff;
+  border-top: 1px solid #e0e0e0;
+}
+
+.footer .voice-button,
+.footer .emoji-button,
+.footer .add-button {
+  width: 25px;
+  height: 25px;
+  border: none;
+  cursor: pointer;
+  flex-shrink: 0; /* 不允许缩小 */
+}
+.footer .emoji-button,
+.footer .add-button {
+  margin: 0px 5px;
+}
+
+
+.footer .input-box {
+  flex: 1;
+  border: 1px solid #e0e0e0;
+  margin-left: 5px;
+}
+
+.text-container {
+  max-height: 7.5em; /* 设置最大高度为6行,根据字体大小调整 */
+  overflow-y: auto; /* 内容超出时显示滚动条 */
+  line-height: 1.5em; /* 行高设置,确保每行高度一致 */
+}
+.el-form-item {
+  margin-bottom: 30px;
+}
+
+.custom-span {
+  display: block; /* 确保元素是块级元素 */
+  height: 110px; /* 设置固定高度 */
+  overflow-y: auto; /* 超出高度时显示滚动条 */
+  word-wrap: break-word; /* 自动换行 */
+  word-break: break-all; /* 在必要时进行换行 */
+}
+.custom-span-description{
+  display: block; /* 确保元素是块级元素 */
+  height: 110px; /* 设置固定高度 */
+  overflow-y: auto; /* 超出高度时显示滚动条 */
+  word-wrap: break-word; /* 自动换行 */
+  word-break: break-all; /* 在必要时进行换行 */
+}
+.custom-span-title {
+  display: block; /* 确保元素是块级元素 */
+  height: 45px; /* 设置固定高度 */
+  overflow-y: auto; /* 超出高度时显示滚动条 */
+  word-wrap: break-word; /* 自动换行 */
+  word-break: break-all; /* 在必要时进行换行 */
+}
+
+/* CSS 样式 */
+.tag-container {
+  display: flex;
+  flex-wrap: wrap; /* 超出宽度时自动换行 */
+  gap: 8px; /* 设置标签之间的间距 */
+}
+.name-background {
+  display: inline-block;
+  background-color: #abece6; /* 背景颜色 */
+  padding: 4px 8px; /* 调整内边距,让背景包裹文字 */
+  border-radius: 4px; /* 可选:设置圆角 */
+}
+.tag-box {
+  padding: 8px 12px;
+  border: 1px solid #989797;
+  border-radius: 4px;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.tag-selected {
+  background-color: #00bc98;
+  color: #fff;
+  border-color: #00bc98;
+}
+
+.el-tag + .el-tag {
+  margin-left: 10px;
+}
+.button-new-tag {
+  margin-left: 10px;
+  height: 32px;
+  line-height: 30px;
+  padding-top: 0;
+  padding-bottom: 0;
+}
+.input-new-tag {
+  width: 90px;
+  margin-left: 10px;
+  vertical-align: bottom;
+}
+.text-container {
+  max-height: 5em; /* 设置最大高度为6行,根据字体大小调整 */
+  overflow-y: auto; /* 内容超出时显示滚动条 */
+  line-height: 1.5em; /* 行高设置,确保每行高度一致 */
+}
+</style>

+ 1603 - 0
src/views/app/sop/info/index.vue

@@ -0,0 +1,1603 @@
+<template>
+   <div class="app-container">
+        <!-- 过滤条件 -->
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+
+            <el-form-item label="规则编号" prop="id">
+                <el-input v-model="queryParams.id" 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"
+                    @change="updateCorpId()">
+                    <el-option v-for="dict in sysSopStatus" :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"
+                    @change="updateCorpId()">
+                    <el-option v-for="dict in appSopType" :key="dict.dictValue" :label="dict.dictLabel"
+                        :value="dict.dictValue" />
+                </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="createTime">
+                <el-date-picker clearable size="small" v-model="queryParams.createTime" type="date"
+                    value-format="yyyy-MM-dd" placeholder="选择创建时间">
+                </el-date-picker>
+            </el-form-item>
+
+            <el-form-item label="标签" prop="tagIds">
+                <div @click="hangleChangeTags()"
+                    style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
+                    <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+                        <el-tag type="success" closable :disable-transitions="false" v-for="list in this.selectTags"
+                            :key="list.id" @close="handleCloseTags(list)" style="margin: 3px;">{{ list.name }}
+                        </el-tag>
+                    </div>
+                </div>
+            </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="['app:sop:add']"
+                >
+                    新增
+                </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="['app:sop:delete']"
+                >
+                    删除
+                </el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    type="success"
+                    plain
+                    icon="el-icon-download"
+                    size="mini"
+                    :loading="exportLoading"
+                    @click="handleExport"
+                    v-hasPermi="['app:sop:export']"
+                >
+                    导出
+                </el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    size="mini"
+                    plain
+                    type="warning"
+                    icon="el-icon-edit"
+                    :disabled="multiple"
+                    @click="handleExecute"
+                    v-hasPermi="['app:sop:exec']"
+                >
+                    批量执行自动化
+                </el-button>
+            </el-col>
+            <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+
+        <!-- 数据表格 -->
+        <el-table v-loading="loading" border :data="sopList" @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" align="center" />
+            <el-table-column label="规则编号" align="center" prop="id" min-width="160" />
+            <el-table-column label="规则名称" align="center" prop="name" min-width="150" />
+            <el-table-column label="规则状态" align="center" prop="status" min-width="150">
+                <template slot-scope="scope">
+                    <dict-tag :options="sysSopStatus" :value="scope.row.status"></dict-tag>
+                </template>
+            </el-table-column>
+            <el-table-column label="发送方式" align="center" prop="sendType">
+                <template slot-scope="scope">
+                    <dict-tag :options="appSopType" :value="scope.row.sendType" />
+                </template>
+            </el-table-column>
+
+            <el-table-column label="模板" align="center" prop="tempId">
+                <template slot-scope="scope">
+                    <div v-for="id in tempList" v-if="id.id == scope.row.tempId">{{ id.name }}</div>
+                </template>
+            </el-table-column>
+
+            <el-table-column label="新客户自动创建自动化" align="center" prop="isAutoSop" min-width="180">
+                <template slot-scope="scope">
+                    <el-switch v-if="scope.row.sendType == 2 || scope.row.sendType != 4" v-model="scope.row.isAutoSop"
+                        active-color="#13ce66" inactive-color="#ff4949" :active-value="1" :inactive-value="2"
+                        @change="switchAutoSopChange(scope.row, scope.row.isAutoSop)">
+                    </el-switch>
+                    <span v-if="scope.row.sendType != 1 && scope.row.sendType != 4 && scope.row.isAutoSop == '1'"
+                        style="margin-left: 10px;color: #13ce66">已开启</span>
+                    <span v-if="scope.row.sendType != 1 && scope.row.sendType != 4 && scope.row.isAutoSop == '2'"
+                        style="margin-left: 10px;color: #ff4949">已关闭</span>
+                </template>
+            </el-table-column>
+            <el-table-column label="过期时间" align="center" prop="expiryTime">
+                <template slot-scope="scope">
+                    {{ scope.row.expiryTime }} 小时
+                </template>
+            </el-table-column>
+            <el-table-column label="创建时间" align="center" prop="createTime" width="180" />
+            <el-table-column label="修改规则状态时间" align="center" prop="stopTime" min-width="180" />
+            <el-table-column label="查看操作" align="center" class-name="small-padding fixed-width" min-width="200"
+                fixed="right">
+                <!-- 任务状态为 非待执行 则有以下操作-->
+                <template slot-scope="scope">
+                    <el-button
+                        v-if="[0, 2, 3, 4].includes(Number(scope.row.status)) && scope.row.sendType != 4"
+                        size="mini" type="text" @click="selectSchedule(scope.row)">营期
+                    </el-button>
+                    <el-button
+                        v-if="[0, 2, 3, 4].includes(Number(scope.row.status)) || scope.row.sendType == 4"
+                        size="mini" type="text" style="color: green;" @click="handleScheduleDetail(scope.row)">执行详情
+                    </el-button>
+                    <el-button
+                        v-if="[0, 2, 3, 4].includes(Number(scope.row.status))"
+                        size="mini" type="text" @click="handleUpdate(scope.row, 2)">查看规则
+                    </el-button>
+                    <el-button
+                        v-if="[0, 2, 3, 4].includes(Number(scope.row.status))"
+                        size="mini" type="text" style="color: green;" @click="handleQueryDetails(scope.row)">查看模板
+                    </el-button>
+
+                </template>
+            </el-table-column>
+            <el-table-column label="修改操作" align="center" class-name="small-padding fixed-width" min-width="100"
+                fixed="right">
+                <template slot-scope="scope">
+                    <el-button
+                        v-if="scope.row.sendType != 4"
+                        size="mini"
+                        type="text"
+                        @click="handleUpdateOutTime(scope.row)"
+                        v-hasPermi="['app:sop:edit']"
+                    >
+                        修改规则
+                    </el-button>
+                    <el-button
+                        v-if="scope.row.status == 1"
+                        size="mini"
+                        type="text"
+                        @click="handleUpdate(scope.row, 1)"
+                        v-hasPermi="['app:sop:edit']"
+                    >
+                        修改任务
+                    </el-button>
+                    <el-button
+                        size="mini"
+                        type="text"
+                        style="color: red;"
+                        @click="handleDelete(scope.row)"
+                        v-hasPermi="['app:sop:delete']"
+                    >删除
+                    </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="1000px" append-to-body>
+            <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="parseInt(dict.dictValue)">{{ dict.dictLabel }}
+                        </el-radio>
+                    </el-radio-group>
+                </el-form-item>
+
+                <el-form-item label="开始时间" prop="startTime">
+                    <el-date-picker clearable size="small" v-model="form.startTime" type="date"
+                        value-format="yyyy-MM-dd" placeholder="选择开始时间">
+                    </el-date-picker>
+                </el-form-item>
+                <el-form-item label="模板" prop="tempId">
+                    <el-select v-model="form.tempId" @focus="selectListSopTemp(form.sendType)" placeholder="请选择模板"
+                        v-loading="tempListLoading">
+                        <el-option v-for="dict in tempList" :label="dict.name" :value="dict.id">
+                        </el-option>
+                        <div v-if="tempListLoading" slot="prefix" class="select-prefix">正在查询相应模板...</div>
+                    </el-select>
+                </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitForm">确 定</el-button>
+                <el-button @click="cancel">取 消</el-button>
+            </div>
+        </el-dialog>
+
+        <el-dialog :title="changeTagDialog.title" :visible.sync="changeTagDialog.open" width="1000px" append-to-body>
+            <div>搜索标签:
+                <el-input v-model="queryTagParams.name" placeholder="请输入标签名称" clearable size="small"
+                    style="width: 200px;margin-right: 10px" />
+                <el-button type="primary" icon="el-icon-search" size="mini"
+                    @click="handleSearchTags(queryTagParams.name)">
+                    搜索
+                </el-button>
+                <el-button type="primary" icon="el-icon-refresh" size="mini" @click="cancelSearchTags">重置</el-button>
+            </div>
+            <div v-for="item in tagGroupList" :key="item.id">
+                <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+                    <span class="name-background">{{ item.name }}</span>
+                </div>
+                <div class="tag-container">
+                    <a v-for="tagItem in item.tags" class="tag-box" @click="tagSelection(tagItem)"
+                        :class="{ 'tag-selected': tagItem.isSelected || (selectTags && selectTags.some(tg => tg.id == tagItem.id)) }">
+                        {{ tagItem.name }}
+                    </a>
+                </div>
+            </div>
+            <pagination v-show="tagTotal > 0" :total="tagTotal" :page.sync="queryTagParams.pageNum"
+                :limit.sync="queryTagParams.pageSize" @pagination="getPageListTagGroup" />
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="tagSubmitForm()">确 定</el-button>
+                <el-button @click="tagCancel()">取消</el-button>
+            </div>
+        </el-dialog>
+
+        <!-- 单独的修改时间 -->
+        <el-dialog :title="outTimeOpen.title" :visible.sync="outTimeOpen.open" width="800px">
+
+            <el-card class="box-card">
+                <el-row>
+
+                    <el-col style="margin-top: 3%; display: flex;">
+                        <span style="font-size: 16px;font-weight: bold; width: 120px; display: flex; align-items: center; justify-content: end;">过期时间:</span>
+                        <el-input-number v-model="outTimeOpen.expiryTime" :min="1" :max="100"/>
+                        <span style="font-size: 16px;font-weight: bold; display: flex; align-items: center; justify-content: end;">(小时)</span>
+                    </el-col>
+
+                    <el-col style="margin-top: 3%; display: flex;">
+                        <span style="font-size: 16px;font-weight: bold; width: 120px; display: flex; align-items: center; justify-content: end;">次要节点天数:</span>
+                        <el-input v-model="outTimeOpen.minConversionDay" style="width: 200px;"/>
+                        <span style="font-size: 16px;font-weight: bold; display: flex; align-items: center; justify-content: end;">(天)</span>
+                    </el-col>
+
+                    <el-col style="margin-top: 3%; display: flex;">
+                        <span style="font-size: 16px;font-weight: bold; width: 120px; display: flex; align-items: center; justify-content: end;">重要节点天数:</span>
+                        <el-input v-model="outTimeOpen.maxConversionDay" style="width: 200px;"/>
+                        <span style="font-size: 16px;font-weight: bold; display: flex; align-items: center; justify-content: end;">(天)</span>
+                    </el-col>
+
+                    <el-col style="margin-top: 3%;">
+                        <span style="font-size: 16px;font-weight: bold;">发课开始天数:</span>
+                        <el-input-number v-model="outTimeOpen.courseDay" :min="1" :max="100"></el-input-number>
+                        (天)
+                        <div style="color: #999;font-size: 14px;display: flex;align-items: center;margin-top: 2%">
+                            <i class="el-icon-info"></i>
+                            作用于【提醒看板】和【学习记录】的统计,由多少天开始计算。比如 模板里
+                            第一天是先导课,第二天是正课,此处设置天数为2,则统计从第二天开始。如果无视模板天数,此处设置几天,则统计面板就从第几天开始统计
+                        </div>
+                    </el-col>
+
+                    <el-col style="margin: 3% ">
+
+                        <div style="float: right;margin-right: 3%">
+                            <el-button type="primary" icon="el-icon-search" size="mini"
+                                @click="handleUpdateExpiryTime">确定修改
+                            </el-button>
+                            <el-button icon="el-icon-refresh" size="mini" @click="resetUpdateExpiryTime">取消</el-button>
+                        </div>
+                    </el-col>
+
+                </el-row>
+            </el-card>
+        </el-dialog>
+
+        <!--  -->
+        <el-dialog :title="autoSopOpen.title" :visible.sync="autoSopOpen.open" width="1300px" append-to-body>
+            <el-form ref="autoSopTimeFrom" :model="form.autoSopTime" :rules="autoSopTimeRules">
+                <el-form-item label="自动类型" prop="type" style="margin: 2%; display: flex; align-items: center;">
+                    <el-radio-group v-model="form.autoSopTime.autoSopType">
+                        <el-radio :label="Number(dict.dictValue)" v-for="dict in autoSopTypeOptions">{{ dict.dictLabel }}</el-radio>
+                    </el-radio-group>
+                </el-form-item>
+                <div style="display: flex; align-items: center; flex-wrap: nowrap;">
+                    <div v-if="form.autoSopTime.autoSopType == 1" style="display: flex; align-items: center">
+                        <el-form-item label="起始时间" prop="autoStartTime" label-width="100px"
+                            style="margin: 2% 0;align-items: center;">
+                            <el-time-select style="width: 120px;" placeholder="起始时间"
+                                v-model="form.autoSopTime.autoStartTime" :picker-options="{
+                                    start: '00:00',
+                                    step: '00:15',
+                                    end: '24:00'
+                                }">
+                            </el-time-select>
+                        </el-form-item>
+                        <el-form-item label="结束时间" prop="autoEndTime" label-width="100px"
+                            style="margin: 2% 0; align-items: center; ">
+                            <el-time-select style="width: 120px;" placeholder="结束时间"
+                                v-model="form.autoSopTime.autoEndTime" :picker-options="{
+                                    start: '00:00',
+                                    step: '00:15',
+                                    end: '24:00',
+                                    minTime: form.autoSopTime.autoEndTime
+                                }">
+                            </el-time-select>
+                        </el-form-item>
+                    </div>
+                </div>
+                <el-form-item v-if="form.autoSopTime.autoSopType == 1" label="过期消息是否发送" prop="autoSopSend"
+                    style="margin: 2%; display: flex; align-items: center;">
+                    <el-radio-group v-model="form.autoSopTime.autoSopSend">
+                        <el-radio :label="1">是
+                        </el-radio>
+                        <el-radio :label="2">否
+                        </el-radio>
+                    </el-radio-group>
+                </el-form-item>
+
+                <div style="display: flex; align-items: center; flex-wrap: nowrap;"
+                    v-if="form.autoSopTime.autoSopType == 3 || form.autoSopTime.autoSopType == 4">
+                    <el-form-item label="选择一个或多个日期" prop="autoDateTime" label-width="150px">
+                        <el-date-picker size="large" type="dates" style="width: 600px"
+                            v-model="form.autoSopTime.autoDateTime" value-format="yyyy-MM-dd"
+                            placeholder="选择一个或多个日期【ps:请先选择开始时间,不得小于任务开始时间】"
+                            @change="sortSelectedDates(form.autoSopTime.autoDateTime)" :disabled="!form.startTime">
+                        </el-date-picker>
+                    </el-form-item>
+                </div>
+
+
+                <el-alert v-if="form.autoSopTime.autoSopType == 1" title="起始时间-结束时间之内的,当天立即创建自动化,时间之外的 次日创建自动化"
+                    type="warning" style="font-size: 30px; margin-top: 3%;" :closable="false" show-icon>
+                </el-alert>
+                <el-alert v-if="form.autoSopTime.autoSopType == 3" type="warning" style="margin-top: 3%;"
+                    :closable="false" show-icon>
+                    <template #title>
+                        <span style="font-size: 23px; line-height: 1.5;">
+                            此功能用于正课区分营期,可以选择你想要开始营期的具体时间,选择后在此期间添加的用户将会自动进入对应营期。
+                            例如今天1月1日,选择时间是1月1日,1月5日,1月10日。
+                            用户1月1日-1月4日添加(添加上对应标签)则会进入1月5日的营期,1月5日-1月9日添加则会进入1月10日营期,1月5日自动给用户发送第一节科普
+                        </span>
+                    </template>
+                </el-alert>
+                <el-alert v-if="form.autoSopTime.autoSopType == 4" title="" type="warning"
+                    style="font-size: 30px; margin-top: 3%;" :closable="false" show-icon>
+                    <template #title>
+                        <span style="font-size: 23px; line-height: 1.5;">
+                            此功能用于先导课区分营期,可以选择先导课开始营期的具体时间,选择后在此期间添加的用户将会自动进入对应营期。
+                            例如今天1月1日,选择时间是1月1日,1月5日,1月10日。
+                            用户1月1日-1月4日添加(添加上对应标签)则会进入1月1日的营期,1月5日-1月9日添加则会进入1月5日营期,自动给用户匹配上对应应该听到天数的时间节点
+                        </span>
+                    </template>
+                </el-alert>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitAutoSopTimeFrom">确 定</el-button>
+                <el-button @click="cancelAutoSopTime">取 消</el-button>
+            </div>
+        </el-dialog>
+
+        <!--  执行详情  -->
+        <el-drawer :title="sopLogsDialog.title" :visible.sync="sopLogsDialog.open" size="85%"
+            style="font-weight: bolder">
+            <sopLogsDetails ref="sopLogsDetails" :rowDetailFrom="sopLogsDialog.sopLogsForm"></sopLogsDetails>
+        </el-drawer>
+    </div>
+</template>
+
+<script>
+import {
+  addSop,
+  courseList,
+  videoList,
+  delSop,
+  exportSop,
+  getSopVoiceList,
+  listSop,
+  updateAutoSopTime,
+  getTempUserList,
+  updateTemp,
+  updateSop,
+  updateSopStatus,
+  updateStatus
+} from "@/api/app/sop/info";
+import {listSopTemp} from "@/api/app/sop/template";
+import ImageUpload from "@/views/app/sop/info/ImageUpload";
+import sopLogsDetails from '@/views/app/sop/userLogs/sopLogsList.vue'
+import { listTagGroupForUserBindTag, } from "@/api/app/tag/tagGroup";
+
+export default {
+    name: "Sop",
+    components: { ImageUpload, sopLogsDetails },
+    data() {
+        return {
+            miniAppList: [],
+            // 添加以下字段
+            selectTags: [], // 已选择的标签
+            tagGroupList: [], // 标签组列表
+            tagTotal: 0, // 标签总数
+            autoSopTypeOptions: [],
+            // 标签弹窗相关
+            changeTagDialog: {
+                title: "",
+                open: false,
+            },
+            queryTagParams: {
+                pageNum: 1,
+                pageSize: 10,
+                total: 0,
+                name: null,
+                corpId: null,
+            },
+            luckyBagList: [],
+            luckyBag: null,
+            liveList: [],
+            // 存储每一行的展开状态
+            expandedRows: {},
+            //模板查询
+            tempListLoading: false,
+            // 遮罩层
+            loading: false,
+            // 导出遮罩层
+            exportLoading: false,
+            // 选中数组
+            ids: [],
+            //选中的map
+            selectionMap: {},
+            myQwCompanyList: [],
+            tempUser: {
+                open: false,
+                id: null,
+                updateOpen: false,
+                loading: true,
+                list: [],
+                form: {
+                    id: null,
+                    liveTempId: null,
+                    liveTempSendTime: null,
+                },
+            },
+            //客服员工列表
+            companyUserLists: [],
+            videoList: [],
+            uploadUrl: process.env.VUE_APP_BASE_API + "/app/common/uploadOSS",
+            uploadUrlByVoice: process.env.VUE_APP_BASE_API + "/app/common/uploadOSSByHOOKVoice",
+            msgForm: {
+                videoId: null,
+                courseId: null,
+                courseType: null,
+                userIdParam: null,
+                sendType: 1,
+                setting: null,
+                ids: null,
+                sopId: null,
+                startTime: null,
+                chatIds: [],
+                isRegister: 2
+            },
+            msgRules: {},
+            courseList: [],
+            // videoList:[],
+            tags: null,
+            excludeTags: null,
+            // 非单个禁用
+            single: true,
+            setting: [],
+            tagList: [],
+            tempList: [],
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 企微sop表格数据
+            sopList: [],
+            // 弹出层标题
+            title: "",
+            // 是否显示弹出层
+            open: false,
+            companyUserList: [],
+            // 状态字典
+            statusOptions: [],
+            //sop状态
+            sysSopStatus: [],
+
+            autoSopOpen: {
+                title: '',
+                open: false,
+                id: null,
+                isAutoSop: null,
+            },
+            outTimeOpen: {
+                title: '',
+                open: false,
+                id: null,
+                tempId: null,
+                expiryTime: null,
+                minConversionDay: null,
+                maxConversionDay: null,
+                isLiveMgs: 0,
+                liveTempId: null,
+                liveTempSendTime: null,
+                appId: null
+            },
+            voiceForm: {
+                list: [],
+                open: false,
+                total: 0,
+                queryParams: {
+                    pageNum: 1,
+                    pageSize: 10,
+                    id: null,
+                },
+            },
+
+            //SOP发送类型
+            appSopType: [],
+            chatList: [],
+
+            //SOP科普观看状态
+            sysFsSopWatchStatus: [],
+            //插件版
+            sysQwSopAiContentType: [],
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                id: null,
+                name: null,
+                status: null,
+                sendType: null,
+                createTime: null,
+                tagIds: null,
+            },
+            sopLogsDialog: {
+                title: '',
+                open: false,
+                sopLogsForm: [],
+            },
+            // 表单参数
+            form: {
+                autoSopTime: { autoSopType: 2, autoStartTime: '00:00', autoEndTime: '24:00', autoSopSend: 2, autoDateTime: [] },
+            },
+            courseLiveTempOption: [],
+            userSelectList: [],
+            listUser: {
+                title: "",
+                open: false
+            },
+            // 表单校验
+            rules: {
+                name: [{ required: true, message: "名称不能为空", trigger: "submit" }],
+                type: [{ required: true, message: "不能为空", trigger: "submit" }],
+                sendType: [{ required: true, message: "不能为空", trigger: "submit" }],
+                startTime: [{ required: true, message: "开始时间不能为空", trigger: "submit" }],
+                tempId: [{ required: true, message: "模板不能为空", trigger: "submit" }],
+            },
+            autoSopTimeRules: {
+                autoSopType: [{ required: true, message: "选项不能为空", trigger: "submit" }],
+                autoStartTime: [{ required: true, message: "起始时间不能为空", trigger: "submit" }],
+                autoEndTime: [{ required: true, message: "结束时间不能为空", trigger: "submit" }],
+                autoDateTime: [{ required: true, message: "日期不能为空", trigger: "submit" }],
+            }
+        };
+    },
+    created() {
+        this.init();
+    },
+    watch: {
+        userSelectList(newList) {
+            this.form.qwUserIds = newList.map(item => item.id);
+        }
+    },
+    methods: {
+        /**
+         * 初始化加载
+         */
+        async init() {
+            this.getDicts("app_sop_status").then(response => {
+                this.sysSopStatus = response.data;
+            });
+            this.getDicts("app_sop_plugin_settingType").then(response => {
+                this.sysQwSopAiContentType = response.data;
+            });
+            this.getDicts("app_sop_watch_status").then(response => {
+                this.sysFsSopWatchStatus = response.data;
+            });
+            this.getDicts("app_normal_disabled_status").then(response => {
+                this.statusOptions = response.data;
+            });
+            this.getDicts("app_sop_autoType").then(response => {
+                this.autoSopTypeOptions = response.data;
+            });
+            this.getDicts("app_sop_type").then(response => {
+                this.appSopType = response.data;
+            });
+            let cl = this.getCourseList(null);
+            this.courseList = cl.list;
+            this.getList();
+        },
+        /**
+         * 获取科普列表
+         * @param keyword
+         */
+        async getCourseList(keyword) {
+            let query = {
+                keyword: keyword,
+                pageNum: 1,
+                pageSize: 50,
+            };
+            return await courseList(query);
+        },
+        // 搜索的标签
+        hangleChangeTags() {
+            this.changeTagDialog.title = "搜索的标签"
+            this.changeTagDialog.open = true;
+            this.queryTagParams.name = null;
+            this.getPageListTagGroup();
+        },
+
+        // 删除一些选择的标签
+        handleCloseTags(list) {
+            const ls = this.selectTags.findIndex(t => t.id === list.id);
+            if (ls !== -1) {
+                this.selectTags.splice(ls, 1);
+                this.selectTags = [...this.selectTags];
+            }
+
+            if (this.selectTags != null && this.selectTags.length > 0) {
+                // 确保 this.form.tags 是数组
+                if (!this.queryParams.tagIds) {
+                    this.queryParams.tagIds = []; // 如果未定义,初始化
+                } else {
+                    this.queryParams.tagIds = []; // 清空已有数据
+                }
+
+                // 遍历并添加 tagId
+                this.selectTags.forEach(tag => {
+                    if (tag.id) { // 确保 tagId 存在
+                        this.queryParams.tagIds.push(tag.id);
+                    }
+                });
+                this.queryParams.tagIds = this.queryParams.tagIds.join(",");
+            } else {
+                this.queryParams.tagIds = null;
+            }
+        },
+
+        // 选择/取消选择标签
+        tagSelection(row) {
+            row.isSelected = !row.isSelected;
+            this.$forceUpdate();
+        },
+
+        // 确定选择标签
+        tagSubmitForm() {
+            for (let i = 0; i < this.tagGroupList.length; i++) {
+                for (let x = 0; x < this.tagGroupList[i].tags.length; x++) {
+                    if (this.tagGroupList[i].tags[x].isSelected === true) {
+                        if (!this.selectTags) {
+                            this.selectTags = [];
+                        }
+
+                        // 检查当前 tag 是否已经存在于 tagListFormIndex[index] 中
+                        let tagExists = this.selectTags.some(
+                            tag => tag.id === this.tagGroupList[i].tags[x].id
+                        );
+
+                        // 如果 tag 不存在于 tagListFormIndex[index] 中,则新增
+                        if (!tagExists) {
+                            this.selectTags.push(this.tagGroupList[i].tags[x]);
+                        }
+                    }
+                }
+            }
+            if (!this.selectTags || this.selectTags.length === 0) {
+                return this.$message('请选择标签');
+            }
+
+            this.changeTagDialog.open = false;
+        },
+
+        // 取消选择标签
+        tagCancel() {
+            this.changeTagDialog.open = false;
+        },
+
+        // 获取标签组列表
+        getPageListTagGroup() {
+            //   this.queryTagParams.corpId = this.queryParams.corpId
+            let qp = {...this.queryTagParams, groupName: this.queryTagParams.name}
+            listTagGroupForUserBindTag(qp).then(response => {
+                let rows = response.rows;
+                this.tagGroupList = rows;
+                this.tagTotal = response.total;
+            });
+        },
+
+        // 搜索标签
+        handleSearchTags(name) {
+            this.getPageListTagGroup();
+        },
+
+        // 重置标签搜索
+        cancelSearchTags() {
+            this.resetSearchQueryTag()
+            this.getPageListTagGroup();
+        },
+
+        // 重置标签查询参数
+        resetSearchQueryTag() {
+            this.queryTagParams = {
+                pageNum: 1,
+                pageSize: 10,
+                total: 0,
+                name: null,
+            };
+        },
+
+        // 在组件销毁前保存当前选择
+        beforeDestroy() {
+            // if (this.queryParams.corpId) {
+            //     localStorage.setItem('selectedCorpId', this.queryParams.corpId);
+            // }
+        },
+        getLuckyBagStatus(content) {
+            const selectedLuckyBag = this.luckyBagList.find(item => item.id === content.luckyBagId);
+            if (selectedLuckyBag) {
+                content.luckyBagDataStatus = selectedLuckyBag.dataStatus; // 自动填充标题
+            } else {
+                // 若未找到对应直播间,清空标题和封面(可选)
+                content.luckyBagDataStatus = null;
+            }
+        },
+        checkLiveMiniprogramTitle(item) {
+            this.$forceUpdate();
+        },
+        liveChange(content) {
+            // content.liveId 是选中的直播间 ID(liveId)
+            const selectedLive = this.liveList.find(live => live.liveId === content.liveId);
+            if (selectedLive) {
+                // 从选中的直播间对象中提取标题和封面,赋值给当前内容的对应字段
+                // 假设直播间对象中标题字段为 liveTitle,封面字段为 coverImg(根据实际接口字段调整)
+                content.miniprogramTitle = selectedLive.liveName || ''; // 自动填充标题
+                content.miniprogramPicUrl = selectedLive.liveImgUrl || ''; // 自动填充封面
+            } else {
+                // 若未找到对应直播间,清空标题和封面(可选)
+                content.miniprogramTitle = '';
+                content.miniprogramPicUrl = '';
+            }
+        },
+        voice(id) {
+            this.voiceForm.queryParams.id = id;
+            getSopVoiceList(this.voiceForm.queryParams).then(res => {
+                this.voiceForm.list = res.rows;
+                this.voiceForm.total = res.total;
+                this.voiceForm.open = true;
+            })
+        },
+        // 单元格点击事件
+        handleCellClick(row, column, cell, event) {
+            // 判断是否为规则编号列
+            if (column.property === 'id') {
+                this.handleRowClick(row); // 触发规则编号点击事件
+            }
+        },
+        // handleUpdateTags(){
+        //
+        // },
+        // handleUpdateQwUser(){
+        //
+        // },
+        // handleUpdateSopTemp(){
+        //
+        // },
+        handleRowClick(row, column, event) {
+            // 判断状态是否符合条件
+            if (row.status == 2 || row.status == 0 || row.status == 3 || row.status == 4) {
+                this.handleUpdate(row, 2);
+            }
+        },
+        // 切换某一行的展开状态
+        toggleRow(row) {
+            this.$set(this.expandedRows, row.id, !this.expandedRows[row.id]);
+        },
+        getSwitchVal(status) {
+            return [2, 3, 4].includes(status) ? 2 : 0;
+        },
+
+        onSwitchChange(row, val) {
+            this.loading = true;
+            // ① 调用接口更新后端
+            updateSopStatus({ id: row.id, status: val })
+                .then(() => {
+                    row.status = val;
+                    this.loading = false;
+                    this.$message.success("切换成功!");
+                })
+                .catch(() => {
+                    this.loading = false;
+                    this.$message.error("操作失败,请重试");
+                });
+        },
+
+        switchSopStatusChange(row, checked) {
+
+            let loadingRock = this.$loading({
+                lock: true,
+                text: '暂停中-请勿刷新页面-重复点击',
+                spinner: 'el-icon-loading',
+                background: 'rgba(0, 0, 0, 0.7)'
+            });
+
+
+            updateSopStatus({ id: row, status: checked }).then(response => {
+                this.$message.success("修改成功");
+                this.getList();
+            }).finally(res => {
+                loadingRock.close();
+            })
+        },
+        /**
+         * 新客户自动创建sop
+         * @param row
+         * @param checked
+         */
+        switchAutoSopChange(row, checked) {
+            this.form.startTime = row.startTime;
+            this.form.autoSopTime = JSON.parse(row.autoSopTime);
+            if (checked == 1) {
+                this.autoSopOpen.title = '请设置相应时间段'
+                this.autoSopOpen.open = true;
+                this.autoSopOpen.id = row.id;
+                this.autoSopOpen.isAutoSop = 1;
+            } else {
+                this.form.autoSopTime.createTime = this.formatDateTo24HourString(new Date());
+                updateAutoSopTime({
+                    id: row.id,
+                    isAutoSop: checked,
+                    autoSopTime: JSON.stringify(this.form.autoSopTime)
+                }).then(response => {
+                    this.getList();
+                })
+            }
+        },
+        updateCorpId() {
+            if (this.queryParams.corpId) {
+                localStorage.setItem('selectedCorpId', this.queryParams.corpId);
+            }
+            this.reset();
+            this.refreshData(this.queryParams.corpId);
+            this.queryParams.qwUserId = null;
+            this.getList();
+        },
+
+        //刷新部分数据
+        refreshData(row) {
+            //   getQwAllUserList(row).then(response => {
+            //     this.companyUserList = response.data;
+            //   });
+
+            //   listTag({corpId: row}).then(response => {
+            //     this.tagList = response.rows;
+            //   });
+
+            if (row != null) {
+                // allList(row).then(e => {
+                //   this.chatList = e.data;
+                // })
+            }
+        },
+
+        //查询模板
+        selectListSopTemp(type) {
+            this.tempListLoading = true; // 开始查询,显示加载提示
+            listSopTemp({ sendType: type }).then(response => {
+                this.tempList = response.rows;
+                this.tempListLoading = false;
+            });
+        },
+        handlelistUser(type, sendType) {
+
+            setTimeout(() => {
+                this.$refs.QwUserList.getDetails(this.queryParams.corpId, type, sendType);
+            }, 1);
+            this.listUser.title = "选择企业成员"
+            this.listUser.open = true;
+
+        },
+        selectUserList(list) {
+            this.listUser.open = false;
+            list.forEach(obj => {
+                if (!this.userSelectList.some(item => item == obj.id)) {
+                    this.userSelectList.push(obj.id);
+                }
+            });
+
+        },
+
+        //修改过期时间
+        handleUpdateTemp(val) {
+            this.tempUser.updateOpen = true;
+            this.tempUser.form = {
+                id: val.id,
+                qwId: val.qwId,
+                liveTempId: val.tempId,
+                liveTempSendTime: val.sendTime,
+            }
+        },
+        //修改过期时间
+        submitTemp() {
+            if (this.tempUser.form.liveTempId == null) {
+                return this.$message.error("请选择直播模板")
+            }
+            if (this.tempUser.form.liveTempSendTime == null) {
+                return this.$message.error("请选择发送时间")
+            }
+            updateTemp({ id: this.tempUser.form.id, sopId: this.tempUser.id, tempId: this.tempUser.form.liveTempId, sendTime: this.tempUser.form.liveTempSendTime, qwId: this.tempUser.form.qwId }).then(response => {
+                this.msgSuccess("修改成功");
+                this.tempUser.updateOpen = false;
+                this.listTemp();
+            });
+        },
+        handleUpdateOutTime(val) {
+            this.outTimeOpen.title = "修改过期时间/评级";
+            this.outTimeOpen.id = val.id;
+            this.outTimeOpen.tempId = val.tempId;
+            this.outTimeOpen.expiryTime = val.expiryTime;
+            this.outTimeOpen.isRating = val.isRating;
+            this.outTimeOpen.minConversionDay = val.minConversionDay;
+            this.outTimeOpen.maxConversionDay = val.maxConversionDay;
+            this.outTimeOpen.courseDay = val.courseDay;
+            this.outTimeOpen.isSampSend = val.isSampSend;
+            this.outTimeOpen.isLiveMgs = val.isLiveMgs;
+            this.outTimeOpen.liveTempId = val.liveTempId;
+            this.outTimeOpen.liveTempSendTime = val.liveTempSendTime;
+            this.outTimeOpen.appId = val.appId;
+            this.outTimeOpen.open = true;
+        },
+
+        handleUpdateExpiryTime() {
+            updateSop({
+                id: this.outTimeOpen.id,
+                tempId: this.outTimeOpen.tempId,
+                expiryTime: this.outTimeOpen.expiryTime,
+                isRating: this.outTimeOpen.isRating,
+                minConversionDay: this.outTimeOpen.minConversionDay,
+                maxConversionDay: this.outTimeOpen.maxConversionDay,
+                courseDay: this.outTimeOpen.courseDay,
+                isLiveMgs: this.outTimeOpen.isLiveMgs,
+                liveTempId: this.outTimeOpen.liveTempId,
+                liveTempSendTime: this.outTimeOpen.liveTempSendTime,
+                isSampSend: this.outTimeOpen.isSampSend,
+                appId: this.outTimeOpen.appId,
+            }).then(response => {
+                this.msgSuccess("修改成功");
+                this.resetUpdateExpiryTime()
+                this.getList();
+            });
+        },
+        resetUpdateExpiryTime() {
+            this.outTimeOpen = {
+                title: '',
+                open: false,
+                id: null,
+                expiryTime: null,
+                courseDay: null,
+                isLiveMgs: 0,
+                liveTempId: null,
+                liveTempSendTime: null,
+                minConversionDay: null,
+                maxConversionDay: null,
+                isRating: null,
+                isSampSend: null,
+                appId: null
+            }
+        },
+        addContent(index) {
+            // this.setting[index].content.push({type:1})
+            if (this.form.sendType == 2) {
+                this.setting[index].content.push({ type: 1 });
+            } else {
+                if (this.setting[index].content.length < 9) {
+                    this.setting[index].content.push({ type: 1 });
+                } else {
+                    this.$message({
+                        message: '最多只能添加 9 个内容',
+                        type: 'warning'
+                    });
+                }
+            }
+
+        },
+
+        //选择变动时的变动
+        handleSendTypeChange(val) {
+            this.tempList = [];
+            this.form.tempId = null;
+            if (val == 1) {
+                // 遍历 this.setting 数组并清空每个对象的 content 属性
+                this.setting.forEach(item => {
+                    if (item.content.length > 9) {
+                        item.content = item.content.slice(0, 9); // 保留前 9 个元素
+                    }
+                });
+            }
+        },
+        delContent(index, contentIndex) {
+            this.setting[index].content.splice(contentIndex, 1)
+        },
+
+        //添加SOP规则类型
+        handleCommand(command) {
+            this.setting.push({
+                sopType: command,
+                ruleType: null,
+                name: null,
+                type: 1,
+                content: [],
+                isBindUrl: 1,
+                url: null,
+                day: "0",
+                hour: "0",
+                minute: "0",
+                time: ""
+            })
+        },
+
+        //添加科普SOP
+        handleCrouseCommand(command, val) {
+            console.log("command", command)
+            console.log("val", val)
+        },
+
+        delSetting(index) {
+            this.setting.splice(index, 1)
+        },
+        handleClosegroupUser(list) {
+            const index = this.userSelectList.findIndex(t => t === list);
+            if (index !== -1) {
+                this.userSelectList.splice(index, 1);
+            }
+        },
+        /**
+         * 查询sop列表
+         */
+        getList() {
+            this.loading = true;
+            listSop(this.queryParams).then(response => {
+                this.sopList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+                listSopTemp().then(res => {
+                    this.tempList = res.rows;
+                });
+
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                id: null,
+                name: null,
+                status: 1,
+                sendType: 2,
+                type: 2,
+                filterType: 2,
+                isLiveMgs: 0,
+                qwUserIds: null,
+                liveTempId: null,
+                liveTempSendTime: null,
+                corpId: null,
+                setting: null,
+                createBy: null,
+                createTime: null,
+                isAutoSop: null,
+                autoSopTime: { autoSopType: 2, autoStartTime: '00:00', autoEndTime: '24:00', autoSopSend: 2, autoDateTime: [] },
+            };
+            this.resetForm("form");
+            this.tags = null;
+            this.excludeTags = null;
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            // 处理标签ID参数
+            if (this.selectTags != null && this.selectTags.length > 0) {
+                this.queryParams.tagIds = this.selectTags.map(tag => tag.id).join(",");
+            } else {
+                this.queryParams.tagIds = null;
+            }
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm("queryForm");
+            // this.queryParams.corpId = this.myQwCompanyList[0].dictValue;
+            this.selectTags = null;
+            this.refreshData(this.queryParams.corpId);
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map(item => item.id)
+            this.selectionMap = selection;
+            this.single = selection.length !== 1
+            this.multiple = !selection.length
+        },
+        // 新增按钮操作
+        handleAdd() {
+            this.reset();
+            this.$router.push('/app/sop/addSop')
+        },
+        /** 修改按钮操作 */
+        handleUpdate(row, type) {
+            this.$router.push('/app/sop/updateSop/' + row.id + '/' + type)
+        },
+        handleTemp(id) {
+            this.tempUser.open = true;
+            this.tempUser.id = id;
+            this.listTemp();
+        },
+        listTemp() {
+            this.tempUser.loading = true;
+            getTempUserList(this.tempUser.id).then(e => {
+                this.tempUser.list = e.data;
+                this.tempUser.loading = false;
+            })
+        },
+
+        /**
+         * 查看SOP任务内营期
+         */
+        selectSchedule(row) {
+            // type 2:我的sop 1:部门sop/客服公司sop
+            const query = {
+                id: row.id,
+                name: row.name,
+                tempId: row.tempId,
+                filterMode: row.filterMode,
+                // corpId: row.corpId,
+                type: 1,
+            }
+            // 使用 params 传递参数
+            this.$router.push({
+                path: `/app/sop/sopUserLogsSchedule/${query.id}`,
+                query // 如果需要传递更多参数,可以使用 query
+            });
+        },
+
+
+        /**
+         * 查看营期内详情
+         */
+        handleScheduleDetail(row) {
+            this.sopLogsDialog.title = '规则执行详情7';
+            this.sopLogsDialog.open = true;
+            this.sopLogsDialog.sopLogsForm = row;
+            // 使用Vue.set或this.$set添加新字段
+            this.$set(this.sopLogsDialog.sopLogsForm, 'filterSopType', 1);
+        },
+
+        /**
+         * 查看模板
+         */
+        handleQueryDetails(row) {
+            this.$router.push(`/user/sopTemp/updateSopTemp/${row.tempId}/3`)
+        },
+        /** 提交按钮 */
+        submitForm() {
+            this.$refs["form"].validate(valid => {
+                if (valid) {
+                    if (this.userSelectList.length <= 0) {
+                        return this.$message("请选择员工")
+                    }
+                    this.form.qwUserIds = this.userSelectList.join(",");
+                    if (this.tags != null && this.tags.length > 0) {
+                        this.form.tags = (this.tags).toString()
+                    } else {
+                        return this.$message.error("选择的标签不能为空!!请选择筛选的标签")
+                    }
+                    if (this.excludeTags != null) {
+                        this.form.excludeTags = (this.excludeTags).toString()
+                    }
+
+                    this.form.setting = JSON.stringify(this.setting)
+                    if (this.form.id != null) {
+                        updateSop(this.form).then(response => {
+                            this.msgSuccess("修改成功");
+                            this.open = false;
+                            this.getList();
+                        });
+                    } else {
+                        addSop(this.form).then(response => {
+                            this.msgSuccess("新增成功");
+                            this.open = false;
+                            this.getList();
+                        });
+                    }
+                }
+            });
+        },
+        sortSelectedDates(dates) {
+
+            if (!this.form.startTime) {
+                this.$message.error("请先选择开始时间!不得小于任务开始时间");
+                this.form.autoSopTime.autoDateTime = [];
+                return;
+            }
+
+            // 1. 过滤掉 < startTime 的日期
+            const validDates = dates.filter(date => new Date(date) >= new Date(this.form.startTime));
+
+            // 2. 如果有被过滤掉的日期,提示用户
+            if (validDates.length < dates.length) {
+                this.$message.warning(`已自动移除小于 ${this.form.startTime} 的日期!`);
+            }
+
+            // 3. 升序排序
+            validDates.sort((a, b) => new Date(a) - new Date(b));
+
+            // 4. 更新数据
+            this.form.autoSopTime.autoDateTime = validDates;
+        },
+
+        submitAutoSopTimeFrom() {
+
+            if (this.form.autoSopTime.autoSopType == 3 || this.form.autoSopTime.autoSopType == 4) {
+                if (this.form.autoSopTime.autoDateTime == null || this.form.autoSopTime.autoDateTime == "") {
+                    return this.$message.error("选择的日期不能为空!!请选择日期")
+                }
+            }
+
+            this.$refs["autoSopTimeFrom"].validate(valid => {
+                if (valid) {
+                    this.autoSopOpen.open = false
+                    this.form.autoSopTime.createTime = this.formatDateTo24HourString(new Date());
+                    updateAutoSopTime({
+                        id: this.autoSopOpen.id,
+                        isAutoSop: this.autoSopOpen.isAutoSop,
+                        autoSopTime: JSON.stringify(this.form.autoSopTime)
+                    }).then(response => {
+                        this.msgSuccess("修改成功");
+                    });
+                }
+                this.getList();
+                this.reset();
+            });
+        },
+
+        formatDateTo24HourString(date) {
+            let year = date.getFullYear();
+            let month = ('0' + (date.getMonth() + 1)).slice(-2); // 月份需要加 1 并补零
+            let day = ('0' + date.getDate()).slice(-2); // 日需要补零
+            let hours = ('0' + date.getHours()).slice(-2); // 小时需要补零
+            let minutes = ('0' + date.getMinutes()).slice(-2); // 分钟需要补零
+            let seconds = ('0' + date.getSeconds()).slice(-2); // 秒需要补零
+
+            return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+        },
+        cancelAutoSopTime() {
+            this.autoSopOpen.open = false
+            this.getList();
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const ids = row.id || this.ids;
+            this.$confirm('是否确认删除企微自动化编号为"' + ids + '"的数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(function () {
+                return delSop(ids);
+            }).then(() => {
+                this.getList();
+                this.msgSuccess("删除成功");
+            }).catch(() => {
+            });
+        },
+        /**
+         * 批量执行SOP任务
+         */
+        handleExecute() {
+            const ids = this.selectionMap
+                .filter(item => item.status === 1)
+                .map(item => item.id);
+            if (ids.length === 0) {
+                this.msgError("选择的任务已经执行或正在执行,请选择待执行的任务");
+                return;
+            }
+            this.$confirm('是否确认立即执行自动化编号为"' + ids + '"的数据项?', "警告【只能立即执行(待执行的)】", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(() => {
+                this.loading = true;
+                // 执行更新操作
+                return updateStatus(ids); // 返回一个Promise,直接在此链式调用
+            }).then(res => {
+                let msg = "";
+
+                // 判断成功和失败的任务,并显示消息
+                if (res.suc.length > 0) {
+                    msg += "执行成功的自动化任务【" + res.suc.join(", ") + "】。<br>"; // 优化:将数组转换为字符串
+                }
+                if (res.err.length > 0) {
+                    msg += "失败的自动化任务【" + res.err.join(", ") + "】,原因是已经执行或正在执行。<br>";
+                }
+
+                // 显示确认框,显示成功与失败信息
+                return this.$confirm(msg, "提示", {
+                    confirmButtonText: "确定",
+                    cancelButtonText: "取消",
+                    type: "warning",
+                    dangerouslyUseHTMLString: true // 允许HTML标签
+                });
+
+            }).then(() => {
+                // 操作完成后刷新列表,并显示成功信息
+                this.getList();
+                this.msgSuccess("执行完成");
+            }).catch(() => {
+                // 处理任何异常,操作取消或者失败时显示提示信息
+                this.msgError("操作失败,请重试");
+            }).finally(() => {
+                this.loading = false; // 操作完成后关闭加载状态
+            });
+
+        },
+        /** 导出按钮操作 */
+        handleExport() {
+            const queryParams = this.queryParams;
+            this.$confirm('是否确认导出所有企微自动化数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(() => {
+                this.exportLoading = true;
+                return exportSop(queryParams);
+            }).then(response => {
+                this.download(response.msg);
+                this.exportLoading = false;
+            }).catch(() => {
+            });
+        },
+        courseChange() {
+            if (this.msgForm.courseId != null) {
+                const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === this.msgForm.courseId);
+                for (let i = 0; i < this.setting.length; i++) {
+                    //响应式直接给链接的标题/封面上值
+                    if (selectedCourse && this.setting[i].contentType == 3 && this.msgForm.courseId != null) {
+                        this.$set(this.setting[i], 'linkTitle', selectedCourse.dictLabel);
+                        this.$set(this.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+                    }
+
+                }
+
+            }
+            videoList(this.msgForm.courseId).then(response => {
+                this.videoList = response.list;
+            });
+        },
+        handleContentTypeChange() {
+
+            //如果是链接的才上
+            if (this.msgForm.courseId != null) {
+                const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === this.msgForm.courseId);
+                for (let i = 0; i < this.setting.length; i++) {
+                    //响应式直接给链接的标题/封面上值
+                    if (selectedCourse && this.msgForm.courseId != null) {
+                        console.log(1, this.setting[i].contentType)
+                        if (this.setting[i].contentType == 3 || this.setting[i].contentType == 9) {
+                            this.$set(this.setting[i], 'linkTitle', selectedCourse.dictLabel);
+                            this.$set(this.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+                        }
+                        if (this.setting[i].contentType == 4) {
+                            this.$set(this.setting[i], 'miniprogramPicUrl', selectedCourse.dictImgUrl);
+                        }
+
+
+                    }
+
+                }
+
+            }
+            if (this.msgForm.videoId != null) {
+                // 查找选中的课节对应的 label
+                const selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === this.msgForm.videoId);
+
+                for (let i = 0; i < this.setting.length; i++) {
+                    //响应式直接给链接的描述上值
+                    if (selectedVideo && this.msgForm.videoId != null) {
+                        console.log(2, this.setting[i].contentType)
+                        if (this.setting[i].contentType == 3 || this.setting[i].contentType == 9) {
+                            this.$set(this.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+                        }
+                        if (this.setting[i].contentType == 4) {
+                            this.$set(this.setting[i], 'miniprogramTitle', selectedVideo.dictLabel);
+                        }
+
+                    }
+                }
+            }
+
+
+        },
+        handleAvatarSuccessFile(res, file, item) {
+            if (res.code === 200) {
+                // 使用 $set 确保响应式更新
+                this.$set(item, '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, item) {
+            if (res.code == 200) {
+                // 使用 $set 确保响应式更新
+                this.$set(item, '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;
+        },
+
+        handleInputVideoText(value, content) {
+            // 允许的字符:中文、英文(大小写)、数字和指定标点符号(,。!?)
+            const regex = /^[\u4e00-\u9fa5,。!?,!?]+$/;
+
+            // 删除不符合条件的字符
+            const filteredValue = value.split('').filter(char => regex.test(char)).join('');
+
+            this.$set(content, 'value', filteredValue);
+
+        },
+        delSetList(index) {
+            this.setting.splice(index, 1)
+        },
+        addSetList() {
+            const newSetting = {
+                contentType: '1',
+                value: '',
+            };
+            // 将新设置项添加到 content.setting 数组中
+            this.setting.push(newSetting);
+
+        },
+        isEmpty(obj) {
+            if (null === obj || undefined === obj || 0 === obj.length) {
+                return true;
+            }
+            return false;
+        },
+        videoIdChange() {
+            if (this.msgForm.videoId != null) {
+                // 查找选中的课节对应的 label
+                const selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === this.msgForm.videoId);
+
+                for (let i = 0; i < this.setting.length; i++) {
+                    //响应式直接给链接的描述上值
+                    if (selectedVideo && this.msgForm.videoId != null) {
+                        if (this.setting[i].contentType == 3 || this.setting[i].contentType == 9) {
+                            this.$set(this.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+                        }
+
+                        if (this.setting[i].contentType == 4) {
+                            this.$set(this.setting[i], 'miniprogramTitle', selectedVideo.dictLabel);
+                        }
+
+
+                    }
+                }
+            }
+        },
+    }
+}
+;
+</script>
+<style scoped>
+.custom-input ::v-deep  .el-input__inner {
+  height: 20px;
+  padding: 0 4px;
+  text-align: center;
+  display: block;
+}
+
+.custom-input ::v-deep  .el-input__icon {
+  line-height: 20px;
+}
+
+.el-button--text {
+  cursor: pointer;
+}
+
+/* CSS 样式 */
+.tag-container {
+  display: flex;
+  flex-wrap: wrap; /* 超出宽度时自动换行 */
+  gap: 8px; /* 设置标签之间的间距 */
+}
+
+.name-background {
+  display: inline-block;
+  background-color: #abece6; /* 背景颜色 */
+  padding: 4px 8px; /* 调整内边距,让背景包裹文字 */
+  border-radius: 4px; /* 可选:设置圆角 */
+}
+
+.tag-box {
+  padding: 8px 12px;
+  border: 1px solid #989797;
+  border-radius: 4px;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.tag-selected {
+  background-color: #00bc98;
+  color: #fff;
+  border-color: #00bc98;
+}
+</style>

+ 624 - 0
src/views/app/sop/logs/index.vue

@@ -0,0 +1,624 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="120px">
+      <el-form-item label="企微员工账号" prop="qwUserid" v-if="queryParams.type==2">
+        <el-input
+          v-model="queryParams.qwUserid"
+          placeholder="请输入企微员工账号"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="企微员工昵称" prop="qwUserName" v-if="queryParams.type==2">
+        <el-input
+          v-model="queryParams.qwUserName"
+          placeholder="请输入企微员工昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="客服昵称" prop="qwUserName" v-if="queryParams.type==1">
+        <el-input
+          v-model="queryParams.qwUserName"
+          placeholder="请输入客服昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="客户昵称" prop="externalUserName">
+        <el-input
+          v-model="queryParams.externalUserName"
+          placeholder="请输入客户昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="发送类型" prop="sendType" >
+        <el-select v-model="queryParams.sendType" placeholder="请选择发送类型" clearable size="small"   @change="handleQuery">
+          <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="sendStatus">
+        <el-select v-model="queryParams.sendStatus" placeholder="请选择发送(成员)状态" clearable size="small">
+          <el-option
+            v-for="dict in sysQwSopLogsStatus"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="预计发送时间" prop="scheduleTime">
+<!--        <el-date-picker clearable size="small"-->
+<!--          v-model="queryParams.sendTime"-->
+<!--          type="datetime"-->
+<!--          value-format="yyyy-MM-dd HH:mm:ss"-->
+<!--          placeholder="选择预计发送时间">-->
+<!--        </el-date-picker>-->
+        <el-date-picker
+          clearable size="small"
+          v-model="scheduleTime"
+          type="datetimerange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          placeholder="选择预计发送时间"
+          @change="handleScheduleTimeChange"
+        >
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="接收(客户)状态" prop="receivingStatus">
+        <el-select v-model="queryParams.receivingStatus" placeholder="请选择接收(客户)状态" clearable size="small">
+          <el-option
+            v-for="dict in groupMsgSendStatusOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['qw:sopLogs:export']"
+        >导出</el-button>
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['qw:sopLogs:remove']"
+        >批量删除</el-button>
+
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="multiple"
+          @click="handleEditCourse"
+          v-hasPermi="['qw:sopLogs:editCourse']"
+        >再次发送记录</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="qwSopLogsList"  @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" prop="sendType" width="80">
+        <template slot-scope="scope">
+          <dict-tag :options="sysQwSopType" :value="scope.row.sendType"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="企微成员账号" align="center" prop="qwUserid" />
+      <el-table-column label="企微成员昵称" align="center" prop="qwUserName" v-if="queryParams.type==2" />
+<!--      <el-table-column label="系统后台昵称" align="center" prop="userName" />-->
+      <el-table-column label="客户昵称" align="center" prop="externalUserName" />
+      <el-table-column label="发送的消息" align="center" prop="contentJson" >
+        <template slot-scope="scope">
+          <el-button type="text" @click="showContentDialog(scope.row.contentJson)">
+            查看详情
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="成员状态" align="center" prop="sendStatus" >
+        <template slot-scope="scope">
+          <dict-tag :options="sysQwSopLogsStatus" :value="scope.row.sendStatus"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="预计发送时间" align="center" prop="sendTime" width="180"/>
+      <el-table-column label="实际发送时间" align="center" prop="realSendTime" width="180"/>
+<!--      <el-table-column label="获取记录" align="center" prop="takeRecords" width="180">-->
+<!--        <template slot-scope="scope">-->
+<!--          <span v-if="scope.row.takeRecords == 0" :style="{ color: 'red' }" >未获取</span>-->
+<!--          <span v-else-if="scope.row.takeRecords == 1" :style="{ color: 'green' }" >已获取</span>-->
+<!--          <span v-else :style="{ color: 'gray' }" >未知状态</span>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+      <el-table-column label="客户" align="center" prop="receivingStatus" >
+        <template slot-scope="scope">
+          <dict-tag :options="groupMsgSendStatusOptions" :value="scope.row.receivingStatus"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="生成时间" align="createTime" prop="createTime" width="180" />
+      <el-table-column label="消息ID" align="center" prop="msgId" />
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="APP备注" align="center" prop="appSendRemark" />
+    </el-table>
+
+    <el-dialog :visible.sync="contentDialog.isDialogVisible" title="消息详情" width="30%" append-to-body>
+      <div>
+        <span>课节:{{contentDialog.videoId}}</span>
+        <div v-for="(item, index) in contentDialog.json || []" :key="index">
+          <el-card class="box-card" style="margin-top: 2%">
+            <div slot="header" class="clearfix" style="display: flex;justify-content: space-between;align-items: center;">
+              <div>
+                <span>类型:</span>
+                <span v-if="item.contentType == 1">文本</span>
+                <span v-if="item.contentType == 2">图片</span>
+                <span v-if="item.contentType == 3">卡片</span>
+                <span v-if="item.contentType == 4">小程序</span>
+                <span v-if="item.contentType == 5">文件</span>
+                <span v-if="item.contentType == 6">视频</span>
+                <span v-if="item.contentType == 7">语音</span>
+                <span v-if="item.contentType == 8">视频号</span>
+                <span v-if="item.contentType == 9">APP</span>
+                <span v-if="item.contentType == 11">APP文本</span>
+                <span v-if="item.contentType == 12">APP语音</span>
+                <span v-if="item.contentType == 13">注册</span>
+                <span v-if="item.contentType == 14">福袋</span>
+                <span v-if="item.contentType == 15">浏览器学习</span>
+              </div>
+              <div v-if="item.sendStatus">
+                <span>发送状态:</span>
+                <el-tag type="success" v-if="item.sendStatus == 1">发送成功</el-tag>
+                <el-tag type="danger" v-if="item.sendStatus == 2">发送失败</el-tag>
+              </div>
+            </div>
+            <div v-if="item.sendStatus && item.sendStatus == 2">发送失败原因:<span style="color: #ff4949">{{item.sendRemarks}}</span></div>
+            <div v-if="item.contentType == 1" v-html="item.value"></div>
+            <div v-if="item.contentType == 2">
+              <el-image
+                style="width: 100px; height: 100px"
+                :src="item.imgUrl"
+                fit="contain"
+                @click.prevent="openImageViewer(item.imgUrl)" />
+            </div>
+            <div v-if="item.contentType == 3 || item.contentType == 9" class="message-style">
+              <div
+                style="background-color: white; padding: 10px; width: 100%"
+                @click="openLink(item.linkUrl)">
+                <span>{{ item.linkTitle }}</span>
+                <div style="display: flex; justify-content: space-between; width: 100%">
+                  <span style="font-size: 13px; flex: 1">{{ item.linkDescribe }}</span>
+                  <!-- 点击图片时停止事件冒泡 -->
+                  <el-image
+                    style="width: 50px; height: 50px; flex-shrink: 0"
+                    :src="item.linkImageUrl"
+                    fit="contain"
+                    @click.stop="openImageViewer(item.linkImageUrl)" />
+                </div>
+              </div>
+            </div>
+            <div v-if="item.contentType == 4 || item.contentType == 15" class="message-style">
+                <div style="display: flex; justify-content: space-between; width: 100%">
+                  <span style="font-size: 13px; flex: 1">{{ item.miniprogramTitle }}</span>
+                  <el-image
+                    style="width: 50px; height: 50px; flex-shrink: 0"
+                    :src="item.miniprogramPicUrl"
+                    fit="contain"
+                    @click.stop="openImageViewer(item.miniprogramPicUrl)" />
+                </div>
+            </div>
+            <div v-if="item.contentType == 5" class="message-style">
+              <el-link type="primary" :href="item.fileUrl" download style="padding: 10px">
+                {{ item.fileUrl }}
+              </el-link>
+            </div>
+            <div v-if="item.contentType == 6" class="message-style">
+              <video
+                :src="item.videoUrl"
+                controls
+                style="width: 200px; height: 100px">
+              </video>
+            </div>
+            <div v-if="item.contentType == 7">
+              <span>
+                {{ item.value }}
+              </span>
+              <div v-if="!item.voiceUrl" style="margin-top: 3px"  >
+                <span style="color: red">无语音地址</span>
+              </div>
+            </div>
+
+            <div v-if="item.contentType == 8" class="message-style">
+              <el-card class="box-card" v-if="item.coverUrl">
+                <el-form-item label="封面标题:"  label-width="100px">
+                  <el-input v-model="item.nickname" style="width: 90%;margin-bottom: 1%" disabled/>
+                </el-form-item>
+                <el-form-item label="头像:"   label-width="100px" >
+                  <el-image
+                    v-if="item.avatar != null"
+                    :src="item.avatar"
+                    :preview-src-list="[item.avatar]"
+                    :style="{ width: '50px', height: '50px' }"
+                  ></el-image>
+                </el-form-item>
+                <el-form-item label="封面:"   label-width="100px" >
+                  <el-image
+                    v-if="item.coverUrl != null"
+                    :src="item.coverUrl"
+                    :preview-src-list="[item.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="item.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="item.url" style="width: 90%;" disabled />
+                </el-form-item >
+              </el-card>
+            </div>
+
+          </el-card>
+        </div>
+      </div>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="contentDialog.isDialogVisible = false">关闭</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 大图预览对话框 -->
+    <el-dialog
+      :visible.sync="dialogVisible"
+      :modal="false"
+      width="500"
+      append-to-body>
+      <img
+        :src="this.dialogImageUrl"
+        style="display: block; max-width: 100%; margin: 0 auto"
+      />
+    </el-dialog>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import {listQwSopLogs, exportQwSopLogs, listQwSopLogsList, delQwSopLogs, editCourseQwSopLogs} from '@/api/qw/sopLogs'
+import {delSopUserLogsInfo} from "@/api/qw/sopUserLogsInfo";
+
+export default {
+  name: "sopLogsList",
+  props:{
+    rowDetailFrom:{},
+  },
+
+  watch:{
+    rowDetailFrom:{
+      handler(newVal){
+        // 当formData变化时重新查询
+        this.getList(newVal);
+      },
+      deep: true
+    }
+  },
+  data() {
+    return {
+
+      //图片放大
+      dialogVisible: false,
+      dialogImageUrl:null,
+      //时间选择
+      scheduleTime: null,
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 企业微信SOP  定时任务表格数据
+      qwSopLogsList: [],
+
+      //成员状态
+      sysQwSopLogsStatus:[],
+
+      //客户接收状态
+      groupMsgSendStatusOptions:[],
+
+      //企微SOP发送类型
+      sysQwSopType: [],
+
+      //发送的消息
+      contentDialog:{
+        isDialogVisible:false,
+        json: [],
+        videoId:null,
+      },
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        qwUserName: null,
+        qwUserid: null,
+        externalUserName: null,
+        sendStatus: null,
+        sendTime: null,
+        corpId:null,
+        receivingStatus: null,
+        sendType: null,
+        type:null,
+        scheduleEndTime:null,
+        scheduleStartTime:null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    this.getList(this.rowDetailFrom);
+
+    //成员状态
+    this.getDicts("sys_qw_sopLogs_status").then(response => {
+      this.sysQwSopLogsStatus = response.data;
+    });
+
+    //客户接收状态
+    this.getDicts("sys_qw_groupMsg_SendStatus").then(response => {
+      this.groupMsgSendStatusOptions = response.data;
+    });
+
+    //发送消息类型
+    this.getDicts("sys_qw_sop_course_type").then(response => {
+      this.sysQwSopType = response.data;
+    });
+
+
+  },
+  methods: {
+    /** 查询企业微信SOP  定时任务列表 */
+    getList(val) {
+
+      this.queryParams.sopId = val.id || this.rowDetailFrom.id;
+      this.queryParams.corpId= val.corpId || this.rowDetailFrom.corpId;
+      this.queryParams.type= val.type || this.rowDetailFrom.type;
+      this.queryParams.filterSopType=val.filterSopType || this.rowDetailFrom.filterSopType;
+      this.loading = true;
+
+      listQwSopLogsList(this.queryParams).then(response => {
+
+        this.qwSopLogsList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+
+    handleScheduleTimeChange(val) {
+      if (val) {
+        this.queryParams.scheduleStartTime = this.formatDateTime(val[0]);
+        this.queryParams.scheduleEndTime = this.formatDateTime(val[1]);
+      } else {
+        this.queryParams.scheduleStartTime = null;
+        this.queryParams.scheduleEndTime = null;
+      }
+    },
+
+    // 格式化日期为 yyyy-MM-dd HH:mm:ss 的北京时间
+    formatDateTime(date) {
+      if (!date) return null;
+
+      // 创建国际化时间格式,指定为 Asia/Shanghai 时区
+      const options = {
+        timeZone: 'Asia/Shanghai',
+        year: 'numeric',
+        month: '2-digit',
+        day: '2-digit',
+        hour: '2-digit',
+        minute: '2-digit',
+        second: '2-digit',
+        hour12: false, // 24小时制
+      };
+
+      // 获取格式化的日期字符串(如 2025-01-23, 16:00:00)
+      const formattedDate = new Intl.DateTimeFormat('zh-CN', options).format(new Date(date));
+
+      // 转换为 yyyy-MM-dd HH:mm:ss 格式
+      const [datePart, timePart] = formattedDate.replace(',', '').split(' ');
+      return `${datePart} ${timePart}`;
+    },
+
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        qwUserName: null,
+        externalUserName: null,
+        logType: null,
+        contentJson: null,
+        sendStatus: null,
+        sendTime: null,
+        companyId: null,
+        receivingStatus: null,
+        msgId: null,
+        sendType: null,
+        sopId: null
+      };
+      this.resetForm("form");
+    },
+
+    openImageViewer(url) {
+      // 打开大图预览对话框
+      this.dialogImageUrl=url
+      this.dialogVisible = true;
+    },
+    openLink(url){
+      // 使用 window.open 打开链接
+      window.open(url, '_blank');
+    },
+    //查看发送的消息体
+    showContentDialog(contentJson) {
+
+      // 解析 JSON 字符串为 JavaScript 对象
+      // 替换非法换行符等控制字符
+      const sanitizedJson = contentJson.replace(/[\u0000-\u001F\u007F]/g, '');
+      const parsedData = JSON.parse(sanitizedJson);
+      console.log(parsedData);
+      this.contentDialog.json = parsedData.setting;
+      this.contentDialog.videoId = parsedData.videoId;
+
+      this.contentDialog.isDialogVisible = true;
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList(this.rowDetailFrom);
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.scheduleStartTime=null;
+      this.queryParams.scheduleEndTime=null;
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除【执行详情】编号为"【' + ids + '】"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return delQwSopLogs(ids);
+      }).then(() => {
+        this.getList(this.rowDetailFrom);
+        this.msgSuccess("删除成功");
+      }).catch(() => {});
+    },
+
+    /**
+    * 批量修改完课的发送
+    */
+    handleEditCourse(row){
+
+      // 获取原始ids(确保是数组)
+      let ids = row.id || this.ids;
+      if (!Array.isArray(ids)) {
+        ids = [ids];
+      }
+
+      // 过滤掉remark为'E级客户不发送'的项
+      const filteredIds = ids.filter(id => {
+        const item = this.qwSopLogsList.find(item => item.id === id);
+        return item?.remark !== 'E级客户不发送';
+      });
+
+      // 如果没有可发送的记录
+      if (filteredIds.length === 0) {
+        this.msgWarning("没有可发送的记录(E级客户已自动排除)");
+        return;
+      }
+
+      this.$confirm('确认要再次发送【执行详情】编号为"【' + filteredIds.join(',') + '】"的【完课】记录?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return editCourseQwSopLogs(filteredIds);
+      }).then(() => {
+        this.getList(this.rowDetailFrom);
+        this.msgSuccess("已经 修改记录为 待发送~");
+      }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有企业微信自动化  定时任务数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportQwSopLogs(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>
+<style scoped>
+.message-stayle{
+  display: flex;
+  justify-content: normal;
+  align-items: center;
+  margin-top: 10px;
+}
+.message-stayle .el-link {
+  white-space: normal; /* 允许换行 */
+  word-break: break-all; /* 单词中间断行 */
+  overflow-wrap: break-word; /* 允许在单词内换行 */
+}
+.message-stayle span {
+  word-break: break-all;
+}
+</style>

+ 579 - 0
src/views/app/sop/template/index.vue

@@ -0,0 +1,579 @@
+<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 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-dropdown
+          @command="handleCommand"
+          trigger="click"
+          placement="bottom-start"
+        >
+          <el-dropdown-menu slot="dropdown" style="width: 140px;">
+            <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"
+                v-hasPermi="['app:template:add']"
+              >
+                新增模板
+              </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="['app:template:delete']"
+        >删除</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="['app:template: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" 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="['app:template:copy']"
+          >复制模板</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-share"
+            @click="shareTemplate(scope.row)"
+            v-hasPermi="['app:template:share']"
+          >分享模板</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-s-promotion"
+            @click="handleQueryDetails(scope.row)"
+          >详情</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['app:template:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate2(scope.row)"
+            v-hasPermi="['app:template:rule']"
+          >管理规则</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="updateImageFun(scope.row.id)"
+            v-hasPermi="['app:template:updateImage']"
+          >更新图片</el-button>
+          <el-button
+            :disabled="scope.row.status == 0"
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['app:template:stop']"
+          >停用</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="状态">
+          <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" v-show="false">
+          <el-input-number v-model="form.gap"  :min="1" label="间隔天数"></el-input-number>
+          <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+            <i class="el-icon-info"></i>
+            设置间隔几天发送sop任务模板内容。比如 设置间隔2天。则第一天,第三天,第五天时 才发送科普模板中的第一天,第二天,第三天的内容。(默认建议1天,好精准匹配不迷糊)
+          </div>
+        </el-form-item>
+        <el-form-item label="排序" prop="sort" v-show="false">
+          <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>
+    </el-dialog>
+    
+  </div>
+</template>
+
+<script>
+import {
+  listSopTemp,
+  getSopTemp,
+  delSopTemp,
+  updateTemp,
+  copyTemplate,
+  addTemp,
+  updateImage,
+  exportSopTemp,
+  shareSopTemp
+} from "@/api/app/sop/template";
+export default {
+  name: "sopTemplate",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      companysloading: false,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      //选中的公司
+      companys:[],
+      deptOptions: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      companyTotal: 0,
+      total:0,
+      sendType:0,
+      // sop模板表格数据
+      sopTempList: [],
+      sysQwSopType:[],
+      companyList:[],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      command: 0,
+      // 状态字典
+      statusOptions: [],
+
+      shareOptions:{
+        title:'分享模板',
+        open:false,
+        templateId:null,
+      },
+      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' }
+        ],
+        // createByDept:[
+        //   { required: true, message: '归属部门不能为空', trigger: 'blur' }
+        // ]
+      },
+      contentRules:{
+        time:[{ required: true, message: '时间不能为空', trigger: 'blur' }],
+      }
+    };
+  },
+  created() {
+    this.getList();
+
+    // this.getDeptTreeSelect();
+    this.getDicts("app_normal_disabled_status").then(response => {
+      this.statusOptions = response.data;
+    });
+
+    // this.getCompanyList();
+
+    this.getDicts("app_sop_type").then(response => {
+      this.sysQwSopType = response.data;
+    });
+  },
+  methods: {
+    updateImage,
+    handleCompanyQuery(){
+      this.queryCompanyParams.pageNum = 1;
+      this.getCompanyList();
+    },
+    resetCompanyQuery(){
+      this.resetForm("queryCompanyForm");
+      this.handleCompanyQuery();
+    },
+
+    getDeptTreeSelect() {
+      treeselect().then((response) => {
+        this.deptOptions = response.data;
+      });
+    },
+
+    /** 查询企业列表 */
+    getCompanyList() {
+      this.companysloading = true;
+      listCompany(this.queryCompanyParams).then(response => {
+        this.companyList = response.rows;
+        this.companyTotal = response.total;
+        this.companysloading = false;
+      });
+    },
+    /** 查询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,
+        status: "1",
+        createByDept:null,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+
+    updateImageFun(id){
+
+      let loadingRock = this.$loading({
+        lock: true,
+        text: '正在执行中请稍后~~请不要刷新页面!!',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)'
+      });
+
+      updateImage(id).then(response => {
+        this.getList();
+        this.$message.success("更新图片 完成!");
+      }).finally(res=>{
+        loadingRock.close();
+      })
+
+
+    },
+    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(`/user/sopTemp/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{
+          this.$router.push(`/user/sopTemp/updateSopTemp/${row.id}/1`)
+        // }
+
+    },
+
+    /** 分享模板 */
+    shareTemplate(row) {
+      return this.msgError('该功能暂未开通!');
+      // 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) {
+          if(this.command == 2){
+            copyTemplate(this.form).then(response => {
+              this.msgSuccess("复制成功");
+              this.open = false;
+              this.getList();
+            });
+          }else{
+            if (this.form.id != null) {
+              updateTemp(this.form).then(response => {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              });
+            } else {
+              addTemp(this.form).then(response => {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                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(() => {});
+    }
+  }
+};
+</script>
+<style lang="scss" scoped>
+  ::v-deep  .el-dialog__footer {
+    height: 55px;
+  }
+</style>

+ 171 - 0
src/views/app/sop/template/sopTemp.vue

@@ -0,0 +1,171 @@
+<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="status">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status == 1" type="success">启用</el-tag>
+          <el-tag v-else 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)"
+          >选择此模板</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/app/sop/template";
+
+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,
+      // 状态字典
+      statusOptions: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        sendType:null,
+        status:1,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+
+    this.getDicts("app_normal_disabled_status").then(response => {
+      this.statusOptions = response.data;
+    });
+
+    this.getDicts("app_sop_type").then(response => {
+      this.sysQwSopType = response.data;
+    });
+  },
+  methods: {
+    /** 查询sop模板列表 */
+    getList(sendType,isUpdate) {
+
+      this.isUpdateSop = isUpdate;
+      this.loading = true;
+      this.queryParams.sendType= sendType;
+      this.queryParams.status=1;
+      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){
+
+        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>

+ 3094 - 0
src/views/app/sop/template/updateSopTemp.vue

@@ -0,0 +1,3094 @@
+<template>
+  <div class="app-container">
+
+    <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="状态">
+          <dict-tag
+            :options="statusOptions"
+            :value="form.status"
+          />
+        </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='addDay()'
+            v-if="formType != 3 && (form.sendType != 4 || days.length < 1)"
+          >
+            添加天数
+          </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-tabs
+            v-model="tabIndex"
+            type="card"
+            @tab-remove="delDay"
+            v-if="days && days.length > 0"
+            :before-leave="leave"
+            @tab-click="tabClick"
+          >
+            <el-tab-pane
+              v-for="(item, dayIndex) in days"
+              :closable="formType != 3"
+              :key="dayIndex"
+              :name="dayIndex + ''"
+            >
+              <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 label="内容名称"
+                        v-if="form.sendType != 4"
+                        style="height: 50px;"
+                      >
+                        <el-input
+                          :disabled="formType == 3"
+                          v-model="item.name"
+                          placeholder="内容名称,仅内部可见"
+                        />
+                      </el-form-item>
+
+                      <!-- 具体规则 -->
+                      <el-form-item label="规则">
+                        <div
+                          v-for="(ruleItem, ruleIndex) in item.rules"
+                          :key="ruleIndex"
+                          style="
+                            background-color: #fdfdfd;
+                            padding: 15px;
+                            border: 1px solid #e6e6e6;
+                            margin-bottom: 20px;
+                          "
+                        >
+                          <el-row>
+                            <el-col
+                              :span="22"
+                            >
+                              <el-form
+                                :model="ruleItem"
+                                label-width="70px"
+                              >
+                                <!-- 第一天 -->
+                                <div v-if="item.dayNum == 1">
+                                  <el-form-item
+                                    label="时间"
+                                  >
+                                    <el-time-picker
+                                      :disabled="formType == 3"
+                                      class="custom-input"
+                                      v-model="ruleItem.time"
+                                      value-format="HH:mm"
+                                      format="HH:mm"
+                                      :picker-options="{ selectableRange: ['01:00:00 - 01:30:00','05:00:00 - 23:59:59'] }"
+                                      placeholder="时间"
+                                      style="width: 100px;height: 20px;"
+                                    />
+                                  </el-form-item>
+                                </div>
+
+                                <!-- 其他天 -->
+                                <div v-else>
+                                  <el-form-item
+                                    label="时间"
+                                  >
+                                    <el-time-picker
+                                      :disabled="formType == 3"
+                                      class="custom-input"
+                                      v-model="ruleItem.time"
+                                      value-format="HH:mm"
+                                      format="HH:mm"
+                                      :picker-options="{ selectableRange: ['00:01:00 - 00:30:00','05:00:00 - 23:59:59']}"
+                                      placeholder="时间"
+                                      style="width: 100px;height: 20px;"
+                                    />
+                                  </el-form-item>
+                                </div>
+
+                                <!-- 填写条件:第一条,且消息类型为科普 -->
+                                <el-form-item label="官方群发"
+                                  v-if="ruleIndex == 0 && ruleItem.type == 2"
+                                >
+                                  <el-switch
+                                    v-model="ruleItem.isOfficial"
+                                    active-color="#13ce66"
+                                    inactive-color="#DCDFE6"
+                                    :active-value="ruleIndex === 0 ? 1 : 0"
+                                    :inactive-value="0"
+                                    @change="isOfficialChange(ruleItem)"
+                                    :disabled="ruleIndex !== 0"
+                                  >
+                                  </el-switch>
+                                  <div
+                                    style="
+                                      color: #999;
+                                      font-size: 14px;
+                                      display: flex;
+                                      align-items: center;
+                                      margin-bottom: 5px
+                                    "
+                                  >
+                                    <i class="el-icon-info"></i>
+                                      请注意:官方群发 【第1天】 只能设置1点-1点30 的时间,【其他天数时间】 只能设置0点-0点30。此处仅为生成发送记录时间!<!-- 实际发送可由 客服在公司规定的【暂早上7点】,在企业微信的【群发助手】处点击发送 -->
+                                  </div>
+                                </el-form-item>
+
+                                <!-- 禁用条件:操作为查看或内容为官方群发 -->
+                                <el-form-item label="消息类别">
+                                  <el-radio-group
+                                    v-model="ruleItem.type"
+                                    :disabled="formType == 3 || ruleItem.isOfficial == '1'"
+                                  >
+                                    <div>
+                                      <el-radio
+                                        v-for="dict in msgTypeOptions"
+                                        :key="dict.dictValue"
+                                        :label="Number(dict.dictValue)"
+                                      >
+                                        {{ dict.dictLabel }}
+                                      </el-radio>
+                                    </div>
+                                  </el-radio-group>
+                                </el-form-item>
+
+                                <el-form-item label="科普"
+                                  v-if="ruleItem.type == 2"
+                                  required
+                                >
+                                  <!-- 禁用条件:操作类型为查看 -->
+                                  <el-select
+                                    :disabled="formType == 3"
+                                    v-model="ruleItem.courseId"
+                                    placeholder="默认50条,可输入关键字检索"
+                                    style=" margin-right: 10px; width: 230px;"
+                                    size="mini"
+                                    filterable
+                                    clearable
+                                    :loading="ruleItem.courseLoading"
+                                    remote
+                                    :remote-method="(keyword) => handleCourseRemoteSearch(keyword, ruleItem)"
+                                    @change="courseChangeUpdate(ruleItem, dayIndex, ruleIndex)"
+                                  >
+                                    <el-option
+                                      v-for="dict in (ruleItem.meta && ruleItem.meta.courseList ? ruleItem.meta.courseList : [])"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    />
+                                  </el-select>
+
+                                  <el-select
+                                    :disabled="formType == 3"
+                                    v-model="ruleItem.videoId"
+                                    placeholder="请选择小节"
+                                    size="mini"
+                                    style=" margin-right: 10px;"
+                                    filterable
+                                    clearable
+                                    @change="videoIdChange(ruleItem, dayIndex, ruleIndex)"
+                                  >
+                                    <el-option
+                                      v-for="dict in (ruleItem.meta && ruleItem.meta.videoList ? ruleItem.meta.videoList : [])"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="parseInt(dict.dictValue)"
+                                    >
+                                      <div :style="{ color: dict.isPause == 1 ? '#f56c6c' : 'inherit', display: 'flex', alignItems: 'center' }">
+                                        <span>{{ dict.dictLabel }}</span>
+                                        <span v-if="dict.isPause == 1" style="font-size: 12px; margin-left: 8px; color: #f56c6c;">
+                                            (该科普已被关闭,无法正常发送)
+                                        </span>
+                                      </div>
+                                    </el-option>
+                                  </el-select>
+
+                                  <!-- 禁用条件:操作类型为查看 -->
+                                  <!-- 渲染条件:消息类型不为AI触达 -->
+                                  <el-select
+                                    :disabled="formType == 3"
+                                    v-model="ruleItem.courseType"
+                                    placeholder="请选择消息类型"
+                                    size="mini"
+                                    style=" margin-right: 10px;"
+                                    v-if="ruleItem.type != 4 "
+                                  >
+                                    <el-option
+                                      v-for="dict in sysFsSopWatchStatus"
+                                      :key="dict.dictValue"
+                                      :label="dict.dictLabel"
+                                      :value="Number(dict.dictValue)"
+                                    />
+                                  </el-select>
+
+                                </el-form-item>
+
+                                <!-- 科普相关填写项 -->
+                                <div v-if="ruleItem.type == 2"
+                                  v-for="(content, contentIndex) in ruleItem.setting"
+                                  :key="contentIndex"
+                                  style="
+                                    background-color: #fdfdfd;
+                                    border: 1px solid #e6e6e6;
+                                    margin-bottom: 20px;
+                                  "
+                                >
+                                  <el-row style="padding-bottom: 20px">
+                                    <el-col :span="22">
+                                      <el-form
+                                        :model="content"
+                                        label-width="100px"
+                                      >
+                                        <el-form-item label="内容类别"
+                                          style="margin: 2%"
+                                        >
+
+                                          <div v-if="form.sendType == 2">
+                                            <el-radio-group
+                                              v-model="content.contentType"
+                                              :disabled="formType == 3"
+                                              @change="handleContentTypeChange(ruleItem)"
+                                            >
+                                              <el-radio
+                                                :key="item.dictValue"
+                                                :label="item.dictValue"
+                                                :disabled="(ruleItem.type != 2  && [9].includes(Number(item.dictValue))
+                                                || (ruleItem.isOfficial == 1 && [6, 9, 12].includes(Number(item.dictValue))))"
+                                                v-for="item in sysQwSopAiContentType"
+                                              >{{ item.dictLabel }}
+                                              </el-radio>
+                                            </el-radio-group>
+                                          </div>
+
+                                        </el-form-item>
+
+                                        <!-- 科普 -->
+                                        <el-form-item label="内容" v-if="content.contentType == 9">
+                                          <div>
+                                            <el-card class="box-card">
+                                              <el-form-item
+                                                label="链接标题:"
+                                                label-width="100px"
+                                                required
+                                              >
+                                                <el-input
+                                                  :disabled="formType == 3"
+                                                  v-model="content.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="content.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="content.linkImageUrl"
+                                                  type="image"
+                                                  :num="1"
+                                                  :file-size="1"
+                                                  :width="150"
+                                                  :height="150"
+                                                  style="margin-top: 1%;"
+                                                />
+                                              </el-form-item>
+                                              <div v-if="ruleItem.type != 2 " style="margin-top: 1%">
+                                                <el-form-item
+                                                  label="链接地址:"
+                                                  label-width="100px"
+                                                  required
+                                                >
+                                                  <el-input
+                                                    :disabled="formType == 3"
+                                                    v-model="content.linkUrl"
+                                                    placeholder="请输入链接地址"
+                                                    style="width: 90%;"
+                                                  />
+                                                </el-form-item>
+                                              </div>
+                                              <div v-if="ruleItem.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-form-item label="课节过期时间">
+                                                <el-row>
+                                                  <el-input
+                                                    type="number"
+                                                    v-model="content.expiresDays"
+                                                    style="width: 200px"
+                                                  >
+                                                    <template slot="append">天</template>
+                                                  </el-input>
+                                                </el-row>
+                                                <el-row>
+                                                  <span class="tip">填写0或不填时,默认为系统配置的默认时间</span>
+                                                </el-row>
+                                              </el-form-item>
+                                            </el-card>
+                                          </div>
+                                        </el-form-item>
+
+                                        <!-- 文本 -->
+                                        <el-form-item label="内容" v-else-if="content.contentType == 11">
+                                          <div
+                                            style="
+                                              color: #999;
+                                              font-size: 14px;
+                                              display: flex;
+                                              align-items: center;
+                                            "
+                                          >
+                                            <i class="el-icon-info"></i>
+                                            多语句分割符:|&| 例如:'语句1|&|语句2'
+                                          </div>
+                                          <el-input
+                                            :disabled="formType == 3"
+                                            v-model="content.value"
+                                            type="textarea"
+                                            :rows="3"
+                                            placeholder="内容"
+                                            style="width: 90%;margin-top: 10px;"
+                                            @keydown.native="handleKeydown($event, dayIndex, ruleIndex, contentIndex)"
+                                            :ref="`textarea-${dayIndex}-${ruleIndex}-${contentIndex}`"
+                                          />
+                                          <!-- 修改按钮部分 -->
+                                          <el-link
+                                            type="primary"
+                                            @click="toggleSalesCall(dayIndex, ruleIndex, contentIndex)"
+                                            style="margin-top: 10px;"
+                                            >
+                                              {{ content.isSalesCallAdded ? '移除#客服称呼#' : '添加#客服称呼#' }}
+                                          </el-link>
+                                          <el-link
+                                            type="primary"
+                                            style="margin-top: 10px;margin-left:10px"
+                                            @click="showOpenContent(dayIndex, ruleIndex, contentIndex)"
+                                          >
+                                            查看分割内容
+                                          </el-link>
+                                          <el-link
+                                            type="primary"
+                                            style="margin-top: 10px;margin-left:10px"
+                                            @click="cppySpliter()"
+                                          >
+                                            复制分隔符
+                                          </el-link>
+                                        </el-form-item>
+
+                                        <!-- 语音 -->
+                                        <el-form-item label="内容" v-else-if="content.contentType == 12">
+                                          <div>
+                                            <el-input
+                                              :disabled="formType == 3"
+                                              v-model="content.value"
+                                              type="textarea"
+                                              :rows="3"
+                                              maxlength="66"
+                                              show-word-limit
+                                              placeholder="输入要转为语音的内容"
+                                              style="width: 90%;margin-top: 10px;"
+                                              @input="handleInputVideoText(content.value, content)"
+                                            />
+                                          </div>
+                                        </el-form-item>
+
+                                        <!-- 图片 -->
+                                        <el-form-item label="内容" v-else-if="content.contentType == 2">
+                                          <ImageUpload
+                                            :disabled="formType == 3"
+                                            v-model="content.imgUrl"
+                                            type="image"
+                                            :num="1"
+                                            :width="150"
+                                            :height="150"
+                                          />
+                                        </el-form-item>
+
+                                        <!-- 视频 -->
+                                        <el-form-item label="内容" v-else-if="content.contentType == 6">
+                                          <div>
+                                            <el-upload
+                                              :disabled="formType == 3"
+                                              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>
+                                          </div>
+                                        </el-form-item>
+
+                                        <!-- 疗法 -->
+                                        <el-form-item label="内容" v-else-if="content.contentType == 20">
+
+                                          <el-card class="box-card">
+                                            <el-form-item
+                                              label="选择疗法:"
+                                              label-width="100px"
+                                              required
+                                            >
+                                              <el-select
+                                                :disabled="formType == 3"
+                                                v-model="content.packageId"
+                                                placeholder="默认50条,可输入关键字检索"
+                                                style=" margin-right: 10px; width: 230px;"
+                                                size="mini"
+                                                filterable
+                                                clearable
+                                                :loading="content.meta.pkgLoading"
+                                                remote
+                                                :remote-method="(keyword) => handlePkgRemoteSearch(keyword, content)"
+                                                @change="pkgChangeUpdate(content)"
+                                              >
+                                                <el-option
+                                                  v-for="dict in (content.meta && content.meta.pkgList ? content.meta.pkgList : [])"
+                                                  :key="dict.packageId"
+                                                  :label="`${dict.packageName}(${dict.packageId})`"
+                                                  :value="parseInt(dict.packageId)"
+                                                />
+                                              </el-select>
+                                            </el-form-item>
+                                            <el-form-item
+                                              label="疗法名称:"
+                                              label-width="100px"
+                                            >
+                                              <el-input
+                                                disabled
+                                                :rows="3"
+                                                v-model="content.packageName"
+                                                style="width: 90%;margin-top: 1%;"
+                                              />
+                                            </el-form-item>
+                                            <el-form-item
+                                              label="疗法封面:"
+                                              label-width="100px"
+                                            >
+                                              <ImageUpload
+                                                :disabled="true"
+                                                v-model="content.packageImgUrl"
+                                                type="image"
+                                                :num="1"
+                                                :file-size="1"
+                                                :width="150"
+                                                :height="150"
+                                                style="margin-top: 1%;"
+                                              />
+                                            </el-form-item>
+                                          </el-card>
+
+                                        </el-form-item>
+
+                                        <!-- 直播 -->
+                                        <el-form-item label="内容" v-else-if="content.contentType == 21">
+                                          <div>
+                                            <el-card class="box-card">
+                                              <el-form-item label="直播分组"
+                                                required
+                                              >
+                                                <el-select
+                                                  v-model="content.liveGroupTypeId"
+                                                  placeholder="请选择直播分组"
+                                                  size="mini"
+                                                  filterable
+                                                  clearable
+                                                  :disabled="formType == 3"
+                                                  @change="liveGroupTypeChange(content)"
+                                                >
+                                                  <el-option
+                                                    v-for="dict in liveGroupTypeList"
+                                                    :key="dict.id"
+                                                    :label="dict.liveGroupType"
+                                                    :value="dict.id"
+                                                  />
+                                                </el-select>
+                                              </el-form-item>
+                                              <el-form-item label="直播间"
+                                                required
+                                              >
+                                                <el-select
+                                                  v-model="content.liveId"
+                                                  placeholder="请先选择直播分组"
+                                                  size="mini"
+                                                  filterable
+                                                  :disabled="formType == 3 || !content.liveGroupTypeId"
+                                                  @change="liveChange(content)"
+                                                >
+                                                  <el-option
+                                                    v-for="dict in (content.meta && content.meta.liveList ? content.meta.liveList : [])"
+                                                    :key="dict.liveId"
+                                                    :label="dict.liveName"
+                                                    :value="parseInt(dict.liveId)"
+                                                  />
+                                                </el-select>
+                                              </el-form-item>
+                                              <el-form-item label="标题"
+                                                prop="liveTitle"
+                                                style="margin-bottom: 5px;"
+                                              >
+                                                <el-input
+                                                  v-model="content.liveTitle"
+                                                  placeholder="请输入消息标题,最长为64字节"
+                                                  :rows="2"
+                                                  maxlength="64"
+                                                  type="textarea"
+                                                  disabled
+                                                  @input="checkByteLength(content.contentType, 'liveTitle')"
+                                                />
+                                              </el-form-item>
+                                              <el-form-item label="封面"
+                                                prop="liveImgUrl"
+                                              >
+                                                <ImageUpload
+                                                  v-model="content.liveImgUrl"
+                                                  type="image"
+                                                  :num="10"
+                                                  :width="150"
+                                                  :height="150"
+                                                  disabled
+                                                />
+                                              </el-form-item>
+                                            </el-card>
+                                          </div>
+                                        </el-form-item>
+
+                                        <!-- 民品 -->
+                                        <el-form-item label="内容" v-else-if="22 == content.contentType">
+                                          <div>
+                                            <el-card class="box-card">
+                                              <el-form-item
+                                                label="选择民品:"
+                                                label-width="100px"
+                                                required
+                                              >
+                                                <el-select
+                                                  v-model="content.productId"
+                                                  placeholder="默认50条,可输入关键字检索"
+                                                  style=" margin-right: 10px; width: 230px;"
+                                                  size="mini"
+                                                  filterable
+                                                  clearable
+                                                  :loading="content.meta.productLoading"
+                                                  remote
+                                                  :remote-method="(keyword) => handleProductRemoteSearch(keyword, content)"
+                                                  @change="productChangeUpdate(content)"
+                                                >
+                                                  <el-option
+                                                    v-for="dict in (content.meta && content.meta.productList ? content.meta.productList : [])"
+                                                    :key="dict.productId"
+                                                    :label="`${dict.productName}(${dict.productId})`"
+                                                    :value="parseInt(dict.productId)"
+                                                  />
+                                                </el-select>
+                                              </el-form-item>
+                                              <el-form-item
+                                                label="民品名称:"
+                                                label-width="100px"
+                                              >
+                                                <el-input
+                                                  disabled
+                                                  :rows="3"
+                                                  v-model="content.productName"
+                                                  style="width: 90%;margin-top: 1%;"
+                                                />
+                                              </el-form-item>
+                                              <el-form-item
+                                                label="民品封面:"
+                                                label-width="100px"
+                                              >
+                                                <ImageUpload
+                                                  :disabled="true"
+                                                  v-model="content.productImgUrl"
+                                                  type="image"
+                                                  :num="1"
+                                                  :file-size="1"
+                                                  :width="150"
+                                                  :height="150"
+                                                  style="margin-top: 1%;"
+                                                />
+                                              </el-form-item>
+                                            </el-card>
+                                          </div>
+                                        </el-form-item>
+
+                                        <!-- 药品 -->
+                                        <el-form-item label="内容" v-else-if="23 == content.contentType">
+                                          <div>
+                                            <el-card class="box-card">
+                                              <el-form-item
+                                                label="选择药品:"
+                                                label-width="100px"
+                                                required
+                                              >
+                                                <el-select
+                                                  v-model="content.medicinesId"
+                                                  placeholder="默认50条,可输入关键字检索"
+                                                  style=" margin-right: 10px; width: 230px;"
+                                                  size="mini"
+                                                  filterable
+                                                  clearable
+                                                  :loading="content.meta.medicinesLoading"
+                                                  remote
+                                                  :remote-method="(keyword) => handleMedicinesRemoteSearch(keyword, content)"
+                                                  @change="medicinesChangeUpdate(content)"
+                                                >
+                                                  <el-option
+                                                    v-for="dict in (content.meta && content.meta.medicinesList ? content.meta.medicinesList : [])"
+                                                    :key="dict.productId"
+                                                    :label="`${dict.productName}(${dict.productId})`"
+                                                    :value="parseInt(dict.productId)"
+                                                  />
+                                                </el-select>
+                                              </el-form-item>
+                                              <el-form-item
+                                                label="药品名称:"
+                                                label-width="100px"
+                                              >
+                                                <el-input
+                                                  disabled
+                                                  :rows="3"
+                                                  v-model="content.medicinesName"
+                                                  style="width: 90%;margin-top: 1%;"
+                                                />
+                                              </el-form-item>
+                                              <el-form-item
+                                                label="药品封面:"
+                                                label-width="100px"
+                                              >
+                                                <ImageUpload
+                                                  :disabled="true"
+                                                  v-model="content.medicinesImgUrl"
+                                                  type="image"
+                                                  :num="1"
+                                                  :file-size="1"
+                                                  :width="150"
+                                                  :height="150"
+                                                  style="margin-top: 1%;"
+                                                />
+                                              </el-form-item>
+                                            </el-card>
+                                          </div>
+                                        </el-form-item>
+
+                                        <!-- 短视频 -->
+                                        <el-form-item label="内容" v-else-if="24 == content.contentType">
+                                          <div>
+                                            <el-card class="box-card">
+                                              <el-form-item
+                                                label="选择短视频:"
+                                                label-width="100px"
+                                                required
+                                              >
+                                                <el-select
+                                                  v-model="content.shortVideoId"
+                                                  placeholder="默认50条,可输入关键字检索"
+                                                  style=" margin-right: 10px; width: 230px;"
+                                                  size="mini"
+                                                  filterable
+                                                  clearable
+                                                  :loading="content.meta.shortVideoLoading"
+                                                  remote
+                                                  :remote-method="(keyword) => handleShortVideoRemoteSearch(keyword, content)"
+                                                  @change="shortVideoChangeUpdate(content)"
+                                                >
+                                                  <el-option
+                                                    v-for="dict in (content.meta && content.meta.shortVideoList ? content.meta.shortVideoList : [])"
+                                                    :key="dict.videoId"
+                                                    :label="`${dict.title}(${dict.videoId})`"
+                                                    :value="parseInt(dict.videoId)"
+                                                  />
+                                                </el-select>
+                                              </el-form-item>
+                                              <el-form-item
+                                                label="短视频名称:"
+                                                label-width="100px"
+                                              >
+                                                <el-input
+                                                  disabled
+                                                  :rows="3"
+                                                  v-model="content.shortVideoName"
+                                                  style="width: 90%;margin-top: 1%;"
+                                                />
+                                              </el-form-item>
+                                              <el-form-item
+                                                label="短视频封面:"
+                                                label-width="100px"
+                                              >
+                                                <ImageUpload
+                                                  :disabled="true"
+                                                  v-model="content.shortVideoImgUrl"
+                                                  type="image"
+                                                  :num="1"
+                                                  :file-size="1"
+                                                  :width="150"
+                                                  :height="150"
+                                                  style="margin-top: 1%;"
+                                                />
+                                              </el-form-item>
+                                            </el-card>
+                                          </div>
+                                        </el-form-item>
+
+                                        <!-- 文章 -->
+                                        <el-form-item label="内容" v-else-if="25 == content.contentType">
+                                          <div>
+                                            <el-card class="box-card">
+                                              <el-form-item
+                                                label="选择文章:"
+                                                label-width="100px"
+                                                required
+                                              >
+                                                <el-select
+                                                  v-model="content.articleId"
+                                                  placeholder="默认50条,可输入关键字检索"
+                                                  style=" margin-right: 10px; width: 230px;"
+                                                  size="mini"
+                                                  filterable
+                                                  clearable
+                                                  :loading="content.meta.articleLoading"
+                                                  remote
+                                                  :remote-method="(keyword) => handleArticleRemoteSearch(keyword, content)"
+                                                  @change="articleChangeUpdate(content)"
+                                                >
+                                                  <el-option
+                                                    v-for="dict in (content.meta && content.meta.articleList ? content.meta.articleList : [])"
+                                                    :key="dict.articleId"
+                                                    :label="`${dict.title}(${dict.articleId})`"
+                                                    :value="parseInt(dict.articleId)"
+                                                  />
+                                                </el-select>
+                                              </el-form-item>
+                                              <el-form-item
+                                                label="文章名称:"
+                                                label-width="100px"
+                                              >
+                                                <el-input
+                                                  disabled
+                                                  :rows="3"
+                                                  v-model="content.articleName"
+                                                  style="width: 90%;margin-top: 1%;"
+                                                />
+                                              </el-form-item>
+                                              <el-form-item
+                                                label="文章封面:"
+                                                label-width="100px"
+                                              >
+                                                <ImageUpload
+                                                  :disabled="true"
+                                                  v-model="content.articleImgUrl"
+                                                  type="image"
+                                                  :num="1"
+                                                  :file-size="1"
+                                                  :width="150"
+                                                  :height="150"
+                                                  style="margin-top: 1%;"
+                                                />
+                                              </el-form-item>
+                                            </el-card>
+                                          </div>
+                                        </el-form-item>
+
+                                        <!-- 公开课 -->
+                                        <el-form-item label="内容" v-else-if="26 == content.contentType">
+                                          <div>
+                                            <el-card class="box-card">
+                                              <el-form-item
+                                                label="选择公开课:"
+                                                label-width="100px"
+                                                required
+                                              >
+                                                <el-select
+                                                  v-model="content.openClassVideoId"
+                                                  placeholder="默认50条,可输入关键字检索"
+                                                  style=" margin-right: 10px; width: 230px;"
+                                                  size="mini"
+                                                  filterable
+                                                  clearable
+                                                  :loading="content.meta.openClassVideoLoading"
+                                                  remote
+                                                  :remote-method="(keyword) => handleOpenClassVideoRemoteSearch(keyword, content)"
+                                                  @change="openClassVideoChangeUpdate(content)"
+                                                >
+                                                  <el-option
+                                                    v-for="dict in (content.meta && content.meta.openClassVideoList ? content.meta.openClassVideoList : [])"
+                                                    :key="dict.videoId"
+                                                    :label="`${dict.title}(${dict.videoId})`"
+                                                    :value="parseInt(dict.videoId)"
+                                                  />
+                                                </el-select>
+                                              </el-form-item>
+                                              <el-form-item
+                                                label="公开课名称:"
+                                                label-width="100px"
+                                              >
+                                                <el-input
+                                                  disabled
+                                                  :rows="3"
+                                                  v-model="content.openClassVideoName"
+                                                  style="width: 90%;margin-top: 1%;"
+                                                />
+                                              </el-form-item>
+                                              <el-form-item
+                                                label="公开课封面:"
+                                                label-width="100px"
+                                              >
+                                                <ImageUpload
+                                                  :disabled="true"
+                                                  v-model="content.openClassVideoImgUrl"
+                                                  type="image"
+                                                  :num="1"
+                                                  :file-size="1"
+                                                  :width="150"
+                                                  :height="150"
+                                                  style="margin-top: 1%;"
+                                                />
+                                              </el-form-item>
+                                            </el-card>
+                                          </div>
+                                        </el-form-item>
+
+                                      </el-form>
+
+                                    </el-col>
+
+                                    <el-col
+                                      :span="1"
+                                      :offset="1"
+                                    >
+                                      <!-- 删除内容节点 -->
+                                      <i
+                                        class="el-icon-delete"
+                                        @click="delContentNode(dayIndex, ruleIndex, contentIndex)"
+                                        style="margin-top: 20px;"
+                                        v-if="ruleItem.setting.length > 1 && (formType != 3)"
+                                      ></i>
+                                    </el-col>
+                                  </el-row>
+                                </div>
+
+                                <!-- 改状态(已停用,暂注释)
+                                <div v-if="ruleItem.type == 5"
+                                  style="
+                                    background-color: #fdfdfd;
+                                    border: 1px solid #e6e6e6;
+                                    margin-bottom: 20px;
+                                    padding: 8px;
+                                  "
+                                >
+
+                                  <el-form-item label="添加标签" style="padding: 5px;">
+                                    <div>
+                                      <el-button size="medium" icon="el-icon-circle-plus-outline" plain
+                                        @click="openTagPage(ruleItem, 0)">添加标签</el-button>
+                                    </div>
+                                    <div style="padding-left: 5px;">
+                                      <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in ruleItem.addTag" closable
+                                        :disable-transitions="false" @close="handleCloseTag(ruleItem, 0, s)">
+                                        <span>{{ s.name }}</span>
+                                      </el-tag>
+                                    </div>
+                                  </el-form-item>
+
+                                  <el-form-item label="移除标签" style="padding: 5px;">
+                                    <div>
+                                      <el-button size="medium" icon="el-icon-circle-plus-outline" plain
+                                        @click="openTagPage(ruleItem, 1)">移除标签</el-button>
+                                    </div>
+                                    <div style="padding-left: 5px;">
+                                      <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in ruleItem.delTag" closable
+                                        :disable-transitions="false" @close="handleCloseTag(ruleItem, 1, s)">
+                                        <span>{{ s.name }}</span>
+                                      </el-tag>
+                                    </div>
+
+                                  </el-form-item>
+
+                                  <el-form-item label="绑定客服" style="padding: 5px;">
+                                    <div>
+                                      <el-button size="medium" icon="el-icon-circle-plus-outline" plain
+                                        @click="openCustomerPage(ruleItem, 0)">绑定客服</el-button>
+                                    </div>
+                                    <div style="padding-left: 5px;">
+                                      <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in ruleItem.addCustomer" closable
+                                        :disable-transitions="false" @close="handleCloseCustomer(ruleItem, 0, s)">
+                                        <span>{{ s.roleName }}</span>
+                                      </el-tag>
+                                    </div>
+                                  </el-form-item>
+
+                                  <el-form-item label="解绑客服" style="padding: 5px;">
+                                    <div>
+                                      <el-button size="medium" icon="el-icon-circle-plus-outline" plain
+                                        @click="openCustomerPage(ruleItem, 1)">解绑客服</el-button>
+                                    </div>
+                                    <div style="padding-left: 5px;">
+                                      <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in ruleItem.delCustomer" closable
+                                        :disable-transitions="false" @close="handleCloseCustomer(ruleItem, 1, s)">
+                                        <span>{{ s.roleName }}</span>
+                                      </el-tag>
+                                    </div>
+                                  </el-form-item>
+
+                                </div>
+                                -->
+
+                                <!-- 添加内容节点 -->
+                                <el-link
+                                  type="primary"
+                                  class="el-icon-plus"
+                                  :underline="false"
+                                  @click='addContentNode(ruleItem)'
+                                  v-if="formType != 3 && [2].includes(Number(ruleItem.type))"
+                                >添加内容
+                                </el-link>
+
+                              </el-form>
+                            </el-col>
+
+                            <el-col
+                              :span="1"
+                              :offset="1"
+                            >
+                              <i
+                                class="el-icon-delete"
+                                @click="delRuleNode(dayIndex, ruleIndex)"
+                                style="margin-top: 20px;"
+                                v-if="item.rules.length > 1 && formType != 3"
+                              ></i>
+                            </el-col>
+                          </el-row>
+                        </div>
+
+                        <!-- 操作为非查看,且模板类型不为 新客对话和群聊则可以添加规则 -->
+                        <el-link
+                          type="primary"
+                          class="el-icon-plus"
+                          :underline="false"
+                          @click='addRuleNode(dayIndex)'
+                          v-if="formType != 3 && form.sendType != 4 && form.sendType != 5"
+                        >
+                          添加规则
+                        </el-link>
+                      </el-form-item>
+                    </el-form>
+
+                  </div>
+
+                  <!-- 操作为非查看 -->
+                  <div
+                    v-if="formType != 3"
+                    style="float: right;"
+                  >
+                    <el-button
+                      type="primary"
+                      @click="saveDay"
+                      v-if="!item.voice || item.voice == 0"
+                    >
+                      保存({{ '第' + (1 + (form.gap * dayIndex)) + '天' }})
+                    </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="修改天数排序"
+      :visible.sync="openSort"
+      width="1500px"
+      append-to-body
+    >
+      <draggable
+        v-model="dayList"
+        @end="onDragEndDay"
+        style="padding: 20px"
+      >
+        <el-button
+          v-for="(item, dayIndex) in dayList"
+          :class="item.newDay != item.dayNum ? 'red':''"
+        >第{{
+            item.newDay
+          }}天(第{{ item.dayNum }}天)
+        </el-button>
+      </draggable>
+
+      <div style="float: right;" slot="footer">
+        <el-button
+          type="primary"
+          @click="saveSorts"
+        >保存</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog
+      title="修改规则排序"
+      :visible.sync="openSort2"
+      width="1500px"
+      append-to-body
+    >
+      <draggable
+        v-model="ruleList"
+        @end="onDragEnd"
+        style="padding: 20px"
+      >
+        <el-button v-for="(item, ruleIndex) in ruleList">{{ item.time }}</el-button>
+      </draggable>
+
+      <div style="float: right;" slot="footer">
+        <el-button @click="autoSortsRules">自动排序</el-button>
+        <el-button
+          type="primary"
+          @click="saveSortsRules"
+        >保存</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog
+      title="查看分割后内容"
+      :visible.sync="openShowContent"
+      width="800px"
+      append-to-body
+    >
+         <el-table
+         stripe
+          :data="contentListData"
+          style="width: 100%"
+         >
+            <el-table-column
+              type="index"
+              label="序号"
+              width="50"
+            >
+            </el-table-column>
+
+          <el-table-column
+            prop="content"
+            label="内容"
+            width="700"
+          >
+             <template #default="{ row }">
+        <div v-html="row.content.replace(/\n/g, '<br>')"></div>
+      </template>
+          </el-table-column>
+        </el-table>
+
+      <div style="float: right;">
+        <el-button
+          type="primary"
+          @click="comfirmContent"
+        >确定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 绑定app客服角色 -->
+    <el-dialog title="选择绑定的客服"
+      :visible.sync="customerConfigForBind.visible"
+      width="1300px"
+      append-to-body
+    >
+      <CusRoleList
+        :selected="customerConfigForBind.selected"
+        @confirm="customerConfirmCallForBind"
+        @selectionChange="customerSelectionChangeCallForBind"
+      />
+    </el-dialog>
+
+    <!-- 解绑app客服角色 -->
+    <el-dialog title="选择解绑的客服"
+      :visible.sync="customerConfigForUnbind.visible"
+      width="1300px"
+      append-to-body
+    >
+      <CusRoleList
+        :selected="customerConfigForUnbind.selected"
+        @confirm="customerConfirmCallForUnbind"
+        @selectionChange="customerSelectionChangeCallForUnbind"
+      />
+    </el-dialog>
+
+    <!-- 绑定标签 -->
+    <el-dialog title="选择绑定的标签"
+      :visible.sync="tagConfigForBind.visible"
+      width="1000px"
+      append-to-body
+    >
+      <select-tag
+        :tagSelected="tagConfigForBind.selected"
+        @tagSubmit="tagSubmitForBind"
+        @tagCancel="tagCancelForBind"
+        @tagSelection="tagSelectionChangeCallForBind"
+      />
+    </el-dialog>
+
+    <!-- 解绑标签 -->
+    <el-dialog title="选择解绑的标签"
+      :visible.sync="tagConfigForUnbind.visible"
+      width="1000px"
+      append-to-body
+    >
+      <select-tag
+        :tagSelected="tagConfigForUnbind.selected"
+        @tagSubmit="tagSubmitForUnbind"
+        @tagCancel="tagCancelForUnbind"
+        @tagSelection="tagSelectionChangeCallForUnbind"
+      />
+    </el-dialog>
+
+  </div>
+</template>
+<script>
+import draggable from 'vuedraggable';
+import {
+  listSopTemp,
+  getSopTemp,
+  sortDay,
+  dayListFun,
+  addOrUpdateSetting,
+  selectRulesInfo,
+  delRules
+} from "@/api/app/sop/template";
+import {courseList, videoList} from "@/api/app/course";
+import { getPackageOptions,} from "@/api/app/pkg";
+import { getLiveOptions as fetchLiveOptionsApi, getLiveGroupTypeList as fetchLiveGroupTypeListApi } from "@/api/app/live";
+import { listRole } from '@/api/app/user/userList';
+import { getTagByIds } from "@/api/app/tag/tagGroup";
+import ImageUpload from "@/views/qw/sop/ImageUpload";
+import { getMedicinesOptions, } from "@/api/app/medicines";
+import { getProductOptions, } from "@/api/app/product";
+import { getShortVideoOptions, } from "@/api/app/shortVideo";
+import { getArticleOptions } from "@/api/app/article";
+import { getOpenClassVideoOptions, } from "@/api/app/openClassVideo";
+
+import selectTag from "@/views/app/tagGroup/selectTag.vue";
+import CusRoleList from '@/views/app/user/CusRoleList.vue';
+
+export default {
+  name: "UpdateSopTemp",
+  components: { ImageUpload, draggable, selectTag, CusRoleList, },
+  data() {
+    return {
+      uploadUrl: process.env.VUE_APP_BASE_API + "/app/common/uploadOSS2",
+      uploadUrlByVoice: process.env.VUE_APP_BASE_API + "/app/common/uploadOSSByHOOKVoice",
+      //上传语音的遮罩层
+      voiceLoading: false,
+      openSort: false,
+      openSort2: false,
+      openShowContent: false,
+      contentList: [],
+      contentListData: [],
+      // 遮罩层
+      loading: false,
+      loading2: false,
+      loading3: false,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      dayList: [],
+      ruleList: [],
+      ids: [],
+      sysFsSopWatchStatus: [],
+      //插件版
+      sysQwSopAiContentType: [],
+      msgTypeOptions: [],//消息类别选项
+      courseList: [],//默认科普
+      pkgList: [],//默认疗法
+      liveGroupTypeList: [],//直播分组
+      liveList: [],//默认直播间
+      productList: [],//默认民品列表
+      medicinesList: [],//默认药品列表
+      shortVideoList: [],//默认短视频列表
+      articleList: [],//默认文章列表
+      openClassVideoList: [],//默认公开课列表
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      id: "",
+      total: 0,
+      tabIndex: null,
+      // sop模板天数列表
+      days: [],
+      data: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 状态字典
+      statusOptions: [],
+      videoNumOptions: {
+        title: '选择视频号',
+        open: false,
+        content: null,
+        rulesIndex: null,
+        contentIndex: null,
+      },
+      //1 修改 2 复制 3 查看
+      formType: null,
+      queryParams1: {
+        pageNum: 1,
+        pageSize: 100,
+        dataStatus: '1',
+        name: null,
+        type: 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' }
+        ],
+      },
+      customerConfigForBind: {
+        visible: false,
+        selected: [],
+        currentRuleItem: null,
+      },
+      customerConfigForUnbind: {
+        visible: false,
+        selected: [],
+        currentRuleItem: null,
+      },
+      tagConfigForBind: {
+        visible: false,
+        selected: [],
+        currentRuleItem: null,
+      },
+      tagConfigForUnbind: {
+        visible: false,
+        selected: [],
+        currentRuleItem: null,
+      },
+    };
+  },
+  created() {
+    this.init();
+    //模板id
+    const id = this.$route.params && this.$route.params.id;
+    this.id = id;
+    //表单操作类型,1 修改 2 复制 3 查看
+    this.formType = this.$route.params && this.$route.params.type;
+    this.initTempInfo(id);
+  },
+  methods: {
+    async init() {
+      this.getDicts("app_sop_plugin_settingType").then(response => {
+        this.sysQwSopAiContentType = response.data;
+      });
+      this.getDicts("app_sop_watch_status").then(response => {
+        this.sysFsSopWatchStatus = response.data;
+      });
+      this.getDicts("app_sop_msg_type").then(response => {
+        // 改状态(dictValue=5)已停用,页面过滤掉该选项
+        this.msgTypeOptions = (response.data || []).filter(d => Number(d.dictValue) !== 5);
+      });
+      this.getDicts("app_normal_disabled_status").then(response => {
+        this.statusOptions = response.data;
+      });
+      try {
+        const [
+          courseInfo,
+          defaultPkg,
+          defaultLiveGroup,
+          defaultArticle,
+          defaultOpenClassVideo,
+        ] = await Promise.all([
+          this.getCourseList(null, null),
+          this.getPkgList(null, null),
+          this.fetchLiveGroupTypeList(),
+          this.getArticleList(null, null),
+          this.getOpenClassVideoList(null, null),
+        ]);
+        this.courseList = (courseInfo && courseInfo.list) || [];
+        this.pkgList = (defaultPkg && defaultPkg.data) || [];
+        this.liveGroupTypeList = (defaultLiveGroup && (defaultLiveGroup.rows || defaultLiveGroup.data)) || [];
+        // 民品/药品/短视频接口暂不放开,避免控制台报错
+        // this.productList = ...
+        // this.medicinesList = ...
+        // this.shortVideoList = ...
+        this.articleList = (defaultArticle && defaultArticle.data) || [];
+        this.openClassVideoList = (defaultOpenClassVideo && defaultOpenClassVideo.data) || [];
+      } catch (e) {
+        console.error('SOP模板下拉数据加载失败', e);
+        this.courseList = this.courseList || [];
+        this.pkgList = this.pkgList || [];
+        this.liveGroupTypeList = this.liveGroupTypeList || [];
+        this.articleList = this.articleList || [];
+        this.openClassVideoList = this.openClassVideoList || [];
+      }
+    },
+    /**
+     * 获取科普信息
+     */
+    async getCourseList(keyword, selectedId) {
+      let data = {
+        keyword: keyword,
+        selectedId: selectedId,
+        pageNum: 1,
+        pageSize: 50,
+      };
+      return await courseList(data);
+    },
+    /**
+     * 获取疗法信息
+     */
+    async getPkgList(keyword, selectedId) {
+      let data = {
+        keyword: keyword,
+        metaId: selectedId,
+        limit: 50
+      };
+      return await getPackageOptions(data);
+    },
+    /**
+     * 获取直播分组下拉
+     */
+    async fetchLiveGroupTypeList() {
+      return await fetchLiveGroupTypeListApi({ pageNum: 1, pageSize: 999 });
+    },
+    /**
+     * 获取直播间下拉
+     * @param liveGroupType 直播分组id
+     */
+    async fetchLiveOptionsByGroup(liveGroupType) {
+      if (!liveGroupType) {
+        return { data: [] };
+      }
+      return await fetchLiveOptionsApi({ liveGroupType });
+    },
+    // /**
+    //  * 获取民品列表
+    //  * @param keyword
+    //  * @param selectedId
+    //  */
+    // async getProductList(keyword, selectedId) {
+    //   let data = {
+    //     keyword: keyword,
+    //     metaId: selectedId,
+    //     limit: 50
+    //   };
+    //   return await getProductOptions(data);
+    // },
+    // /**
+    //  * 获取药品列表
+    //  * @param keyword
+    //  * @param selectedId
+    //  */
+    // async getMedicinesList(keyword, selectedId) {
+    //   let data = {
+    //     keyword: keyword,
+    //     metaId: selectedId,
+    //     limit: 50
+    //   };
+    //   return await getMedicinesOptions(data);
+    // },
+    // /**
+    //  * 获取短视频列表
+    //  * @param keyword
+    //  * @param selectedId
+    //  */
+    // async getShortVideoList(keyword, selectedId) {
+    //   let data = {
+    //     keyword: keyword,
+    //     metaId: selectedId,
+    //     limit: 50
+    //   };
+    //   return await getShortVideoOptions(data);
+    // },
+    /**
+     * 获取文章列表
+     * @param keyword
+     * @param selectedId
+     */
+    async getArticleList(keyword, selectedId) {
+      let data = {
+        keyword: keyword,
+        metaId: selectedId,
+        limit: 50
+      };
+      return await getArticleOptions(data);
+    },
+    /**
+     * 获取公开课列表
+     * @param keyword
+     * @param selectedId
+     */
+    async getOpenClassVideoList(keyword, selectedId) {
+      let data = {
+        keyword: keyword,
+        metaId: selectedId,
+        limit: 50
+      };
+      return await getOpenClassVideoOptions(data);
+    },
+    /**
+     * 处理输入值不为5的倍数
+     * @param content
+     */
+    handleIntervalTimeInput(content) {
+      // 确保输入值为5的倍数
+      if (content.intervalTime % 5 !== 0) {
+        // 四舍五入到最近的5的倍数
+        content.intervalTime = Math.round(content.intervalTime / 5) * 5;
+      }
+    },
+    /**
+     * 检查字节长度
+     * @param content
+     * @param key
+     */
+    checkByteLength(content, key) {
+      let cont = content[key];
+      if (cont) {
+        const byteLength = this.getByteLength(cont); // 获取当前字节数
+        // 如果字节数超过64,截断输入内容
+        if (byteLength > 64) {
+          this.$set(content, key, this.truncateTextByByteLength(cont, 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;
+    },
+    /**
+     * 保存排序
+     */
+    saveSorts() {
+      let list = this.dayList.filter(e => e.dayNum != e.newDay).map(e => {
+        return { dayNum: e.newDay, id: e.id }
+      })
+      this.loading3 = true;
+      if (!list || list.length == 0) {
+        this.$message.closeAll();
+        return this.msgError('请修改排序!');
+      }
+      sortDay(list).then(e => {
+        window.location.reload();
+      })
+    },
+    /**
+     *
+     */
+    saveSortsRules() {
+      this.$set(this.days[this.tabIndex], 'rules', 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.days[this.tabIndex].rules));
+      if (this.ruleList == null || this.ruleList.length == 0) {
+        this.$message.error("暂无规则")
+      } else {
+        this.openSort2 = true;
+      }
+    },
+    onDragEndDay() {
+      this.dayList.forEach((item, dayIndex) => {
+        item.newDay = (this.form.gap * dayIndex) + 1;
+      })
+      this.$forceUpdate()
+    },
+    onDragEnd() {
+    },
+    /**
+     * 切换标签保存数据,默认打开会触发一次
+     * @param tabIndex
+     * @param oldIndex
+     */
+    async leave(tabIndex, oldIndex) {
+      const newData = this.days[tabIndex];
+      if (newData.dayNum && newData.id) {
+        this.loading = true;
+        let rd = await selectRulesInfo(newData.id);
+        this.$nextTick(async () => {
+          let defaultRule = {
+            type: 2,//默认消息类别为科普
+            contentType: 1,
+            courseId: null,
+            videoId: null,
+            meta: {
+              courseList: this.courseList,
+              courseLoading: false,
+              videoList: [],
+            },
+            addTag: [],
+            delTag: [],
+            addCustomer: [],
+            delCustomer: [],
+            setting: [{ contentType: '9', value: "", }]//默认是文本
+          }
+          this.$set(this.days, tabIndex, {
+            ...newData,
+            voice: rd.data.voice,
+            rules: rd.data.list || [defaultRule] //为空默认普通消息,内容为文本
+          });
+
+            //遍历当天所有规则
+          for (let j = 0; j < this.days[tabIndex].rules.length; j++) {
+            let ruleItem = this.days[tabIndex].rules[j];
+            //消息类别为科普,动态加载科普下拉
+            if (ruleItem.type == 2) {
+              if (ruleItem.courseId) {
+                //获取科普下拉
+                let cl = await this.getCourseList(null, ruleItem.courseId);
+                //根据科普获取视频小节下拉
+                let vl = await this.getVideoList(ruleItem.courseId);
+                this.$set(ruleItem, 'meta', {
+                  courseList: cl.list,
+                  courseLoading: false,
+                  videoList: vl.list,
+                });
+              }
+              //初始遍历当前规则的所有内容
+              for (let i = 0; i < ruleItem.setting.length; i++) {
+                let setting = ruleItem.setting[i];
+                let contentType = Number(setting.contentType);
+                //疗法
+                if (contentType === 20) {
+                  if (setting.packageId) {
+                    let pkgList = await this.getPkgList(null, setting.packageId);
+                    this.$set(setting, 'meta', {
+                      pkgLoading: false,
+                      pkgList: pkgList.data,
+                    })
+                  } else {
+                    this.$set(setting, 'meta', {
+                      pkgLoading: false,
+                      pkgList: this.pkgList,
+                    })
+                  }
+                }
+                //直播
+                else if (contentType === 21) {
+                  if (setting.liveGroupTypeId) {
+                    let liveRes = await this.fetchLiveOptionsByGroup(setting.liveGroupTypeId);
+                    this.$set(setting, 'meta', {
+                      liveLoading: false,
+                      liveList: liveRes.data || liveRes.rows || [],
+                    });
+                  } else {
+                    this.$set(setting, 'meta', {
+                      liveLoading: false,
+                      liveList: [],
+                    });
+                  }
+                }
+                //民品
+                else if(contentType === 22) {
+                  if (!setting.productId) {
+                    this.$set(setting, 'meta', {
+                      productLoading: false,
+                      productList: this.productList,
+                    });
+                  } else {
+                    let productList = await this.getProductList(null, setting.productId);
+                    this.$set(setting, 'meta', {
+                      productLoading: false,
+                      productList: productList.data,
+                    });
+                  }
+                }
+                //药品
+                else if(contentType === 23) {
+                  if (!setting.medicinesId) {
+                    this.$set(setting, 'meta', {
+                      medicinesLoading: false,
+                      medicinesList: this.medicinesList,
+                    });
+                  } else {
+                    let medicinesList = await this.getMedicinesList(null, setting.medicinesId);
+                    this.$set(setting, 'meta', {
+                      medicinesLoading: false,
+                      medicinesList: medicinesList.data,
+                    });
+                  }
+                }
+                //短视频
+                else if(contentType === 24) {
+                  if (!setting.shortVideoId) {
+                    this.$set(setting, 'meta', {
+                      shortVideoLoading: false,
+                      shortVideoList: this.shortVideoList,
+                    });
+                  } else {
+                    let shortVideoList = await this.getShortVideoList(null, setting.shortVideoId);
+                    this.$set(setting, 'meta', {
+                      shortVideoLoading: false,
+                      shortVideoList: shortVideoList.data,
+                    });
+                  }
+                }
+                //文章
+                else if(contentType === 25) {
+                  if (!setting.articleId) {
+                    this.$set(setting, 'meta', {
+                      articleLoading: false,
+                      articleList: this.articleList,
+                    });
+                  } else {
+                    let articleList = await this.getArticleList(null, setting.articleId);
+                    this.$set(setting, 'meta', {
+                      articleLoading: false,
+                      articleList: articleList.data,
+                    });
+                  }
+                }
+                //公开课
+                else if(contentType === 26) {
+                  if (!setting.openClassVideoId) {
+                    this.$set(setting, 'meta', {
+                      openClassVideoLoading: false,
+                      openClassVideoList: this.openClassVideoList,
+                    });
+                  } else {
+                    let openClassVideoList = await this.getOpenClassVideoList(null, setting.openClassVideoId);
+                    this.$set(setting, 'meta', {
+                      openClassVideoLoading: false,
+                      openClassVideoList: openClassVideoList.data,
+                    });
+                  }
+                }
+              }
+            }
+            //动态加载绑定/解绑 标签和客服
+            if (ruleItem.addTag) {
+              getTagByIds(ruleItem.addTag.split(',')).then(tagsRes => {
+                ruleItem.addTag = tagsRes.data;
+                this.tagConfigForBind.selected = tagsRes.data;
+              });
+            } else {
+              ruleItem.addTag = [];
+              this.tagConfigForBind.selected = [];
+            }
+            if (ruleItem.delTag) {
+              getTagByIds(ruleItem.delTag.split(',')).then(tagsRes => {
+                ruleItem.delTag = tagsRes.data;
+                this.tagConfigForUnbind.selected = tagsRes.data;
+              });
+            } else {
+              ruleItem.delTag = [];
+              this.tagConfigForUnbind.selected = [];
+            }
+            if (ruleItem.addCustomer) {
+              listRole({ids: ruleItem.addCustomer.split(',')}).then(customerRes => {
+                ruleItem.addCustomer = customerRes.rows;
+                this.customerConfigForBind.selected = customerRes.rows;
+              });
+            } else {
+              ruleItem.addCustomer = [];
+              this.customerConfigForBind.selected = [];
+            }
+            if (ruleItem.delCustomer) {
+              listRole({ids: ruleItem.delCustomer.split(',')}).then(customerRes => {
+                ruleItem.delCustomer = customerRes.rows;
+                this.customerConfigForUnbind.selected = customerRes.rows;
+              });
+            } else {
+              ruleItem.delCustomer = [];
+              this.customerConfigForUnbind.selected = [];
+            }
+          }
+          this.loading = false;
+        })
+      }
+    },
+    /**
+     * 保存天
+     */
+    saveDay() {
+      let data = JSON.parse(JSON.stringify(this.days[this.tabIndex]));
+      let check = this.checkData(data);
+      if (!check) {
+        return;
+      }
+
+      for (let j = 0; j < data.rules.length; j++) {
+        let e = data.rules[j];
+        //如果不是每天第一条,全刷为非官方
+        if (j !== 0) {
+          e.isOfficial = 0;
+        }
+        e.name = data.name;
+        e.tempId = this.id;
+        e.sorts = Number(this.tabIndex);
+        e.dayNum = (this.form.gap * e.sorts) + 1;
+        e.sorts = j;
+        data.dayNum = e.dayNum;
+        e.contentType = e.type;
+        delete e.meta;
+        e.setting.forEach(item => delete item.meta);
+        e.settingList = e.setting;
+        e.settingList.forEach(e => e.content = JSON.stringify(e));
+        e.addTag = e?.addTag?.map(tag => tag.id).join(',') || null;
+        e.delTag = e?.delTag?.map(tag => tag.id).join(',') || null;
+        e.addCustomer = e?.addCustomer?.map(cus => cus.id).join(',') || null;
+        e.delCustomer = e?.delCustomer?.map(cus => cus.id).join(',') || null;
+      }
+      data.tempId = this.id;
+      data.list = data.rules;
+      this.loading = true;
+      addOrUpdateSetting(data).then(e => {
+        this.loading = false;
+        // 使用Vue.set确保响应式更新
+        this.$set(this.days, this.tabIndex, {
+          ...this.days[this.tabIndex],
+          id: e.data.id,
+        });
+      });
+    },
+    isEmpty(obj) {
+      if (null === obj || undefined === obj || 0 === obj.length) {
+        return true;
+      }
+      return false;
+    },
+    /**
+     * 校验数据
+     * @param data
+     */
+    checkData(data) {
+      this.$message.closeAll();
+      if (this.isEmpty(data.name)) {
+        this.$message.error("内容名称不能为空")
+        return false;
+      }
+      for (let j = 0; j < data.rules.length; j++) {
+        let dc = data.rules[j];
+        if (this.isEmpty(dc.time)) {
+          this.$message.error("时间不能为空")
+          return false;
+        }
+        //规则消息类别-科普
+        if (dc.type == 2) {
+          if (this.isEmpty(dc.courseId)) {
+            this.$message.error("请选择科普")
+            return false;
+          }
+          if (this.isEmpty(dc.videoId)) {
+            this.$message.error("请选择课节")
+            return false;
+          }
+          if (this.isEmpty(dc.courseType)) {
+            this.$message.error("请选择消息类型")
+            return false;
+          }
+
+          for (let k = 0; k < dc.setting.length; k++) {
+            let dcs = data.rules[j].setting[k];
+            if (dcs.contentType == 11 && this.isEmpty(dcs.value)) {
+              this.$message.error("内容不能为空")
+              return false;
+            }
+            if (dcs.contentType == 12 && this.isEmpty(dcs.value)) {
+              this.$message.error("语音文本不能为空")
+              return false;
+            }
+            if (dcs.contentType == 2 && this.isEmpty(dcs.imgUrl)) {
+              this.$message.error("图片不能为空")
+              return false;
+            }
+            if ([9].includes(Number(dcs.contentType)) && this.isEmpty(dcs.linkTitle)) {
+              this.$message.error("链接标题不能为空")
+              return false;
+            }
+            if ([9].includes(Number(dcs.contentType)) && this.isEmpty(dcs.linkDescribe)) {
+              this.$message.error("链接描述不能为空")
+              return false;
+            }
+            if ([9].includes(Number(dcs.contentType)) && this.isEmpty(dcs.linkImageUrl)) {
+              this.$message.error("链接图片不能为空")
+              return false;
+            }
+            if ([20].includes(Number(dcs.contentType)) && this.isEmpty(dcs.packageId)) {
+              this.$message.error("疗法不能为空")
+              return false;
+            }
+            if ([21].includes(Number(dcs.contentType)) && this.isEmpty(dcs.liveGroupTypeId)) {
+              this.$message.error("直播分组不能为空")
+              return false;
+            }
+            if ([21].includes(Number(dcs.contentType)) && this.isEmpty(dcs.liveId)) {
+              this.$message.error("直播间不能为空")
+              return false;
+            }
+          }
+        }
+        //规则消息类别-改状态(绑定/解绑-标签/客服)(已停用,暂注释)
+        // else if (dc.type == 5) {
+        //   if (this.isEmpty(dc.addTag) && this.isEmpty(dc.delTag)
+        //     && this.isEmpty(dc?.addCustomer) && this.isEmpty(dc?.delCustomer)) {
+        //     this.$message.error(`第${j + 1}条规则配置错误,未改动任何状态`);
+        //     return false;
+        //   }
+        // }
+      }
+      return true;
+    },
+    /**
+     * 天数选项卡点击
+     * @param obj
+     */
+    tabClick(obj) {
+
+    },
+    /**
+     * 新增内容节点
+     * @param ruleItem
+     */
+    addContentNode(ruleItem) {
+      const newSetting = {
+        contentType: '9',//默认内容类型为科普
+        value: '',
+      };
+      // 将新设置项添加到 rules.setting 数组中
+      ruleItem.setting.push(newSetting);
+      this.handleContentTypeChange(ruleItem);
+    },
+    /**
+     * 删除内容节点
+     * @param dayIndex
+     * @param ruleIndex
+     * @param contentIndex
+     */
+    delContentNode(dayIndex, ruleIndex, contentIndex) {
+      this.days[dayIndex].rules[ruleIndex].setting.splice(contentIndex, 1)
+    },
+    /**
+     * 添加规则节点
+     * @param dayIndex
+     */
+    addRuleNode(dayIndex) {
+      this.days[dayIndex].rules.push(
+        {
+          type: 2,//默认消息类别为科普
+          contentType: 2,
+          courseId: null,
+          videoId: null,
+          meta: {
+            courseList: this.courseList,
+            courseLoading: false,
+            videoList: [],
+          },
+          setting: [{ contentType: '9', value: "", }]//默认内容类型为科普
+        }
+      )
+    },
+    /**
+     * 删除规则节点
+     * @param dayIndex
+     * @param rulesIndex
+     */
+    delRuleNode(dayIndex, rulesIndex) {
+      this.days[dayIndex].rules.splice(rulesIndex, 1);
+    },
+    /**
+     * 添加天数
+     */
+    addDay() {
+      let rules = [
+        {
+          contentType: '1',
+          setting: [{ contentType: '9', value: "", }],//默认内容类型为科普
+          meta: {
+            courseList: this.courseList,
+            courseLoading: false,
+            videoList: [],
+          }
+        }
+      ];
+      rules[0]['type'] = 2;//默认消息类别为科普
+      this.days.push({
+        name: null,
+        dayNum: (this.form.gap * this.days.length) + 1,
+        rules
+      })
+    },
+    /**
+     * 删除天数
+     * @param dayIndex
+     */
+    delDay(dayIndex) {
+      const newData = this.days[dayIndex];
+      if (newData.id && newData.dayNum) {
+        this.$confirm('此操作将永久删除, 是否继续?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          delRules(newData.id, newData.name, newData.dayNum).then(res => {
+            this.del(dayIndex);
+          });
+        });
+      } else {
+        this.del(dayIndex);
+      }
+    },
+    del(dayIndex) {
+      if (dayIndex == 0) {
+        this.tabIndex = "0";
+      } else {
+        this.tabIndex = (dayIndex - 1) + "";
+      }
+      this.days.splice(dayIndex, 1);
+
+    },
+
+    /**
+     * 选择视频号
+     * @param content
+     * @param dayIndex
+     * @param rulesIndex
+     * @param contentIndex
+     */
+    hanldeSelectVideoNum(content, dayIndex, rulesIndex, contentIndex) {
+      this.videoNumOptions.content = content;
+      this.videoNumOptions.rulesIndex = rulesIndex;
+      this.videoNumOptions.contentIndex = contentIndex;
+      this.videoNumOptions.open = true;
+    },
+    /**
+     * 科普下拉搜索
+     */
+    async handleCourseRemoteSearch(keyword, ruleItem) {
+      ruleItem.meta.courseLoading = true;
+      // 模拟远程接口请求(替换为你的真实接口)
+      let cl = await this.getCourseList(this.isEmpty(keyword) ? null : keyword, null);
+      ruleItem.meta.courseLoading = false;
+      ruleItem.meta.courseList = cl.list;
+    },
+    /**
+     * 疗法下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handlePkgRemoteSearch(keyword, content) {
+      content.meta.pkgLoading = true;
+      // 模拟远程接口请求(替换为你的真实接口)
+      let cl = await this.getPkgList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.pkgLoading = false;
+      content.meta.pkgList = cl.data;
+    },
+
+    /**
+     * 民品下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handleProductRemoteSearch(keyword, content) {
+      content.meta.productLoading = true;
+      let cl = await this.getProductList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.productLoading = false;
+      content.meta.productList = cl.data;
+    },
+    /**
+     * 民品变动处理
+     * @param content
+     */
+    async productChangeUpdate(content) {
+      this.$set(content, 'productName', null);
+      this.$set(content, 'productImgUrl', null);
+      if (content.productId) {
+        // 查找选中民品的信息
+        const selected = content.meta.productList.find(pro => parseInt(pro.productId) === content.productId);
+        this.$set(content, 'productName', selected.productName);
+        this.$set(content, 'productImgUrl', selected.image);
+      }
+    },
+    /**
+     * 药品下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handleMedicinesRemoteSearch(keyword, content) {
+      content.meta.medicinesLoading = true;
+      let cl = await this.getMedicinesList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.medicinesLoading = false;
+      content.meta.medicinesList = cl.data;
+    },
+    /**
+     * 药品变动处理
+     * @param content
+     */
+    async medicinesChangeUpdate(content) {
+      this.$set(content, 'medicinesName', null);
+      this.$set(content, 'medicinesImgUrl', null);
+      if (content.medicinesId) {
+        // 查找选中药品的信息
+        const selected = content.meta.medicinesList.find(pro => parseInt(pro.productId) === content.medicinesId);
+        this.$set(content, 'medicinesName', selected.productName);
+        this.$set(content, 'medicinesImgUrl', selected.image);
+      }
+    },
+    /**
+     * 短视频下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handleShortVideoRemoteSearch(keyword, content) {
+      content.meta.shortVideoLoading = true;
+      let cl = await this.getShortVideoList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.shortVideoLoading = false;
+      content.meta.shortVideoList = cl.data;
+    },
+    /**
+     * 短视频变动处理
+     * @param content
+     */
+    async shortVideoChangeUpdate(content) {
+      this.$set(content, 'shortVideoName', null);
+      this.$set(content, 'shortVideoImgUrl', null);
+      if (content.shortVideoId) {
+        // 查找选中短视频的信息
+        const selected = content.meta.shortVideoList.find(pro => parseInt(pro.videoId) === content.shortVideoId);
+        this.$set(content, 'shortVideoName', selected.title);
+        this.$set(content, 'shortVideoImgUrl', selected.thumbnail);
+      }
+    },
+    /**
+     * 文章下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handleArticleRemoteSearch(keyword, content) {
+      content.meta.articleLoading = true;
+      let cl = await this.getArticleList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.articleLoading = false;
+      content.meta.articleList = cl.data;
+    },
+    /**
+     * 文章变动处理
+     * @param content
+     */
+    async articleChangeUpdate(content) {
+      this.$set(content, 'articleName', null);
+      this.$set(content, 'articleImgUrl', null);
+      if (content.articleId) {
+        // 查找选中文章的信息
+        const selected = content.meta.articleList.find(pro => parseInt(pro.articleId) === content.articleId);
+        this.$set(content, 'articleName', selected.title);
+        this.$set(content, 'articleImgUrl', selected.imageUrl);
+      }
+    },
+    /**
+     * 公开课下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handleOpenClassVideoRemoteSearch(keyword, content) {
+      content.meta.openClassVideoLoading = true;
+      let cl = await this.getOpenClassVideoList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.openClassVideoLoading = false;
+      content.meta.openClassVideoList = cl.data;
+    },
+    /**
+     * 公开课变动处理
+     * @param content
+     */
+    async openClassVideoChangeUpdate(content) {
+      this.$set(content, 'openClassVideoName', null);
+      this.$set(content, 'openClassVideoImgUrl', null);
+      if (content.openClassVideoId) {
+        // 查找选中文章的信息
+        const selected = content.meta.openClassVideoList.find(pro => parseInt(pro.videoId) === content.openClassVideoId);
+        this.$set(content, 'openClassVideoName', selected.title);
+        this.$set(content, 'openClassVideoImgUrl', selected.thumbnail);
+      }
+    },
+    /**
+     * 科普变动处理
+     * @param ruleItem
+     * @param dayIndex
+     * @param ruleIndex
+     */
+    async courseChangeUpdate(ruleItem, dayIndex, ruleIndex) {
+      //将当前规则已选中的视频小节清空
+      this.$set(ruleItem, 'videoId', null);
+      this.$set(ruleItem.meta, 'videoList', []);
+      // 查找选中的科普的信息
+      const selectedCourse = ruleItem.meta.courseList.find(course => parseInt(course.dictValue) === ruleItem.courseId);
+      for (let i = 0; i < ruleItem.setting.length; i++) {
+        this.$set(ruleItem.setting[i], 'linkTitle', null);
+        this.$set(ruleItem.setting[i], 'linkDescribe', null);
+        this.$set(ruleItem.setting[i], 'linkImageUrl', null);
+
+        //如果是科普才上
+        if (selectedCourse && ruleItem.type == 2 && !this.isEmpty(ruleItem.courseId)) {
+          //响应式直接给链接的标题/封面上值
+
+          if ([9].includes(Number(ruleItem.setting[i].contentType))) {
+            this.$set(ruleItem.setting[i], 'linkTitle', selectedCourse.dictLabel);
+            this.$set(ruleItem.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+        }
+      }
+      if (!this.isEmpty(ruleItem.courseId)) {
+        let vl = await this.getVideoList(ruleItem.courseId, null);
+        ruleItem.meta.videoList = vl.list;
+      } else {
+        let cl = await this.getCourseList(null, null);
+        ruleItem.meta.courseList = cl.list;
+      }
+    },
+    /**
+     * 疗法变动处理
+     * @param content
+     */
+    async pkgChangeUpdate(content) {
+      //将当前规则已选中的视频小节清空
+      this.$set(content, 'packageName', null);
+      this.$set(content, 'packageImgUrl', null);
+      if (content.packageId) {
+        // 查找选中疗法的信息
+        const selectedPkg = content.meta.pkgList.find(pkg => parseInt(pkg.packageId) === content.packageId);
+        this.$set(content, 'packageName', selectedPkg.packageName);
+        this.$set(content, 'packageImgUrl', selectedPkg.imgUrl);
+      }
+    },
+    /**
+     * 直播分组切换
+     * @param content
+     */
+    async liveGroupTypeChange(content) {
+      this.$set(content, 'liveId', null);
+      this.$set(content, 'liveTitle', null);
+      this.$set(content, 'liveImgUrl', null);
+      if (!content.liveGroupTypeId) {
+        this.$set(content, 'meta', {
+          ...(content.meta || {}),
+          liveLoading: false,
+          liveList: [],
+        });
+        return;
+      }
+      this.$set(content, 'meta', {
+        ...(content.meta || {}),
+        liveLoading: true,
+        liveList: [],
+      });
+      const liveRes = await this.fetchLiveOptionsByGroup(content.liveGroupTypeId);
+      this.$set(content, 'meta', {
+        ...(content.meta || {}),
+        liveLoading: false,
+        liveList: liveRes.data || liveRes.rows || [],
+      });
+    },
+    /**
+     * 直播间切换
+     * @param content
+     */
+    liveChange(content) {
+      content.liveTitle = null;
+      content.liveImgUrl = null;
+      const list = (content.meta && content.meta.liveList) ? content.meta.liveList : [];
+      const selectedLive = list.find(live => parseInt(live.liveId) === parseInt(content.liveId));
+      if (selectedLive) {
+        content.liveTitle = selectedLive.liveName; // 自动填充标题
+        content.liveImgUrl = selectedLive.liveImgUrl; // 自动填充封面
+      }
+    },
+    /**
+     * 处理输入内容
+     * @param value
+     * @param content
+     */
+    handleInputVideoText(value, content) {
+      // 允许的字符:中文、英文(大小写)、数字和指定标点符号(,。!?)
+      const regex = /^[\u4e00-\u9fa5,。!?,!?]+$/;
+      // 删除不符合条件的字符
+      const filteredValue = value.split('').filter(char => regex.test(char)).join('');
+      this.$set(content, 'value', filteredValue);
+    },
+    courseUpdate(ruleItem, dayIndex, ruleIndex) {
+      videoList(ruleItem.courseId).then(response => {
+        ruleItem.meta.videoList = response.list;
+      });
+    },
+    /**
+     * 根据科普获取视频小节
+     * @param courseId
+     * @param title
+     */
+    async getVideoList(courseId, title) {
+      return await videoList({
+        courseId: courseId,
+        title: title
+      });
+    },
+    toggleSalesCall(dayIndex, rulesIndex, contentIndex) {
+      // 获取目标对象
+      const setItem = this.days[dayIndex].rules[rulesIndex].setting[contentIndex];
+      const salesCall = '#客服称呼#';
+      const refKey = `textarea-${dayIndex}-${rulesIndex}-${contentIndex}`;
+      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, dayIndex, rulesIndex, contentIndex) {
+
+      const setItem = this.days[dayIndex].rules[rulesIndex].setting[contentIndex];
+      const refKey = `textarea-${dayIndex}-${rulesIndex}-${contentIndex}`;
+      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;
+            }
+          }
+        });
+      }
+    },
+    /**
+     * 内容类型切换
+     * @param ruleItem
+     */
+    handleContentTypeChange(ruleItem) {
+      if (ruleItem.type === 2) {
+        const selectedCourse = ruleItem.courseId != null
+          ? this.courseList.find(course => parseInt(course.dictValue) === ruleItem.courseId)
+          : null;
+        const selectedVideo = ruleItem.videoId != null
+          ? ruleItem.meta.videoList.find(course => parseInt(course.dictValue) === ruleItem.videoId)
+          : null;
+        ruleItem.setting.forEach(async setting => {
+          const contentType = Number(setting.contentType);
+          // 科普相关赋值(contentType=9)
+          if (selectedCourse && contentType === 9) {
+            this.$set(setting, 'linkTitle', selectedCourse.dictLabel);
+            this.$set(setting, 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+          // 视频相关赋值(contentType=9)
+          if (selectedVideo && contentType === 9) {
+            this.$set(setting, 'linkDescribe', selectedVideo.dictLabel);
+          }
+          //疗法相关赋值(contentType=20)
+          if (contentType === 20) {
+            if (!setting.packageId) {
+              this.$set(setting, 'meta', {
+                pkgLoading: false,
+                pkgList: this.pkgList,
+              });
+            } else {
+              let pkgList = await this.getPkgList(null, setting.packageId);
+              this.$set(setting, 'meta', {
+                pkgLoading: false,
+                pkgList: pkgList.data,
+              });
+            }
+          }
+          //直播
+          else if (contentType === 21) {
+            if (setting.liveGroupTypeId) {
+              let liveRes = await this.fetchLiveOptionsByGroup(setting.liveGroupTypeId);
+              this.$set(setting, 'meta', {
+                liveLoading: false,
+                liveList: liveRes.data || liveRes.rows || [],
+              });
+            } else {
+              this.$set(setting, 'meta', {
+                liveLoading: false,
+                liveList: [],
+              });
+            }
+          }
+          //民品
+          else if(contentType === 22) {
+            if (!setting.productId) {
+              this.$set(setting, 'meta', {
+                productLoading: false,
+                productList: this.productList,
+              });
+            } else {
+              let productList = await this.getProductList(null, setting.productId);
+              this.$set(setting, 'meta', {
+                productLoading: false,
+                productList: productList.data,
+              });
+            }
+          }
+          //药品
+          else if(contentType === 23) {
+            if (!setting.medicinesId) {
+              this.$set(setting, 'meta', {
+                medicinesLoading: false,
+                medicinesList: this.medicinesList,
+              });
+            } else {
+              let medicinesList = await this.getMedicinesList(null, setting.medicinesId);
+              this.$set(setting, 'meta', {
+                medicinesLoading: false,
+                medicinesList: medicinesList.data,
+              });
+            }
+          }
+          //短视频
+          else if(contentType === 24) {
+            if (!setting.shortVideoId) {
+              this.$set(setting, 'meta', {
+                shortVideoLoading: false,
+                shortVideoList: this.shortVideoList,
+              });
+            } else {
+              let shortVideoList = await this.getShortVideoList(null, setting.shortVideoId);
+              this.$set(setting, 'meta', {
+                shortVideoLoading: false,
+                shortVideoList: shortVideoList.data,
+              });
+            }
+          }
+          //文章
+          else if(contentType === 25) {
+            if (!setting.articleId) {
+              this.$set(setting, 'meta', {
+                articleLoading: false,
+                articleList: this.articleList,
+              });
+            } else {
+              let articleList = await this.getArticleList(null, setting.articleId);
+              this.$set(setting, 'meta', {
+                articleLoading: false,
+                articleList: articleList.data,
+              });
+            }
+          }
+          //公开课
+          else if(contentType === 26) {
+            if (!setting.openClassVideoId) {
+              this.$set(setting, 'meta', {
+                openClassVideoLoading: false,
+                openClassVideoList: this.openClassVideoList,
+              });
+            } else {
+              let openClassVideoList = await this.getOpenClassVideoList(null, setting.openClassVideoId);
+              this.$set(setting, 'meta', {
+                openClassVideoLoading: false,
+                openClassVideoList: openClassVideoList.data,
+              });
+            }
+          }
+        });
+      }
+    },
+    /**
+     * 科普小节变化
+     * @param ruleItem
+     * @param dayIndex
+     * @param ruleIndex
+     */
+    videoIdChange(ruleItem, dayIndex, ruleIndex) {
+      //选择了科普小节则 默认绑上
+      let selectedVideo;
+      // 查找选中的科普对应的 label
+      if (ruleItem.videoId != null) {
+        selectedVideo = ruleItem.meta.videoList.find(course => parseInt(course.dictValue) === ruleItem.videoId);
+      }
+      for (let i = 0; i < ruleItem.setting.length; i++) {
+        //科普消息-文本内容
+        if (ruleItem.setting[i].contentType == 11 && ruleItem.type == 2) {
+          this.$set(ruleItem.setting[i], 'isBindUrl', '2');
+        }
+        //如果是科普的才上
+        if (selectedVideo && ruleItem.type == 2 && ruleItem.videoId != null) {
+
+          if (ruleItem.setting[i].contentType == 9) {
+            this.$set(ruleItem.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+          }
+        }
+      }
+    },
+    /**
+     * 打开选择标签的页面
+     * @param ruleItem
+     * @param type 0-绑定 1-解绑
+     */
+    openTagPage(ruleItem, type) {
+      if (type === 0) {
+        this.tagConfigForBind.visible = true;
+        this.tagConfigForBind.currentRuleItem = ruleItem;
+        this.tagConfigForBind.selected = ruleItem.addTag || [];
+      }
+      //解绑
+      else {
+        this.tagConfigForUnbind.visible = true;
+        this.tagConfigForUnbind.currentRuleItem = ruleItem;
+        this.tagConfigForUnbind.selected = ruleItem.delTag || [];
+      }
+    },
+    /**
+     * 绑定标签取消
+     */
+    tagCancelForBind() {
+      this.tagConfigForBind.visible = false;
+    },
+    /**
+     * 绑定标签确认
+     */
+    tagSubmitForBind() {
+      this.tagConfigForBind.visible = false;
+    },
+    /**
+     * 解绑标签取消
+     */
+    tagCancelForUnbind() {
+      this.tagConfigForUnbind.visible = false;
+    },
+    /**
+     * 解绑标签确认
+     */
+    tagSubmitForUnbind() {
+      this.tagConfigForUnbind.visible = false;
+    },
+    /**
+     * 删除标签操作
+     * @param ruleItem
+     * @param type 0-绑定 1-解绑
+     * @param tag 标签
+     */
+    handleCloseTag(ruleItem, type, tag) {
+      //绑定
+      if (type === 0) {
+        const index = this.tagConfigForBind.selected.findIndex(t => t === tag);
+        if (index !== -1) {
+          this.tagConfigForBind.selected.splice(index, 1);
+          ruleItem.addTag = this.tagConfigForBind.selected;
+        }
+      }
+      //解绑
+      else {
+        const index = this.tagConfigForUnbind.selected.findIndex(t => t === tag);
+        if (index !== -1) {
+          this.tagConfigForUnbind.selected.splice(index, 1);
+          ruleItem.delTag = this.tagConfigForUnbind.selected;
+        }
+      }
+    },
+    /**
+     * 选中的绑定标签列表
+     * @param group 标签所属组
+     * @param tag 具体操作的标签
+     */
+    tagSelectionChangeCallForBind(group, tag) {
+      if (!tag.isSelected) {
+        this.tagConfigForBind.selected = this.tagConfigForBind.selected.filter(item => tag.id != item.id);
+      } else {
+        if (!this.tagConfigForBind.selected.some(st => st.id == tag.id)) {
+          this.tagConfigForBind.selected.push(tag);
+        }
+      }
+      this.$set(this.tagConfigForBind.currentRuleItem, 'addTag', this.tagConfigForBind.selected);
+    },
+    /**
+     * 选中的解绑标签列表
+     * @param group 标签所属组
+     * @param tag 具体操作的标签
+     */
+    tagSelectionChangeCallForUnbind(group, tag) {
+      if (!tag.isSelected) {
+        this.tagConfigForUnbind.selected = this.tagConfigForUnbind.selected.filter(item => tag.id != item.id);
+      } else {
+        if (!this.tagConfigForUnbind.selected.some(st => st.id == tag.id)) {
+          this.tagConfigForUnbind.selected.push(tag);
+        }
+      }
+      this.$set(this.tagConfigForUnbind.currentRuleItem, 'delTag', this.tagConfigForUnbind.selected);
+    },
+    /**
+     * 打开选择客服的页面
+     * @param ruleItem
+     * @param type 0-绑定 1-解绑
+     */
+    openCustomerPage(ruleItem, type) {
+      if (type === 0) {
+        this.customerConfigForBind.visible = true;
+        this.customerConfigForBind.currentRuleItem = ruleItem;
+        this.customerConfigForBind.selected = ruleItem.addCustomer || [];
+      }
+      //解绑
+      else {
+        this.customerConfigForUnbind.visible = true;
+        this.customerConfigForUnbind.currentRuleItem = ruleItem;
+        this.customerConfigForUnbind.selected = ruleItem.delCustomer || [];
+      }
+    },
+    /**
+     * 确定绑定客服
+     */
+    customerConfirmCallForBind() {
+      this.customerConfigForBind.visible = false;
+    },
+    /**
+     * 确定解绑客服
+     */
+    customerConfirmCallForUnbind() {
+      this.customerConfigForUnbind.visible = false;
+    },
+    /**
+     * 删除客服操作
+     * @param ruleItem
+     * @param type 0-绑定 1-解绑
+     * @param customer 客服
+     */
+    handleCloseCustomer(ruleItem, type, customer) {
+      //绑定
+      if (type === 0) {
+        const index = this.customerConfigForBind.selected.findIndex(t => t === customer);
+        if (index !== -1) {
+          this.customerConfigForBind.selected.splice(index, 1);
+          ruleItem.addCustomer = this.customerConfigForBind.selected;
+        }
+      }
+      //解绑
+      else {
+        const index = this.customerConfigForUnbind.selected.findIndex(t => t === customer);
+        if (index !== -1) {
+          this.customerConfigForUnbind.selected.splice(index, 1);
+          ruleItem.delCustomer = this.customerConfigForUnbind.selected;
+        }
+      }
+    },
+    /**
+     * 选中的绑定客服列表
+     * @param list
+     */
+    customerSelectionChangeCallForBind(config) {
+      let selected = config.selected;
+      //单行操作
+      if (config.mode === 0) {
+        let row = config.row;
+        if (selected) {
+          if (!this.customerConfigForBind.selected.some(s => s.id == row.id)) {
+            this.customerConfigForBind.selected.push(row)
+          }
+        } else {
+          this.customerConfigForBind.selected = this.customerConfigForBind.selected.filter(m => m.id != row.id);
+        }
+      }
+      //操作当前页所有行
+      else {
+        let list = config.rows;
+        if (selected) {
+          list.map(l => {
+            if (!this.customerConfigForBind.selected.some(s => s.id == l.id)) {
+              this.customerConfigForBind.selected.push(l);
+            }
+          });
+        } else {
+          list.map(l => {
+            for (let i = 0; i < this.customerConfigForBind.selected.length; i++) {
+              if (l.id == this.customerConfigForBind.selected[i].id) {
+                this.customerConfigForBind.selected.splice(i, 1);
+              }
+            }
+          })
+        }
+      }
+      this.$set(this.customerConfigForBind.currentRuleItem, 'addCustomer', this.customerConfigForBind.selected);
+    },
+    /**
+     * 选中的解绑客服列表
+     * @param list
+     */
+    customerSelectionChangeCallForUnbind(config) {
+      let selected = config.selected;
+      //单行操作
+      if (config.mode === 0) {
+        let row = config.row;
+        if (selected) {
+          if (!this.customerConfigForUnbind.selected.some(s => s.id == row.id)) {
+            this.customerConfigForUnbind.selected.push(row)
+          }
+        } else {
+          this.customerConfigForUnbind.selected = this.customerConfigForUnbind.selected.filter(m => m.id != row.id);
+        }
+      }
+      //操作当前页所有行
+      else {
+        let list = config.rows;
+        if (selected) {
+          list.map(l => {
+            if (!this.customerConfigForUnbind.selected.some(s => s.id == l.id)) {
+              this.customerConfigForUnbind.selected.push(l)
+            }
+          });
+        } else {
+          list.map(l => {
+            for (let i = 0; i < this.customerConfigForUnbind.selected.length; i++) {
+              if (l.id == this.customerConfigForUnbind.selected[i].id) {
+                this.customerConfigForUnbind.selected.splice(i, 1);
+              }
+            }
+          })
+        }
+      }
+      this.$set(this.customerConfigForUnbind.currentRuleItem, 'delCustomer', this.customerConfigForUnbind.selected);
+    },
+    /**
+     * 上传文件前置处理
+     * @param file
+     */
+    beforeAvatarUploadFile(file) {
+      const isLt1M = file.size / 1024 / 1024 < 10;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 10MB!');
+      }
+      return isLt1M;
+    },
+    /**
+     * 上传文件成功回调
+     * @param res
+     * @param file
+     * @param content
+     */
+    handleAvatarSuccessFile(res, file, content) {
+      if (res.code === 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(content, 'fileUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+
+    //下载文件
+    downloadUrl(materialUrl) {
+      // 直接返回文件 URL
+      return materialUrl;
+    },
+    /**
+     * 上传视频
+     * @param file
+     */
+    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;
+    },
+        /**
+     * 上传视频成功回调
+     * @param res
+     * @param file
+     * @param rules
+     */
+    handleAvatarSuccessVideo(res, file, rules) {
+      if (res.code == 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(rules, 'videoUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+    /**
+     * 查询sop模板列表
+     */
+    getList() {
+      this.loading = true;
+      listSopTemp(this.queryParams).then(response => {
+        this.sopTempList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    /**
+     * 表单重置
+     */
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        setting: null,
+        status: "0",
+        sort: null,
+        createTime: null,
+        createBy: null,
+        companyId: null
+      };
+      this.resetForm("form");
+    },
+    /**
+     * 初始化操作
+     * @param id
+     */
+    async initTempInfo(id) {
+      getSopTemp(id).then(response => {
+        this.form = response.data || {};
+        //获取当前模板已维护天数
+        this.days = this.form.days || [];
+        //解释
+        //days:一共多少天
+        //days[dayIndex].rules[ruleIndex]:消息(包含时间:time,消息类别:contentType,具体内容:setting【可能存在多个】)
+        //days[dayIndex].rules[ruleIndex].isOfficial,0-非官方,1-官方
+        if (Array.isArray(this.days)) {
+          //遍历所有天
+          this.days.forEach(day => {
+            if (day && Array.isArray(day.rules)) {
+              //遍历当天的所有规则
+              day.rules.forEach(ruleItem => {
+                this.$set(ruleItem, 'meta', {courseList: [], videoList: []});
+                if (ruleItem && Array.isArray(ruleItem.setting)) {
+                  ruleItem.addTag = [];
+                  ruleItem.delTag = [];
+                  ruleItem.addCustomer = [];
+                  ruleItem.delCustomer = [];
+                  //遍历当前规则的所有内容
+                  ruleItem.setting.forEach(content => {
+                    this.$set(content, 'meta', {
+                      pkgLoading: false,
+                      pkgList: this.pkgList,
+                    });
+                    if (content && !Object.prototype.hasOwnProperty.call(content, 'isSalesCallAdded')) {
+                      this.$set(content, 'isSalesCallAdded', false);
+                    }
+                  });
+                }
+              });
+            }
+          });
+        }
+        if (Array.isArray(this.form.days)) {
+          this.form.days.forEach(e => e.newDay = e.dayNum);
+          this.dayList = JSON.parse(JSON.stringify(this.form.days));
+        } else {
+          this.form.days = [];
+          this.dayList = [];
+        }
+      }).catch(e => {
+        console.error('加载SOP模板详情失败', e);
+      });
+    },
+    isOfficialChange(rules) {
+      this.$set(rules, 'time', '00:30');
+    },
+    /**
+     * 确认分割后的内容
+     */
+    comfirmContent() {
+      this.openShowContent = false;
+    },
+    showOpenContent(dayIndex, rulesIndex, contentIndex) {
+      let setItem = this.days[dayIndex].rules[rulesIndex].setting[contentIndex];
+      let refKey = `textarea-${dayIndex}-${rulesIndex}-${contentIndex}`;
+      let textarea = this.$refs[refKey][0]?.$refs?.textarea;
+      if (!textarea) return;
+      this.openShowContent = true;
+      this.contentList = [];
+      this.contentListData = [];
+      let str = setItem.value;
+      this.contentList = str.split("|&|");
+      let idx = 1;
+      this.contentList.forEach(a => {
+        this.contentListData.push({ index: idx++, content: a });
+      })
+    },
+    /**
+     * 复制分隔符
+     */
+    cppySpliter() {
+      navigator.clipboard.writeText("|&|").then(() => {
+        this.$message({
+          message: '复制成功',
+          type: 'success'
+        });
+      }).catch(err => {
+        this.$message({
+          message: '复制失败',
+          type: 'warning'
+        });
+      });
+    }
+  }
+};
+</script>
+<style scoped>
+.el-button {
+  margin-left: 10px;
+}
+
+.red:hover {
+  color: #dbdbdb !important;
+}
+
+.red {
+  background-color: #F56C6C !important;
+  color: #fff !important;
+}
+
+::v-deep  .el-dialog__footer {
+  height: 55px;
+}
+
+.custom-input ::v-deep  .el-input__inner {
+  height: 20px;
+  text-align: center;
+}
+
+.custom-input ::v-deep  .el-input__icon {
+  line-height: 10px;
+}
+
+.el-icon-delete {
+  cursor: pointer;
+}
+
+::v-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>

+ 1207 - 0
src/views/app/sop/userLogs/sopLogsList.vue

@@ -0,0 +1,1207 @@
+<template>
+  <!-- 执行详情 -->
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="120px">
+      <el-form-item label="会员ID" prop="fsUserId">
+        <el-input
+          v-model="queryParams.fsUserId"
+          placeholder="请输入会员ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="fsUserName">
+        <el-input
+          v-model="queryParams.fsUserName"
+          placeholder="请输入用户昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="加密号码" prop="phone">
+        <el-input
+          v-model="queryParams.phone"
+          placeholder="请输入手机号码"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="标签" prop="tagIds">
+        <div @click="openTagSearchPage"
+             style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
+          <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+            <el-tag type="success"
+                closable
+                :disable-transitions="false"
+                v-for="list in this.tagSearchConfig.selected"
+                :key="list.tagId"
+                @close="handleCloseTags(list)"
+                style="margin: 3px;"
+            >{{ list.tagName }}
+            </el-tag>
+          </div>
+        </div>
+      </el-form-item>
+      <el-form-item label="客服" prop="customerRoles">
+        <div @click="openCusSearchPage"
+             style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
+          <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+            <el-tag type="success"
+                closable
+                :disable-transitions="false"
+                v-for="list in this.bindAISearch.selected"
+                :key="list.id"
+                @close="handleCloseCus(list)"
+                style="margin: 3px;"
+            >{{ list.roleName }}
+            </el-tag>
+          </div>
+        </div>
+      </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="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+        >导出</el-button>
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+        >批量删除</el-button>
+
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="multiple"
+          @click="handleEditCourse"
+        >再次发送记录</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="sopLogsList"  @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="ID" align="center" prop="id" />
+      <el-table-column label="消息来源" align="center" prop="sourceFrom" min-width="120">
+        <template slot-scope="scope">
+          <dict-tag :options="appMessageSendSourceFromOptions" :value="scope.row.sourceFrom + ''"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="用户id" align="center" prop="fsUserId" min-width="170"/>
+      <el-table-column label="用户昵称" align="center" prop="fsUserName"min-width="140"/>
+      <el-table-column label="客服昵称" align="center" prop="customerName" min-width="150"/>
+      <el-table-column label="发送的消息" align="center" prop="contentJson" >
+        <template slot-scope="scope">
+          <el-button type="text" @click="showContentDialog(scope.row)">
+            查看详情
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="发送状态" align="center" prop="appSendStatus" min-width="120">
+        <template slot-scope="scope">
+          <dict-tag :options="sopLogsStatus" :value="scope.row.appSendStatus + ''"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="预计发送时间" align="center" prop="sendTime" min-width="180"/>
+      <el-table-column label="实际发送时间" align="center" prop="realSendTime" min-width="180"/>
+      <el-table-column label="生成时间" align="createTime" prop="createTime" min-width="180"/>
+      <el-table-column label="备注" align="center" prop="appSendRemark" min-width="180"/>
+    </el-table>
+
+    <el-dialog :visible.sync="contentDialog.isDialogVisible" title="消息详情" width="30%" append-to-body>
+      <div>
+        <span>课节:{{contentDialog.videoId}}</span>
+        <!-- 已发送 -->
+        <template v-if="contentDialog.row.appSendStatus !== '0'">
+          <div v-for="(item, index) in contentDialog.json || []" :key="index">
+            <el-card class="box-card" style="margin-top: 2%">
+              <div slot="header" class="clearfix" style="display: flex;justify-content: space-between;align-items: center;">
+                <div>
+                  <span>类型:</span>
+                  <el-tag
+                    type="primary"
+                    v-for="op in contentTypeOptions"
+                    :key="op.dictValue"
+                    v-if="item.contentType == op.dictValue"
+                  >
+                    {{ op.dictLabel }}
+                </el-tag>
+                </div>
+                <div>
+                  发送状态:<el-tag :type="item.success ? 'success' : 'danger'" v-text="item.success ? '成功' : '失败'"/>
+                </div>
+              </div>
+              <div v-if="!item.success">失败原因:<span style="color: #ff4949">{{item.message}}</span></div>
+
+              <!-- 文本 -->
+              <div v-if="item.contentType == 11" v-html="item.value"></div>
+
+              <!-- 图片 -->
+              <div v-else-if="item.contentType == 2" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: end; width: 100%">
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.imgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 视频 -->
+              <div v-else-if="item.contentType == 6" class="message-style">
+                <video
+                  :src="item.videoUrl"
+                  controls
+                  style="width: 200px; height: 100px">
+                </video>
+              </div>
+
+              <!-- 语音 -->
+              <div v-else-if="item.contentType == 12" class="message-style">
+                <span>
+                  {{ item.value }}
+                </span>
+                <div v-if="!item.voiceUrl" style="margin-top: 3px"  >
+                  <span style="color: red">无语音地址</span>
+                </div>
+              </div>
+
+              <!-- 科普 -->
+              <div v-else-if="item.contentType == 9" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%"
+                  @click="openLink(item.linkUrl)">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.linkTitle }}</br>
+                      {{ item.linkDescribe }}
+                    </div>
+                    <!-- 点击图片时停止事件冒泡 -->
+                    <el-image
+                      style="width: 50px; height: 50px; flex-shrink: 0"
+                      :src="item.linkImageUrl"
+                      fit="contain"
+                      @click.stop="openImageViewer(item.linkImageUrl)" />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 疗法 -->
+              <div v-else-if="item.contentType == 20" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.packageName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.packageImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 直播 -->
+              <div v-else-if="item.contentType == 21" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.liveTitle }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.liveImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 民品 -->
+              <div v-else-if="item.contentType == 22" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.productName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.productImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 药品 -->
+              <div v-else-if="item.contentType == 23" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.medicinesName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.medicinesImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 短视频 -->
+              <div v-else-if="item.contentType == 24" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.shortVideoName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.shortVideoImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 文章 -->
+              <div v-else-if="item.contentType == 25" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.articleName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.articleImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 公开课 -->
+              <div v-else-if="item.contentType == 26" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.openClassVideoName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.openClassVideoImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+            </el-card>
+          </div>
+        </template>
+        <!-- 待发送 -->
+        <template v-else>
+          <div v-for="(item, index) in contentDialog.contentJson.setting || []" :key="index">
+            <el-card class="box-card" style="margin-top: 2%">
+
+              <div slot="header" class="clearfix" style="display: flex;justify-content: space-between; align-items: center;">
+                <div>
+                  <span>类型:</span>
+                  <el-tag
+                    type="primary"
+                    v-for="op in contentTypeOptions"
+                    :key="op.dictValue"
+                    v-if="item.contentType == op.dictValue"
+                  >
+                    {{ op.dictLabel }}
+                </el-tag>
+                </div>
+                <div>
+                  发送状态:<el-tag type="primary" v-text="'待发送'"/>
+                </div>
+              </div>
+
+              <!-- 文本 -->
+              <div v-if="item.contentType == 11" v-html="item.value"></div>
+
+              <!-- 图片 -->
+              <div v-else-if="item.contentType == 2" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: end; width: 100%">
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.imgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 视频 -->
+              <div v-else-if="item.contentType == 6" class="message-style">
+                <video
+                  :src="item.videoUrl"
+                  controls
+                  style="width: 200px; height: 100px">
+                </video>
+              </div>
+
+              <!-- 语音 -->
+              <div v-else-if="item.contentType == 12" class="message-style">
+                <span>
+                  {{ item.value }}
+                </span>
+                <div v-if="!item.voiceUrl" style="margin-top: 3px"  >
+                  <span style="color: red">无语音地址</span>
+                </div>
+              </div>
+
+              <!-- 科普 -->
+              <div v-else-if="item.contentType == 9" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%"
+                  @click="openLink(item.linkUrl)">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.linkTitle }}</br>
+                      {{ item.linkDescribe }}
+                    </div>
+                    <!-- 点击图片时停止事件冒泡 -->
+                    <el-image
+                      style="width: 50px; height: 50px; flex-shrink: 0"
+                      :src="item.linkImageUrl"
+                      fit="contain"
+                      @click.stop="openImageViewer(item.linkImageUrl)" />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 疗法 -->
+              <div v-else-if="item.contentType == 20" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.packageName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.packageImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 直播 -->
+              <div v-else-if="item.contentType == 21" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.liveTitle }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.liveImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 民品 -->
+              <div v-else-if="item.contentType == 22" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.productName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.productImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 药品 -->
+              <div v-else-if="item.contentType == 23" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.medicinesName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.medicinesImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 短视频 -->
+              <div v-else-if="item.contentType == 24" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.shortVideoName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.shortVideoImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 文章 -->
+              <div v-else-if="item.contentType == 25" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.articleName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.articleImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+              <!-- 公开课 -->
+              <div v-else-if="item.contentType == 26" class="message-style">
+                <div
+                  style="background-color: white; padding: 10px; width: 100%">
+                  <div style="display: flex; justify-content: space-between; width: 100%">
+                    <div style="font-size: 13px; flex: 1; display: flex; align-items: center;">
+                      {{ item.openClassVideoName }}
+                    </div>
+                    <ImageUpload
+                      :disabled="true"
+                      v-model="item.openClassVideoImgUrl"
+                      type="image"
+                      :num="1"
+                      :file-size="1"
+                      :width="80"
+                      :height="80"
+                      style="margin-top: 1%;"
+                    />
+                  </div>
+                </div>
+              </div>
+
+            </el-card>
+          </div>
+        </template>
+      </div>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="contentDialog.isDialogVisible = false">关闭</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 大图预览对话框 -->
+    <el-dialog
+      :visible.sync="dialogVisible"
+      :modal="false"
+      width="500"
+      append-to-body>
+      <img
+        :src="this.dialogImageUrl"
+        style="display: block; max-width: 100%; margin: 0 auto"
+      />
+    </el-dialog>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-dialog
+      title="标签搜索"
+      :visible.sync="tagSearchConfig.visible"
+      custom-class="dialog-search-tag"
+      append-to-body
+    >
+      <div>搜索标签:
+        <el-input v-model="tagSearchConfig.queryParams.groupName" placeholder="请输入标签名称" clearable size="small"
+          style="width: 200px;margin-right: 10px" />
+        <el-button type="primary" icon="el-icon-search" size="mini"
+          @click="handleSearchTags()">
+          搜索
+        </el-button>
+        <el-button type="primary" icon="el-icon-refresh" size="mini" @click="cancelSearchTags">重置</el-button>
+      </div>
+      <div class="dialog_body_tag">
+        <div v-for="item in tagSearchConfig.tagGroupList" :key="item.id">
+          <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+            <span class="name-background">{{ item.name }}</span>
+          </div>
+          <div class="tag-container">
+            <a v-for="tagItem in item.tags" class="tag-box" @click="tagSelection(tagItem)"
+                :class="{ 'tag-selected': tagItem.isSelected || (tagSearchConfig.selectTags && tagSearchConfig.selectTags.some(tg => tg.id == tagItem.id)) }">
+                {{ tagItem.name }}
+            </a>
+          </div>
+        </div>
+      </div>
+
+      <pagination
+        v-show="tagSearchConfig.total > 0"
+        :total="tagSearchConfig.total"
+        :page.sync="tagSearchConfig.queryParams.pageNum"
+        :limit.sync="tagSearchConfig.queryParams.pageSize"
+        @pagination="getSearchTagGroupList"
+      />
+      <div slot="footer" class="dialog-footer">
+          <el-button type="primary" @click="tagSearchConfig.visible = false">确 定</el-button>
+          <el-button @click="tagSearchConfig.visible = false">取消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="客服搜索" :visible.sync="bindAISearch.visible" width="1000px" append-to-body>
+      <CusRoleList
+        ref="QwUserList"
+        @confirm="customerSelectedConfirmCallForBind"
+        @selectionChange="customerSelectionChangeCallForBind"
+        :selected="bindAISearch.selected"
+        :ids="customerIds"
+      />
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { exportSopLogs, listSopLogsList, delSopLogs, editCourseSopLogs} from '@/api/app/sopLogs';
+
+import { listTagGroupForUserBindTag } from "@/api/app/tag/tagGroup";
+// import { getLiveOptions,} from "@/api/app/live";
+
+
+import CusRoleList from '@/views/app/user/CusRoleList.vue';
+import ImageUpload from "@/views/qw/sop/ImageUpload.vue";
+
+export default {
+  name: "sopLogsList",
+  components: {CusRoleList, ImageUpload},
+  props:{
+    rowDetailFrom:{},
+  },
+
+  watch:{
+    rowDetailFrom:{
+      handler(newVal){
+        // 当formData变化时重新查询
+        this.getList(newVal);
+      },
+      deep: true
+    }
+  },
+  computed: {
+    customerIds() {
+      return this.rowDetailFrom.qwUserIds ? this.rowDetailFrom.qwUserIds.split(',') : []
+    },
+  },
+  data() {
+    return {
+      //图片放大
+      dialogVisible: false,
+      dialogImageUrl:null,
+      //时间选择
+      scheduleTime: null,
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // SOP  定时任务表格数据
+      sopLogsList: [],
+
+      //成员状态
+      sopLogsStatus:[],
+      //消息来源
+      appMessageSendSourceFromOptions: [],
+
+      //SOP发送类型
+      sysQwSopType: [],
+
+      //发送的消息
+      contentDialog:{
+        isDialogVisible:false,
+        json: [],
+        videoId: null,
+        row: {},
+      },
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        fsUserId: null,
+        fsUserName: null,
+        phone: null,
+        tagIds: [],
+        customerIds: [],
+        sopId: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      },
+      tagSearchConfig: {
+        visible: false,//对话框是否展示
+        queryParams: {
+          groupName: null,
+          pageNum: 1,
+          pageSize: 10,
+        },
+        total: 0,
+        tagGroupList: [],
+        selected: [],
+      },
+      bindAISearch: {
+        visible: false,
+        selected: [],
+      },
+      contentTypeOptions: [],//发送消息的内容类型
+    };
+  },
+  created() {
+    this.getList(this.rowDetailFrom);
+
+    //发送状态
+    this.getDicts("app_sopLogs_status").then(response => {
+      this.sopLogsStatus = response.data;
+    });
+
+    //消息来源
+    this.getDicts("app_message_send_source_from").then(response => {
+      this.appMessageSendSourceFromOptions = response.data;
+    });
+
+    this.getDicts("app_sop_plugin_settingType").then(response => {
+      this.contentTypeOptions = response.data;
+    });
+
+  },
+  methods: {
+    // async getLiveList() {
+    //   return await getLiveOptions();
+    // },
+    getList(val) {
+      let qp = {...this.queryParams};
+      qp['sopId'] = val.id || this.rowDetailFrom.id;
+      qp['tagIds'] = this.tagSearchConfig.selected.map(tag => tag.tagId);
+      qp['customerIds'] = this.bindAISearch.selected.map(s => s.id);
+      this.loading = true;
+      listSopLogsList(qp).then(response => {
+        this.sopLogsList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        qwUserName: null,
+        externalUserName: null,
+        logType: null,
+        contentJson: null,
+        sendStatus: null,
+        sendTime: null,
+        companyId: null,
+        receivingStatus: null,
+        msgId: null,
+        sendType: null,
+        sopId: null
+      };
+      this.resetForm("form");
+    },
+
+    openImageViewer(url) {
+      // 打开大图预览对话框
+      this.dialogImageUrl=url
+      this.dialogVisible = true;
+    },
+    openLink(url){
+      // 使用 window.open 打开链接
+      window.open(url, '_blank');
+    },
+    //查看发送的消息体
+    showContentDialog(row) {
+      this.contentDialog.row = row;
+      const sanitizedJson = row.contentJson.replace(/[\u0000-\u001F\u007F]/g, '');
+      const parsedData = JSON.parse(sanitizedJson);
+      if (row.appSendStatus !== '0') {
+        const remarkDetailStr = row.remarkDetail.replace(/[\u0000-\u001F\u007F]/g, '');
+        const remarkDetailArray = JSON.parse(remarkDetailStr);
+        remarkDetailArray.map(item => {
+          if (item.meta) {
+            let metaSource = item.meta.replace(/[\u0000-\u001F\u007F]/g, '');
+            let targetMeta = JSON.parse(metaSource);
+            parsedData.setting.map(async s => {
+              if (targetMeta.contentType == s.contentType) {
+                let flag = false;
+                //科普
+                if (s.contentType == 9 && s.appLinkUrl == targetMeta.appLinkUrl) {
+                  flag = true;
+                }
+                //文本
+                else if (s.contentType == 11 && s.value == targetMeta.value) {
+                  flag = true;
+                }
+                //图片
+                else if (s.contentType == 2 && s.imgUrl == targetMeta.imgUrl) {
+                  flag = true;
+                }
+                //视频
+                else if (s.contentType == 6 && s.videoUrl == targetMeta.videoUrl) {
+                  flag = true;
+                }
+                //语音
+                else if (s.contentType == 12 && s.voiceUrl == targetMeta.voiceUrl) {
+                  flag = true;
+                }
+                //疗法
+                else if (s.contentType == 20 && s.packageId == targetMeta.packageId) {
+                  flag = true;
+                }
+                //直播
+                else if (s.contentType == 21 && s.liveId == targetMeta.liveId) {
+                  flag = true;
+                  let defaultLive = await this.getLiveList();
+                  let liveInfo = defaultLive.data.find(tmp => tmp.liveId == s.liveId);
+                  if (liveInfo && liveInfo.liveName) {
+                    s['liveTitle'] = liveInfo.liveName;
+                  } else {
+                    s['liveTitle'] = '--';
+                  }
+                }
+                //民品
+                else if (s.contentType == 22 && s.productId == targetMeta.productId) {
+                  flag = true;
+                }
+                //药品
+                else if (s.contentType == 23 && s.medicinesId == targetMeta.medicinesId) {
+                  flag = true;
+                }
+                //短视频
+                else if (s.contentType == 24 && s.shortVideoId == targetMeta.shortVideoId) {
+                  flag = true;
+                }
+                //文章
+                else if (s.contentType == 25 && s.articleId == targetMeta.articleId) {
+                  flag = true;
+                }
+                //公开课
+                else if (s.contentType == 26 && s.openClassVideoId == targetMeta.openClassVideoId) {
+                  flag = true;
+                }
+                if (flag) {
+                  // 把源对象所有key 赋值给目标响应式对象,不破坏代理
+                  Object.keys(s).forEach(key => {
+                    this.$set(item, key, s[key]);
+                  })
+                }
+              }
+            })
+          }
+        })
+        this.contentDialog.json = remarkDetailArray;
+      }
+      this.contentDialog.contentJson = parsedData;
+      this.contentDialog.videoId = parsedData.videoId;
+      this.contentDialog.isDialogVisible = true;
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList(this.rowDetailFrom);
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.tagSearchConfig.selected = [];
+      this.bindAISearch.selected = [];
+      this.queryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        userId: null,
+        nickName: null,
+        phone: null,
+        tagsId: [],
+        customerIds: [],
+      };
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除【执行详情】id为"【' + ids + '】"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return delSopLogs(ids);
+      }).then(() => {
+        this.getList(this.rowDetailFrom);
+        this.msgSuccess("删除成功");
+      }).catch(() => {});
+    },
+
+    /**
+    * 批量修改完课的发送
+    */
+    handleEditCourse(row){
+
+      // 获取原始ids(确保是数组)
+      let ids = row.id || this.ids;
+      if (!Array.isArray(ids)) {
+        ids = [ids];
+      }
+
+      // 过滤掉remark为'E级客户不发送'的项
+      const filteredIds = ids.filter(id => {
+        const item = this.sopLogsList.find(item => item.id === id);
+        return item?.remark !== 'E级客户不发送';
+      });
+
+      // 如果没有可发送的记录
+      if (filteredIds.length === 0) {
+        this.msgWarning("没有可发送的记录(E级客户已自动排除)");
+        return;
+      }
+
+      this.$confirm('确认要再次发送【执行详情】编号为"【' + filteredIds.join(',') + '】"的【完课】记录?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return editCourseSopLogs(filteredIds);
+      }).then(() => {
+        this.getList(this.rowDetailFrom);
+        this.msgSuccess("已经 修改记录为 待发送~");
+      }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      let qp = {...this.queryParams};
+      qp['sopId'] = val.id || this.rowDetailFrom.id;
+      qp['tagIds'] = this.tagSearchConfig.selected.map(tag => tag.tagId);
+      qp['customerIds'] = this.bindAISearch.selected;
+      this.$confirm('是否确认导出所有自动化  定时任务数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportSopLogs(qp);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    },
+    openTagSearchPage() {
+      this.getSearchTagGroupList();
+      this.tagSearchConfig.visible = true;
+    },
+    getSearchTagGroupList() {
+      listTagGroupForUserBindTag(this.tagSearchConfig.queryParams).then(response => {
+        let tRows = response.rows;
+        if (tRows && tRows.length > 0) {
+          tRows.forEach(group => {
+            group.tags.forEach(tag => {
+              if (this.tagSearchConfig.selected.some(item => item.tagId === tag.id)) {
+                tag.isSelected = true;
+              } else {
+                tag.isSelected = false;
+              }
+            })
+          });
+        }
+        this.tagSearchConfig.tagGroupList = tRows;
+        this.tagSearchConfig.total = response.total;
+      });
+    },
+    getTagGroupList() {
+      listTagGroupForUserBindTag(this.tagSearchConfig.queryParams).then(response => {
+        this.tagSearchConfig.tagGroupList = response.rows;
+        this.tagSearchConfig.total = response.total;
+      });
+    },
+    openCusSearchPage() {
+      this.bindAISearch.visible = true;
+    },
+    cancelSearchTags() {
+      this.tagSearchConfig.queryParams = {
+        groupName: null,
+        pageNum: 1,
+        pageSize: 10,
+      }
+      this.getSearchTagGroupList();
+    },
+    handleSearchTags() {
+      this.tagSearchConfig.queryParams.pageNum = 1;
+      this.getSearchTagGroupList();
+    },
+    tagSelection(row) {
+      row.isSelected = !row.isSelected;
+      if (row.isSelected) {
+        this.tagSearchConfig.selected.push({tagId: row.id, tagName: row.name});
+      }
+      //
+      else {
+        this.tagSearchConfig.selected = this.tagSearchConfig.selected.filter(item => item.tagId !== row.id);
+      }
+      this.$forceUpdate();
+    },
+    /**
+     * 客服选中
+     */
+    customerSelectionChangeCallForBind(config) {
+      let selected = config.selected;
+      //单行操作
+      if (config.mode === 0) {
+        let row = config.row;
+        if (selected) {
+          if (!this.bindAISearch.selected.some(s => s.id == row.id)) {
+            this.bindAISearch.selected.push(row)
+          }
+        } else {
+          this.bindAISearch.selected = this.bindAISearch.selected.filter(m => m.id != row.id);
+        }
+      }
+      //操作当前页所有行
+      else {
+        let list = config.rows;
+        if (selected) {
+          list.map(l => {
+            if (!this.bindAISearch.selected.some(s => s.id == l.id)) {
+              this.bindAISearch.selected.push(l)
+            }
+          });
+        } else {
+          list.map(l => {
+            for (let i = 0; i < this.bindAISearch.selected.length; i++) {
+              if (l.id == this.bindAISearch.selected[i].id) {
+                this.bindAISearch.selected.splice(i, 1);
+              }
+            }
+          })
+        }
+      }
+    },
+    /**
+     * 确认客服
+     * @param list
+     */
+    customerSelectedConfirmCallForBind() {
+      this.bindAISearch.visible = false;
+    },
+    handleCloseCus(cus) {
+      this.bindAISearch.selected = this.bindAISearch.selected.filter(item => item.id != cus.id);
+    },
+    handleCloseTags(tag) {
+      this.tagSearchConfig.selected = this.tagSearchConfig.selected.filter(item => item.tagId != tag.tagId);
+    },
+  }
+};
+</script>
+<style scoped>
+.message-stayle{
+  display: flex;
+  justify-content: normal;
+  align-items: center;
+  margin-top: 10px;
+}
+.message-stayle .el-link {
+  white-space: normal; /* 允许换行 */
+  word-break: break-all; /* 单词中间断行 */
+  overflow-wrap: break-word; /* 允许在单词内换行 */
+}
+.message-stayle span {
+  word-break: break-all;
+}
+
+.tag-container {
+  display: flex;
+  flex-wrap: wrap; /* 超出宽度时自动换行 */
+  gap: 8px; /* 设置标签之间的间距 */
+}
+
+.name-background {
+  display: inline-block;
+  background-color: #abece6; /* 背景颜色 */
+  padding: 4px 8px; /* 调整内边距,让背景包裹文字 */
+  border-radius: 4px; /* 可选:设置圆角 */
+}
+
+.tag-box {
+  padding: 8px 12px;
+  border: 1px solid #989797;
+  border-radius: 4px;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.tag-selected {
+  background-color: #00bc98;
+  color: #fff;
+  border-color: #00bc98;
+}
+
+.el-tag + .el-tag {
+  margin-left: 10px;
+}
+
+.dialog_body_tag {
+  height: 450px;
+  overflow: auto;
+}
+</style>

+ 613 - 0
src/views/app/sop/userLogs/sopUserLogsSchedule.vue

@@ -0,0 +1,613 @@
+<template>
+  <div class="app-container">
+    <div style="margin-bottom: 10px">
+      <el-card>
+        <span class="custom-style" style="display: block; margin-bottom: 10px">自动化规则名称:{{ sopName }}</span>
+        <span class="custom-style" style="display: block; margin-bottom: 10px">自动化规则编号:{{ queryParams.sopId }}</span>
+        <span class="custom-style" style="display: block;">模板编号:{{ tempId }}</span>
+      </el-card>
+    </div>
+
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="客服id" prop="customerId">
+        <el-input
+          v-model="queryParams.customerId"
+          @input="queryParams.customerId = queryParams.customerId.replace(/[^\d]/g, '')"
+          placeholder="请输入客服id"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="客服名称" prop="customerName">
+        <el-input v-model="queryParams.customerName" placeholder="请输入客服名称" clearable size="small"
+          @keyup.enter.native="handleQuery" />
+      </el-form-item>
+      <el-form-item label="会员id" prop="userId">
+        <el-input
+          v-model="queryParams.userId"
+          @input="queryParams.userId = queryParams.userId.replace(/[^\d]/g, '')"
+          placeholder="请输入会员id"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="营期时间" prop="startTime">
+        <el-date-picker clearable size="small" v-model="queryParams.startTime" type="date" value-format="yyyy-MM-dd"
+          placeholder="选择营期时间">
+        </el-date-picker>
+      </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 sopUserLogsDelStatus" :key="dict.dictValue" :label="dict.dictLabel"
+            :value="dict.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+
+      <el-col :span="1.5">
+        <el-tooltip class="item" effect="dark"
+            content="此功能用于给 选中的 营期 内【所有的】客户发送消息" placement="top">
+          <el-button
+            type="warning"
+            icon="el-icon-s-promotion"
+            size="medium"
+            :disabled="multiple"
+            @click="openGroupSendMessagePage"
+          >一键群发
+          </el-button>
+        </el-tooltip>
+      </el-col>
+
+      <el-col :span="1.5">
+        <el-tooltip class="item" effect="dark" content="删除营期之后,将不会在给原营期的客户发送消息,ps:删除之后不可恢复" placement="top">
+          <el-button type="danger" icon="el-icon-s-promotion" size="medium" :disabled="multiple"
+            @click="handleDeleteUserLogs">批量删除营期
+          </el-button>
+
+        </el-tooltip>
+      </el-col>
+      <el-col :span="1.5">
+        <el-tooltip class="item" effect="dark" content="批量修改营期之后,将不会在给原营期的客户发送消息,ps:修改日期之后原日期不可恢复" placement="top">
+          <el-button type="info" icon="el-icon-s-promotion" size="medium" :disabled="multiple"
+            @click="handleUpdateTimeUserLogs">批量修改营期时间
+          </el-button>
+        </el-tooltip>
+      </el-col>
+    </el-row>
+
+    <div style="color: #999;font-size: 14px;display: flex;align-items: center;margin-bottom: 5px">
+      <i class="el-icon-info"></i>
+      【营期一键群发】:此功能用于给 选中的 营期 内【所有的】客户发送 消息
+    </div>
+
+    <div style="color: #999;font-size: 14px;display: flex;align-items: center;margin-bottom: 5px">
+      <i class="el-icon-info"></i>
+      【批量删除营期】:此功能用于删除选中的【整个营期】,删除之后将不会在给原营期的客户发送消息,ps:删除之后不可恢复
+    </div>
+
+    <div style="color: #999;font-size: 14px;display: flex;align-items: center;margin-bottom: 5px">
+      <i class="el-icon-info"></i>
+      【批量修改营期时间】:此功能用于修改选中的【整个营期时间】,修改之后将不会在给原营期时间的客户发送消息,ps:修改之后不可恢复
+    </div>
+
+    <div style="color: #999;font-size: 14px;display: flex;align-items: center;margin-bottom: 5px">
+      <i class="el-icon-info"></i>
+      【天数】:【列表:营期时间】对应列表中的天数是几 就代表着 插件助手 会发送【任务模板】里的第几天的消息
+    </div>
+    
+    <el-table border v-loading="loading" :data="sopUserLogsList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="营期编号" align="center" prop="id" />
+      <el-table-column label="客服id" align="center" prop="customerId" />
+      <el-table-column label="客服名称" align="center" prop="customerName" />
+      <el-table-column label="营期时间" align="center" prop="startTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="待发天数" align="center" prop="countDays"/>
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <div v-if="scope.row.userId && scope.row.userId.includes('null')">
+            <span style="color: orange;">营期异常</span>
+          </div>
+          <div v-else>
+            <dict-tag :options="sopUserLogsDelStatus" :value="scope.row.status" />
+          </div>
+
+        </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.status == 1" size="mini" type="text" icon="el-icon-edit"
+            @click="handleSelect(scope.row)">营期详情
+          </el-button>
+          <el-button v-if="scope.row.userId && scope.row.userId.includes('null')" size="mini" type="text"
+            icon="el-icon-s-check" @click="handleRepairLogs(scope.row)">修复营期
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
+      @pagination="getList" />
+
+    <!--  执行详情  -->
+    <el-drawer :title="sopUserLogsInfoDialog.title" :visible.sync="sopUserLogsInfoDialog.visible" size="88%"
+      style="font-weight: bolder">
+      <sop-user-logs-info-details
+        :refreshCount="sopUserLogsInfoDialog.refreshCount"
+        :params="sopUserLogsInfoDialog.params"
+      />
+    </el-drawer>
+  
+    <el-dialog title="批量修改客户营期" :visible.sync="updateLogsForm.visible" width="500px" append-to-body>
+      <el-form ref="updateLogsForm" :model="updateLogsForm" :rules="updateLogsForm.rules" label-width="120px">
+        <el-form-item label="选择营期时间" prop="paramTime">
+          <el-date-picker clearable size="small"
+            v-model="updateLogsForm.pTime"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择营期时间">
+          </el-date-picker>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="一键群发"
+      :visible.sync="groupSendMessageForm.visible" width="1000px" append-to-body :close-on-click-modal="false">
+      <group-send-message
+        :visible="groupSendMessageForm.visible"
+        :ids="groupSendMessageForm.ids"
+        :sourceType="0"
+        @changeGroupSendMessageVisible="changeGroupSendMessageVisible"
+      />
+    </el-dialog>
+
+  </div>
+
+</template>
+
+<script>
+import {
+  delSopUserLogs,
+  exportSopUserLogs,
+  listSopUserLogs,
+  repairSopUserLogs,
+  addGroupChat,
+  updateLogDate,
+  updateTimeSopUserLogs
+} from "@/api/app/userLogs";
+import SopUserLogsInfoDetails from "@/views/app/sop/userLogsInfo/sopUserLogsInfoDetails.vue";
+import groupSendMessage from "@/views/app/sop/userLogsInfo/groupSendMessage.vue";
+
+export default {
+  name: "sopUserLogsSchedule",
+  components: {
+    SopUserLogsInfoDetails,
+    groupSendMessage,
+  },
+  props: {
+    rowDetailFrom: {},
+  },
+  data() {
+    return {
+      groupSendMessageForm: {
+        ids: [],
+        visible: false,
+      },
+      qwUserIds: [],
+      sopUserLogId: null,
+      companyUserLists: [],
+      sopName: '',
+      tempId: '',
+      listUser: {
+        title: '选择发送人',
+        open: false
+      },
+      updateLogsForm: {
+        visible: false,
+        pTime: null,
+        rules: {
+          pTime: { required: true, message: '选择的时间不能为空', trigger: 'blur' }
+        }
+      },
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      filterMode: 1,
+      // 选中数组
+      ids: [],
+      chatNames: [],
+      statusOptions: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      sysQwSopAiContentType: [],
+      userSelectList: [],
+      // sopUserLogs表格数据
+      sopUserLogsList: [],
+      sopUserLogsDelStatus: [],
+      // 弹出层标题
+      title: "",
+      mini: {
+        open: false,
+        loading: false,
+        row: {},
+      },
+      // 是否显示弹出层
+      open: false,
+      // 一键发群相关数据
+      uploadUrl: process.env.VUE_APP_BASE_API + "/app/common/uploadOSS",
+      uploadUrlByVoice: process.env.VUE_APP_BASE_API + "/app/common/uploadOSSByHOOKVoice",
+      msgForm: {
+        videoId: null,
+        courseId: null,
+        courseType: null,
+        userIdParam: null,
+        sendType: 1,
+        setting: null,
+        ids: null,
+        sopId: null,
+        startTime: null,
+        chatIds: [],
+        filterMode: 2,
+        sendTime: null,
+        sendTimeType: 1, // 添加字段:1-当天发送,2-次日发送
+      },
+      sendMsgOpen: {
+        title: '一键批量群发',
+        open: false,
+        row: {},
+        ids: null,
+        chatIds: [],
+      },
+      msgRules: {},
+      videoList: [],
+      chatList: [],
+      setting: [],
+      // SOP科普观看状态
+      sysFsSopWatchStatus: [],
+      // 查询参数
+      addGroupData: {
+        open: false,
+        userOpen: false,
+        selectChat: [],
+        form: {
+          chatIds: [],
+        },
+      },
+      updateTimeData: {
+        open: false,
+        form: {
+          date: null,
+        },
+      },
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        sopId: null,
+        userLogsId: null,
+        sopTempId: null,
+        useId: null,
+        customerId: null,
+        customerName: null,
+        startTime: null,
+        status: null,
+        type: null,
+        chatName: null,
+      },
+      // 表单参数
+      form: {},
+      tempForm: {
+        setting: null,
+        videoIdSet: null,
+        courseIdSet: null,
+      },
+      // 表单校验
+      rules: {},
+      sopUserLogsInfoDialog: {
+        title: null,
+        visible: false,
+        refreshCount: 1,
+        params: null,
+      },
+    };
+  },
+  created() {
+    this.queryParams.sopId = this.$route.params.id;
+    this.sopName = this.$route.query.name;
+    this.filterMode = this.$route.query.filterMode;
+    this.tempId = this.$route.query.tempId;
+    this.queryParams.type = this.$route.query.type;
+    this.getDicts("app_normal_disabled_status").then(response => {
+      this.statusOptions = response.data;
+    });
+
+    this.getDicts("app_normal_del_status").then(response => {
+      this.sopUserLogsDelStatus = response.data;
+    });
+    this.getDicts("app_sop_plugin_settingType").then(response => {
+      this.sysQwSopAiContentType = response.data;
+    });
+    this.getDicts("app_sop_watch_status").then(response => {
+      this.sysFsSopWatchStatus = response.data;
+    });
+
+    this.queryParams.sopId = this.$route.params.id;
+    this.sopName = this.$route.query.name;
+    this.filterMode = this.$route.query.filterMode;
+    this.tempId = this.$route.query.tempId;
+    this.queryParams.type = this.$route.query.type;
+    this.getList()
+  },
+  methods: {
+
+    /** 查询sopUserLogs列表 */
+    getList() {
+      this.loading = true;
+      listSopUserLogs(this.queryParams).then(response => {
+        this.sopUserLogsList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+
+    addSetList() {
+      const newSetting = {
+        contentType: '1',
+        value: '',
+        sendTimeType: 1, // 为每个规则项添加发送时间类型
+      };
+      this.setting.push(newSetting);
+    },
+
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        sopId: null,
+        sopTempId: null,
+        qwUserId: null,
+        externalId: null,
+        corpId: null,
+        startTime: null,
+        status: 0,
+        userId: null
+      };
+      this.resetForm("form");
+    },
+
+    /**
+     * 营期一键群发
+     */
+    openGroupSendMessagePage() {
+      if (this.ids.length === 0) {
+        return this.$message.error('请勾选具体群发记录!');
+      }
+      this.groupSendMessageForm.ids = this.ids;
+      this.groupSendMessageForm.visible = true;
+    },
+    changeGroupSendMessageVisible(visible) {
+      this.groupSendMessageForm.visible = visible;
+    },
+    /**
+     *  批量修改营期
+     */
+    handleUpdateTimeUserLogs() {
+      this.updateLogsForm.visible = true;
+    },
+
+    /**
+     *  删除营期
+     */
+    handleDeleteUserLogs() {
+      const ids = this.ids;
+      this.$confirm('是否确认删除编号为"' + ids + '"的数据项【注意!!删除后不可恢复,请谨慎操作】?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return delSopUserLogs(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {
+      });
+    },
+
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+
+    flashNotify() {
+      this.getList();
+    },
+
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      if (this.filterMode == 2) {
+        this.chatNames = selection.map(item => item.chatName);
+      }
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+
+    handleSelect(val) {
+      val.filterMode = this.filterMode;
+      this.sopUserLogsInfoDialog.title = '【客服id/名称】:' + val.customerId + '/' + val.customerName + ',  ' + '【营期时间】:' + val.startTime + '  ' + '【天数】:' + val.countDays;
+      this.sopUserLogsInfoDialog.params = val;
+      this.sopUserLogsInfoDialog.refreshCount = this.sopUserLogsInfoDialog.refreshCount + 1;
+      this.sopUserLogsInfoDialog.visible = true;
+    },
+
+    handleRepairLogs(val) {
+      this.loading = true;
+      let loadingRock = this.$loading({
+        lock: true,
+        text: '正在修复中请稍后~~!!',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)'
+      });
+
+      repairSopUserLogs(val).then(res => {
+        this.msgSuccess("修复成功成功");
+      }).catch(res => {
+      }).finally(res => {
+        loadingRock.close();
+        this.loading = false;
+        this.getList();
+      })
+    },
+
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有sopUserLogs数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportSopUserLogs(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {
+      });
+    },
+
+    updateGroupTime() {
+      this.updateTimeData.open = true;
+      this.updateTimeData.form = { date: new Date() };
+    },
+
+    submitUpdateTimeForm() {
+      let form = {
+        date: this.updateTimeData.form.date,
+        ids: this.ids,
+      }
+      updateLogDate(form).then(e => {
+        this.updateTimeData.open = false;
+        this.getList();
+      });
+    },
+
+    submitAddGroupForm() {
+      let form = {
+        id: this.queryParams.sopId,
+        qwUserIds: this.userSelectList.join(),
+        chatIds: this.addGroupData.form.chatIds.join(),
+        date: this.addGroupData.form.date,
+      }
+      addGroupChat(form).then(e => {
+        this.addGroupData.open = false;
+        this.getList();
+      });
+    },
+
+    handleCompanyUser() {
+      setTimeout(() => {
+        this.$refs.QwUserList.getDetails(this.queryParams.corpId, this.queryParams.type, 2);
+      }, 1);
+      this.addGroupData.userOpen = true;
+    },
+
+    handleClosegroupUser(list) {
+      const index = this.userSelectList.findIndex(t => t === list);
+      if (index !== -1) {
+        this.userSelectList.splice(index, 1);
+        this.qwUserIds.splice(index, 1);
+        this.loadChatList()
+      }
+    },
+
+    loadChatList() {
+      // chatListAll(this.qwUserIds.join(), this.queryParams.corpId, this.queryParams.sopId).then(e => {
+      //   this.addGroupData.selectChat = e.data;
+      // })
+    },
+
+    selectUserList(list) {
+      this.addGroupData.userOpen = false;
+      list.forEach(obj => {
+        if (!this.userSelectList.some(item => item == obj.id)) {
+          this.userSelectList.push(obj.id);
+          this.qwUserIds.push(obj.qwUserId);
+        }
+      });
+      this.loadChatList()
+    },
+    submitForm() {
+      this.$refs["updateLogsForm"].validate(valid => {
+        if (valid) {
+
+          this.updateLogsForm.ids = this.ids;
+          this.updateLogsForm.sopId= this.queryParams.sopId;
+
+          let loadingRock = this.$loading({
+            lock: true,
+            text: '正在执行中请稍后~~请不要刷新页面!!',
+            spinner: 'el-icon-loading',
+            background: 'rgba(0, 0, 0, 0.7)'
+          });
+          updateTimeSopUserLogs(this.updateLogsForm).then(response => {
+            this.msgSuccess("修改成功");
+            loadingRock.close();
+          }).finally(res=>{
+            loadingRock.close();
+          })
+        }
+      });
+    },
+    cancel() {
+      this.resetBatch();
+    },
+    resetBatch() {
+      this.updateLogsForm.visible = false;
+      this.updateLogsForm.pTime = null;
+    },
+  }
+};
+</script>
+
+<style>
+.custom-style {
+  font-weight: bold; /* 加粗 */
+}
+</style>

+ 1837 - 0
src/views/app/sop/userLogsInfo/groupSendMessage.vue

@@ -0,0 +1,1837 @@
+<template>
+  <div class="app-container">
+
+    <el-alert type="warning" :closable="false" show-icon>
+      <template #title>
+        <span style="font-size: 25px; line-height: 1.5;">
+          此功能用于给 选中的 营期 内【所有的】客户发送 消息
+        </span>
+      </template>
+    </el-alert>
+
+    <el-form
+      ref="groupSendMessageForm"
+      :model="groupSendMessageForm"
+      :rules="groupSendRules"
+      label-width="100px"
+      style="margin-top: 8px;"
+      :loading="loading"
+    >
+      <el-form-item label="科普"
+        style="margin-bottom: 15px;"
+        prop="videoId"
+      >
+        <!-- 禁用条件:操作类型为查看 -->
+        <el-select
+          v-model="groupSendMessageForm.courseId"
+          placeholder="默认50条,可输入关键字检索"
+          style=" margin-right: 10px;"
+          size="mini"
+          filterable
+          clearable
+          :loading="courseLoading"
+          remote
+          :remote-method="(keyword) => handleCourseRemoteSearch(keyword)"
+          @change="courseChangeUpdate()"
+        >
+          <el-option
+            v-for="dict in courseList"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="parseInt(dict.dictValue)"
+          />
+        </el-select>
+
+        <el-select
+          v-model="groupSendMessageForm.videoId"
+          placeholder="请选择小节"
+          size="mini"
+          style=" margin-right: 10px;"
+          filterable
+          clearable
+          @change="videoIdChange()"
+        >
+          <el-option
+            v-for="dict in videoList"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="parseInt(dict.dictValue)"
+          >
+            <div :style="{ color: dict.isPause == 1 ? '#f56c6c' : 'inherit', display: 'flex', alignItems: 'center' }">
+              <span>{{ dict.dictLabel }}</span>
+              <span v-if="dict.isPause == 1" style="font-size: 12px; margin-left: 8px; color: #f56c6c;">
+                  (该科普已被关闭,无法正常发送)
+              </span>
+            </div>
+          </el-option>
+        </el-select>
+
+        <!-- 禁用条件:操作类型为查看 -->
+        <!-- 渲染条件:消息类型不为AI触达 -->
+        <el-select
+          v-model="groupSendMessageForm.courseType"
+          placeholder="请选择消息类型"
+          size="mini"
+          style=" margin-right: 10px;"
+        >
+          <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="规则" style="height: 500px; overflow: auto;">
+        <!-- 遍历规则 所有内容【存在多个】 -->
+        <div
+          v-for="(content, contentIndex) in groupSendMessageForm.setting"
+          :key="contentIndex"
+          style="
+            background-color: #fdfdfd;
+            border: 1px solid #e6e6e6;
+            margin-bottom: 20px;
+          "
+        >
+          <el-row style="padding-bottom: 20px">
+            <el-col :span="22">
+              <el-form
+                :model="content"
+                label-width="100px"
+              >
+                <el-form-item label="内容类别"
+                  style="margin: 2%"
+                >
+                  <div>
+                    <el-radio-group
+                      v-model="content.contentType"
+                      @change="handleContentTypeChange()"
+                    >
+                      <el-radio
+                        :key="item.dictValue"
+                        :label="item.dictValue"
+                        v-for="item in sysQwSopAiContentType"
+                      >{{ item.dictLabel }}
+                      </el-radio>
+                    </el-radio-group>
+                  </div>
+
+                </el-form-item>
+
+                <!-- 科普 -->
+                <el-form-item label="内容" v-if="9 == content.contentType">
+
+                  <div>
+                    <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"
+                          :file-size="1"
+                          :width="150"
+                          :height="150"
+                          style="margin-top: 1%;"
+                        />
+                      </el-form-item>
+                      <el-form-item label="链接地址:"
+                        label-width="100px"
+                      >
+                        <el-tag
+                          type="warning"
+                          v-model="content.isBindUrl"
+                        >选择的科普小节
+                          即为卡片链接地址
+                        </el-tag>
+                      </el-form-item>
+                      <el-form-item label="课节过期时间">
+                        <el-row>
+                          <el-input
+                            type="number"
+                            v-model="content.expiresDays"
+                            style="width: 200px"
+                          >
+                            <template slot="append">天</template>
+                          </el-input>
+                        </el-row>
+                        <el-row>
+                          <span class="tip">填写0或不填时,默认为系统配置的默认时间</span>
+                        </el-row>
+                      </el-form-item>
+                    </el-card>
+                  </div>
+
+                </el-form-item>
+
+                <!-- 文本 -->
+                 <el-form-item label="内容" v-else-if="11 == content.contentType">
+                  <div
+                    style="
+                      color: #999;
+                      font-size: 14px;
+                      display: flex;
+                      align-items: center;
+                    "
+                  >
+                    <i class="el-icon-info"></i>
+                    多语句分割符:|&| 例如:'语句1|&|语句2'
+                  </div>
+                  <el-input
+                    v-model="content.value"
+                    type="textarea"
+                    :rows="3"
+                    placeholder="内容"
+                    style="width: 90%; margin-top: 10px;"
+                    @keydown.native="handleKeydown($event, contentIndex)"
+                    :ref="`textarea-${contentIndex}`"
+                  />
+
+                  <!-- 修改按钮部分 -->
+                  <el-link
+                    type="primary"
+                    @click="toggleSalesCall(contentIndex)"
+                    style="margin-top: 10px;"
+                    >
+                      {{ content.isSalesCallAdded ? '移除#客服称呼#' : '添加#客服称呼#' }}
+                  </el-link>
+
+                  <el-link
+                    type="primary"
+                    style="margin-top: 10px;margin-left:10px"
+                    @click="showOpenContent(contentIndex)"
+                  >
+                    查看分割内容
+                  </el-link>
+
+                  <el-link
+                    type="primary"
+                    style="margin-top: 10px;margin-left:10px"
+                    @click="copySpliter()"
+                  >
+                    复制分隔符
+                  </el-link>
+
+                </el-form-item>
+
+                <!-- 语音 -->
+                <el-form-item label="内容" v-else-if="12 == content.contentType">
+
+                  <div>
+                    <el-input
+                      v-model="content.value"
+                      type="textarea"
+                      :rows="3"
+                      maxlength="66"
+                      show-word-limit
+                      placeholder="输入要转为语音的内容"
+                      style="width: 90%;margin-top: 10px;"
+                      @input="handleInputVoiceText(content.value, content)"
+                    />
+                  </div>
+
+                </el-form-item>
+
+                <!-- 图片 -->
+                <el-form-item label="内容" v-else-if="2 == content.contentType">
+
+                  <ImageUpload
+                    v-model="content.imgUrl"
+                    type="image"
+                    :num="1"
+                    :width="150"
+                    :height="150"
+                  />
+
+                </el-form-item>
+
+                <!-- 视频 -->
+                <el-form-item label="内容" v-else-if="6 == content.contentType">
+
+                  <div>
+                    <el-form-item
+                      label="上传视频:"
+                      prop="videoUrl"
+                      label-width="100px"
+                    >
+                      <el-upload
+                        v-model="content.videoUrl"
+                        class="avatar-uploader"
+                        :action="uploadUrl"
+                        :headers="uploadHeaders"
+                        :show-file-list="false"
+                        :on-success="(res, file) => handleSuccessVideo(res, file, content)"
+                        :before-upload="beforeUploadVideo"
+                      >
+                        <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>
+
+                </el-form-item>
+
+                <!-- 疗法 -->
+                <el-form-item label="内容" v-else-if="20 == content.contentType">
+
+                  <el-card class="box-card">
+                    <el-form-item
+                      label="选择疗法:"
+                      label-width="100px"
+                      required
+                    >
+                      <el-select
+                        v-model="content.packageId"
+                        placeholder="默认50条,可输入关键字检索"
+                        style=" margin-right: 10px; width: 230px;"
+                        size="mini"
+                        filterable
+                        clearable
+                        :loading="content.meta.pkgLoading"
+                        remote
+                        :remote-method="(keyword) => handlePkgRemoteSearch(keyword, content)"
+                        @change="pkgChangeUpdate(content)"
+                      >
+                        <el-option
+                          v-for="dict in (content.meta && content.meta.pkgList ? content.meta.pkgList : [])"
+                          :key="dict.packageId"
+                          :label="`${dict.packageName}(${dict.packageId})`"
+                          :value="parseInt(dict.packageId)"
+                        />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item
+                      label="疗法名称:"
+                      label-width="100px"
+                    >
+                      <el-input
+                        disabled
+                        :rows="3"
+                        v-model="content.packageName"
+                        style="width: 90%;margin-top: 1%;"
+                      />
+                    </el-form-item>
+                    <el-form-item
+                      label="疗法封面:"
+                      label-width="100px"
+                    >
+                      <ImageUpload
+                        :disabled="true"
+                        v-model="content.packageImgUrl"
+                        type="image"
+                        :num="1"
+                        :file-size="1"
+                        :width="150"
+                        :height="150"
+                        style="margin-top: 1%;"
+                      />
+                    </el-form-item>
+                  </el-card>
+
+                </el-form-item>
+
+                <!-- 直播 -->
+                <el-form-item label="内容" v-else-if="21 == content.contentType">
+                  <div>
+                    <el-card class="box-card">
+                      <el-form-item label="直播分组"
+                        required
+                      >
+                        <el-select
+                          v-model="content.liveGroupTypeId"
+                          placeholder="请选择直播分组"
+                          size="mini"
+                          filterable
+                          clearable
+                          @change="liveGroupTypeChange(content)"
+                        >
+                          <el-option
+                            v-for="dict in liveGroupTypeList"
+                            :key="dict.id"
+                            :label="dict.liveGroupType"
+                            :value="dict.id"
+                          />
+                        </el-select>
+                      </el-form-item>
+                      <el-form-item label="直播间"
+                        required
+                      >
+                        <el-select
+                          v-model="content.liveId"
+                          placeholder="请先选择直播分组"
+                          size="mini"
+                          filterable
+                          :disabled="!content.liveGroupTypeId"
+                          @change="liveChange(content)"
+                        >
+                          <el-option
+                            v-for="dict in (content.meta && content.meta.liveList ? content.meta.liveList : [])"
+                            :key="dict.liveId"
+                            :label="dict.liveName"
+                            :value="parseInt(dict.liveId)"
+                          />
+                        </el-select>
+                      </el-form-item>
+                      <el-form-item label="标题"
+                        prop="liveTitle"
+                        style="margin-bottom: 5px;"
+                      >
+                        <el-input
+                          v-model="content.liveTitle"
+                          placeholder="请输入消息标题,最长为64字节"
+                          :rows="2"
+                          maxlength="64"
+                          type="textarea"
+                          disabled
+                          @input="checkByteLength(content.contentType, 'liveTitle')"
+                        />
+                      </el-form-item>
+                      <el-form-item label="封面"
+                        prop="liveImgUrl"
+                      >
+                        <ImageUpload
+                          v-model="content.liveImgUrl"
+                          type="image"
+                          :num="10"
+                          :width="150"
+                          :height="150"
+                          disabled
+                        />
+                      </el-form-item>
+                    </el-card>
+                  </div>
+                </el-form-item>
+
+                <!-- 民品 -->
+                <el-form-item label="内容" v-else-if="22 == content.contentType">
+                  <div>
+                    <el-card class="box-card">
+                      <el-form-item
+                        label="选择民品:"
+                        label-width="100px"
+                        required
+                      >
+                        <el-select
+                          v-model="content.productId"
+                          placeholder="默认50条,可输入关键字检索"
+                          style=" margin-right: 10px; width: 230px;"
+                          size="mini"
+                          filterable
+                          clearable
+                          :loading="content.meta.productLoading"
+                          remote
+                          :remote-method="(keyword) => handleProductRemoteSearch(keyword, content)"
+                          @change="productChangeUpdate(content)"
+                        >
+                          <el-option
+                            v-for="dict in (content.meta && content.meta.productList ? content.meta.productList : [])"
+                            :key="dict.productId"
+                            :label="`${dict.productName}(${dict.productId})`"
+                            :value="parseInt(dict.productId)"
+                          />
+                        </el-select>
+                      </el-form-item>
+                      <el-form-item
+                        label="民品名称:"
+                        label-width="100px"
+                      >
+                        <el-input
+                          disabled
+                          :rows="3"
+                          v-model="content.productName"
+                          style="width: 90%;margin-top: 1%;"
+                        />
+                      </el-form-item>
+                      <el-form-item
+                        label="民品封面:"
+                        label-width="100px"
+                      >
+                        <ImageUpload
+                          :disabled="true"
+                          v-model="content.productImgUrl"
+                          type="image"
+                          :num="1"
+                          :file-size="1"
+                          :width="150"
+                          :height="150"
+                          style="margin-top: 1%;"
+                        />
+                      </el-form-item>
+                    </el-card>
+                  </div>
+                </el-form-item>
+
+                <!-- 药品 -->
+                <el-form-item label="内容" v-else-if="23 == content.contentType">
+                  <div>
+                    <el-card class="box-card">
+                      <el-form-item
+                        label="选择药品:"
+                        label-width="100px"
+                        required
+                      >
+                        <el-select
+                          v-model="content.medicinesId"
+                          placeholder="默认50条,可输入关键字检索"
+                          style=" margin-right: 10px; width: 230px;"
+                          size="mini"
+                          filterable
+                          clearable
+                          :loading="content.meta.medicinesLoading"
+                          remote
+                          :remote-method="(keyword) => handleMedicinesRemoteSearch(keyword, content)"
+                          @change="medicinesChangeUpdate(content)"
+                        >
+                          <el-option
+                            v-for="dict in (content.meta && content.meta.medicinesList ? content.meta.medicinesList : [])"
+                            :key="dict.productId"
+                            :label="`${dict.productName}(${dict.productId})`"
+                            :value="parseInt(dict.productId)"
+                          />
+                        </el-select>
+                      </el-form-item>
+                      <el-form-item
+                        label="药品名称:"
+                        label-width="100px"
+                      >
+                        <el-input
+                          disabled
+                          :rows="3"
+                          v-model="content.medicinesName"
+                          style="width: 90%;margin-top: 1%;"
+                        />
+                      </el-form-item>
+                      <el-form-item
+                        label="药品封面:"
+                        label-width="100px"
+                      >
+                        <ImageUpload
+                          :disabled="true"
+                          v-model="content.medicinesImgUrl"
+                          type="image"
+                          :num="1"
+                          :file-size="1"
+                          :width="150"
+                          :height="150"
+                          style="margin-top: 1%;"
+                        />
+                      </el-form-item>
+                    </el-card>
+                  </div>
+                </el-form-item>
+
+                <!-- 短视频 -->
+                <el-form-item label="内容" v-else-if="24 == content.contentType">
+                  <div>
+                    <el-card class="box-card">
+                      <el-form-item
+                        label="选择短视频:"
+                        label-width="100px"
+                        required
+                      >
+                        <el-select
+                          v-model="content.shortVideoId"
+                          placeholder="默认50条,可输入关键字检索"
+                          style=" margin-right: 10px; width: 230px;"
+                          size="mini"
+                          filterable
+                          clearable
+                          :loading="content.meta.shortVideoLoading"
+                          remote
+                          :remote-method="(keyword) => handleShortVideoRemoteSearch(keyword, content)"
+                          @change="shortVideoChangeUpdate(content)"
+                        >
+                          <el-option
+                            v-for="dict in (content.meta && content.meta.shortVideoList ? content.meta.shortVideoList : [])"
+                            :key="dict.videoId"
+                            :label="`${dict.title}(${dict.videoId})`"
+                            :value="parseInt(dict.videoId)"
+                          />
+                        </el-select>
+                      </el-form-item>
+                      <el-form-item
+                        label="短视频名称:"
+                        label-width="100px"
+                      >
+                        <el-input
+                          disabled
+                          :rows="3"
+                          v-model="content.shortVideoName"
+                          style="width: 90%;margin-top: 1%;"
+                        />
+                      </el-form-item>
+                      <el-form-item
+                        label="短视频封面:"
+                        label-width="100px"
+                      >
+                        <ImageUpload
+                          :disabled="true"
+                          v-model="content.shortVideoImgUrl"
+                          type="image"
+                          :num="1"
+                          :file-size="1"
+                          :width="150"
+                          :height="150"
+                          style="margin-top: 1%;"
+                        />
+                      </el-form-item>
+                    </el-card>
+                  </div>
+                </el-form-item>
+
+                <!-- 文章 -->
+                <el-form-item label="内容" v-else-if="25 == content.contentType">
+                  <div>
+                    <el-card class="box-card">
+                      <el-form-item
+                        label="选择文章:"
+                        label-width="100px"
+                        required
+                      >
+                        <el-select
+                          v-model="content.articleId"
+                          placeholder="默认50条,可输入关键字检索"
+                          style=" margin-right: 10px; width: 230px;"
+                          size="mini"
+                          filterable
+                          clearable
+                          :loading="content.meta.articleLoading"
+                          remote
+                          :remote-method="(keyword) => handleArticleRemoteSearch(keyword, content)"
+                          @change="articleChangeUpdate(content)"
+                        >
+                          <el-option
+                            v-for="dict in (content.meta && content.meta.articleList ? content.meta.articleList : [])"
+                            :key="dict.articleId"
+                            :label="`${dict.title}(${dict.articleId})`"
+                            :value="parseInt(dict.articleId)"
+                          />
+                        </el-select>
+                      </el-form-item>
+                      <el-form-item
+                        label="文章名称:"
+                        label-width="100px"
+                      >
+                        <el-input
+                          disabled
+                          :rows="3"
+                          v-model="content.articleName"
+                          style="width: 90%;margin-top: 1%;"
+                        />
+                      </el-form-item>
+                      <el-form-item
+                        label="文章封面:"
+                        label-width="100px"
+                      >
+                        <ImageUpload
+                          :disabled="true"
+                          v-model="content.articleImgUrl"
+                          type="image"
+                          :num="1"
+                          :file-size="1"
+                          :width="150"
+                          :height="150"
+                          style="margin-top: 1%;"
+                        />
+                      </el-form-item>
+                    </el-card>
+                  </div>
+                </el-form-item>
+
+                <!-- 公开课 -->
+                <el-form-item label="内容" v-else-if="26 == content.contentType">
+                  <div>
+                    <el-card class="box-card">
+                      <el-form-item
+                        label="选择公开课:"
+                        label-width="100px"
+                        required
+                      >
+                        <el-select
+                          v-model="content.openClassVideoId"
+                          placeholder="默认50条,可输入关键字检索"
+                          style=" margin-right: 10px; width: 230px;"
+                          size="mini"
+                          filterable
+                          clearable
+                          :loading="content.meta.openClassVideoLoading"
+                          remote
+                          :remote-method="(keyword) => handleOpenClassVideoRemoteSearch(keyword, content)"
+                          @change="openClassVideoChangeUpdate(content)"
+                        >
+                          <el-option
+                            v-for="dict in (content.meta && content.meta.openClassVideoList ? content.meta.openClassVideoList : [])"
+                            :key="dict.videoId"
+                            :label="`${dict.title}(${dict.videoId})`"
+                            :value="parseInt(dict.videoId)"
+                          />
+                        </el-select>
+                      </el-form-item>
+                      <el-form-item
+                        label="公开课名称:"
+                        label-width="100px"
+                      >
+                        <el-input
+                          disabled
+                          :rows="3"
+                          v-model="content.openClassVideoName"
+                          style="width: 90%;margin-top: 1%;"
+                        />
+                      </el-form-item>
+                      <el-form-item
+                        label="公开课封面:"
+                        label-width="100px"
+                      >
+                        <ImageUpload
+                          :disabled="true"
+                          v-model="content.openClassVideoImgUrl"
+                          type="image"
+                          :num="1"
+                          :file-size="1"
+                          :width="150"
+                          :height="150"
+                          style="margin-top: 1%;"
+                        />
+                      </el-form-item>
+                    </el-card>
+                  </div>
+                </el-form-item>
+
+                <!-- 舌诊:无实际内容 -->
+                <!-- <el-form-item label="内容" v-else-if="27 == content.contentType">
+                  <div>
+                    <el-card class="box-card">
+
+                    </el-card>
+                  </div>
+                </el-form-item> -->
+
+                <!-- 健康周报:无实际内容 -->
+                <!-- <el-form-item label="内容" v-else-if="28 == content.contentType">
+                  <div>
+                    <el-card class="box-card">
+
+                    </el-card>
+                  </div>
+                </el-form-item> -->
+
+              </el-form>
+            </el-col>
+
+            <el-col
+              :span="1"
+              :offset="1"
+            >
+              <!-- 删除内容节点 -->
+              <i
+                class="el-icon-delete"
+                @click="delContentNode(contentIndex)"
+                style="margin-top: 20px;"
+                v-if="groupSendMessageForm.setting.length > 1"
+              ></i>
+            </el-col>
+          </el-row>
+
+        </div>
+        <el-link type="primary" class="el-icon-plus" :underline="false" @click='addContent()'>添加内容</el-link>
+      </el-form-item>
+
+      <el-form-item label="发送时间类型" prop="sendTimeType">
+        <el-radio-group v-model="groupSendMessageForm.sendTimeType">
+          <el-radio :label="1">当天发送</el-radio>
+          <el-radio :label="2">次日发送</el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <el-form-item label="发送时间" prop="time">
+        <el-time-picker class="custom-input" v-model="groupSendMessageForm.time" value-format="HH:mm" format="HH:mm"
+          placeholder="时间" style="width: 100px;height: 20px;">
+        </el-time-picker>
+        <span class="tip" style="margin-left: 2%"> 不填时,默认为系统当前时间(立即发送)</span>
+      </el-form-item>
+
+    </el-form>
+
+    <div slot="footer" class="dialog-footer" style="text-align: right;">
+      <el-button type="primary" @click="submitGroupSendMessage" :disabled="loading">确 定</el-button>
+      <el-button @click="cancelGroupSendMessage">取 消</el-button>
+    </div>
+
+    <el-dialog
+      title="查看分割后内容"
+      :visible.sync="openShowContent"
+      width="800px"
+      append-to-body
+    >
+      <el-table
+        stripe
+        :data="contentListData"
+        style="width: 100%"
+        >
+          <el-table-column
+            type="index"
+            label="序号"
+            width="50"
+          />
+
+          <el-table-column
+            prop="content"
+            label="内容"
+            width="700"
+          >
+            <template #default="{ row }">
+              <div v-html="row.content.replace(/\n/g, '<br>')"></div>
+            </template>
+        </el-table-column>
+      </el-table>
+
+      <div slot="footer" class="dialog-footer" style="text-align: right;">
+        <el-button
+          type="primary"
+          @click="comfirmContent"
+        >确定</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import ImageUpload from "@/views/qw/sop/ImageUpload.vue";
+import {courseList, videoList} from "@/api/app/course";
+import { getPackageOptions,} from "@/api/app/pkg";
+import { groupSendMessage, } from '@/api/app/userLogs'
+import { getLiveOptions as fetchLiveOptionsApi, getLiveGroupTypeList as fetchLiveGroupTypeListApi } from "@/api/app/live";
+// import { getMedicinesOptions, } from "@/api/app/medicines";
+// import { getProductOptions, } from "@/api/app/product";
+// import { getShortVideoOptions, } from "@/api/app/shortVideo";
+import { getArticleOptions } from "@/api/app/article";
+import { getOpenClassVideoOptions, } from "@/api/app/openClassVideo";
+import { getToken } from "@/utils/auth";
+
+export default {
+  name: "sendMsgOpenTool",
+  components: { ImageUpload },
+  data() {
+    return {
+      uploadUrl: process.env.VUE_APP_BASE_API + "/app/common/uploadOSS2",
+      uploadUrlByVoice: process.env.VUE_APP_BASE_API + "/app/common/uploadOSSByHOOKVoice",
+      uploadHeaders: {
+        Authorization: "Bearer " + getToken(),
+      },
+      // 遮罩层
+      loading: false,
+      courseLoading: false,
+      sysFsSopWatchStatus: [],
+      isSalesCallAdded:false,
+      isSalesCallCustomerAdded:false,
+      courseList:[],
+      videoList:[],
+      pkgList: [],//默认疗法列表
+      liveGroupTypeList: [],//直播分组
+      liveList: [],//默认直播列表
+      productList: [],//默认民品列表
+      medicinesList: [],//默认药品列表
+      shortVideoList: [],//默认短视频列表
+      articleList: [],//默认文章列表
+      openClassVideoList: [],//默认公开课列表
+      sysQwSopAiContentType:[],
+      groupSendMessageForm: {
+        courseId: null,
+        videoId: null,
+        courseType: null,
+        sendTimeType: 1,
+        time: null,
+        setting: [],
+      },
+      groupSendRules: {
+        courseId: [
+          { required: true, message: '科普/课节不能为空', trigger: 'blur' }
+        ],
+        videoId: [
+          { required: true, message: '科普/课节不能为空', trigger: 'blur' }
+        ],
+        courseType: [
+          { required: true, message: '消息类型', trigger: 'blur' }
+        ]
+      },
+      openShowContent: false,
+      contentList: [],
+      contentListData: [],
+    };
+  },
+  props: {
+    visible: {
+      default: false,
+      type: Boolean,
+    },
+    ids: {
+      default: () => [],
+      type: Array,
+    },
+    sourceType: {
+      default: 0,//0-营期的一键群发,1-营期详情的一键群发
+      type: Number,
+    },
+  },
+  async created() {
+    // 仅屏蔽民品/药品/短视频,直播(21)需要展示
+    const disabledContentTypes = ['22', '23', '24'];
+    this.getDicts("app_sop_plugin_settingType").then(response => {
+      this.sysQwSopAiContentType = response.data.filter(
+        item => !disabledContentTypes.includes(String(item.dictValue))
+      );
+    });
+    this.getDicts("app_sop_watch_status").then(response => {
+      this.sysFsSopWatchStatus = response.data;
+    });
+    const [
+      courseInfo,
+      defaultPkg,
+      defaultLiveGroup,
+      defaultArticle,
+      defaultOpenClassVideo,
+    ] = await Promise.all([
+      courseList(),
+      this.getPkgList(null, null),
+      this.fetchLiveGroupTypeList(),
+      this.getArticleList(null, null),
+      this.getOpenClassVideoList(null, null),
+    ]);
+    this.courseList = courseInfo.list;
+    this.pkgList = defaultPkg.data;
+    this.liveGroupTypeList = defaultLiveGroup.rows || defaultLiveGroup.data || [];
+    this.articleList = defaultArticle.data;
+    this.openClassVideoList = defaultOpenClassVideo.data;
+    // const [
+    //   courseInfo,
+    //   defaultPkg,
+    //   defaultLive,
+    //   defaultProduct,
+    //   defaultMedicines,
+    //   defaultShortVideo,
+    //   defaultArticle,
+    //   defaultOpenClassVideo,
+    // ] = await Promise.all([
+    //   courseList(),
+    //   this.getPkgList(null, null),
+    //   this.fetchLiveOptionsByGroup(),
+    //   this.getProductList(null, null),
+    //   this.getMedicinesList(null, null),
+    //   this.getShortVideoList(null, null),
+    //   this.getArticleList(null, null),
+    //   this.getOpenClassVideoList(null, null),
+    // ]);
+    // this.courseList = courseInfo.list;
+    // this.pkgList = defaultPkg.data;
+    // this.liveList = defaultLive.data;
+    // this.productList = defaultProduct.data;
+    // this.medicinesList = defaultMedicines.data;
+    // this.shortVideoList = defaultShortVideo.data;
+    // this.articleList = defaultArticle.data;
+    // this.openClassVideoList = defaultOpenClassVideo.data;
+  },
+  watch: {
+    visible: {
+      handler(n, o) {
+        //打开新建
+        if (n) {
+          this.addContent();
+        }
+        //关闭销毁
+        else {
+          this.resetForm();
+        }
+      },
+      immediate: true,
+    },
+  },
+  methods: {
+    /**
+     * 获取科普信息
+     */
+    async getCourseList(keyword, selectedId) {
+      let data = {
+        keyword: keyword,
+        selectedId: selectedId,
+        pageNum: 1,
+        pageSize: 50,
+      };
+      return await courseList(data);
+    },
+    /**
+     * 根据科普获取视频小节
+     * @param courseId
+     * @param title
+     */
+    async getVideoList(courseId, title) {
+      return await videoList({
+        courseId: courseId,
+        title: title
+      });
+    },
+    /**
+     * 获取疗法信息
+     */
+    async getPkgList(keyword, selectedId) {
+      let data = {
+        keyword: keyword,
+        metaId: selectedId,
+        limit: 50
+      };
+      return await getPackageOptions(data);
+    },
+    /**
+     * 获取直播分组下拉
+     */
+    async fetchLiveGroupTypeList() {
+      return await fetchLiveGroupTypeListApi({ pageNum: 1, pageSize: 999 });
+    },
+    /**
+     * 获取直播间信息
+     * @param liveGroupType 直播分组id
+     */
+    async fetchLiveOptionsByGroup(liveGroupType) {
+      if (!liveGroupType) {
+        return { data: [] };
+      }
+      return await fetchLiveOptionsApi({ liveGroupType });
+    },
+    // /**
+    //  * 获取民品列表
+    //  * @param keyword
+    //  * @param selectedId
+    //  */
+    // async getProductList(keyword, selectedId) {
+    //   let data = {
+    //     keyword: keyword,
+    //     metaId: selectedId,
+    //     limit: 50
+    //   };
+    //   return await getProductOptions(data);
+    // },
+    // /**
+    //  * 获取药品列表
+    //  * @param keyword
+    //  * @param selectedId
+    //  */
+    // async getMedicinesList(keyword, selectedId) {
+    //   let data = {
+    //     keyword: keyword,
+    //     metaId: selectedId,
+    //     limit: 50
+    //   };
+    //   return await getMedicinesOptions(data);
+    // },
+    // /**
+    //  * 获取短视频列表
+    //  * @param keyword
+    //  * @param selectedId
+    //  */
+    // async getShortVideoList(keyword, selectedId) {
+    //   let data = {
+    //     keyword: keyword,
+    //     metaId: selectedId,
+    //     limit: 50
+    //   };
+    //   return await getShortVideoOptions(data);
+    // },
+    /**
+     * 获取文章列表
+     * @param keyword
+     * @param selectedId
+     */
+    async getArticleList(keyword, selectedId) {
+      let data = {
+        keyword: keyword,
+        metaId: selectedId,
+        limit: 50
+      };
+      return await getArticleOptions(data);
+    },
+    /**
+     * 获取公开课列表
+     * @param keyword
+     * @param selectedId
+     */
+    async getOpenClassVideoList(keyword, selectedId) {
+      let data = {
+        keyword: keyword,
+        metaId: selectedId,
+        limit: 50
+      };
+      return await getOpenClassVideoOptions(data);
+    },
+    /**
+     * 科普变动处理
+     * @param ruleItem
+     * @param dayIndex
+     * @param ruleIndex
+     */
+    async courseChangeUpdate() {
+      //将当前规则已选中的视频小节清空
+      this.groupSendMessageForm.videoId = null;
+      this.videoList = [];
+      // 查找选中的科普的信息
+      const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) == this.groupSendMessageForm.courseId);
+      for (let i = 0; i < this.groupSendMessageForm.setting.length; i++) {
+        let contentItem = this.groupSendMessageForm.setting[i];
+        this.$set(contentItem, 'linkTitle', null);
+        this.$set(contentItem, 'linkDescribe', null);
+        this.$set(contentItem, 'linkImageUrl', null);
+
+        //如果是科普才上
+        if (selectedCourse && this.groupSendMessageForm.courseId) {
+          //响应式直接给链接的标题/封面上值
+          if (contentItem.contentType == 9) {
+            this.$set(contentItem, 'linkTitle', selectedCourse.dictLabel);
+            this.$set(contentItem, 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+        }
+      }
+      if (this.groupSendMessageForm.courseId) {
+        let vl = await this.getVideoList(this.groupSendMessageForm.courseId, null);
+        this.videoList = vl.list;
+      } else {
+        let cl = await this.getCourseList(null, null);
+        this.courseList = cl.list;
+      }
+    },
+    /**
+     * 课程远程搜索
+     */
+    async handleCourseRemoteSearch(keyword) {
+      this.courseLoading = true;
+      // 模拟远程接口请求(替换为你的真实接口)
+      let cl = await this.getCourseList(this.isEmpty(keyword) ? null : keyword, null);
+      this.courseLoading = false;
+      this.courseList = cl.list;
+    },
+    /**
+     * 疗法下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handlePkgRemoteSearch(keyword, content) {
+      content.meta.pkgLoading = true;
+      let cl = await this.getPkgList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.pkgLoading = false;
+      content.meta.pkgList = cl.data;
+    },
+    /**
+     * 疗法变动处理
+     * @param content
+     */
+    async pkgChangeUpdate(content) {
+      //将当前规则已选中的视频小节清空
+      this.$set(content, 'packageName', null);
+      this.$set(content, 'packageImgUrl', null);
+      if (content.packageId) {
+        // 查找选中疗法的信息
+        const selectedPkg = content.meta.pkgList.find(pkg => parseInt(pkg.packageId) === content.packageId);
+        this.$set(content, 'packageName', selectedPkg.packageName);
+        this.$set(content, 'packageImgUrl', selectedPkg.imgUrl);
+      }
+    },
+    /**
+     * 民品下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handleProductRemoteSearch(keyword, content) {
+      content.meta.productLoading = true;
+      let cl = await this.getProductList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.productLoading = false;
+      content.meta.productList = cl.data;
+    },
+    /**
+     * 民品变动处理
+     * @param content
+     */
+    async productChangeUpdate(content) {
+      this.$set(content, 'productName', null);
+      this.$set(content, 'productImgUrl', null);
+      if (content.productId) {
+        // 查找选中民品的信息
+        const selected = content.meta.productList.find(pro => parseInt(pro.productId) === content.productId);
+        this.$set(content, 'productName', selected.productName);
+        this.$set(content, 'productImgUrl', selected.image);
+      }
+    },
+    /**
+     * 药品下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handleMedicinesRemoteSearch(keyword, content) {
+      content.meta.medicinesLoading = true;
+      let cl = await this.getMedicinesList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.medicinesLoading = false;
+      content.meta.medicinesList = cl.data;
+    },
+    /**
+     * 药品变动处理
+     * @param content
+     */
+    async medicinesChangeUpdate(content) {
+      this.$set(content, 'medicinesName', null);
+      this.$set(content, 'medicinesImgUrl', null);
+      if (content.medicinesId) {
+        // 查找选中药品的信息
+        const selected = content.meta.medicinesList.find(pro => parseInt(pro.productId) === content.medicinesId);
+        this.$set(content, 'medicinesName', selected.productName);
+        this.$set(content, 'medicinesImgUrl', selected.image);
+      }
+    },
+    /**
+     * 短视频下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handleShortVideoRemoteSearch(keyword, content) {
+      content.meta.shortVideoLoading = true;
+      let cl = await this.getShortVideoList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.shortVideoLoading = false;
+      content.meta.shortVideoList = cl.data;
+    },
+    /**
+     * 短视频变动处理
+     * @param content
+     */
+    async shortVideoChangeUpdate(content) {
+      this.$set(content, 'shortVideoName', null);
+      this.$set(content, 'shortVideoImgUrl', null);
+      if (content.shortVideoId) {
+        // 查找选中短视频的信息
+        const selected = content.meta.shortVideoList.find(pro => parseInt(pro.videoId) === content.shortVideoId);
+        this.$set(content, 'shortVideoName', selected.title);
+        this.$set(content, 'shortVideoImgUrl', selected.thumbnail);
+      }
+    },
+    /**
+     * 文章下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handleArticleRemoteSearch(keyword, content) {
+      content.meta.articleLoading = true;
+      let cl = await this.getArticleList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.articleLoading = false;
+      content.meta.articleList = cl.data;
+    },
+    /**
+     * 文章变动处理
+     * @param content
+     */
+    async articleChangeUpdate(content) {
+      this.$set(content, 'articleName', null);
+      this.$set(content, 'articleImgUrl', null);
+      if (content.articleId) {
+        // 查找选中文章的信息
+        const selected = content.meta.articleList.find(pro => parseInt(pro.articleId) === content.articleId);
+        this.$set(content, 'articleName', selected.title);
+        this.$set(content, 'articleImgUrl', selected.imageUrl);
+      }
+    },
+    /**
+     * 公开课下拉搜索
+     * @param keyword
+     * @param content
+     */
+    async handleOpenClassVideoRemoteSearch(keyword, content) {
+      content.meta.openClassVideoLoading = true;
+      let cl = await this.getOpenClassVideoList(this.isEmpty(keyword) ? null : keyword, null);
+      content.meta.openClassVideoLoading = false;
+      content.meta.openClassVideoList = cl.data;
+    },
+    /**
+     * 公开课变动处理
+     * @param content
+     */
+    async openClassVideoChangeUpdate(content) {
+      this.$set(content, 'openClassVideoName', null);
+      this.$set(content, 'openClassVideoImgUrl', null);
+      if (content.openClassVideoId) {
+        // 查找选中文章的信息
+        const selected = content.meta.openClassVideoList.find(pro => parseInt(pro.videoId) === content.openClassVideoId);
+        this.$set(content, 'openClassVideoName', selected.title);
+        this.$set(content, 'openClassVideoImgUrl', selected.thumbnail);
+      }
+    },
+    /**
+     * 直播分组切换
+     * @param content
+     */
+    async liveGroupTypeChange(content) {
+      this.$set(content, 'liveId', null);
+      this.$set(content, 'liveTitle', null);
+      this.$set(content, 'liveImgUrl', null);
+      if (!content.liveGroupTypeId) {
+        this.$set(content, 'meta', {
+          ...(content.meta || {}),
+          liveLoading: false,
+          liveList: [],
+        });
+        return;
+      }
+      this.$set(content, 'meta', {
+        ...(content.meta || {}),
+        liveLoading: true,
+        liveList: [],
+      });
+      const liveRes = await this.fetchLiveOptionsByGroup(content.liveGroupTypeId);
+      this.$set(content, 'meta', {
+        ...(content.meta || {}),
+        liveLoading: false,
+        liveList: liveRes.data || liveRes.rows || [],
+      });
+    },
+    /**
+     * 直播间切换
+     * @param content
+     */
+    liveChange(content) {
+      content.liveTitle = null;
+      content.liveImgUrl = null;
+      const list = (content.meta && content.meta.liveList) ? content.meta.liveList : [];
+      const selectedLive = list.find(live => parseInt(live.liveId) === parseInt(content.liveId));
+      if (selectedLive) {
+        content.liveTitle = selectedLive.liveName; // 自动填充标题
+        content.liveImgUrl = selectedLive.liveImgUrl; // 自动填充封面
+      }
+    },
+    /**
+     * 科普小节变化
+     * @param ruleItem
+     * @param dayIndex
+     * @param ruleIndex
+     */
+    videoIdChange() {
+      //选择了科普小节则 默认绑上
+      let selectedVideo;
+      // 查找选中的科普对应的 label
+      if (this.groupSendMessageForm.videoId) {
+        selectedVideo = this.videoList.find(course => parseInt(course.dictValue) == this.groupSendMessageForm.videoId);
+      }
+      for (let i = 0; i < this.groupSendMessageForm.setting.length; i++) {
+        let contentItem = this.groupSendMessageForm.setting[i];
+        //科普消息-文本内容
+        if (contentItem == 11) {
+          this.$set(contentItem, 'isBindUrl', '2');
+        }
+        //如果是科普的才上
+        if (selectedVideo && this.groupSendMessageForm.videoId != null) {
+
+          if (contentItem.contentType == 9) {
+            this.$set(contentItem, 'linkDescribe', selectedVideo.dictLabel);
+          }
+        }
+      }
+    },
+    /**
+     * 上传视频成功处理
+     * @param res
+     * @param file
+     * @param item
+     */
+    handleSuccessVideo(res, file, item) {
+      if(res.code == 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(item, 'videoUrl', res.url);
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+    /**
+     * 上传视频前置处理
+     * @param file
+     */
+    beforeUploadVideo(file){
+      const isLt30M = file.size / 1024 / 1024 < 10;
+      const fileName = (file.name || '').toLowerCase();
+      const isMP4 = file.type === 'video/mp4' || fileName.endsWith('.mp4');
+      if (!isMP4) {
+        this.$message.error('仅支持上传 MP4 格式的视频文件!');
+        return false;
+      }
+      if (!isLt30M) {
+        this.$message.error('上传大小不能超过 10MB!');
+        return false;
+      }
+      return true;
+    },
+    /**
+     * 语音文本录入
+     * @param value
+     * @param content
+     */
+    handleInputVoiceText(value,content){
+      // 允许的字符:中文、英文(大小写)、数字和指定标点符号(,。!?)
+      const regex = /^[\u4e00-\u9fa5a-zA-Z0-9,。!?,!?]+$/;
+      // 删除不符合条件的字符
+      const filteredValue = value.split('').filter(char => regex.test(char)).join('');
+      this.$set(content, 'value', filteredValue);
+    },
+    /**
+     * 删除内容节点
+     * @param index
+     */
+    delContentNode(index){
+      this.groupSendMessageForm.setting.splice(index, 1)
+    },
+    /**
+     * 添加规则内容
+     */
+    addContent() {
+      let content = {
+        contentType: '9',//默认科普类型
+        value: '',
+      };
+      this.groupSendMessageForm.setting.push(content);
+      this.handleContentTypeChange();
+    },
+    /**
+     *
+     * @param event
+     * @param index
+     */
+    handleKeydown(event, index) {
+      const item = this.groupSendMessageForm.setting[index];
+      const textarea = this.$refs[`textarea-${index}`][0].$refs.textarea;
+      const cursorPosition = textarea.selectionStart;
+      // 检查是否按下了 Backspace 或 Delete 键
+      if (event.key === 'Backspace' || event.key === 'Delete') {
+        const tags = ['#客服称呼#', '#客户称呼#']; // 需要检查的标签
+        const value = item.value;
+        // 遍历标签,检查是否需要删除
+        for (const tag of tags) {
+          let start, end;
+          if (event.key === 'Backspace') {
+            // 检查光标前是否是当前标签的一部分
+            start = cursorPosition - tag.length;
+            if (start >= 0 && value.slice(start, cursorPosition) === tag) {
+              // 删除整个标签
+              item.value = value.slice(0, start) + value.slice(cursorPosition);
+              // 更新光标位置
+              this.$nextTick(() => {
+                textarea.setSelectionRange(start, start);
+              });
+              // 更新状态
+              if (tag === '#客服称呼#') {
+                item.isSalesCallAdded = false;
+              }
+              if (tag === '#客户称呼#') {
+                item.isSalesCallCustomerAdded = false;
+              }
+              event.preventDefault(); // 阻止默认删除行为
+              break; // 找到匹配的标签后退出循环
+            }
+          } else if (event.key === 'Delete') {
+            // 检查光标后是否是当前标签的一部分
+            end = cursorPosition + tag.length;
+            if (end <= value.length && value.slice(cursorPosition, end) === tag) {
+              // 删除整个标签
+              item.value = value.slice(0, cursorPosition) + value.slice(end);
+              // 更新状态
+              if (tag === '#客服称呼#') {
+                item.isSalesCallAdded = false;
+              }
+              if (tag === '#客户称呼#') {
+                item.isSalesCallCustomerAdded = false;
+              }
+              event.preventDefault(); // 阻止默认删除行为
+              break; // 找到匹配的标签后退出循环
+            }
+          }
+          // 检查光标是否位于标签的中间
+          for (let i = 0; i <= tag.length; i++) {
+            const tagStart = cursorPosition - i;
+            const tagEnd = tagStart + tag.length;
+            if (
+              tagStart >= 0 &&
+              tagEnd <= value.length &&
+              value.slice(tagStart, tagEnd) === tag
+            ) {
+              // 删除整个标签
+              item.value = value.slice(0, tagStart) + value.slice(tagEnd);
+              // 更新光标位置
+              this.$nextTick(() => {
+                textarea.setSelectionRange(tagStart, tagStart);
+              });
+              // 更新状态
+              if (tag === '#客服称呼#') {
+                item.isSalesCallAdded = false;
+              }
+              if (tag === '#客户称呼#') {
+                item.isSalesCallCustomerAdded = false;
+              }
+              event.preventDefault(); // 阻止默认删除行为
+              break; // 找到匹配的标签后退出循环
+            }
+          }
+        }
+      }
+    },
+    /**
+     * 切换添加客服称呼按钮点击事件
+     * @param index
+     */
+    toggleSalesCall(index) {
+      const item = this.groupSendMessageForm.setting[index];
+      const salesCall = '#客服称呼#';
+      const textarea = this.$refs[`textarea-${index}`][0].$refs.textarea;
+
+      // 获取当前光标位置
+      const cursorPosition = textarea.selectionStart;
+
+      if (item && !Object.hasOwn(item, 'isSalesCallAdded')) {
+        this.$set(item, 'isSalesCallAdded', false);
+      }
+
+      if (item.isSalesCallAdded) {
+        // 移除所有的 #客服称呼#
+        item.value = item.value.replace(new RegExp(salesCall, 'g'), '');
+      } else {
+        // 添加 #客服称呼#
+        item.value = item.value.slice(0, cursorPosition) + salesCall + item.value.slice(cursorPosition);
+      }
+
+      // 切换状态
+      item.isSalesCallAdded = !item.isSalesCallAdded;
+
+      // 保持光标位置
+      this.$nextTick(() => {
+        textarea.setSelectionRange(cursorPosition, cursorPosition);
+      });
+    },
+    /**
+     * 修改内容类别,避免万一,把所有的content刷一遍
+     */
+    handleContentTypeChange() {
+      const form = this.groupSendMessageForm;
+      //科普
+      const course = this.courseList.find(c => parseInt(c.dictValue) == form.courseId);
+      //课节
+      const video = this.videoList.find(v => parseInt(v.dictValue) == form.videoId);
+      form.setting.forEach(async item => {
+        //科普
+        if (item.contentType == 9) {
+          course && this.$set(item, 'linkTitle', course.dictLabel);
+          course && this.$set(item, 'linkImageUrl', course.dictImgUrl);
+          video && this.$set(item, 'linkDescribe', video.dictLabel);
+        }
+        //疗法
+        else if(item.contentType == 20) {
+          if (!item.packageId) {
+            this.$set(item, 'meta', {
+              pkgLoading: false,
+              pkgList: this.pkgList,
+            });
+          } else {
+            let pkgList = await this.getPkgList(null, item.packageId);
+            this.$set(item, 'meta', {
+              pkgLoading: false,
+              pkgList: pkgList.data,
+            });
+          }
+        }
+        //直播
+        else if(item.contentType == 21) {
+          if (item.liveGroupTypeId) {
+            let liveRes = await this.fetchLiveOptionsByGroup(item.liveGroupTypeId);
+            this.$set(item, 'meta', {
+              liveLoading: false,
+              liveList: liveRes.data || liveRes.rows || [],
+            });
+          } else {
+            this.$set(item, 'meta', {
+              liveLoading: false,
+              liveList: [],
+            });
+          }
+        }
+        // //民品
+        // else if(item.contentType == 22) {
+        //   if (!item.productId) {
+        //     this.$set(item, 'meta', {
+        //       productLoading: false,
+        //       productList: this.productList,
+        //     });
+        //   } else {
+        //     let productList = await this.getProductList(null, item.productId);
+        //     this.$set(item, 'meta', {
+        //       productLoading: false,
+        //       productList: productList.data,
+        //     });
+        //   }
+        // }
+        // //药品
+        // else if(item.contentType == 23) {
+        //   if (!item.medicinesId) {
+        //     this.$set(item, 'meta', {
+        //       medicinesLoading: false,
+        //       medicinesList: this.medicinesList,
+        //     });
+        //   } else {
+        //     let medicinesList = await this.getMedicinesList(null, item.medicinesId);
+        //     this.$set(item, 'meta', {
+        //       medicinesLoading: false,
+        //       medicinesList: medicinesList.data,
+        //     });
+        //   }
+        // }
+        // //短视频
+        // else if(item.contentType == 24) {
+        //   if (!item.shortVideoId) {
+        //     this.$set(item, 'meta', {
+        //       shortVideoLoading: false,
+        //       shortVideoList: this.shortVideoList,
+        //     });
+        //   } else {
+        //     let shortVideoList = await this.getShortVideoList(null, item.shortVideoId);
+        //     this.$set(item, 'meta', {
+        //       shortVideoLoading: false,
+        //       shortVideoList: shortVideoList.data,
+        //     });
+        //   }
+        // }
+        //文章
+        else if(item.contentType == 25) {
+          if (!item.articleId) {
+            this.$set(item, 'meta', {
+              articleLoading: false,
+              articleList: this.articleList,
+            });
+          } else {
+            let articleList = await this.getArticleList(null, item.articleId);
+            this.$set(item, 'meta', {
+              articleLoading: false,
+              articleList: articleList.data,
+            });
+          }
+        }
+        //公开课
+        else if(item.contentType == 26) {
+          if (!item.openClassVideoId) {
+            this.$set(item, 'meta', {
+              openClassVideoLoading: false,
+              openClassVideoList: this.openClassVideoList,
+            });
+          } else {
+            let openClassVideoList = await this.getOpenClassVideoList(null, item.openClassVideoId);
+            this.$set(item, 'meta', {
+              openClassVideoLoading: false,
+              openClassVideoList: openClassVideoList.data,
+            });
+          }
+        }
+        //舌诊
+        else if(item.contentType == 27) {
+
+        }
+        //健康周报
+        else if(item.contentType == 28) {
+
+        }
+      })
+    },
+    /**
+     * 检查数据
+     */
+    checkData() {
+      if (this.isEmpty(this.groupSendMessageForm.courseId)) {
+        return this.$message.error("科普不能为空")
+      }
+      if (this.isEmpty(this.groupSendMessageForm.videoId)) {
+        return this.$message.error("课节不能为空")
+      }
+      if (this.isEmpty(this.groupSendMessageForm.courseType)) {
+        return this.$message.error("消息类型不能为空")
+      }
+      let contentList = this.groupSendMessageForm.setting;
+      if (contentList.length === 0) {
+        return this.$message.error("请添加规则")
+      }
+      for (let k = 0; k < contentList.length; k++) {
+        let dcs = contentList[k];
+        if (dcs.contentType == 11 && this.isEmpty(dcs.value)) {
+          return this.$message.error("内容不能为空");
+        }
+        if (dcs.contentType == 12 && this.isEmpty(dcs.value)) {
+          return this.$message.error("语音文本不能为空");
+        }
+        if (dcs.contentType == 2 && this.isEmpty(dcs.imgUrl)) {
+          return this.$message.error("图片不能为空");
+        }
+        if ([9].includes(Number(dcs.contentType)) && this.isEmpty(dcs.linkTitle)) {
+          return this.$message.error("链接标题不能为空");
+        }
+        if ([9].includes(Number(dcs.contentType)) && this.isEmpty(dcs.linkDescribe)) {
+          return this.$message.error("链接描述不能为空");
+        }
+        if ([9].includes(Number(dcs.contentType)) && this.isEmpty(dcs.linkImageUrl)) {
+          return this.$message.error("链接图片不能为空");
+        }
+        if ([20].includes(Number(dcs.contentType)) && this.isEmpty(dcs.packageId)) {
+          return this.$message.error("疗法不能为空");
+        }
+        if ([21].includes(Number(dcs.contentType)) && this.isEmpty(dcs.liveGroupTypeId)) {
+          return this.$message.error("直播分组不能为空");
+        }
+        if ([21].includes(Number(dcs.contentType)) && this.isEmpty(dcs.liveId)) {
+          return this.$message.error("直播间不能为空");
+        }
+        // if ([22].includes(Number(dcs.contentType)) && this.isEmpty(dcs.productId)) {
+        //   return this.$message.error("民品不能为空");
+        // }
+        // if ([23].includes(Number(dcs.contentType)) && this.isEmpty(dcs.medicinesId)) {
+        //   return this.$message.error("药品不能为空");
+        // }
+        // if ([24].includes(Number(dcs.contentType)) && this.isEmpty(dcs.shortVideoId)) {
+        //   return this.$message.error("短视频不能为空");
+        // }
+        if ([25].includes(Number(dcs.contentType)) && this.isEmpty(dcs.articleId)) {
+          return this.$message.error("文章不能为空");
+        }
+        if ([26].includes(Number(dcs.contentType)) && this.isEmpty(dcs.openClassVideoId)) {
+          return this.$message.error("公开课不能为空");
+        }
+      }
+      if (this.groupSendMessageForm.sendTimeType == 2 && this.isEmpty(this.groupSendMessageForm.time)) {
+        return this.$message.error("请指定次日发送时间")
+      }
+      return 1;
+    },
+    /**
+     * 空校验
+     * @param obj
+     */
+    isEmpty(obj) {
+      return obj === null || obj === undefined || obj.length === 0;
+    },
+    /**
+     * 确认一键群发
+     */
+    submitGroupSendMessage() {
+      // 表单验证
+      this.loading = true;
+      this.$refs["groupSendMessageForm"].validate(async valid => {
+        if (valid) {
+          if (this.checkData() !== 1) {
+            this.loading = false;
+            return;
+          }
+          let params = {...this.groupSendMessageForm, ids: this.ids};
+          params['sourceType'] = this.sourceType;
+          let res = await groupSendMessage(params);
+          this.msgSuccess("一键群发成功");
+          this.$emit('changeGroupSendMessageVisible', false);
+        }
+        this.loading = false;
+      });
+    },
+    /**
+     * 取消一键群发
+     */
+    cancelGroupSendMessage() {
+      this.$emit('changeGroupSendMessageVisible', false);
+    },
+    /**
+     * 重置表单
+     */
+    resetForm() {
+      this.groupSendMessageForm = {
+        courseId: null,
+        videoId: null,
+        courseType: null,
+        sendTimeType: 1,
+        time: null,
+        setting: [],
+      };
+    },
+    /**
+     * 确认分割后的内容
+     */
+    comfirmContent() {
+      this.openShowContent = false;
+    },
+    /**
+     * 显示分割后的内容
+     * @param contentIndex
+     */
+    showOpenContent(contentIndex) {
+      let setItem = this.groupSendMessageForm.setting[contentIndex];
+      let refKey = `textarea-${contentIndex}`;
+      let textarea = this.$refs[refKey][0]?.$refs?.textarea;
+      if (!textarea) return;
+      this.openShowContent = true;
+      this.contentList = [];
+      this.contentListData = [];
+      let str = setItem.value;
+      this.contentList = str.split("|&|");
+      let idx = 1;
+      this.contentList.forEach(a => {
+        this.contentListData.push({ index: idx++, content: a });
+      })
+    },
+    /**
+     * 复制分隔符
+     */
+    copySpliter() {
+      navigator.clipboard.writeText("|&|").then(() => {
+        this.$message({
+          message: '复制成功',
+          type: 'success'
+        });
+      }).catch(err => {
+        this.$message({
+          message: '复制失败',
+          type: 'warning'
+        });
+      });
+    }
+  }
+};
+</script>
+<style lang="scss" scoped>
+.app-container {
+  padding: 8px;
+
+  .el-alert {
+    padding: 5px 16px;
+  }
+}
+</style>

+ 1269 - 0
src/views/app/sop/userLogsInfo/sendMsgOpenTool.vue

@@ -0,0 +1,1269 @@
+<template>
+  <div class="app-container">
+    <el-dialog :title="sendMsgOpen.title" :visible.sync="sendMsgOpen.open"  width="1000px" append-to-body>
+        <el-alert
+          type="error"
+          :closable="false"
+          show-icon>
+          <template #title>
+                <span style="font-size: 25px; line-height: 1.5;">
+                    此功能用于给 选中的 营期 内【所有的】客户发送 消息
+                </span>
+          </template>
+        </el-alert>
+        <el-form ref="msgForm" :model="msgForm" :rules="msgRules" label-width="100px">
+          <el-form-item label="策略" prop="draftStrategy">
+            <el-radio-group v-model="msgForm.draftStrategy">
+              <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="msgForm.draftStrategy==1" >
+            <el-select  v-model="msgForm.courseId" placeholder="请选择科普" style=" margin-right: 10px;" size="mini"  @change="courseChange()">
+              <el-option
+                v-for="dict in courseList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="parseInt(dict.dictValue)"
+              />
+            </el-select>
+            <el-select  v-model="msgForm.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;" @change="videoIdChange()"  >
+              <el-option
+                v-for="dict in videoList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="parseInt(dict.dictValue)"
+                :disabled="dict.isPause == 1"
+              >
+                <div :style="{ color: dict.isPause == 1 ? '#f56c6c' : 'inherit', display: 'flex', alignItems: 'center' }">
+                  <span>{{ dict.dictLabel }}</span>
+                  <span v-if="dict.isPause == 1" style="font-size: 12px; margin-left: 8px; color: #f56c6c;">
+                      (该科普已被关闭,无法正常发送)
+                  </span>
+                </div>
+              </el-option>
+            </el-select>
+            <el-select  v-model="msgForm.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="规则" prop="setting" v-if="msgForm.draftStrategy==1"  >
+            <div v-for="(item, index) in setting" :key="index" style="background-color: #fdfdfd; border: 1px solid #e6e6e6; margin-bottom: 20px;">
+              <el-row>
+                <el-col :span="22">
+                  <el-form :model="item" label-width="70px">
+                    <el-form-item label="内容类别" style="margin: 2%">
+                      <el-radio-group  v-model="item.contentType">
+                        <el-radio   :label="items.dictValue" v-for="items in sysQwSopAiContentType" v-if="items.dictValue !== '19' || (items.dictValue === '19' && item.openClass=== 1)"   @change="handleContentTypeChange()">{{items.dictLabel}}</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                    <el-form-item label="内容" style="margin-bottom: 2%" >
+                      <el-input
+                        v-if="item.contentType == 1 || item.contentType == 11|| item.contentType == 13"
+                        v-model="item.value"
+                        type="textarea"
+                        :rows="3"
+                        placeholder="内容"
+                        style="width: 90%; margin-top: 10px;"
+                        @keydown.native="handleKeydown($event, index)"
+                        :ref="`textarea-${index}`"
+                      >
+                      </el-input>
+                      <el-link
+                        v-if="item.contentType == 1 || item.contentType == 11 || item.contentType == 13"
+                        type="primary"
+                        @click="toggleSalesCall(index)"
+                        style="margin-top: 10px;"
+                      >
+                        {{ item.isSalesCallAdded ? '移除#客服称呼#' : '添加#客服称呼#' }}
+                      </el-link>
+                      <el-link
+                        v-if="item.contentType == 1 || item.contentType == 11|| item.contentType == 13"
+                        type="primary"
+                        @click="toggleSalesCallCustomer(index)"
+                        style="margin-top: 10px;margin-left: 2%"
+                      >
+                        {{ item.isSalesCallCustomerAdded ? '移除#客户称呼#' : '添加#客户称呼#' }}
+                      </el-link>
+
+
+                      <ImageUpload v-if="item.contentType == 2 " v-model="item.imgUrl" type="image" :num="1"  :width="150" :height="150" />
+
+                      <div v-if="item.contentType == 3 || item.contentType ==9 ">
+                        <el-card class="box-card">
+                          <el-form-item label="链接标题:"  label-width="100px">
+                            <el-input v-model="item.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
+                          </el-form-item>
+                          <el-form-item label="链接描述:"   label-width="100px" >
+                            <el-input type="textarea" :rows="3" v-model="item.linkDescribe" placeholder="请输入链接描述" style="width: 90%;margin-top: 1%;"/>
+                          </el-form-item>
+                          <el-form-item label="链接封面:"   label-width="100px">
+                            <ImageUpload v-model="item.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" v-model="item.isBindUrl" value = "1">选择的科普小节 即为卡片链接地址</el-tag>
+                          </el-form-item>
+                        </el-card>
+                      </div>
+                      <div v-if="item.contentType == 4 || item.contentType == 15|| item.contentType == 19">
+                        <el-card class="box-card">
+                          <el-form-item label="标题" prop="miniprogramTitle">
+                            <el-input v-model="item.miniprogramTitle" placeholder="请输入小程序消息标题,最长为64字"  />
+                          </el-form-item>
+                          <el-form-item label="封面" prop="miniprogramPicUrl">
+                            <ImageUpload v-model="item.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="item.miniprogramAppid" value = '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="item.miniprogramPage" placeholder="小程序消息打开后的路径"  disabled />
+                          </el-form-item>
+                        </el-card>
+                      </div>
+                      <div v-if="item.contentType == 5 ">
+
+                        <el-form-item label="上传文件:" prop="fileUrl" label-width="100px">
+                          <el-upload
+                            v-model="item.fileUrl"
+                            class="avatar-uploader"
+                            :action="uploadUrl"
+                            :show-file-list="false"
+                            :on-success="(res, file) => handleAvatarSuccessFile(res, file, item)"
+                            :before-upload="beforeAvatarUploadFile">
+                            <i class="el-icon-plus avatar-uploader-icon"></i>
+                          </el-upload>
+                          <el-link v-if="item.fileUrl" type="primary" :href="downloadUrl(item.fileUrl)" download>
+                            {{item.fileUrl}}
+                          </el-link>
+                        </el-form-item>
+
+                      </div>
+
+                      <div v-if="item.contentType == 6 ">
+                        <el-form-item label="上传视频:" prop="videoUrl" label-width="100px">
+                          <el-upload
+                            v-model="item.videoUrl"
+                            class="avatar-uploader"
+                            :action="uploadUrl"
+                            :show-file-list="false"
+                            :on-success="(res, file) => handleAvatarSuccessVideo(res, file, item)"
+                            :before-upload="beforeAvatarUploadVideo">
+                            <i class="el-icon-plus avatar-uploader-icon"></i>
+                          </el-upload>
+                          <video v-if="item.videoUrl"
+                                 :src="item.videoUrl"
+                                 controls style="width: 200px;height: 100px">
+                          </video>
+                        </el-form-item>
+                      </div>
+                      <div v-if="item.contentType == 7 || item.contentType == 12">
+                        <el-input
+                          v-model="item.value"
+                          type="textarea" :rows="3" maxlength="66" show-word-limit
+                          placeholder="输入要转为语音的内容" style="width: 90%;margin-top: 10px;"
+                          @input="handleInputVideoText(item.value,item)"/>
+                      </div>
+                      <div v-if="item.contentType == 8">
+                          <el-button type="primary"
+                                     style="margin-bottom: 1%"
+                                     @click="hanldeSelectVideoNum(setting,index)">
+                            选择视频号
+                          </el-button>
+                          <el-card class="box-card" v-if="item.coverUrl">
+                            <el-form-item label="封面标题:" label-width="100px">
+                              <el-input v-model="item.nickname"
+                                        style="width: 90%;margin-bottom: 1%" disabled/>
+                            </el-form-item>
+                            <el-form-item label="头像:" label-width="100px">
+                              <el-image
+                                v-if="item.avatar != null"
+                                :src="item.avatar"
+                                :preview-src-list="[item.avatar]"
+                                :style="{ width: '50px', height: '50px' }"
+                              ></el-image>
+                            </el-form-item>
+                            <el-form-item label="封面:" label-width="100px">
+                              <el-image
+                                v-if="item.coverUrl != null"
+                                :src="item.coverUrl"
+                                :preview-src-list="[item.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="item.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="item.url"
+                                        style="width: 90%;" disabled/>
+                            </el-form-item>
+                          </el-card>
+                      </div>
+
+                      <div v-if="item.contentType == 14">
+                        <el-card class="box-card">
+                          <el-form-item label="福袋名称" >
+                            <el-select  v-model="item.luckyBagId"
+                                        placeholder="请选择福袋" size="mini"
+                                        @change="getLuckyBagStatus(item)" >
+                              <el-option
+                                v-for="dict in luckyBagList"
+                                :key="dict.id"
+                                :label="dict.name"
+                                :value="dict.id"
+                              />
+                            </el-select>
+                          </el-form-item>
+                          <el-form-item label="福袋状态" >
+                            <el-tag :type="item.luckyBagDataStatus == 1 ? 'success' : 'danger'">
+                              {{ item.luckyBagDataStatus == 1 ? '启用' : '禁用' }}
+                            </el-tag>
+                          </el-form-item>
+                        </el-card>
+                      </div>
+
+                      <div v-if="item.contentType == 17">
+                        <el-card class="box-card">
+                          <el-form-item label="短信模板" >
+                            <el-select v-model="item.smsTemplateId"
+                                       placeholder="请选择短信模板" size="mini"
+                                       @change="getSmsTemplateInfo(item)" >
+                              <el-option
+                                v-for="template in smsTemplateList"
+                                :key="template.tempId"
+                                :label="template.title"
+                                :value="template.tempId"
+                              />
+                            </el-select>
+                          </el-form-item>
+                          <el-form-item label="内容预览" v-if="item.smsTemplateContent">
+                            <div style="color: #666; font-size: 12px; padding: 8px; border: 1px solid #eee; border-radius: 4px; background-color: #f9f9f9; max-height: 100px; overflow-y: auto;">
+                              {{ item.smsTemplateContent }}
+                            </div>
+                          </el-form-item>
+                        </el-card>
+                      </div>
+
+                    </el-form-item>
+                    <div v-if="item.contentType == 20">
+                      <!--                                           <div >-->
+                      <el-card class="box-card">
+                        <el-form-item label="直播分组" required>
+                          <el-select
+                            v-model="item.liveGroupTypeId"
+                            placeholder="请选择直播分组"
+                            size="mini"
+                            filterable
+                            clearable
+                            @change="liveGroupTypeChange(item)"
+                          >
+                            <el-option
+                              v-for="dict in liveGroupTypeList"
+                              :key="dict.id"
+                              :label="dict.liveGroupType"
+                              :value="dict.id"
+                            />
+                          </el-select>
+                        </el-form-item>
+                        <el-form-item label="直播间" required>
+                          <el-select
+                            v-model="item.liveId"
+                            placeholder="请先选择直播分组"
+                            size="mini"
+                            filterable
+                            :disabled="!item.liveGroupTypeId"
+                            @change="liveChange(item)"
+                          >
+                            <el-option
+                              v-for="dict in (item.meta && item.meta.liveList ? item.meta.liveList : [])"
+                              :key="dict.liveId"
+                              :label="dict.liveName"
+                              :value="dict.liveId"
+                            />
+                          </el-select>
+                        </el-form-item>
+
+                        <el-form-item label="标题" prop="miniprogramTitle">
+                          <el-input v-model="item.miniprogramTitle" placeholder="请输入小程序消息标题,最长为64字节" :rows="2" maxlength="64"
+                                    type="textarea"   @input="checkLiveMiniprogramTitle(item)"/>
+                        </el-form-item>
+                        <el-form-item label="封面" prop="miniprogramPicUrl">
+                          <ImageUpload v-model="item.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="item.miniprogramAppid" value = 'wx776d6bd6848eec49' 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="item.miniprogramPage" placeholder="小程序消息打开后的路径"  disabled />
+                        </el-form-item>
+                      </el-card>
+                    </div>
+
+                    <el-form-item label="添加短链" v-if="item.contentType == 1 || item.contentType == 11"  >
+                      <el-tooltip content="请先根据科普选定科普小节之后再添加" effect="dark" :disabled="!!msgForm.videoId">
+                        <el-switch
+                          v-model="item.isBindUrl"
+                          :disabled="!msgForm.videoId"
+                          active-color="#13ce66"
+                          inactive-color="#DCDFE6"
+                          active-value="1"
+                          inactive-value="2">
+                        </el-switch>
+                      </el-tooltip>
+
+                      <span v-if="item.isBindUrl == '1'" style="margin-left: 10px; color: #13ce66">添加URL</span>
+                      <span v-if="item.isBindUrl == '2'" style="margin-left: 10px; color: #b1b4ba">不加URL</span>
+                    </el-form-item>
+
+                    <el-form-item label="程序类型" v-if="item.contentType == 4"  >
+                      <el-tooltip content="请先根据科普选定科普小节之后再添加" effect="dark" :disabled="!!msgForm.videoId">
+                        <el-radio-group v-model="item.isSortUrl" :disabled="!msgForm.videoId">
+                          <el-radio :label="1">发小程序卡片</el-radio>
+                          <el-radio :label="2">发微信的短链</el-radio>
+                        </el-radio-group>
+                      </el-tooltip>
+
+                    </el-form-item>
+                    <span v-if="item.contentType == 4" >不选默认【发小程序卡片】</span>
+
+                    <el-form-item label="课节过期时间" v-if="item.isBindUrl == '1'
+                                                          && item.contentType != 2
+                                                          && item.contentType != 5
+                                                          && item.contentType != 6
+                                                          && item.contentType != 8"
+                                  style="margin-top: 1%" label-width="100px">
+                      <el-row>
+                        <el-input-number  v-model="item.expiresDays"  :min="1" :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)" style="margin-top: 20px;" v-if="setting.length>1"></i>
+                </el-col>
+              </el-row>
+            </div>
+            <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetList()'  >添加内容</el-link>
+
+          </el-form-item>
+
+          <el-form-item label="发送时间类型" prop="sendTimeType" v-if="msgForm.draftStrategy==1" >
+            <el-radio-group v-model="msgForm.sendTimeType">
+              <el-radio :label="1">当天发送</el-radio>
+              <el-radio :label="2">次日发送</el-radio>
+            </el-radio-group>
+          </el-form-item>
+
+          <el-form-item label="发送时间" prop="sendTime" v-if="msgForm.draftStrategy==1" >
+            <el-time-picker
+              class="custom-input"
+              v-model="msgForm.sendTime"
+              value-format="HH:mm"
+              format="HH:mm"
+              placeholder="时间"
+              style="width: 100px;height: 20px;">
+            </el-time-picker>
+            <span class="tip" style="margin-left: 2%"> 不填时,默认为系统当前时间(立即发送)</span>
+          </el-form-item>
+
+        </el-form>
+        <div slot="footer" class="dialog-footer">
+          <el-button type="primary" @click="submitMsgForm">确 定</el-button>
+          <el-button @click="cancelMsgForm">取 消</el-button>
+        </div>
+    </el-dialog>
+
+    <!-- 添加或修改sopUserLogsInfo对话框 -->
+    <el-dialog title="批量修改客户营期" :visible.sync="updateMsgOpen.updateOpen" width="500px" append-to-body>
+      <el-form ref="updateLogsInfoFrom" :model="updateLogsInfoFrom" :rules="batchRules" label-width="120px">
+        <el-form-item label="选择营期时间" prop="paramTime">
+          <el-date-picker clearable size="small"
+                          v-model="updateLogsInfoFrom.paramTime"
+                          type="date"
+                          value-format="yyyy-MM-dd"
+                          placeholder="选择营期时间">
+          </el-date-picker>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+
+
+    <!-- <el-dialog :title="videoNumOptions.title" :visible.sync="videoNumOptions.open" width="1500px" append-to-body>
+      <userVideo ref="QwUserVideo" @videoResult="qwUserVideoResult"></userVideo>
+    </el-dialog> -->
+  </div>
+</template>
+
+<script>
+import { sendMsgSopType,} from "@/api/app/userLogsInfo";
+import ImageUpload from "@/views/qw/sop/ImageUpload.vue";
+import {courseList, videoList} from "@/api/qw/sop";
+import {updateTimeSopUserLogs} from "@/api/qw/sopUserLogs";
+import { getLiveOptions as fetchLiveOptionsApi, getLiveGroupTypeList as fetchLiveGroupTypeListApi } from "@/api/app/live";
+
+
+export default {
+  name: "sendMsgOpenTool",
+  components: { ImageUpload},
+  data() {
+    return {
+      //上传语音的遮罩层
+      voiceLoading :false,
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS2",
+      uploadUrlByVoice:process.env.VUE_APP_BASE_API+"/common/uploadOSSByHOOKVoice",
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // sopUserLogsInfo表格数据
+      sopUserLogsInfoList: [],
+      sysFsSopWatchStatus: [],
+      isSalesCallAdded:false,
+      videoNumOptions: {
+        title: '选择视频号',
+        open: false,
+        content: null,
+        contentIndex: null,
+      },
+      isSalesCallCustomerAdded:false,
+      tagList:[],
+      selectTags:[],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      updateOpen:false,
+
+      //修改营期
+      updateLogsInfoFrom:{},
+      queryParams1: {
+        pageNum: 1,
+        pageSize: 100,
+        dataStatus: '1',
+        name: null,
+        type: null
+      },
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        tagIds:null,
+        remark:null,
+        sopId: null,
+        userLogsId: null,
+        userIdParam:null,
+        startTimeParam:null,
+        externalContactId: null,
+        qwUserId: null,
+        corpId: null,
+        externalId: null,
+        fsUserId: null,
+        externalUserName: null,
+        createTime: null,
+      },
+
+      courseList:[],
+      videoList:[],
+      liveGroupTypeList: [],//直播分组
+      liveList: [],//直播间下拉
+      //插件版
+      sysQwSopAiContentType:[],
+
+      sendMsgOpen:{
+        title:'一键批量群发',
+        open:false,
+        ids:null,
+      },
+      updateMsgOpen:{
+        title:'批量修改客户营期',
+        updateOpen:false,
+        ids:null,
+      },
+
+      smsTemplateList: [],
+      // 表单参数
+      form: {},
+      setting:[{contentType:'1', value: '',}],
+      msgForm:{
+        videoId:null,
+        courseId:null,
+        courseType:null,
+        userIdParam:null,
+        setting:null,
+        ids:null,
+        type:null,
+        corpId:null,
+        sopId: null,
+        startTime: null,
+        sendTime: null,
+        qwUserId: null,
+        draftStrategy:1,
+        sendTimeType: 1, // 添加字段:1-当天发送,2-次日发送
+      },
+      // 表单校验
+      rules: {},
+      batchRules:{
+        paramTime: [
+          { required: true, message: '选择的时间不能为空', trigger: 'blur' }
+        ],
+      },
+      msgRules:{},
+    };
+  },
+
+  created() {
+    // 本页内容类别字典:sys_qwSopAi_contentType(直播对应 dictValue=20)
+    // 注意:与群发页 app_sop_plugin_settingType(直播=21)不是同一套字典
+    this.getDicts("sys_qwSopAi_contentType").then(response => {
+      this.sysQwSopAiContentType = response.data;
+    });
+    this.getDicts("sys_fs_sop_watch_status").then(response => {
+      this.sysFsSopWatchStatus = response.data;
+    });
+
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+
+    fetchLiveGroupTypeListApi({ pageNum: 1, pageSize: 999 }).then(response => {
+      this.liveGroupTypeList = response.rows || response.data || [];
+    });
+
+    this.loadSmsTemplates();
+  },
+  methods: {
+    getLuckyBagStatus(content) {
+      const selectedLuckyBag = this.luckyBagList.find(item => item.id === content.luckyBagId);
+      if (selectedLuckyBag) {
+        content.luckyBagDataStatus = selectedLuckyBag.dataStatus; // 自动填充标题
+      } else {
+        // 若未找到对应直播间,清空标题和封面(可选)
+        content.luckyBagDataStatus = null;
+      }
+    },
+    checkLiveMiniprogramTitle(item) {
+      this.$forceUpdate();
+    },
+    /**
+     * 直播分组切换
+     */
+    async liveGroupTypeChange(content) {
+      this.$set(content, 'liveId', null);
+      this.$set(content, 'miniprogramTitle', '');
+      this.$set(content, 'miniprogramPicUrl', '');
+      if (!content.liveGroupTypeId) {
+        this.$set(content, 'meta', Object.assign({}, content.meta || {}, {
+          liveLoading: false,
+          liveList: [],
+        }));
+        return;
+      }
+      this.$set(content, 'meta', Object.assign({}, content.meta || {}, {
+        liveLoading: true,
+        liveList: [],
+      }));
+      const liveRes = await fetchLiveOptionsApi({ liveGroupType: content.liveGroupTypeId });
+      this.$set(content, 'meta', Object.assign({}, content.meta || {}, {
+        liveLoading: false,
+        liveList: liveRes.data || liveRes.rows || [],
+      }));
+    },
+    liveChange(content) {
+      // content.liveId 是选中的直播间 ID(liveId)
+      const list = (content.meta && content.meta.liveList) ? content.meta.liveList : [];
+      const selectedLive = list.find(live => parseInt(live.liveId) === parseInt(content.liveId));
+      if (selectedLive) {
+        // 从选中的直播间对象中提取标题和封面,赋值给当前内容的对应字段
+        content.miniprogramTitle = selectedLive.liveName || ''; // 自动填充标题
+        content.miniprogramPicUrl = selectedLive.liveImgUrl || ''; // 自动填充封面
+      } else {
+        // 若未找到对应直播间,清空标题和封面(可选)
+        content.miniprogramTitle = '';
+        content.miniprogramPicUrl = '';
+      }
+    },
+    oneClickGroupSending(val,type,corpId){
+
+      this.sendMsgOpen.open= true;
+      this.msgForm.ids = val;
+      this.msgForm.type = type;
+      this.msgForm.corpId = corpId;
+
+    },
+    updateTimeUserLogs(val,type,corpId,sopId){
+
+      this.updateMsgOpen.updateOpen= true;
+      this.msgForm.ids = val;
+      this.msgForm.type = type;
+      this.msgForm.corpId = corpId;
+      this.msgForm.sopId = sopId;
+
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["updateLogsInfoFrom"].validate(valid => {
+        if (valid) {
+
+          this.updateLogsInfoFrom.ids=this.msgForm.ids;
+          this.updateLogsInfoFrom.sopId= this.msgForm.sopId
+          this.updateLogsInfoFrom.corpId= this.msgForm.corpId
+
+          let loadingRock = this.$loading({
+            lock: true,
+            text: '正在执行中请稍后~~请不要刷新页面!!',
+            spinner: 'el-icon-loading',
+            background: 'rgba(0, 0, 0, 0.7)'
+          });
+
+          updateTimeSopUserLogs(this.updateLogsInfoFrom).then(response => {
+            this.msgSuccess("修改成功");
+            this.open = false;
+            this.updateMsgOpen.updateOpen=false;
+
+            this.$emit('flashNotify')
+            loadingRock.close();
+          }).finally(res=>{
+            loadingRock.close();
+          })
+
+        }
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.updateMsgOpen.updateOpen = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        sopId: null,
+        userLogsId: null,
+        externalContactId: null,
+        qwUserId: null,
+        corpId: null,
+        externalId: null,
+        fsUserId: null,
+        externalUserName: null,
+        createTime: null,
+        crtTime: null,
+        updateTime: null
+      };
+      this.resetForm("form");
+    },
+    courseChange() {
+      if (this.msgForm.courseId != null ) {
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === this.msgForm.courseId);
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的标题/封面上值
+          if (selectedCourse && this.msgForm.courseId != null) {
+            if ( this.setting[i].contentType == 3 || this.setting[i].contentType == 9 ){
+              this.$set(this.setting[i], 'linkTitle', selectedCourse.dictLabel);
+              this.$set(this.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+            }
+
+            if ( this.setting[i].contentType == 4 || this.setting[i].contentType == 15|| this.setting[i].contentType == 19){
+              this.$set(this.setting[i], 'miniprogramPicUrl', selectedCourse.dictImgUrl);
+            }
+          }
+
+        }
+
+      }
+      videoList(this.msgForm.courseId).then(response => {
+        this.videoList=response.list;
+      });
+    },
+
+    videoIdChange() {
+      if (this.msgForm.videoId != null ) {
+        // 查找选中的课节对应的 label
+        const selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === this.msgForm.videoId);
+        if (selectedVideo) {
+          console.log("selectedVideo={}", selectedVideo)
+
+          // 重新获取内容类型字典数据
+          this.getDicts("sys_qwSopAi_contentType").then(response => {
+            let contentTypeList = response.data;
+
+            if (!selectedVideo || selectedVideo.openClass != 1) {
+              contentTypeList = contentTypeList.filter(item => item.dictValue !== '19');
+            }
+
+            // 使用 Vue.set 确保响应式
+            this.$set(this, 'sysQwSopAiContentType', contentTypeList);
+
+            // 强制更新每个 setting 项
+            this.setting.forEach((item, index) => {
+              if (item.contentType === '19' && (!selectedVideo || selectedVideo.openClass != 1)) {
+                // 将 contentType 重置为默认值(比如1)
+                this.$set(item, 'contentType', '1');
+              }
+              this.$set(this.setting[index], 'openClass', selectedVideo.openClass);
+            });
+
+            // 强制整个组件更新
+            this.$forceUpdate();
+          });
+        }
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的描述上值
+          if (selectedVideo && this.msgForm.videoId != null) {
+            if (this.setting[i].contentType == 3 || this.setting[i].contentType == 9 ){
+              this.$set(this.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+            }
+
+            if (this.setting[i].contentType == 4 || this.setting[i].contentType == 15|| this.setting[i].contentType == 19){
+              this.$set(this.setting[i], 'miniprogramTitle', selectedVideo.dictLabel);
+            }
+
+
+          }
+        }
+      }
+    },
+    handleAvatarSuccessFile(res, file, item) {
+      if (res.code === 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(item, '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, item) {
+      if(res.code==200){
+        // 使用 $set 确保响应式更新
+        this.$set(item, '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;
+    },
+
+    handleInputVideoText(value,content){
+      // 允许的字符:中文、英文(大小写)、数字和指定标点符号(,。!?)
+      const regex = /^[\u4e00-\u9fa5,。!?,!?]+$/;
+
+      // 删除不符合条件的字符
+      const filteredValue = value.split('').filter(char => regex.test(char)).join('');
+
+      this.$set(content, 'value', filteredValue);
+
+    },
+
+    delSetList(index){
+      this.setting.splice(index,1)
+    },
+    addSetList(){
+      const newSetting = {
+        contentType:'1',
+        value: '',
+        sendTimeType: 1, // 为每个规则项添加发送时间类型
+      };
+      // 将新设置项添加到 content.setting 数组中
+      this.setting.push(newSetting);
+
+    },
+
+
+    handleKeydown(event, index) {
+      const item = this.setting[index];
+      const textarea = this.$refs[`textarea-${index}`][0].$refs.textarea;
+      const cursorPosition = textarea.selectionStart;
+
+      // 检查是否按下了 Backspace 或 Delete 键
+      if (event.key === 'Backspace' || event.key === 'Delete') {
+        const tags = ['#客服称呼#', '#客户称呼#']; // 需要检查的标签
+        const value = item.value;
+
+        // 遍历标签,检查是否需要删除
+        for (const tag of tags) {
+          let start, end;
+
+          if (event.key === 'Backspace') {
+            // 检查光标前是否是当前标签的一部分
+            start = cursorPosition - tag.length;
+            if (start >= 0 && value.slice(start, cursorPosition) === tag) {
+              // 删除整个标签
+              item.value = value.slice(0, start) + value.slice(cursorPosition);
+              // 更新光标位置
+              this.$nextTick(() => {
+                textarea.setSelectionRange(start, start);
+              });
+              // 更新状态
+              if (tag === '#客服称呼#') item.isSalesCallAdded = false;
+              if (tag === '#客户称呼#') item.isSalesCallCustomerAdded = false;
+              event.preventDefault(); // 阻止默认删除行为
+              break; // 找到匹配的标签后退出循环
+            }
+          } else if (event.key === 'Delete') {
+            // 检查光标后是否是当前标签的一部分
+            end = cursorPosition + tag.length;
+            if (end <= value.length && value.slice(cursorPosition, end) === tag) {
+              // 删除整个标签
+              item.value = value.slice(0, cursorPosition) + value.slice(end);
+              // 更新状态
+              if (tag === '#客服称呼#') item.isSalesCallAdded = false;
+              if (tag === '#客户称呼#') item.isSalesCallCustomerAdded = false;
+              event.preventDefault(); // 阻止默认删除行为
+              break; // 找到匹配的标签后退出循环
+            }
+          }
+
+          // 检查光标是否位于标签的中间
+          for (let i = 0; i <= tag.length; i++) {
+            const tagStart = cursorPosition - i;
+            const tagEnd = tagStart + tag.length;
+            if (
+              tagStart >= 0 &&
+              tagEnd <= value.length &&
+              value.slice(tagStart, tagEnd) === tag
+            ) {
+              // 删除整个标签
+              item.value = value.slice(0, tagStart) + value.slice(tagEnd);
+              // 更新光标位置
+              this.$nextTick(() => {
+                textarea.setSelectionRange(tagStart, tagStart);
+              });
+              // 更新状态
+              if (tag === '#客服称呼#') item.isSalesCallAdded = false;
+              if (tag === '#客户称呼#') item.isSalesCallCustomerAdded = false;
+              event.preventDefault(); // 阻止默认删除行为
+              break; // 找到匹配的标签后退出循环
+            }
+          }
+        }
+      }
+    },
+
+    // 切换添加客服称呼按钮点击事件
+    toggleSalesCall(index) {
+      const item = this.setting[index];
+      const salesCall = '#客服称呼#';
+      const textarea = this.$refs[`textarea-${index}`][0].$refs.textarea;
+
+      // 获取当前光标位置
+      const cursorPosition = textarea.selectionStart;
+
+      if (item.isSalesCallAdded) {
+        // 移除所有的 #客服称呼#
+        item.value = item.value.replace(new RegExp(salesCall, 'g'), '');
+      } else {
+        // 添加 #客服称呼#
+        item.value = item.value.slice(0, cursorPosition) + salesCall + item.value.slice(cursorPosition);
+      }
+
+      // 切换状态
+      item.isSalesCallAdded = !item.isSalesCallAdded;
+
+      // 保持光标位置
+      this.$nextTick(() => {
+        textarea.setSelectionRange(cursorPosition, cursorPosition);
+      });
+    },
+    toggleSalesCallCustomer(index) {
+      const item = this.setting[index];
+      const salesCall = '#客户称呼#';
+      const textarea = this.$refs[`textarea-${index}`][0].$refs.textarea;
+
+      // 获取当前光标位置
+      const cursorPosition = textarea.selectionStart;
+
+      if (item.isSalesCallCustomerAdded) {
+        // 移除所有的 #客服称呼#
+        item.value = item.value.replace(new RegExp(salesCall, 'g'), '');
+      } else {
+        // 添加 #客户称呼#
+        item.value = item.value.slice(0, cursorPosition) + salesCall + item.value.slice(cursorPosition);
+      }
+
+      // 切换状态
+      item.isSalesCallCustomerAdded = !item.isSalesCallCustomerAdded;
+
+      // 保持光标位置
+      this.$nextTick(() => {
+        textarea.setSelectionRange(cursorPosition, cursorPosition);
+      });
+    },
+
+    handleContentTypeChange() {
+
+      //如果是链接的才上
+      if (this.msgForm.courseId != null ) {
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === this.msgForm.courseId);
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的标题/封面上值
+          if (selectedCourse  && this.msgForm.courseId != null) {
+
+            if (this.setting[i].contentType == 3 || this.setting[i].contentType == 9){
+              this.$set(this.setting[i], 'linkTitle', selectedCourse.dictLabel);
+              this.$set(this.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+            }
+            if (this.setting[i].contentType == 4 || this.setting[i].contentType == 15|| this.setting[i].contentType == 19){
+              this.$set(this.setting[i], 'miniprogramPicUrl', selectedCourse.dictImgUrl);
+            }
+
+
+          }
+
+        }
+
+      }
+      if (this.msgForm.videoId != null ) {
+        // 查找选中的课节对应的 label
+        const selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === this.msgForm.videoId);
+
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的描述上值
+          if (selectedVideo  && this.msgForm.videoId != null) {
+
+            if (this.setting[i].contentType == 3 || this.setting[i].contentType == 9){
+              this.$set(this.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+            }
+            if (this.setting[i].contentType == 4 || this.setting[i].contentType == 15|| this.setting[i].contentType == 19){
+              this.$set(this.setting[i], 'miniprogramTitle', selectedVideo.dictLabel);
+            }
+
+          }
+        }
+      }
+
+
+    },
+
+    resetSendMsgSop() {
+      this.msgForm = {
+        videoId:null,
+        courseId:null,
+        courseType:null,
+        corpId:null,
+        setting:null,
+        sendTime:null,
+        draftStrategy:1,
+        sendTimeType: 1, // 添加默认值
+      };
+      this.resetForm("msgForm");
+    },
+
+    // 获取短信模板信息(类似福袋的处理方式)
+    getSmsTemplateInfo(content) {
+      // 如果短信模板列表为空,先加载数据
+      if (this.smsTemplateList.length === 0) {
+        getSmsTempList().then(response => {
+          this.smsTemplateList = response.rows || response.data || [];
+          // 加载完成后再次调用自身来处理选中项
+          this.processSelectedSmsTemplate(content);
+        }).catch(error => {
+          console.error('加载短信模板失败:', error);
+          this.$message.error('加载短信模板失败');
+        });
+      } else {
+        // 直接处理选中项
+        this.processSelectedSmsTemplate(content);
+      }
+    },
+
+    // 处理选中的短信模板
+    processSelectedSmsTemplate(content) {
+      const selectedTemplate = this.smsTemplateList.find(item => item.tempId === content.smsTemplateId);
+      if (selectedTemplate) {
+        // 自动填充模板内容预览
+        this.$set(content, 'smsTemplateContent', selectedTemplate.content || selectedTemplate.templateContent || '');
+      } else {
+        // 清空相关内容
+        this.$set(content, 'smsTemplateContent', '');
+      }
+    },
+
+    // 加载短信模板列表
+    loadSmsTemplates() {
+      if (this.smsTemplateList.length > 0) {
+        return; // 如果已经有数据,不再重复加载
+      }
+
+      getSmsTempList().then(response => {
+        this.smsTemplateList = response.rows || response.data || [];
+        if (this.smsTemplateList.length === 0) {
+          this.$message.info('暂无可用的短信模板');
+        }
+      }).catch(error => {
+        console.error('加载短信模板失败:', error);
+        this.$message.error('加载短信模板失败');
+      });
+    },
+
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+
+// 修改后的方法:在原有的基础上增加一条消息
+    processContentType13(settings) {
+      const result = [];
+
+      settings.forEach(item => {
+        // 如果 contentType 是 13 且 value 不为空,则额外添加一条 contentType 为 1 的消息
+        if (item.contentType == 13 && item.value && item.value.trim() !== '') {
+          // 按换行符分割内容,取第一部分
+          const contentParts = item.value.split('\n')
+            .map(part => part.trim())
+            .filter(part => part !== '');
+
+          if (contentParts.length > 0) {
+            // 创建一条额外的消息,使用第一部分内容
+            const additionalMessage = {
+              ...item,
+              contentType: 1, // 改为文本类型
+              value: contentParts[0]
+            };
+            result.push(additionalMessage);
+          }
+        }
+        if (item.contentType == 20) {
+          item.miniprogramAppid = 'wx776d6bd6848eec49'
+        }
+        result.push(item);
+      });
+
+      return result;
+    },
+    submitMsgForm() {
+      // 表单验证
+      this.$refs["msgForm"].validate(valid => {
+        if (valid) {
+          // 确保发送时间类型被传递
+          this.msgForm.sendTimeType = this.msgForm.sendTimeType;
+          if (this.msgForm.draftStrategy == 1) {
+            // 处理 contentType 13 的拆分逻辑
+            const processedSetting = this.processContentType13(this.setting);
+            this.msgForm.setting = JSON.stringify(processedSetting);
+
+            // 基础验证
+            if (processedSetting.length <= 0) {
+              return this.$message.error("请添加规则")
+            }
+            if (this.msgForm.courseId === null || this.msgForm.courseId === '') {
+              return this.$message.error("科普不能为空")
+            }
+            if (this.msgForm.videoId === null || this.msgForm.videoId === '') {
+              return this.$message.error("课节不能为空")
+            }
+            if (this.msgForm.courseType === null || this.msgForm.courseType === '') {
+              return this.$message.error("消息类型不能为空")
+            }
+
+            // 内容类型验证
+            for (let i = 0; i < processedSetting.length; i++) {
+              const item = processedSetting[i];
+
+              // 文本内容验证
+              if (item.contentType == 1 && (item.value == null || item.value == "")) {
+                return this.$message.error("内容不能为空")
+              }
+              // 图片内容验证
+              if (item.contentType == 2 && (item.imgUrl == null || item.imgUrl == "")) {
+                return this.$message.error("图片不能为空")
+              }
+              // 链接内容验证
+              if ((item.contentType == 3 || item.contentType == 9) && (item.linkTitle == null || item.linkTitle == "")) {
+                return this.$message.error("链接标题不能为空")
+              }
+              if ((item.contentType == 3 || item.contentType == 9) && (item.linkDescribe == null || item.linkDescribe == "")) {
+                return this.$message.error("链接描述不能为空")
+              }
+              if ((item.contentType == 3 || item.contentType == 9) && (item.linkImageUrl == null || item.linkImageUrl == "")) {
+                return this.$message.error("链接图片不能为空")
+              }
+              if ((item.contentType == 3 || item.contentType == 9) && item.type == 1 && (item.linkUrl == null || item.linkUrl == "")) {
+                return this.$message.error("链接地址不能为空")
+              }
+              // 小程序内容验证
+              if ((item.contentType == 4 || item.contentType == 15  || item.contentType == 19) && (item.miniprogramTitle == null || item.miniprogramTitle == "")) {
+                return this.$message.error("小程序消息标题不能为空")
+              }
+              if ((item.contentType == 4 || item.contentType == 15 || item.contentType == 19) && (item.miniprogramPicUrl == null || item.miniprogramPicUrl == "")) {
+                return this.$message.error("小程序封面地址不能为空")
+              }
+              // 文件内容验证
+              if (item.contentType == 5 && (item.fileUrl == null || item.fileUrl == "")) {
+                return this.$message.error("文件不能为空")
+              }
+              // 视频内容验证
+              if (item.contentType == 6 && (item.videoUrl == null || item.videoUrl == "")) {
+                return this.$message.error("视频不能为空")
+              }
+              // 语音内容验证
+              if (item.contentType == 7 && (item.value == null || item.value == "")) {
+                return this.$message.error("语音不能为空")
+              }
+              // 直播间内容验证
+              if (item.contentType == 20 && (item.liveGroupTypeId == null || item.liveGroupTypeId === "")) {
+                this.$message.error("直播分组不能为空")
+                return false;
+              }
+              if (item.contentType == 20 && (item.liveId == null || item.liveId == "")) {
+                this.$message.error("直播间不能为空")
+                return false;
+              }
+              if (item.contentType == 20 && (item.miniprogramTitle == null || item.miniprogramTitle == "")) {
+                this.$message.error("标题不能为空")
+                return false;
+              }
+              if (item.contentType == 20 && (item.miniprogramPicUrl == null || item.miniprogramPicUrl == "")) {
+                this.$message.error("封面不能为空")
+                return false;
+              }
+              // 福袋内容验证
+              if (item.contentType == 14 && (item.luckyBagId == null || item.luckyBagId === "")) {
+                return this.$message.error("福袋不能为空")
+              }
+              // 短信模板验证
+              if (item.contentType == 17 && (item.smsTemplateId == null || item.smsTemplateId === "")) {
+                return this.$message.error("短信模板不能为空")
+              }
+            }
+
+            // 短信模板数据映射
+            for (let j = 0; j < processedSetting.length; j++) {
+              const item = processedSetting[j];
+              if (item.contentType == 17 && item.smsTemplateId) {
+                // 根据选中的短信模板ID查找对应的模板信息
+                const selectedTemplate = this.smsTemplateList.find(template => template.tempId === item.smsTemplateId);
+                if (selectedTemplate) {
+                  // 设置短信模板的相关字段
+                  this.$set(item, 'smsTemplateCode', selectedTemplate.tempCode || '');
+                  this.$set(item, 'smsTemplateTitle', selectedTemplate.title || '');
+                }
+              }
+            }
+
+            // 重新序列化处理后的数据
+            this.msgForm.setting = JSON.stringify(processedSetting);
+
+            // 关闭弹窗
+            this.sendMsgOpen.open = false;
+
+            // 显示加载状态
+            const loading = this.$loading({
+              lock: true,
+              text: '正在执行中请稍后~~请不要刷新页面!!',
+              spinner: 'el-icon-loading',
+              background: 'rgba(0, 0, 0, 0.7)'
+            });
+
+            // 发送消息
+            sendMsgSopType(this.msgForm).then(response => {
+              this.msgSuccess("一键群发成功");
+              loading.close();
+              this.setting = [{ contentType: '1', value: '' }];
+              this.resetSendMsgSop();
+            }).catch(error => {
+              loading.close();
+              this.msgError("发送失败:" + (error.message || '未知错误'));
+            });
+            // 测试用 - 查看处理后的数据
+          /*  console.log('原始setting:', this.setting);
+            console.log('处理后的setting:', processedSetting);
+            console.log('最终提交的数据:', this.msgForm);
+            loading.close();*/
+          }
+        }
+      });
+    },
+    cancelMsgForm(){
+      this.sendMsgOpen.open = false;
+      this.resetSendMsgSop();
+    },
+
+    //选择视频号
+    hanldeSelectVideoNum(content, index) {
+      this.videoNumOptions.content = content;
+      this.videoNumOptions.contentIndex = index;
+      this.videoNumOptions.open = true;
+    },
+
+    qwUserVideoResult(val) {
+
+      // 根据选中的内容,将返回的数据更新到相应的表单项
+      const content = this.videoNumOptions.content;
+      const setList = content[this.videoNumOptions.contentIndex];
+      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;
+      setList.videoId = val.id;
+      console.info(setList)
+
+      this.videoNumOptions.open = false;
+
+    },
+  }
+};
+</script>

+ 689 - 0
src/views/app/sop/userLogsInfo/sopUserLogsInfoDetails.vue

@@ -0,0 +1,689 @@
+<template>
+    <div class="app-container">
+
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+            <el-form-item label="会员id" prop="fsUserId">
+                <el-input
+                    v-model="queryParams.fsUserId"
+                    @input="queryParams.fsUserId = queryParams.fsUserId.replace(/[^\d]/g, '')"
+                    placeholder="请输入会员id"
+                    clearable
+                    size="small"
+                    @keyup.enter.native="handleQuery"
+                />
+            </el-form-item>
+            <el-form-item label="会员名称" prop="fsUserName">
+                <el-input v-model="queryParams.fsUserName" placeholder="请输入会员名称" clearable size="small"
+                    @keyup.enter.native="handleQuery" />
+            </el-form-item>
+
+            <el-form-item label="进线时间" prop="inComTime">
+                <el-date-picker clearable size="small" v-model="queryParams.inComTime" type="date"
+                    value-format="yyyy-MM-dd" placeholder="选择进线时间">
+                </el-date-picker>
+            </el-form-item>
+
+            <el-form-item label="标签" prop="tagIds">
+                <div @click="hangleChangeTags()"
+                    style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
+                    <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+                        <el-tag type="success" closable :disable-transitions="false" v-for="list in this.selectTags"
+                            :key="list.id" @close="handleCloseTags(list)" style="margin: 3px;">{{ list.name }}
+                        </el-tag>
+                    </div>
+                </div>
+
+            </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" v-if="queryParams.filterMode == 1">
+            <el-col :span="1.5">
+                <el-tooltip class="item" effect="dark"
+                    content="此功能用于给 选中的 营期 内【所有的】客户发送消息" placement="top">
+                    <el-button
+                        type="warning"
+                        icon="el-icon-s-promotion"
+                        size="medium"
+                        :disabled="multiple"
+                        @click="openGroupSendMessagePage"
+                    >一键群发
+                    </el-button>
+                </el-tooltip>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button type="danger" plain icon="el-icon-edit" size="medium" :disabled="multiple"
+                    @click="handleUpdate">批量修改客户营期</el-button>
+            </el-col>
+        </el-row>
+
+        <div style="color: #999;font-size: 14px;display: flex;align-items: center;margin-bottom: 5px">
+            <i class="el-icon-info"></i>
+            【营期一键群发】:此功能用于给 选中的 营期 内【所有的】客户发送 消息
+        </div>
+        <div style="color: #999;font-size: 14px;display: flex;align-items: center;margin-bottom: 5px">
+            <i class="el-icon-info"></i>
+            【批量修改客户营期】:选中 相应的客户 修改到 想进的相应的营期
+        </div>
+        <el-table border v-loading="loading" :data="sopUserLogsInfoList" @selection-change="handleSelectionChange"
+            v-if="queryParams.filterMode == 1">
+            <el-table-column type="selection" width="55" align="center" />
+            <el-table-column label="编号" align="center" prop="id" min-width="180" />
+            <el-table-column label="会员id" align="center" prop="fsUserId" min-width="150"/>
+            <el-table-column label="会员名称" align="center" prop="fsUserName" min-width="150"/>
+            <el-table-column label="会员标签" align="center" prop="tagsName" min-width="250">
+                <template slot-scope="scope">
+                    <div v-for="name in (scope.row.tagsName ? scope.row.tagsName.split(',') : [])" style="display: inline;">
+                        <el-tag type="success">{{ name }}</el-tag>
+                    </div>
+                </template>
+            </el-table-column>
+
+            <el-table-column label="进线时间" align="center" prop="createTime" min-width="180" />
+            <el-table-column label="修改时间" align="center" prop="updateTime" min-width="180" />
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width" min-width="120" fixed="right">
+                <template slot-scope="scope">
+                    <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除客户营期</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <pagination v-show="total > 0" :total="total" :page.sync="queryParams.startIndex"
+            :limit.sync="queryParams.pageLimit" @pagination="getList" />
+
+        <!-- 添加或修改sopUserLogsInfo对话框 -->
+        <el-dialog title="批量修改客户营期" :visible.sync="updateOpen" width="500px" height="300px" append-to-body>
+            <el-form ref="updateLogsInfoFrom" :model="updateLogsInfoFrom" :rules="batchRules" label-width="120px">
+                <el-form-item label="选择营期时间" prop="paramTime">
+                    <el-date-picker clearable size="small" v-model="updateLogsInfoFrom.pTime" type="date"
+                        value-format="yyyy-MM-dd" placeholder="选择营期时间">
+                    </el-date-picker>
+                </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitForm">确 定</el-button>
+                <el-button @click="cancel">取 消</el-button>
+            </div>
+        </el-dialog>
+
+
+        <!--  搜索标签   -->
+        <el-dialog :title="changeTagDialog.title" :visible.sync="changeTagDialog.open" width="1000px" custom-class="search-tag-dialog"
+            append-to-body>
+            <select-tag 
+                :tagSelected="selectTags"
+                @tagSubmit="tagSubmitForm"
+                @tagCancel="tagCancel" 
+                @tagSelection="tagSelection"
+            />
+        </el-dialog>
+
+        <el-dialog title="一键群发"
+            :visible.sync="groupSendMessageForm.visible" width="1000px" append-to-body :close-on-click-modal="false">
+        <group-send-message
+            :visible="groupSendMessageForm.visible"
+            :ids="groupSendMessageForm.ids"
+            :sourceType="1"
+            @changeGroupSendMessageVisible="changeGroupSendMessageVisible"
+        />
+        </el-dialog>
+
+    </div>
+</template>
+
+<script>
+import {
+    listSopUserLogsInfo,
+    delSopUserLogsInfo,
+    exportSopUserLogsInfo,
+    sendMsgSop, batchUpdateSopUserLogsInfoToTime
+} from "@/api/app/userLogsInfo";
+import ImageUpload from "@/views/app/sop/info/ImageUpload";
+import selectTag from "@/views/app/tagGroup/selectTag.vue";
+import groupSendMessage from "@/views/app/sop/userLogsInfo/groupSendMessage.vue";
+
+export default {
+    name: "sopUserLogsInfoDetails",
+    components: { ImageUpload, selectTag, groupSendMessage, },
+    data() {
+        return {
+            groupSendMessageForm: {
+                ids: [],
+                visible: false,
+            },
+            luckyBagList: [],
+            luckyBag: null,
+            liveList: [],
+            //上传语音的遮罩层
+            voiceLoading: false,
+            uploadUrl: process.env.VUE_APP_BASE_API + "/app/common/uploadOSS2",
+            uploadUrlByVoice: process.env.VUE_APP_BASE_API + "/app/common/uploadOSSByHOOKVoice",
+            // 遮罩层
+            loading: true,
+            // 导出遮罩层
+            exportLoading: false,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            videoNumOptions: {
+                title: '选择视频号',
+                open: false,
+                content: null,
+                contentIndex: null,
+            },
+            // sopUserLogsInfo表格数据
+            sopUserLogsInfoList: [],
+            isSalesCallAdded: false,
+            isSalesCallCustomerAdded: false,
+            selectTags: [],
+            // 弹出层标题
+            title: "",
+            // 是否显示弹出层
+            open: false,
+            updateOpen: false,
+            // 查询参数
+            queryParams: {
+                startIndex: 1,
+                pageLimit: 10,
+                tagIds: null,
+                remark: null,
+                sopId: null,
+                userLogsId: null,
+                userIdParam: null,
+                startTimeParam: null,
+                fsUserId: null,
+                fsUserName: null,
+                inComTime: null,
+                createTime: null,
+                entryTime: null,
+                fsUserIdStatus: null,
+            },
+            tagTotal: 0,
+
+            //标签
+            changeTagDialog: {
+                title: "",
+                open: false,
+            },
+
+            courseList: [],
+            videoList: [],
+
+            sendMsgOpen: {
+                title: '一键批量群发',
+                open: false,
+                ids: null,
+            },
+            // 表单参数
+            form: {},
+            updateLogsInfoFrom: {},
+            setting: [{ contentType: '1', value: '', }],
+            msgForm: {
+                videoId: null,
+                courseId: null,
+                courseType: null,
+                userIdParam: null,
+                setting: null,
+                ids: null,
+                sopId: null,
+                startTime: null,
+            },
+            // 表单校验
+            rules: {},
+            batchRules: {
+                pTime: [
+                    { required: true, message: '选择的时间不能为空', trigger: 'blur' }
+                ],
+            },
+            msgRules: {},
+        };
+    },
+    props: {
+        refreshCount: {
+            default: 1,
+            type: Number,
+        },
+        params: {
+            default: {},
+            type: Object,
+        }
+    },
+    watch: {
+        refreshCount: {
+            handler(o, n) {
+                this.getList();
+            },
+            immediate: true
+        },
+    },
+    methods: {
+
+        hangleChangeTags() {
+            this.changeTagDialog.open = true;
+        },
+        tagSelection(group, tag) {
+            if (!tag.isSelected) {
+                this.selectTags = this.selectTags.filter(item => tag.id != item.id);
+            } else {
+                if (!this.selectTags.some(st => st.id == tag.id)) {
+                    this.selectTags.push(tag);
+                }
+            }
+            this.queryParams.tagIds = this.selectTags.map(st => st.id);
+        },
+
+        //确定选择标签
+        tagSubmitForm() {
+            this.updateLogsInfoFrom.pTime = null;
+            this.changeTagDialog.open = false;
+        },
+        //取消选择标签
+        tagCancel() {
+            this.updateLogsInfoFrom.pTime = null;
+            this.changeTagDialog.open = false;
+        },
+
+        //删除一些选择的标签
+        handleCloseTags(tagItem) {
+            this.selectTags = this.selectTags.filter(t => t.id != tagItem.id);
+            if (this.selectTags != null && this.selectTags.length > 0) {
+                // 确保 this.form.tags 是数组
+                if (!this.queryParams.tagIds) {
+                    this.queryParams.tagIds = []; // 如果未定义,初始化
+                } else {
+                    this.queryParams.tagIds = []; // 清空已有数据
+                }
+                // 遍历并添加 tagId
+                this.selectTags.forEach(tag => {
+                    if (tag.tagId) { // 确保 tagId 存在
+                        this.queryParams.tagIds.push(tag.id);
+                    }
+                });
+                this.queryParams.tagIds = this.queryParams.tagIds.join(",");
+            } else {
+                this.queryParams.tagIds = null;
+            }
+        },
+        /** 查询sopUserLogsInfo列表 */
+        getList() {
+            this.loading = true;
+            this.queryParams.sopId = this.params.sopId;
+            this.queryParams.filterMode = this.params.filterMode;
+            this.queryParams.userLogsId = this.params.id;
+            let qp = {...this.queryParams};
+            listSopUserLogsInfo(qp).then(response => {
+                this.sopUserLogsInfoList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.updateOpen = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                id: null,
+                sopId: null,
+                userLogsId: null,
+                externalContactId: null,
+                qwUserId: null,
+                corpId: null,
+                externalId: null,
+                fsUserId: null,
+                externalUserName: null,
+                createTime: null,
+                crtTime: null,
+                updateTime: null
+            };
+            this.resetForm("form");
+        },
+
+
+        resetSendMsgSop() {
+            this.msgForm = {
+                corpId: null,
+                videoId: null,
+                courseId: null,
+                courseType: null,
+                setting: null,
+                ids: null,
+            };
+            this.resetForm("msgForm");
+        },
+
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.selectTags = [];
+            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 = "添加客户营期";
+        },
+
+        /**
+         * 一键群发
+         */
+        handleSendMsg() {
+            this.sendMsgOpen.open = true;
+            this.sendMsgOpen.ids = this.ids;
+        },
+
+        /** 修改按钮操作 */
+        handleUpdate() {
+            this.updateOpen = true;
+        },
+
+        // 修改后的方法:在原有的基础上增加一条消息
+        processContentType13(settings) {
+            const result = [];
+
+            settings.forEach(item => {
+                // 如果 contentType 是 13 且 value 不为空,则额外添加一条 contentType 为 1 的消息
+                if (item.contentType == 13 && item.value && item.value.trim() !== '') {
+                    // 按换行符分割内容,取第一部分
+                    const contentParts = item.value.split('\n')
+                        .map(part => part.trim())
+                        .filter(part => part !== '');
+
+                    if (contentParts.length > 0) {
+                        // 创建一条额外的消息,使用第一部分内容
+                        const additionalMessage = {
+                            ...item,
+                            contentType: 1, // 改为文本类型
+                            value: contentParts[0]
+                        };
+                        result.push(additionalMessage);
+                    }
+                }
+                if (item.contentType == 10) {
+                    item.miniprogramAppid = 'wx4d225cc86cc7885d'
+                }
+                result.push(item);
+            });
+
+            return result;
+        },
+
+        submitMsgForm() {
+            this.$refs["msgForm"].validate(valid => {
+                if (valid) {
+
+                    // 处理 contentType 13 的拆分逻辑
+                    const processedSetting = this.processContentType13(this.setting);
+                    console.log(processedSetting)
+                    this.msgForm.setting = JSON.stringify(processedSetting);
+                    this.msgForm.ids = this.ids;
+                    this.msgForm.sopId = this.queryParams.sopId;
+                    this.msgForm.userIdParam = this.queryParams.userIdParam;
+                    this.msgForm.startTime = this.queryParams.startTimeParam;
+                    this.msgForm.corpId = this.queryParams.corpIdParam;
+                    this.msgForm.filterMode = this.queryParams.filterMode;
+
+                    if (processedSetting.length <= 0) {
+                        return this.$message.error("请添加规则")
+                    }
+                    if (this.msgForm.courseId === null || this.msgForm.courseId === '') {
+                        return this.$message.error("科普不能为空")
+                    }
+
+                    if (this.msgForm.videoId === null || this.msgForm.videoId === '') {
+                        return this.$message.error("课节不能为空")
+                    }
+
+                    if (this.msgForm.courseType === null || this.msgForm.courseType === '') {
+                        return this.$message.error("消息类型不能为空")
+                    }
+
+                    for (let i = 0; i < processedSetting.length; i++) {
+                        const item = processedSetting[i];
+                        if (item.contentType == 1 && (item.value == null || item.value == "")) {
+                            return this.$message.error("内容不能为空")
+                        }
+                        if (item.contentType == 2 && (item.imgUrl == null || item.imgUrl == "")) {
+                            return this.$message.error("图片不能为空")
+                        }
+                        if ((item.contentType == 3 || item.contentType == 9) && (item.linkTitle == null || item.linkTitle == "")) {
+                            return this.$message.error("链接标题不能为空")
+                        }
+                        if ((item.contentType == 3 || item.contentType == 9) && (item.linkDescribe == null || item.linkDescribe == "")) {
+                            return this.$message.error("链接描述不能为空")
+                        }
+                        if ((item.contentType == 3 || item.contentType == 9) && (item.linkImageUrl == null || item.linkImageUrl == "")) {
+                            return this.$message.error("链接图片不能为空")
+                        }
+                        if ((item.contentType == 3 || item.contentType == 9) && item.type == 1 && (item.linkUrl == null || item.linkUrl == "")) {
+                            return this.$message.error("链接地址不能为空")
+                        }
+                        if ((item.contentType == 4 || item.contentType == 15) && (item.miniprogramTitle == null || item.miniprogramTitle == "")) {
+                            return this.$message.error("小程序消息标题不能为空")
+                        }
+                        if ((item.contentType == 4 || item.contentType == 15) && (item.miniprogramPicUrl == null || item.miniprogramPicUrl == "")) {
+                            return this.$message.error("小程序封面地址不能为空")
+                        }
+                        if (item.contentType == 5 && (item.fileUrl == null || item.fileUrl == "")) {
+                            return this.$message.error("文件不能为空")
+                        }
+                        if (item.contentType == 6 && (item.videoUrl == null || item.videoUrl == "")) {
+                            return this.$message.error("视频不能为空")
+                        }
+                        if (item.contentType == 7 && (item.value == null || item.value == "")) {
+                            return this.$message.error("语音不能为空")
+                        }
+                        if (item.contentType == 14 && (item.luckyBagId == null || item.luckyBagId == "")) {
+                            return this.$message.error("福袋不能为空")
+                        }
+                    }
+
+                    this.sendMsgOpen.open = false;
+
+                    const loading = this.$loading({
+                        lock: true,
+                        text: '正在执行中请稍后~~请不要刷新页面!!',
+                        spinner: 'el-icon-loading',
+                        background: 'rgba(0, 0, 0, 0.7)'
+                    });
+                    sendMsgSop(this.msgForm).then(response => {
+                        this.msgSuccess("一键群发成功");
+                        loading.close();
+                        this.setting = [];
+                        this.msgForm = {
+                            videoId: null,
+                            courseId: null,
+                            courseType: null,
+                            setting: null,
+                        }
+                        this.getList();
+                    }).finally(() => {
+                        loading.close();
+                    });
+
+                }
+            });
+        },
+        cancelMsgForm() {
+            this.sendMsgOpen.open = false;
+            this.resetSendMsgSop();
+        },
+        /** 提交按钮 */
+        submitForm() {
+            this.$refs["updateLogsInfoFrom"].validate(valid => {
+                if (valid) {
+                    this.updateLogsInfoFrom.ids = this.ids;
+                    this.updateLogsInfoFrom.sopId = this.queryParams.sopId;
+                    this.updateLogsInfoFrom.uTime = this.updateLogsInfoFrom.pTime;
+                    let loadingRock = this.$loading({
+                        lock: true,
+                        text: '正在执行中请稍后~~请不要刷新页面!!',
+                        spinner: 'el-icon-loading',
+                        background: 'rgba(0, 0, 0, 0.7)'
+                    });
+                    
+                    batchUpdateSopUserLogsInfoToTime(this.updateLogsInfoFrom).then(response => {
+                        this.msgSuccess("修改成功");
+                        this.open = false;
+                        this.updateOpen = false;
+                        this.getList();
+                        loadingRock.close();
+                    }).finally(res => {
+                        loadingRock.close();
+                    })
+                }
+            });
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const ids = row.id || this.ids;
+            this.$confirm('是否确认删除sopUserLogsInfo编号为"' + ids + '"的数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(function () {
+                return delSopUserLogsInfo(ids);
+            }).then(() => {
+                this.getList();
+                this.msgSuccess("删除成功");
+            }).catch(() => { });
+        },
+        /** 导出按钮操作 */
+        handleExport() {
+            const queryParams = this.queryParams;
+            this.$confirm('是否确认导出所有营期数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(() => {
+                this.exportLoading = true;
+                return exportSopUserLogsInfo(queryParams);
+            }).then(response => {
+                this.download(response.msg);
+                this.exportLoading = false;
+            }).catch(() => { });
+        },
+        //选择视频号
+        hanldeSelectVideoNum(content, index) {
+            this.videoNumOptions.content = content;
+            this.videoNumOptions.contentIndex = index;
+            this.videoNumOptions.open = true;
+        },
+
+        qwUserVideoResult(val) {
+
+            // 根据选中的内容,将返回的数据更新到相应的表单项
+            const content = this.videoNumOptions.content;
+            const setList = content[this.videoNumOptions.contentIndex];
+            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;
+            setList.videoId = val.id;
+            console.info(setList)
+
+            this.videoNumOptions.open = false;
+
+        },
+        /**
+         * 营期一键群发
+         */
+        openGroupSendMessagePage() {
+            if (this.ids.length === 0) {
+                return this.$message.error('请勾选具体群发记录!');
+            }
+            this.groupSendMessageForm.ids = this.ids;
+            this.groupSendMessageForm.visible = true;
+        },
+        changeGroupSendMessageVisible(visible) {
+            this.groupSendMessageForm.visible = visible;
+        },
+    }
+};
+</script>
+
+<style lang="scss" scoped>
+
+::v-deep  .search-tag-dialog {
+    height: 800px !important;
+    overflow: auto !important;
+}
+
+/* CSS 样式 */
+.tag-container {
+    display: flex;
+    flex-wrap: wrap;
+    /* 超出宽度时自动换行 */
+    gap: 8px;
+    /* 设置标签之间的间距 */
+}
+
+.name-background {
+    display: inline-block;
+    background-color: #abece6;
+    /* 背景颜色 */
+    padding: 4px 8px;
+    /* 调整内边距,让背景包裹文字 */
+    border-radius: 4px;
+    /* 可选:设置圆角 */
+}
+
+.tag-box {
+    padding: 8px 12px;
+    border: 1px solid #989797;
+    border-radius: 4px;
+    cursor: pointer;
+    display: inline-block;
+}
+
+.tag-selected {
+    background-color: #00bc98;
+    color: #fff;
+    border-color: #00bc98;
+}
+
+.el-tag+.el-tag {
+    margin-left: 10px;
+}
+
+
+.button-new-tag {
+    margin-left: 10px;
+    height: 32px;
+    line-height: 30px;
+    padding-top: 0;
+    padding-bottom: 0;
+}
+
+.input-new-tag {
+    width: 90px;
+    margin-left: 10px;
+    vertical-align: bottom;
+}
+</style>

+ 269 - 0
src/views/app/tagGroup/index.vue

@@ -0,0 +1,269 @@
+<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.groupName"
+          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"
+          v-hasPermi="['vip:tagGroup:add']"
+          @click="handleAdd"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="single"
+          v-hasPermi="['vip:tagGroup:edit']"
+          @click="handleUpdate"
+        >修改</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="tagGroupList" @selection-change="handleSelectionChange" border>
+      <el-table-column type="selection" width="55" align="center" />
+
+      <el-table-column label="名称" align="center" prop="name" />
+      <el-table-column label="排序" align="center" prop="sortOrder" />
+      <el-table-column label="标签" align="center" prop="tags" >
+        <template slot-scope="scope" >
+            <el-tag type="success" v-for="i in scope.row.tags" :key="i.id" style="margin: 2px 5px 0 0;">{{i.name}}</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-edit"
+            v-hasPermi="['vip:tagGroup:edit']"
+            @click="handleUpdate(scope.row)"
+          >修改</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改会员标签组对话框 -->
+    <el-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="sortOrder">
+          <el-input-number v-model="form.sortOrder" :min="0" placeholder="请输入排序" />
+        </el-form-item>
+        <el-row>
+          <el-col :span="3"><div style="margin-top: 9px;font-weight: bold;float: right; margin-right: 10px;">标签</div></el-col>
+          <el-col :span="21">
+            <el-table :data="form.tags">
+              <el-table-column label="名称" prop="name" align="center" >
+                <template slot-scope="scope">
+                  <el-input v-model="scope.row.name"   ></el-input>
+                </template>
+              </el-table-column>
+              <el-table-column label="排序" prop="sortOrder" align="center" >
+                <template slot-scope="scope">
+                  <el-input-number v-model="scope.row.sortOrder" :min="0" ></el-input-number>
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="150px" align="center">
+                <template slot-scope="scope" >
+                    <el-button @click="addRow" size="mini" type="text" >新增</el-button>
+                    <el-button @click="deleteRow(scope.$index,scope.row)"   size="mini" type="text" v-if="form.tags.length>1">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-col>
+        </el-row>
+      </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 { listTagGroup, addTagGroup, getTagGroup, updateTagGroup } from "@/api/app/tag/tagGroup";
+export default {
+  name: "TagGroup",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 会员标签组表格数据
+      tagGroupList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        groupName: null,
+      },
+      tagJson:[],
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        name: [
+          { required: true, message: "名称不能为空", trigger: "blur" }
+        ],
+      }
+    };
+  },
+  mounted() {
+    this.handleQuery();
+  },
+  methods: {
+    /** 查询会员标签组列表 */
+    getList() {
+      this.loading = true;
+      listTagGroup(this.queryParams).then(response => {
+        this.tagGroupList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        groupId: null,
+        name: null,
+        sortOrder: null,
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    addRow() {
+      this.form.tags.push({ name: "标签", sortOrder: 0},);
+    },
+    deleteRow(index,row) {
+      this.form.tags.splice(index, 1);
+      //存在groupId则是数据库中元数据,删除时需标记
+      if (row.groupId) {
+        if (!this.form.removeTagIds) {
+          this.form.removeTagIds = [row.id];
+        } else {
+          this.form.removeTagIds.push(row.id);
+        }
+      }
+    },
+
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.queryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        groupName: null,
+      };
+      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.form.tags= [
+        { name: "标签", sortOrder: 0},
+      ];
+      this.title = "添加会员标签组";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getTagGroup(id).then(response => {
+        let td = response.data;
+        if (!td.tags || td.tags.length === 0) {
+          td.tags = [{ name: "标签", sortOrder: 0}]
+        }
+        this.form = td;
+        this.open = true;
+        this.title = "修改会员标签组";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateTagGroup(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addTagGroup(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+  }
+};
+</script>

+ 163 - 0
src/views/app/tagGroup/selectTag.vue

@@ -0,0 +1,163 @@
+<template>
+    <div>
+        <div>搜索标签:
+            <el-input v-model="queryParams.name" placeholder="请输入标签名称" clearable size="small"
+                style="width: 200px;margin-right: 10px" />
+            <el-button type="primary" icon="el-icon-search" size="mini"
+                @click="search">
+                搜索
+            </el-button>
+            <el-button type="primary" icon="el-icon-refresh" size="mini" @click="reset">重置</el-button>
+        </div>
+        <div v-for="item in tagGroupList" :key="item.id">
+            <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+                <span class="name-background">{{ item.name }}</span>
+            </div>
+            <div class="tag-container">
+                <a v-for="tagItem in item.tags" class="tag-box" @click="tagSelection(item, tagItem)"
+                    :class="{ 'tag-selected': tagItem.isSelected || (tagSelected && tagSelected.some(tg => tg.id == tagItem.id)) }">
+                    {{ tagItem.name }}
+                </a>
+            </div>
+        </div>
+        <pagination v-show="tagTotal > 0" :total="tagTotal" :page.sync="queryParams.pageNum"
+            :limit.sync="queryParams.pageSize" @pagination="search"/>
+        <div slot="footer" class="dialog-footer">
+            <el-button type="primary" @click="tagSubmit">确 定</el-button>
+            <el-button @click="tagCancel">取消</el-button>
+        </div>
+    </div>
+</template>
+<script>
+
+import { searchTags, } from "@/api/app/tag/tagGroup";
+export default {
+    name: "searchTags",
+    props: {
+        defaultPageNum: {
+            type: Number,
+            default: 1
+        },
+        defaultPageSize: {
+            type: Number,
+            default: 10
+        },
+        tagSelected: {
+            type: Array,
+            default: () => [],
+        }
+    },
+    data() {
+        return {
+            queryParams: {
+                name: null,//标签搜索名称
+                pageNum: 1,//页码
+                pageSize: 10,//页记录数
+            },
+            tagGroupList: [],//标签组
+            tagTotal: 0,//总数
+        }
+    },
+    created() {
+        this.reset();
+        //默认查询一次
+        this.search();
+    },
+    watch: {
+        tagSelected: {
+            handler(n) {
+                this.setDefaultSelected();
+            },
+            deep: true,
+        }
+    },
+    methods: {
+        setDefaultSelected() {
+            this.tagGroupList.map(tg => {
+                tg.tags.map(t => {
+                    if (this.tagSelected.some(ts => ts.id == t.id)) {
+                        t.isSelected = true;
+                    } else {
+                        t.isSelected = false;
+                    }
+                })
+            })
+        },
+        async search() {
+            let qp = {...this.queryParams, groupName: this.queryParams.name}
+            let dd = await searchTags(qp);
+            let rows = dd.rows;
+            rows.map(tg => {
+                tg.tags.map(t => {
+                    if (this.tagSelected.some(ts => ts.id == t.id)) {
+                        t.isSelected = true;
+                    } else {
+                        t.isSelected = false;
+                    }
+                })
+            })
+            this.tagGroupList = rows;
+            this.tagTotal = dd.total;
+        },
+        reset() {
+            this.queryParams.pageNum = this.defaultPageNum;
+            this.queryParams.pageSize = this.defaultPageSize;
+            this.queryParams.name = null;
+            this.search();
+        },
+        tagSubmit() {
+            try {
+                this.$emit('tagSubmit');
+            } catch (e) {
+                console.warn('执行提交失败!');
+            }
+        },
+        tagCancel() {
+            try {
+                this.$emit('tagCancel');
+            } catch (e) {
+                this.warn('执行取消失败!')
+            }
+        },
+        tagSelection(groupItem, tagItem) {
+            this.$set(tagItem, 'isSelected', !tagItem.isSelected)
+            this.$forceUpdate();
+            try {
+                this.$emit('tagSelection', groupItem, tagItem);
+            } catch (e) {
+
+            }
+        },
+    }
+}
+</script>
+<style lang="scss">
+    
+/* CSS 样式 */
+.tag-container {
+  display: flex;
+  flex-wrap: wrap; /* 超出宽度时自动换行 */
+  gap: 8px; /* 设置标签之间的间距 */
+}
+
+.name-background {
+  display: inline-block;
+  background-color: #abece6; /* 背景颜色 */
+  padding: 4px 8px; /* 调整内边距,让背景包裹文字 */
+  border-radius: 4px; /* 可选:设置圆角 */
+}
+
+.tag-box {
+  padding: 8px 12px;
+  border: 1px solid #989797;
+  border-radius: 4px;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.tag-selected {
+  background-color: #00bc98;
+  color: #fff;
+  border-color: #00bc98;
+}
+</style>

+ 2068 - 0
src/views/app/urgentClass/CustomerDetail.vue

@@ -0,0 +1,2068 @@
+<template>
+    <div class="customer-container">
+
+        <div class="main-grid-three-columns">
+
+            <div class="left-column">
+                <!-- 用户信息 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3><i class="fas fa-id-card"></i>用户信息</h3>
+                    </div>
+                    <div class="profile-grid">
+                        <template v-if="Object.keys(customerPortraitData).length > 0">
+                            <div
+                                v-if="customerPortraitData[key]"
+                                v-for="(value, key) in userFieldComments"
+                                :key="key"
+                                class="profile-item"
+                                :class="key === 'name' ? 'profile-item-main' : ''"
+                            >
+                                <template
+                                    v-if="key === 'name'"
+                                >
+                                    <span class="label"><i class="fas fa-user"></i> 姓名:</span>
+                                    <span class="value highlight">{{ customerPortraitData[key] }}</span>
+                                </template>
+                                <template
+                                    v-else
+                                >
+                                    <span class="label" :title="value">
+                                        <i class="fas fa-info-circle"></i> {{ value }}:
+                                    </span>
+                                    <span class="value">{{ customerPortraitData[key] }}</span>
+                                </template>
+                            </div>
+                        </template>
+                        <template v-else>
+                            <div style="width: 100%; text-align: center;">
+                                暂无数据
+                            </div>
+                        </template>
+                        
+                    </div>
+                </div>
+                <!-- 标签 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3>
+                            <i class="fas fa-tags"></i> 用户画像标签
+                        </h3>
+                    </div>
+                    <div class="tags-container">
+                        <div v-if="userTags.length > 0" class="tags-list">
+                            <div
+                                v-for="(item, index) in userTags"
+                                :key="index"
+                                class="tag-item tag-highlight"
+                            >
+                                <span class="tag-key">{{ item.name }}</span>
+                                <span class="tag-separator">:</span>
+                                <span class="tag-value">{{ item.weight }}</span>
+                            </div>
+                        </div>
+                        <div v-else class="empty-tags">
+                            <i class="fas fa-inbox"></i>
+                            <span>暂无标签</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="middle-column">
+                <!-- 沟通摘要 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3><i class="fas fa-comment-dots"></i>客户信息汇总</h3>
+                    </div>
+                    <div class="summary-text compact">
+                        {{ getSummaryInfo() }}
+                    </div>
+                </div>
+                <!-- AI 本次沟通建议 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3><i class="fas fa-robot"></i>本次沟通建议</h3>
+                    </div>
+                    <div class="summary-text compact">
+                        {{ getCommunicationSuggestion() }}
+                    </div>
+                </div>
+                <!-- 沟通记录 -->
+                <div class="card card-table">
+                    <div class="card-header">
+                        <h3><i class="fas fa-history"></i> 沟通记录</h3>
+                    </div>
+                    <div class="records-table-wrapper">
+                        <table class="records-table">
+                            <thead>
+                            <tr>
+                                <th><i class="fas fa-user"></i> 客户名称</th>
+                                <th><i class="fas fa-chart-line"></i> 流失等级</th>
+                                <th><i class="fas fa-heart"></i> 外呼人员</th>
+                                <th><i class="far fa-clock"></i> 创建时间</th>
+                                <th><i class="fas fa-cog"></i> 操作</th>
+                            </tr>
+                            </thead>
+                            <tbody>
+                            <tr v-for="record in communicationRecords" :key="record.id" class="record-row">
+                                <td class="record-cell">{{ 'customerData.customerName' }}</td>
+                                <td class="record-cell">
+                                        <span class="risk-level-tag" :class="getRecordRiskLevelClass(record)">
+                                            {{ getRecordRiskLevelLabel(record) }}
+                                        </span>
+                                </td>
+                                <td class="record-cell">
+                                        <span class="intention-degree">
+                                            {{ getIntentionDegreeFromRecord(record) }}
+                                        </span>
+                                </td>
+                                <td class="record-cell">{{ parseTime(record.createTime, '{y}-{m}-{d} {h}:{i}:{s}') || '-' }}</td>
+                                <td class="record-cell">
+                                    <button @click="viewChat(record)" class="btn-view-chat">
+                                        <i class="fas fa-comments"></i> 聊天详情
+                                    </button>
+                                </td>
+                            </tr>
+                            <tr v-if="!communicationRecords.length">
+                                <td colspan="5" class="empty-tip">
+                                    <i class="fas fa-inbox"></i> 暂无沟通记录
+                                </td>
+                            </tr>
+                            </tbody>
+                        </table>
+
+                        <!-- 分页组件 -->
+                        <div class="pagination-container" v-if="communicationRecordsTotal > 0">
+                            <el-pagination
+                                @current-change="handleCommunicationRecordsPageChange"
+                                @size-change="handleCommunicationRecordsSizeChange"
+                                :current-page="communicationRecordsPageNum"
+                                :page-sizes="[4, 10, 20, 50]"
+                                :page-size="communicationRecordsPageSize"
+                                layout="total, sizes, prev, pager, next, jumper"
+                                :total="communicationRecordsTotal"
+                            />
+                        </div>
+                    </div>
+                </div>
+                <!-- 微信风格聊天弹窗 -->
+                <div v-if="chatDialogVisible" class="chat-dialog-overlay" @click.self="closeChatDialog">
+                    <div class="chat-dialog">
+                        <div class="chat-dialog-header">
+                            <div class="chat-title">
+                                <i class="fas fa-comments"></i>
+                                <span>{{
+                                        (currentChatRecord && currentChatRecord.customerName) || ('customerData && customerData.customerName')
+                                    }} - 历史聊天记录</span>
+                            </div>
+                            <button @click="closeChatDialog" class="btn-close">
+                                ×
+                            </button>
+                        </div>
+                        <div class="chat-dialog-body">
+                            <div class="chat-messages">
+                                <!-- 根据 aiChatRecord 数组循环显示聊天记录 -->
+                                <div
+                                    v-for="(msg, index) in parseChatMessages(currentChatRecord && currentChatRecord.aiChatRecord)"
+                                    :key="index"
+                                    class="message-item"
+                                    :class="msg.type === 'ai' ? 'message-left' : 'message-right'"
+                                >
+                                    <!-- AI 消息:头像在左,名称在聊天内容上方靠左 -->
+                                    <div v-if="msg.type === 'ai'" class="message-wrapper message-wrapper-left">
+                                        <div class="message-avatar message-avatar-ai">
+                                            <img src="/static/images/ai-avatar.svg" alt="AI"
+                                                 @error="handleAvatarError($event, 'ai')"/>
+                                        </div>
+                                        <div class="message-content">
+                                            <div class="message-name message-name-ai">AI</div>
+                                            <div class="message-bubble">
+                                                {{ msg.content }}
+                                            </div>
+                                        </div>
+                                    </div>
+
+                                    <!-- 客户消息:强制头像在右侧 -->
+                                    <div v-else class="message-item message-item-customer">
+                                        <div class="message-content-right">
+                                            <div class="message-bubble message-bubble-right">
+                                                {{ msg.content }}
+                                            </div>
+                                        </div>
+                                        <div class="message-avatar message-avatar-customer">
+                                            <img src="/static/images/customer-avatar.svg" alt="客户"
+                                                 @error="handleAvatarError($event, 'customer')"/>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <!-- 空数据提示 -->
+                                <div
+                                    v-if="!parseChatMessages(currentChatRecord && currentChatRecord.aiChatRecord).length"
+                                    class="empty-chat-tip">
+                                    <i class="fas fa-inbox"></i> 暂无聊天内容
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="right-column">
+                <div class="card card-focus">
+                    <div class="card-header">
+                        <h3><i class="fas fa-lightbulb"></i>操作</h3>
+                    </div>
+                    <div class="focus-points">
+                        <el-button type="primary" plain @click="toggleStatus">点击处理</el-button>
+                        <el-button
+                            type="success"
+                            plain
+                            @click="doChat"
+                            :loading="imLoading"
+                            :disabled="imLoading"
+                        >
+                            聊天
+                        </el-button>
+                    </div>
+                </div>
+                <div class="card card-focus">
+                    <div class="card-header">
+                        <h3><i class="fas fa-lightbulb"></i>活跃度 & 注册时间</h3>
+                    </div>
+                    <div class="focus-points">
+                        <div class="focus-title">
+                            <i class="fas fa-search"></i>活跃度
+                        </div>
+                        <ul class="focus-list">
+                            <li class="focus-item">
+                                <i class="fas fa-dot-circle"></i>{{ activityLevel }}
+                            </li>
+                        </ul>
+                        <div class="focus-title">
+                            <i class="fas fa-search"></i>注册时间
+                        </div>
+                        <ul class="focus-list">
+                            <li class="focus-item">
+                                <i class="fas fa-dot-circle"></i> {{ registerTime }}
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="intention-section">
+                        <div class="card-header" style="justify-content: left;">
+                            <h3><i class="fas fa-lightbulb"></i>客户消费等级</h3>
+                        </div>
+                        <div class="intention-watermark"
+                             v-if="consumptionLevel != '--'"
+                             :class="getIntentionColorClass(consumptionLevel)">
+                            {{ consumptionLevel }}
+                        </div>
+                        <div class="no-intention-tip" v-else>
+                            暂无评级
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</template>
+
+<script>
+
+import { getAIUserInfoByUserId, getUserTags, } from "@/api/app/user/userInfo";
+
+export default {
+    props: {
+        userInfo: {
+            type: Object,
+            default: null,
+        },
+        refreshCount: {
+            type: Number,
+            default: 0,
+        },
+        imLoading: {
+            type: Boolean,
+            default: false,
+        },
+    },
+    data() {
+        return {
+            // 聊天记录分页相关
+            communicationRecords: [],
+            communicationRecordsTotal: 0,
+            communicationRecordsPageNum: 1,
+            communicationRecordsPageSize: 4,
+            // 聊天弹窗相关
+            chatDialogVisible: false, // 聊天弹窗是否显示
+            currentChatRecord: null, // 当前查看的聊天记录
+            customerPortraitData: {},//用户信息
+            userFieldComments: {},//用户信息字段描述
+            //标签信息
+            userTags: [],
+            comsumptionLevelMapper: {
+                'S+': 'S+',
+                'S': 'S',
+                'A': 'A',
+                'B': 'B',
+                'C': 'C',
+                'D': 'D',
+                'E': 'E',
+                'F': '代客下单'
+            },
+        }
+    },
+    computed: {
+        // 根据是否展开控制显示的标签数量
+        visibleTags() {
+            if (this.isExpanded) {
+                return this.allAiTags;
+            } else {
+                // 未展开时只显示前 3 条
+                return this.allAiTags.slice(0, this.tagsPageSize);
+            }
+        },
+        //活跃度
+        activityLevel() {
+            return !this.isEmpty(this.customerPortraitData) && !this.isEmpty(this.customerPortraitData.activityLevel) ? this.customerPortraitData.activityLevel : "--";
+        },
+        //注册时间
+        registerTime() {
+            return !this.isEmpty(this.customerPortraitData) && !this.isEmpty(this.customerPortraitData.registerTime) ? this.customerPortraitData.registerTime : "--";
+        },
+        //消费等级
+        consumptionLevel() {
+            return !this.isEmpty(this.customerPortraitData) && !this.isEmpty(this.customerPortraitData.consumptionLevel) ? this.comsumptionLevelMapper[this.customerPortraitData.consumptionLevel] : "--";
+        },
+    },
+    created() {
+        this.init();
+    },
+    watch: {
+        refreshCount: {
+            immediate: true,
+            async handler(n, o) {
+                //获取用户信息,注册时间,消费等级,活跃度
+                this.getUserInfo();
+                //获取用户标签
+                this.getUserTags();
+                //客户信息汇总
+                //todo
+                //本次沟通建议
+                //todo
+                //沟通记录
+                //todo
+            }
+        },
+    },
+    methods: {
+        /**
+         * 初始化
+         */
+        async init() {
+            //初始化用户字段信息描述
+            let userFieldComments = {
+                name: "姓名",
+                sex: "性别",
+                age: "年龄",
+                address: "地区",
+                habits: "行为习惯",
+                illnessTime: "患病时间",
+                body: "身体状态",
+                disease: "疾病",
+                family: "提及的家人",
+                isLine: "是否线下就诊",
+                familyDisease: "家人的疾病",
+                userType: "用户分类",
+                isSelf: "是否本人会诊",
+                intensify: "什么情况加重或缓解",
+                isCold: "是否怕热或者怕冷",
+                coldBody: "怕冷或怕热的部位",
+                sweat: "出汗情况",
+                other: "其他情况",
+                toilet: "大小便情况",
+                eat: "饮食情况",
+                menses: "经期如何",
+                medicine: "现在使用的药品",
+                constitution: "体质",
+                recommendMedicine: "推荐用药",
+                consultProduct: "咨询产品",
+                isBuy: "是否已经购买产品",
+                buyProduct: "已经购买的产品",
+                productTalk: "产品交流",
+                diseaseTalk: "疾病交流",
+                channelType: "渠道类型"
+            };
+            this.$set(this, 'userFieldComments', userFieldComments);
+        },
+        /**
+         * 获取用户信息
+         */
+        async getUserInfo() {
+            let fsUser = await getAIUserInfoByUserId(this.userInfo.fsUserId);
+            if (fsUser.data) {
+                this.customerPortraitData = fsUser.data;
+            }
+        },
+        isEmpty(value) {
+            return value === null || value === undefined || value.length === 0;
+        },
+        /**
+         * 获取用户兴趣标签
+         */
+        async getUserTags() {
+            let userTag = await getUserTags(this.userInfo.fsUserId);
+            if (userTag.data) {
+                this.userTags = userTag.data;
+            }
+        },
+        toggleStatus() {
+            this.$emit('toggleStatus', this.userInfo, 1);
+        },
+        doChat() {
+            this.$emit('chat', this.userInfo);
+        },
+        // 查看聊天内容
+        viewChat(record) {
+            this.currentChatRecord = record;
+            this.chatDialogVisible = true;
+        },
+        // 关闭聊天弹窗
+        closeChatDialog() {
+            this.chatDialogVisible = false;
+            this.currentChatRecord = null;
+        },
+        // 解析聊天消息数组
+        parseChatMessages(content) {
+            if (!content) {
+                return [];
+            }
+            // 如果 content 是字符串,尝试解析为 JSON 数组
+            if (typeof content === 'string') {
+                try {
+                    const parsed = JSON.parse(content);
+                    // 如果是数组,直接返回
+                    if (Array.isArray(parsed)) {
+                        return parsed.map(item => ({
+                            content: item.ai || item.user,
+                            type: item.ai ? 'ai' : 'user'
+                        }));
+                    }
+                    // 如果是对象,转换为数组
+                    return [{content: parsed.content, type: parsed.type || 'ai'}];
+                } catch (e) {
+                    // 解析失败,返回空数组
+                    console.error('解析聊天记录失败:', e);
+                    return [];
+                }
+            }
+            // 如果已经是数组,直接返回
+            if (Array.isArray(content)) {
+                return content;
+            }
+            // 如果是对象,转换为数组
+            return [content];
+        },
+        // 处理头像加载失败
+        handleAvatarError(event, type) {
+            const img = event.target;
+            if (type === 'ai') {
+                // AI 头像加载失败时,使用渐变色背景 + 机器人图标
+                img.style.display = 'none';
+                img.parentElement.innerHTML = '<i class="fas fa-robot" style="font-size: 24px; color: white;"></i>';
+            } else {
+                // 客户头像加载失败时,使用渐变色背景 + 用户图标
+                img.style.display = 'none';
+                img.parentElement.innerHTML = '<i class="fas fa-user" style="font-size: 24px; color: white;"></i>';
+            }
+        },
+        // 分页改变事件
+        handleCommunicationRecordsPageChange(pageNum) {
+            this.communicationRecordsPageNum = pageNum;
+            this.getCustomerInfoList();
+        },
+        // 每页条数改变事件
+        handleCommunicationRecordsSizeChange(pageSize) {
+            this.communicationRecordsPageSize = pageSize;
+            this.communicationRecordsPageNum = 1; // 重置为第一页
+            this.getCustomerInfoList();
+        },
+        // 获取汇总信息
+        getSummaryInfo() {
+            //todo
+            return '暂无汇总信息';
+        },
+        // 获取 AI 沟通建议
+        getCommunicationSuggestion() {
+            //todo
+            return '暂无沟通建议';
+        },
+        // 根据意向度等级获取颜色样式类
+        getIntentionColorClass(grade) {
+            if (!grade) return '';
+            const gradeUpper = grade.toUpperCase();
+            const colorMap = {
+                'S+': 'intention-grade-s0',
+                'S': 'intention-grade-s',
+                '代客下单': 'intention-grade-f',
+                'A': 'intention-grade-a',
+                'B': 'intention-grade-b',
+                'C': 'intention-grade-c',
+                'D': 'intention-grade-d',
+                'E': 'intention-grade-e'
+            };
+            return colorMap[gradeUpper] || '';
+        },
+        // 获取单条记录的风险等级数值
+        getRecordAttritionLevel(record) {
+            if (!record) return 0;
+            return parseInt(record.attritionLevel) || 0;
+        },
+        // 获取单条记录的风险等级标签
+        getRecordRiskLevelLabel(record) {
+            const level = this.getRecordAttritionLevel(record);
+            const labels = ['未知', '无风险', '低风险', '中风险', '高风险'];
+            return labels[level] || '未知';
+        },
+        // 获取单条记录的风险等级样式类
+        getRecordRiskLevelClass(record) {
+            const level = this.getRecordAttritionLevel(record);
+            const classes = ['risk-unknown', 'risk-none', 'risk-low', 'risk-medium', 'risk-high'];
+            return classes[level] || 'risk-unknown';
+        },
+        // 获取单条记录的客户意向度
+        getIntentionDegreeFromRecord(record) {
+            if (!record) return 0;
+            return record.intentionDegree;
+        },
+    }
+}
+</script>
+
+<style scoped lang="scss">
+* {
+    box-sizing: border-box;
+}
+
+.left-column {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.middle-column {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.right-column {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.customer-container {
+    max-width: 1600px;
+    margin: 0 auto;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+    background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+    padding: 8px;
+    min-height: calc(100vh - 50px);
+}
+
+@keyframes pulse {
+    0%, 100% {
+        transform: scale(1);
+    }
+    50% {
+        transform: scale(1.1);
+    }
+}
+
+.toolbar {
+    padding: 5px 0px;
+    text-align: right;
+}
+
+.main-grid-three-columns {
+    display: grid;
+    grid-template-columns: 380px 1fr 340px;
+    gap: 12px;
+    animation: fadeIn 0.6s ease-in-out;
+    align-items: stretch;
+}
+
+@keyframes fadeIn {
+    from {
+        opacity: 0;
+        transform: translateY(20px);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+@media (max-width: 1400px) {
+    .main-grid-three-columns {
+        grid-template-columns: 360px 1fr 320px;
+        gap: 24px;
+    }
+}
+
+@media (max-width: 1200px) {
+    .main-grid-three-columns {
+        grid-template-columns: 1fr;
+    }
+}
+
+.card {
+    background: white;
+    border-radius: 12px;
+    padding: 12px;
+    margin-bottom: 12px;
+    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
+    transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+    border: 1px solid rgba(226, 232, 240, 0.5);
+    position: relative;
+    overflow: hidden;
+}
+
+.card::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    height: 3px;
+    background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+    transform: scaleX(0);
+    transition: transform 0.4s ease;
+}
+
+.card:hover::before {
+    transform: scaleX(1);
+}
+
+.card:hover {
+    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15);
+    transform: translateY(-4px);
+    border-color: rgba(102, 126, 234, 0.3);
+}
+
+/* 高亮卡片(AI 总结) */
+.card-highlight {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    border: none;
+    box-shadow: 0 8px 32px rgba(102, 126, 234, 0.4);
+    position: relative;
+    overflow: hidden;
+}
+
+.card-highlight::after {
+    content: '';
+    position: absolute;
+    top: -50%;
+    right: -50%;
+    width: 200%;
+    height: 200%;
+    background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
+    animation: shimmer 3s infinite;
+}
+
+@keyframes shimmer {
+    0%, 100% {
+        transform: translate(0, 0);
+    }
+    50% {
+        transform: translate(-30%, -30%);
+    }
+}
+
+.card-highlight .card-header h3 {
+    color: white;
+}
+
+.card-highlight .summary-text {
+    color: white;
+    font-size: 17px;
+    line-height: 1.6;
+    max-height: 120px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar {
+    width: 6px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-track {
+    background: rgba(255, 255, 255, 0.2);
+    border-radius: 3px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-thumb {
+    background: rgba(255, 255, 255, 0.4);
+    border-radius: 3px;
+}
+
+.card-highlight .summary-text::-webkit-scrollbar-thumb:hover {
+    background: rgba(255, 255, 255, 0.6);
+}
+
+.card-highlight .summary-meta span {
+    color: rgba(255, 255, 255, 0.9);
+}
+
+/* 表格卡片 */
+.card-table {
+    background: white;
+}
+
+/* 风险卡片 */
+.risk-card {
+    border-left: 4px solid #10b981;
+    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
+    transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.risk-card.risk-unknown {
+    border-left-color: #6b7280;
+    background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
+}
+
+.risk-card.risk-unknown::before {
+    background: linear-gradient(90deg, #6b7280 0%, #4b5563 100%);
+}
+
+.risk-card::before {
+    background: linear-gradient(90deg, #10b981 0%, #34d399 100%);
+}
+
+/* 徽章改为“标签”风格(类似 AI 标签) */
+.risk-unknown .risk-badge {
+    background: #f8fafc;
+    border-color: #e5eaf1;
+    color: #64748b;
+}
+
+.risk-none .risk-badge {
+    background: #f0fdf4;
+    border-color: #bbf7d0;
+    color: #16a34a;
+}
+
+.risk-low .risk-badge {
+    background: #eff6ff;
+    border-color: #bfdbfe;
+    color: #2563eb;
+}
+
+.risk-medium .risk-badge {
+    background: #fffbeb;
+    border-color: #fde68a;
+    color: #d97706;
+}
+
+.risk-high .risk-badge {
+    background: #fef2f2;
+    border-color: #fecaca;
+    color: #dc2626;
+}
+
+.risk-card:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.1);
+}
+
+/* 风险等级标签 */
+.risk-badge {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    padding: 5px 10px;
+    border-radius: 10px;
+    font-size: 13px;
+    font-weight: 600;
+    border: 1px solid #e5eaf1;
+    box-shadow: none;
+    transition: all 0.2s ease;
+}
+
+.risk-badge::before {
+    content: '';
+    width: 8px;
+    height: 8px;
+    border-radius: 3px;
+    background: currentColor;
+    opacity: 0.35;
+}
+
+.risk-card:hover .risk-badge {
+    transform: translateY(-1px);
+}
+
+/* 风险分析内容 */
+.risk-analysis {
+    margin-top: 10px;
+    padding: 10px;
+    background: rgba(255, 255, 255, 0.7);
+    border-radius: 10px;
+    backdrop-filter: blur(10px);
+    max-height: 180px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.risk-analysis::-webkit-scrollbar {
+    width: 6px;
+}
+
+.risk-analysis::-webkit-scrollbar-track {
+    background: rgba(255, 255, 255, 0.5);
+    border-radius: 3px;
+}
+
+.risk-analysis::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.risk-analysis::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+.risk-text {
+    font-size: 16px;
+    line-height: 1.7;
+    color: #475569;
+    margin: 0;
+}
+
+.risk-tip {
+    margin-top: 8px;
+    padding: 8px;
+    background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 250, 252, 0.9) 100%);
+    border-left: 3px solid #f59e0b;
+    border-radius: 6px;
+    font-size: 15px;
+    color: #92400e;
+    display: flex;
+    align-items: flex-start;
+    gap: 6px;
+}
+
+.risk-tip i {
+    font-size: 12px;
+    margin-top: 1px;
+    color: #f59e0b;
+}
+
+/* 关注点卡片 */
+.card-focus {
+    background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+    border: 2px solid #bae6fd;
+    box-shadow: 0 4px 16px rgba(186, 230, 253, 0.3);
+}
+
+.card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-bottom: 1px solid #eef2ff;
+    padding-bottom: 6px;
+    margin-bottom: 10px;
+    flex-wrap: wrap;
+}
+
+.card-header h3 {
+    font-size: 19px;
+    font-weight: 700;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    color: #0f172a;
+    letter-spacing: -0.02em;
+}
+
+.records-table-wrapper {
+    overflow-x: auto;
+}
+
+/* 分页容器样式 */
+.pagination-container {
+    padding: 16px 0 12px;
+    display: flex;
+    justify-content: center;
+    background: white;
+    border-top: 1px solid #f1f5f9;
+    margin-top: 12px;
+}
+
+/* Element UI 分页样式覆盖 */
+
+.pagination-container .el-pagination li {
+    min-width: 32px;
+    height: 32px;
+    line-height: 32px;
+    border-radius: 6px;
+    transition: all 0.3s ease;
+}
+
+.pagination-container .el-pagination li:hover {
+    background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+    border-color: #667eea;
+}
+
+.records-table {
+    width: 100%;
+    border-collapse: collapse;
+    font-size: 15px;
+}
+
+.records-table thead {
+    background: transparent;
+    color: #475569;
+}
+
+.records-table th {
+    padding: 12px 16px;
+    text-align: center !important;
+    font-weight: 600;
+    font-size: 14px;
+    border-bottom: 2px solid #e2e8f0;
+    color: #64748b;
+}
+
+.records-table th i {
+    margin-right: 6px;
+    opacity: 0.8;
+}
+
+.records-table tbody tr {
+    border-bottom: 1px solid #f1f5f9;
+    transition: all 0.2s ease;
+}
+
+.records-table tbody tr:hover {
+    background-color: #f8fafc;
+    transform: none;
+    box-shadow: none;
+}
+
+.records-table td {
+    padding: 12px 16px;
+    vertical-align: middle;
+}
+
+.record-cell {
+    font-size: 15px;
+    color: #334155;
+    text-align: center !important;
+}
+
+/* 风险等级标签 */
+.risk-level-tag {
+    display: inline-block;
+    padding: 4px 12px;
+    border-radius: 6px;
+    font-size: 13px;
+    font-weight: 500;
+    border: 1px solid;
+}
+
+/* 客户意向度 */
+.intention-degree {
+    display: inline-block;
+    padding: 4px 12px;
+    border-radius: 6px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #6366f1;
+    background: rgba(99, 102, 241, 0.08);
+    border: 1px solid rgba(99, 102, 241, 0.2);
+}
+
+.btn-view-chat {
+    background: transparent;
+    color: #667eea;
+    border: 1px solid #667eea;
+    padding: 6px 12px;
+    border-radius: 6px;
+    font-size: 14px;
+    cursor: pointer;
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    transition: all 0.2s ease;
+}
+
+.btn-view-chat:hover {
+    background: #f0f4ff;
+}
+
+.empty-tip {
+    color: #94a3b8;
+    font-size: 16px;
+    text-align: center;
+    padding: 40px 20px;
+    background: transparent;
+    border: none;
+}
+
+/* 沟通摘要样式 */
+.summary-text.compact {
+    max-height: 100px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    line-height: 1.6;
+    font-size: 16px;
+    color: #475569;
+    padding-right: 4px;
+}
+
+.summary-text.compact::-webkit-scrollbar {
+    width: 6px;
+}
+
+.summary-text.compact::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.summary-text.compact::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.summary-text.compact::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+/* AI 标签美化样式 */
+.tags-container {
+    padding: 0;
+    min-height: calc(16px * 5 + 6px * 4 + 12px * 2);
+    overflow-y: auto;
+    overflow-x: hidden;
+    display: flex;
+    flex-direction: column;
+}
+
+.tags-container::-webkit-scrollbar {
+    width: 6px;
+}
+
+.tags-container::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.tags-container::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.tags-container::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+/* 标签列表 - 每行一个 */
+.tags-list {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+    margin-bottom: 6px;
+}
+
+.tag-item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 6px 10px;
+    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+    border: 1px solid #e2e8f0;
+    border-radius: 6px;
+    font-size: 15px;
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    cursor: default;
+    position: relative;
+    overflow: hidden;
+}
+
+.tag-item::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 4px;
+    height: 100%;
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+    transition: width 0.3s ease;
+}
+
+.tag-item:hover {
+    background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
+    border-color: #cbd5e1;
+    transform: translateX(6px);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.tag-item:hover::before {
+    width: 5px;
+    background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
+}
+
+.tag-highlight {
+    background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+    border-color: #bfdbfe;
+}
+
+.tag-highlight::before {
+    background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
+}
+
+.tag-key {
+    font-weight: 600;
+    color: #475569;
+    white-space: nowrap;
+    flex-shrink: 0;
+    font-size: 15px;
+}
+
+.tag-separator {
+    color: #94a3b8;
+    font-weight: 300;
+    flex-shrink: 0;
+    font-size: 15px;
+}
+
+.tag-value {
+    color: #1e293b;
+    font-weight: 500;
+    word-break: break-word;
+    flex: 1;
+    min-width: 0;
+    font-size: 15px;
+}
+
+.empty-tags {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 40px 20px;
+    color: #94a3b8;
+    font-size: 17px;
+    background: linear-gradient(135deg, rgba(248, 250, 252, 0.5) 0%, rgba(241, 245, 249, 0.5) 100%);
+    border-radius: 12px;
+    border: 2px dashed #e2e8f0;
+}
+
+.empty-tags i {
+    font-size: 32px;
+    margin-bottom: 12px;
+    opacity: 0.5;
+}
+
+.tags-actions {
+    display: flex;
+    justify-content: center;
+    padding: 16px 0;
+    border-top: 1px solid #f1f5f9;
+    margin-top: auto;
+    background: white;
+    position: sticky;
+    bottom: 0;
+    z-index: 10;
+}
+
+.btn-expand-tags,
+.btn-collapse-tags {
+    background: transparent;
+    color: #667eea;
+    border: 1px solid #667eea;
+    padding: 8px 16px;
+    border-radius: 6px;
+    font-size: 16px;
+    cursor: pointer;
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    transition: all 0.2s ease;
+}
+
+.btn-expand-tags:hover,
+.btn-collapse-tags:hover {
+    background: rgba(102, 126, 234, 0.05);
+    border-color: #5a67d8;
+    color: #5a67d8;
+}
+
+.btn-expand-tags:active,
+.btn-collapse-tags:active {
+    transform: scale(0.98);
+}
+
+/* 微信风格聊天弹窗 */
+.chat-dialog-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.5);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 9999;
+    backdrop-filter: blur(4px);
+}
+
+.chat-dialog {
+    background: white;
+    border-radius: 16px;
+    width: 90%;
+    max-width: 800px;
+    height: 600px;
+    display: flex;
+    flex-direction: column;
+    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+    animation: slideIn 0.3s ease-out;
+    position: relative;
+}
+
+@keyframes slideIn {
+    from {
+        opacity: 0;
+        transform: translateY(-20px);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.chat-dialog-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 16px 20px;
+    border-bottom: 1px solid #eef2ff;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    border-radius: 16px 16px 0 0;
+    color: white;
+}
+
+.chat-title {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    font-size: 16px;
+    font-weight: 700;
+}
+
+.btn-close {
+    position: absolute;
+    top: 16px;
+    right: 16px;
+    background: white;
+    border: 2px solid #e2e8f0;
+    color: #1a202c;
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.3s ease;
+    font-size: 24px;
+    font-weight: bold;
+    line-height: 1;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    z-index: 10;
+}
+
+.btn-close:hover {
+    background: #ef4444;
+    border-color: #ef4444;
+    color: white;
+    transform: rotate(90deg) scale(1.1);
+    box-shadow: 0 4px 16px rgba(239, 68, 68, 0.4);
+}
+
+.chat-dialog-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 20px;
+    background: #f5f7fa;
+}
+
+.chat-messages {
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+}
+
+.message-item {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: 8px;
+}
+
+.message-left {
+    justify-content: flex-start;
+}
+
+.message-right {
+    justify-content: flex-end;
+}
+
+/* 客户消息强制布局:头像在右 */
+.message-item-customer {
+    display: flex !important;
+    flex-direction: row !important;
+    justify-content: flex-end !important;
+    gap: 10px !important;
+}
+
+.message-wrapper {
+    display: flex;
+    align-items: flex-start;
+    gap: 10px;
+    max-width: 75%;
+}
+
+.message-wrapper-left {
+    flex-direction: row;
+}
+
+.message-name {
+    font-size: 12px;
+    color: #94a3b8;
+    white-space: nowrap;
+    text-align: left;
+    margin-bottom: 4px;
+    line-height: 1.2;
+}
+
+.message-name-ai {
+    color: #667eea;
+    font-weight: 500;
+}
+
+.message-avatar {
+    width: 32px;
+    height: 32px;
+    border-radius: 6px;
+    overflow: hidden;
+    flex-shrink: 0;
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
+    background: #f0f0f0;
+}
+
+.message-avatar img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    transition: transform 0.3s ease;
+}
+
+.message-avatar:hover img {
+    transform: scale(1.1);
+}
+
+.message-wrapper-left .message-avatar {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+}
+
+.message-wrapper-right .message-avatar {
+    background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+}
+
+.message-content {
+    display: flex;
+    flex-direction: column;
+    max-width: calc(100% - 42px);
+}
+
+.message-wrapper-left .message-content {
+    align-items: flex-start;
+    margin-left: 4px;
+}
+
+.message-wrapper-right .message-content {
+    align-items: flex-end !important;
+    margin-right: 4px;
+}
+
+/* 客户聊天内容容器 */
+.message-content-right {
+    flex: 0 0 auto !important;
+    max-width: calc(100% - 42px) !important;
+    display: flex;
+    align-items: flex-start !important;
+}
+
+.message-bubble {
+    background: white;
+    padding: 9px 13px;
+    border-radius: 6px;
+    font-size: 14px;
+    line-height: 1.5;
+    color: #0f172a;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+    word-break: break-word;
+    position: relative;
+    max-width: 500px;
+}
+
+.message-bubble::before {
+    content: '';
+    position: absolute;
+    left: -6px;
+    top: 12px;
+    width: 0;
+    height: 0;
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+    border-right: 6px solid white;
+}
+
+.message-bubble-right {
+    background: #d9fdd3;
+    color: #0f172a;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+    display: inline-block;
+    position: relative;
+}
+
+.message-bubble-right::before {
+    content: '';
+    position: absolute;
+    right: -6px;
+    left: auto;
+    top: 16px !important;
+    transform: none !important;
+    width: 0;
+    height: 0;
+    border-top: 5px solid transparent;
+    border-bottom: 5px solid transparent;
+    border-left: 6px solid #d9fdd3;
+    border-right: none;
+}
+
+.empty-chat-tip {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 60px 20px;
+    color: #94a3b8;
+    font-size: 14px;
+}
+
+.empty-chat-tip i {
+    font-size: 48px;
+    margin-bottom: 12px;
+    opacity: 0.5;
+}
+
+/* 客户画像样式 */
+.profile-grid {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+    max-height: 350px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+    padding-top: 0;
+}
+
+.profile-grid::-webkit-scrollbar {
+    width: 6px;
+}
+
+.profile-grid::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.profile-grid::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.profile-grid::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+.profile-item {
+    display: grid;
+    grid-template-columns: 35% 65%;
+    align-items: flex-start;
+    gap: 6px;
+    padding: 4px 10px;
+    border-radius: 6px;
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+    border: 1px solid #e2e8f0;
+    word-break: break-word;
+}
+
+.profile-item:hover {
+    background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
+    transform: translateX(4px);
+    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
+    border-color: #cbd5e1;
+}
+
+.profile-item-main {
+    background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+    border-color: #bfdbfe;
+    position: sticky;
+    top: 0;
+    z-index: 10;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.profile-item-main:hover {
+    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
+}
+
+.profile-item-full {
+    grid-template-columns: 35% 65%;
+}
+
+.profile-item .label {
+    font-size: 15px;
+    color: #64748b;
+    font-weight: 600;
+    white-space: normal;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    line-height: 1.4;
+    min-width: 0;
+}
+
+.profile-item .label i {
+    color: #94a3b8;
+    font-size: 12px;
+    width: 14px;
+    text-align: center;
+    flex-shrink: 0;
+}
+
+.profile-item .value {
+    font-size: 16px;
+    color: #0f172a;
+    font-weight: 500;
+    word-break: break-word;
+    line-height: 1.4;
+    min-width: 0;
+}
+
+.profile-item .value.highlight {
+    color: #0369a1;
+    font-size: 16px;
+    font-weight: 600;
+}
+
+.profile-item .value.long-text {
+    color: #334155;
+    font-weight: 400;
+}
+
+.update-time-corner {
+    position: absolute;
+    bottom: 12px;
+    right: 16px;
+    font-size: 12px;
+    color: #94a3b8;
+    font-style: italic;
+}
+
+/* 客户关注点 & 意向度样式 */
+.focus-points {
+    padding: 0;
+    margin-bottom: 10px;
+}
+
+.focus-title {
+    font-size: 17px;
+    color: #64748b;
+    font-weight: 600;
+    margin-bottom: 8px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.focus-title i {
+    color: #3b82f6;
+    font-size: 14px;
+}
+
+.focus-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+    max-height: 200px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    padding-right: 4px;
+}
+
+.focus-list::-webkit-scrollbar {
+    width: 6px;
+}
+
+.focus-list::-webkit-scrollbar-track {
+    background: #f1f5f9;
+    border-radius: 3px;
+}
+
+.focus-list::-webkit-scrollbar-thumb {
+    background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
+    border-radius: 3px;
+}
+
+.focus-list::-webkit-scrollbar-thumb:hover {
+    background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
+}
+
+.focus-item {
+    display: flex;
+    align-items: flex-start;
+    gap: 6px;
+    padding: 6px 10px;
+    margin-bottom: 6px;
+    background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
+    border-radius: 6px;
+    border: 1px solid #e2e8f0;
+    transition: all 0.3s ease;
+    font-size: 15px;
+    color: #334155;
+    line-height: 1.5;
+}
+
+.focus-item:hover {
+    background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+    border-color: #bfdbfe;
+    transform: translateX(4px);
+    box-shadow: 0 2px 6px rgba(59, 130, 246, 0.1);
+}
+
+.focus-item i {
+    color: #3b82f6;
+    font-size: 10px;
+    margin-top: 1px;
+    flex-shrink: 0;
+}
+
+/* 意向度样式 - 水印风格 */
+.intention-section {
+    margin-top: 10px;
+    padding-top: 8px;
+    border-top: 1px solid #e2e8f0;
+}
+
+.intention-header {
+    margin-bottom: 6px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.intention-info-icon {
+    color: #94a3b8;
+    font-size: 16px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+}
+
+.intention-info-icon:hover {
+    color: #3b82f6;
+}
+
+/* 意向度提示框样式 */
+.intention-tooltip {
+    line-height: 1.8;
+    font-size: 13px;
+}
+
+.intention-tooltip div {
+    padding: 2px 0;
+}
+
+.intention-tooltip strong {
+    color: #1e293b;
+    font-weight: 600;
+}
+
+.intention-label {
+    font-size: 16px;
+    font-weight: 600;
+    color: #64748b;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.intention-label::before {
+    content: '';
+    width: 3px;
+    height: 14px;
+    background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
+    border-radius: 2px;
+}
+
+.intention-label i {
+    width: 20px;
+    height: 20px;
+    border-radius: 6px;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 12px;
+    color: #fff;
+    background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
+    box-shadow: 0 2px 6px rgba(245, 158, 11, 0.35);
+}
+
+/* 水印风格意向度显示 - 按等级着色 */
+.intention-watermark {
+    font-size: 59px;
+    font-weight: 800;
+    text-align: center;
+    padding: 18px 12px;
+    border-radius: 10px;
+    border: 2px solid;
+    position: relative;
+    overflow: hidden;
+    letter-spacing: 6px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+    transition: all 0.3s ease;
+}
+
+/* S/S+ 级 - 金色/绿色,最高级别 */
+
+.intention-grade-s {
+    background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, #fef3c7 100%);
+    border-color: #f59e0b;
+    color: #92400e;
+    text-shadow: 0 2px 4px rgba(146, 64, 14, 0.2);
+}
+
+.intention-grade-s::after {
+    content: 'S';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(245, 158, 11, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+.intention-grade-s0 {
+    background: linear-gradient(135deg, #fef3c7 0%, #fde68a 50%, #fef3c7 100%);
+    border-color: #f59e0b;
+    color: #92400e;
+    text-shadow: 0 2px 4px rgba(146, 64, 14, 0.2);
+}
+
+.intention-grade-s0::after {
+    content: 'S+';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(245, 158, 11, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* A 级 - 蓝色 */
+.intention-grade-a {
+    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 50%, #dbeafe 100%);
+    border-color: #3b82f6;
+    color: #1e40af;
+    text-shadow: 0 2px 4px rgba(30, 64, 175, 0.2);
+}
+
+.intention-grade-a::after {
+    content: 'A';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(59, 130, 246, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* F 级 - 蓝色 */
+.intention-grade-f {
+    background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 50%, #dbeafe 100%);
+    border-color: #3b82f6;
+    color: #1e40af;
+    text-shadow: 0 2px 4px rgba(30, 64, 175, 0.2);
+    font-size: 40px !important;
+}
+
+.intention-grade-f::after {
+    content: '代客下单';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 30px;
+    font-weight: 800;
+    color: rgba(59, 130, 246, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* B 级 - 紫色 */
+.intention-grade-b {
+    background: linear-gradient(135deg, #e9d5ff 0%, #d8b4fe 50%, #e9d5ff 100%);
+    border-color: #a855f7;
+    color: #6b21a8;
+    text-shadow: 0 2px 4px rgba(107, 33, 168, 0.2);
+}
+
+.intention-grade-b::after {
+    content: 'B';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(168, 85, 247, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* C 级 - 橙色 */
+.intention-grade-c {
+    background: linear-gradient(135deg, #fed7aa 0%, #fdba74 50%, #fed7aa 100%);
+    border-color: #f97316;
+    color: #9a3412;
+    text-shadow: 0 2px 4px rgba(154, 52, 18, 0.2);
+}
+
+.intention-grade-c::after {
+    content: 'C';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(249, 115, 22, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* D 级 - 粉红色 */
+.intention-grade-d {
+    background: linear-gradient(135deg, #fbcfe8 0%, #f9a8d4 50%, #fbcfe8 100%);
+    border-color: #ec4899;
+    color: #9d174d;
+    text-shadow: 0 2px 4px rgba(157, 23, 77, 0.2);
+}
+
+.intention-grade-d::after {
+    content: 'D';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(236, 72, 153, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+/* E 级 - 红色,最低级别 */
+.intention-grade-e {
+    background: linear-gradient(135deg, #fecaca 0%, #fca5a5 50%, #fecaca 100%);
+    border-color: #ef4444;
+    color: #991b1b;
+    text-shadow: 0 2px 4px rgba(153, 27, 27, 0.2);
+}
+
+.intention-grade-e::after {
+    content: 'E';
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%) rotate(-25deg);
+    font-size: 90px;
+    font-weight: 900;
+    color: rgba(239, 68, 68, 0.08);
+    z-index: 0;
+    pointer-events: none;
+}
+
+.intention-watermark span {
+    position: relative;
+    z-index: 1;
+}
+
+/* 暂无评级提示 */
+.no-intention-tip {
+    text-align: center;
+    padding: 18px 12px;
+    font-size: 16px;
+    color: #94a3b8;
+    font-style: italic;
+    background: linear-gradient(135deg, rgba(248, 250, 252, 0.5) 0%, rgba(241, 245, 249, 0.5) 100%);
+    border-radius: 10px;
+    border: 2px dashed #e2e8f0;
+}
+
+.card-header h3 i {
+    width: 24px;
+    height: 24px;
+    border-radius: 7px;
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 13px;
+    color: #fff;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    box-shadow: 0 3px 8px rgba(102, 126, 234, 0.35);
+    -webkit-text-fill-color: #fff;
+}
+
+.card-highlight .card-header h3 i {
+    color: white;
+    background: none;
+    -webkit-text-fill-color: white;
+}
+
+/* 模仿参考图的企业微信分析台风格(覆盖) */
+.customer-container {
+    max-width: 100%;
+    padding: 12px;
+    background: #f4f6fa;
+}
+
+.main-grid-three-columns {
+    grid-template-columns: 300px minmax(640px, 1fr) 320px;
+    gap: 12px;
+    align-items: start;
+}
+
+.card {
+    border-radius: 10px;
+    border: 1px solid #e6ebf2;
+    box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
+    margin-bottom: 10px;
+    padding: 12px;
+    background: #fff;
+}
+
+.card::before {
+    display: none;
+}
+
+.card:hover {
+    transform: none;
+    box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
+    border-color: #dbe4f0;
+}
+
+.card-header {
+    border-bottom: 1px solid #edf1f7;
+    margin-bottom: 10px;
+    padding-bottom: 8px;
+}
+
+.card-header h3 {
+    font-size: 16px;
+    font-weight: 600;
+    color: #1f2937;
+}
+
+.card-header h3 i {
+    width: 18px;
+    height: 18px;
+    border-radius: 4px;
+    font-size: 10px;
+    background: #4f7cff;
+    box-shadow: none;
+}
+
+.summary-text.compact {
+    font-size: 14px;
+    line-height: 1.75;
+    color: #334155;
+    max-height: 132px;
+}
+
+.risk-card,
+.card-focus {
+    background: #fff;
+    border: 1px solid #e6ebf2;
+}
+
+.risk-analysis {
+    background: #f8fafc;
+    border: 1px solid #edf2f7;
+}
+
+.tag-item,
+.focus-item,
+.profile-item {
+    background: #f8fafc;
+    border-color: #e5eaf1;
+}
+
+.intention-label i {
+    width: 18px;
+    height: 18px;
+    border-radius: 4px;
+    font-size: 10px;
+    background: #4f7cff;
+    box-shadow: none;
+}
+
+.records-table th {
+    background: #f8fafc;
+    color: #475569;
+}
+
+.records-table tbody tr:hover {
+    background: #f8fbff;
+}
+
+
+</style>

+ 1245 - 0
src/views/app/urgentClass/index.vue

@@ -0,0 +1,1245 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="用户ID" prop="fsUserId">
+        <el-input
+          v-model="queryParams.fsUserId"
+          @input="queryParams.fsUserId = queryParams.fsUserId.replace(/[^\d]/g, '')"
+          placeholder="请输入用户ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="客服" prop="customerRoles">
+        <div @click="openCusSearchPage"
+             style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
+          <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+            <el-tag type="success"
+                    closable
+                    :disable-transitions="false"
+                    v-for="list in this.customerRoleSearch.selected"
+                    :key="list.id"
+                    @close="handleCloseCus(list)"
+                    style="margin: 3px;"
+            >{{ list.roleName }}
+            </el-tag>
+          </div>
+        </div>
+      </el-form-item>
+      <el-form-item label="类别" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择类别" clearable size="small">
+          <el-option v-for="dict in typeOptions" :key="dict.dictValue" :label="dict.dictLabel"
+                     :value="dict.dictValue" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="处理状态" prop="trackType">
+        <el-select v-model="queryParams.trackType" placeholder="处理状态" clearable size="small">
+          <el-option v-for="dict in trackTypeOptions" :key="dict.dictValue" :label="dict.dictLabel"
+                     :value="dict.dictValue" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="sop编号" prop="sopId">
+        <el-input v-model="queryParams.sopId" placeholder="请输入sopid" clearable size="small"
+                  @keyup.enter.native="handleQuery" />
+      </el-form-item>
+      <el-form-item label="标题" prop="title">
+        <el-input v-model="queryParams.title" placeholder="请输入标题" clearable size="small"
+                  @keyup.enter.native="handleQuery" />
+      </el-form-item>
+      <el-form-item label="创建日期" prop="crtTime">
+        <el-date-picker size="small" v-model="queryParams.crtTime"  type="date" placeholder="选择日期"
+                        value-format="yyyy-MM-dd" format="yyyy-MM-dd" @change="handleQuery">
+        </el-date-picker>
+      </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-button icon="el-icon-refresh" size="mini" @click="isSM = !isSM">色盲模式</el-button>
+      </el-form-item>
+    </el-form>
+
+    <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"/>
+
+    <el-tabs type="card" v-model="actName" @tab-click="toggleTab">
+      <el-tab-pane v-for="(item, index) in statusOptions" :label="item.dictLabel"
+                   :name="item.dictValue"></el-tab-pane>
+    </el-tabs>
+
+    <el-table border v-loading="loading" :data="urgentClassTaskList">
+      <el-table-column label="用户ID" align="center" prop="fsUserId" min-width="120"/>
+      <el-table-column label="用户名称" align="center" prop="userName" min-width="120"/>
+      <el-table-column label="客服名称" align="center" prop="customerRoleName" min-width="180">
+        <template slot-scope="scope">
+          <el-tag type="primary" style="margin: 3px;">{{ scope.row.customerRoleName }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status" min-width="120">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status != null ? scope.row.status.toString() : null" />
+        </template>
+      </el-table-column>
+      <el-table-column label="类别" align="center" prop="type" min-width="120">
+        <template slot-scope="scope">
+          <dict-tag :options="typeOptions" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column label="处理状态" align="center" prop="trackType" min-width="120">
+        <template slot-scope="scope">
+          <dict-tag :options="trackTypeOptions" :value="scope.row.trackType"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="标题" align="center" prop="title" min-width="150"/>
+      <el-table-column label="描述" align="center" prop="remark" />
+<!--      <el-table-column label="分值" align="center" prop="score" min-width="150">-->
+<!--        <template slot-scope="scope">-->
+<!--          <span :style="getScoreStyle(scope.row.score)">{{ scope.row.score }}</span>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+      <el-table-column label="用户过往学习记录" align="center" min-width="200">
+        <template slot-scope="scope">
+          <div v-if="isSM" style="display: flex; gap: 4px; justify-content: center;">
+                        <span v-for="(log, index) in scope.row.userWatchLogs" :key="index" :style="{
+                            display: 'inline-block',
+                            width: '16px',
+                            height: '16px',
+                            borderRadius: log == 2 ? '50%' : log == 4 ? '2px 8px' : '2px',
+                            backgroundColor: log == 3 ? 'transparent' : '#f0f0f0',
+                            border: log == 3 ? '2px solid #666' : '1px solid #ddd',
+                            position: 'relative',
+                            transform: log == 4 ? 'rotate(45deg)' : 'none'
+                        }">
+                            <!-- 已完成 - 圆形 + 对勾 -->
+                            <span v-if="log == 2" style="
+                                position: absolute;
+                                top: 45%;
+                                left: 50%;
+                                width: 6px;
+                                height: 3px;
+                                border: solid #333;
+                                border-width: 0 0 2px 2px;
+                                transform: translate(-50%, -50%) rotate(-45deg);
+                            "></span>
+
+                          <!-- 待学习 - 方框 + 叉(与底部图例一致) -->
+                            <span v-if="log == 3" style="
+                                position: absolute;
+                                top: 50%;
+                                left: 50%;
+                                width: 8px;
+                                height: 2px;
+                                background: #666;
+                                transform: translate(-50%, -50%) rotate(45deg);">
+                            </span>
+                            <span v-if="log == 3" style="
+                                position: absolute;
+                                top: 50%;
+                                left: 50%;
+                                width: 8px;
+                                height: 2px;
+                                background: #666;
+                                transform: translate(-50%, -50%) rotate(-45deg);">
+                            </span>
+
+
+                            <span v-if="log == 1" style="
+                                position: absolute;
+                                top: 50%;
+                                left: 50%;
+                                width: 8px;
+                                height: 2px;
+                                background: #666;
+                                transform: translate(-50%, -10%) rotate(-15deg);
+                            "></span>
+
+                          <!-- 部分完成 - 菱形 -->
+                            <span v-if="log == 4" style="
+                                position: absolute;
+                                top: 50%;
+                                left: 50%;
+                                width: 6px;
+                                height: 6px;
+                                border: 2px solid #333;
+                                transform: translate(-50%, -50%) rotate(-45deg);
+                            "></span>
+
+                          <!-- 未开始 - 减号 -->
+                            <span v-if="!log" style="
+                                position: absolute;
+                                top: 50%;
+                                left: 50%;
+                                width: 8px;
+                                height: 2px;
+                                background: #999;
+                                transform: translate(-50%, -50%);
+                            "></span>
+
+                          <!-- 图像 - 使用img标签 -->
+                            <img v-if="log == 5" src="/path/to/your-image.png" alt="Custom icon" style="
+                                position: absolute;
+                                top: 50%;
+                                left: 50%;
+                                width: 12px;
+                                height: 12px;
+                                transform: translate(-50%, -50%);
+                            ">
+                        </span>
+          </div>
+
+          <div v-else style="display: flex; gap: 4px; justify-content: center;">
+                        <span v-for="(log, index) in scope.row.userWatchLogs" :key="index" :style="{
+                            display: 'inline-block',
+                            width: '16px',
+                            height: '16px',
+                            borderRadius: '2px',
+                            backgroundColor:
+                                log == 2 ? '#67C23A' :
+                                    log == 3 ? '#f55a4f' :
+                                        log == 4 ? '#FFD700' :
+                                            log == 1 ? '#0bc6ff' :
+                                                '#909399'
+                        }"></span>
+          </div>
+
+        </template>
+      </el-table-column>
+      <el-table-column label="最近章节" align="center" min-width="150">
+        <template slot-scope="scope">
+          {{ scope.row.latestCourseName }}
+        </template>
+      </el-table-column>
+      <el-table-column label="最晚学习时间" align="center" min-width="150">
+        <template slot-scope="scope">
+          {{ formatTime(scope.row.lastWatchDate) }}
+        </template>
+      </el-table-column>
+
+      <el-table-column label="sopId" align="center" prop="sopId" min-width="160"/>
+      <el-table-column label="创建时间" align="center" prop="createTime" min-width="150"/>
+      <el-table-column label="修改时间" align="center" prop="updateTime" min-width="150"/>
+      <el-table-column label="操作" align="center" fixed="right" min-width="150">
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.status == 0"
+            size="mini"
+            type="text"
+            @click="toggleStatus(scope.row, 1)"
+            v-hasPermi="['app:urgentClass:handle']"
+          >
+            点击处理
+          </el-button>
+<!--          <el-button-->
+<!--            v-if="scope.row.status == 0"-->
+<!--            size="mini"-->
+<!--            type="text"-->
+<!--            @click="quickReply(scope.row)"-->
+<!--            v-hasPermi="['app:urgentClass:quickReply']"-->
+<!--          >-->
+<!--            快速回复-->
+<!--          </el-button>-->
+<!--          <el-button-->
+<!--            size="mini"-->
+<!--            type="text"-->
+<!--            :loading="imLoading && loadingRowId === scope.row.customerRoleId"-->
+<!--            :disabled="imLoading"-->
+<!--            @click="loginIm(scope.row)"-->
+<!--            v-hasPermi="['app:urgentClass:talk']"-->
+<!--          >-->
+<!--            聊天-->
+<!--          </el-button>-->
+<!--          <el-button-->
+<!--            v-if="scope.row.fsUserId && scope.row.fsUserId !== ''"-->
+<!--            size="mini"-->
+<!--            type="text"-->
+<!--            @click="handleCallPhone(scope.row)"-->
+<!--            v-hasPermi="['app:urgentClass:getFsUserPhone']"-->
+<!--          >-->
+<!--            外呼-->
+<!--          </el-button>-->
+<!--          <el-button-->
+<!--            size="mini"-->
+<!--            type="text"-->
+<!--            @click="handledetails(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 v-if="isSM" style="margin-top: 20px; padding: 12px; background: #f8f8f8; border-radius: 4px;">
+      <div style="display: flex; gap: 20px; flex-wrap: wrap;">
+        <div style="display: flex; align-items: center; gap: 8px;">
+                    <span style="
+                        display: inline-block;
+                        width: 16px;
+                        height: 16px;
+                        border-radius: 50%;
+                        background-color: #f0f0f0;
+                        position: relative;
+                    ">
+                        <span style="
+                            position: absolute;
+                            top: 45%;
+                            left: 50%;
+                            width: 6px;
+                            height: 3px;
+                            border: solid #333;
+                            border-width: 0 0 2px 2px;
+                            transform: translate(-50%, -50%) rotate(-45deg);
+                        "></span>
+                    </span>
+          <span>完课</span>
+        </div>
+
+        <div style="display: flex; align-items: center; gap: 8px;">
+                    <span style="
+                        display: inline-block;
+                        width: 16px;
+                        height: 16px;
+                        border-radius: 2px;
+                        border: 2px solid #666;
+                        background-color: transparent;
+                        position: relative;
+                    ">
+                        <span style="
+                            position: absolute;
+                            top: 50%;
+                            left: 50%;
+                            width: 8px;
+                            height: 2px;
+                            background: #666;
+                            transform: translate(-50%, -50%) rotate(45deg);
+                        "></span>
+                        <span style="
+                            position: absolute;
+                            top: 50%;
+                            left: 50%;
+                            width: 8px;
+                            height: 2px;
+                            background: #666;
+                            transform: translate(-50%, -50%) rotate(-45deg);
+                        "></span>
+                    </span>
+          <span>待学习</span>
+        </div>
+
+        <div style="display: flex; align-items: center; gap: 8px;">
+                    <span style="
+                        display: inline-block;
+                        width: 16px;
+                        height: 16px;
+                        border-radius: 2px 8px;
+                        background-color: #f0f0f0;
+                        border: 1px solid #ddd;
+                        position: relative;
+                        transform: rotate(45deg);
+                    ">
+                        <span style="
+                            position: absolute;
+                            top: 50%;
+                            left: 50%;
+                            width: 6px;
+                            height: 6px;
+                            border: 2px solid #333;
+                            transform: translate(-50%, -50%) rotate(-45deg);
+                        "></span>
+                    </span>
+          <span>学习中断</span>
+        </div>
+
+        <div style="display: flex; align-items: center; gap: 8px;">
+                    <span style="
+                        display: inline-block;
+                        width: 16px;
+                        height: 16px;
+                        border-radius: 2px;
+                        background-color: #f0f0f0;
+                        border: 1px solid #ddd;
+                        position: relative;
+	                ">
+                        <span style="
+                            position: absolute;
+                            top: 50%;
+                            left: 50%;
+                            width: 8px;
+                            height: 2px;
+                            background: #999;
+                            transform: translate(-50%, -50%);
+	                    "></span>
+                    </span>
+          <span>没发课</span>
+        </div>
+
+        <div style="display: flex; align-items: center; gap: 8px;">
+                    <span style="
+                        display: inline-block;
+                        width: 16px;
+                        height: 16px;
+                        border-radius: 2px;
+                        background-color: #f0f0f0;
+                        border: 1px solid #ddd;
+                        position: relative;
+                    ">
+                        <span style="
+                            position: absolute;
+                            top: 50%;
+                            left: 50%;
+                            width: 8px;
+                            height: 2px;
+                            background: #666;
+                            transform: translate(-50%, -10%) rotate(-15deg);
+                        "></span>
+                    </span>
+          <span>学习中</span>
+        </div>
+
+      </div>
+    </div>
+
+    <div v-else style="margin-top: 20px; padding: 12px; background: #f8f8f8; border-radius: 4px;">
+      <div style="display: flex; gap: 20px; flex-wrap: wrap;">
+        <div style="display: flex; align-items: center; gap: 8px;">
+                    <span
+                      style="display: inline-block; width: 16px; height: 16px; border-radius: 2px; background-color: #67C23A;"></span>
+          <span>完课</span>
+        </div>
+        <div style="display: flex; align-items: center; gap: 4px;">
+                    <span
+                      style="display: inline-block; width: 16px; height: 16px; border-radius: 2px; background-color: #f55a4f;"></span>
+          <span>待学习</span>
+        </div>
+        <div style="display: flex; align-items: center; gap: 4px;">
+                    <span
+                      style="display: inline-block; width: 16px; height: 16px; border-radius: 2px; background-color: #FFD700;"></span>
+          <span>学习中断</span>
+        </div>
+        <div style="display: flex; align-items: center; gap: 4px;">
+                    <span
+                      style="display: inline-block; width: 16px; height: 16px; border-radius: 2px; background-color: #909399;"></span>
+          <span>没发课</span>
+        </div>
+        <div style="display: flex; align-items: center; gap: 4px;">
+                    <span
+                      style="display: inline-block; width: 16px; height: 16px; border-radius: 2px; background-color: #0bc6ff;"></span>
+          <span>学习中</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- app客服搜索 -->
+    <el-dialog title="客服搜索" :visible.sync="customerRoleSearch.visible" width="1000px" append-to-body>
+      <CusRoleList ref="customerSearchRef"
+                   @confirm="customerSelectedConfirmCall"
+                   @selectionChange="customerSelectionChangeCall"
+                   :selected="customerRoleSearch.selected"
+      />
+    </el-dialog>
+
+    <!--  -->
+    <el-drawer
+      :with-header="false"
+      size="75%"
+      title="用户详情"
+      :visible.sync="userDetails.visible"
+    >
+      <userDetails
+        ref="userDetails"
+        :userInfo="userDetails.rowInfo"
+        :refreshCount="userDetails.count"
+        @chat="login"
+        @toggleStatus="toggleStatus"
+        :imLoading="imLoading"
+      />
+    </el-drawer>
+
+    <!-- AI外呼对话框 -->
+    <ai-sip-call-dialog
+      :visible.sync="callDialogVisible"
+      :phone-number="currentCallPhone"
+      :dial-mode="currentDialMode"
+      :record-id="currentRecordId"
+      @close="handleCallDialogClose"
+      @call-ended="handleCallEnded"
+    />
+
+    <!-- 查看详情 -->
+    <el-dialog
+      :title="quickReplyDialog.title"
+      :visible.sync="quickReplyDialog.visible"
+      width="1000px"
+      append-to-body
+      :close-on-click-modal="false"
+      :before-close="closeQuickReply"
+    >
+      <div v-loading="quickReplyDialog.loading">
+        <div style="height: 600px; overflow: auto;" ref="scrollWrap">
+          <el-timeline>
+            <el-timeline-item
+              v-for="(chatLog, idx) in quickReplyDialog.chatLogs"
+              :timestamp="chatLog.sendTime"
+              placement="top"
+              :type="chatLog.userType === 0 ? 'success' : 'primary'"
+              :key="idx"
+            >
+              <el-card shadow="hover" :body-style="{'padding': '8px', 'background': (chatLog.userType === 0 ? '#9df29f' : 'eeeef0'), 'color': '#161616'}">
+                  <div
+                    :class="chatLog.userType === 0 ? '' : 'no-copy1'"
+                    v-html="chatLog.sendContent || '--'"
+                  >
+                  </div>
+              </el-card>
+            </el-timeline-item>
+          </el-timeline>
+        </div>
+
+        <div style="padding: 5px 0 0 44px;">
+          <div class="quick-reply">
+            <el-input type="textarea"
+              :autosize="{ minRows: 3, maxRows: 3}"
+              :resize="'none'"
+              placeholder="请输入快捷回复"
+              v-model="quickReplyDialog.replyContent"
+              @keydown.native="handleKeyDown"
+            />
+            <div style="text-align: right;">
+              <el-tooltip class="item" effect="dark" content="Enter发送,Ctrl+Enter换行" placement="top-start">
+                <el-button
+                  :type="quickReplyDialog.replyContent.trim() ? 'success' : 'info'"
+                  :disabled="!quickReplyDialog.replyContent.trim() || quickReplyDialog.loading"
+                  size="mini"
+                  @click="sendReplyContent"
+                >
+                  发送
+                </el-button>
+              </el-tooltip>
+            </div>
+          </div>
+        </div>
+
+        <div slot="footer" class="dialog-footer" style="text-align: center">
+          <el-button @click="closeQuickReplyDialog">关 闭</el-button>
+        </div>
+      </div>
+
+
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listUrgentClassTask, setStatus,getFsUserPhone } from "@/api/app/urgentClass";
+import CusRoleList from '@/views/app/user/CusRoleList.vue';
+import { getOpenIM, getCbEvents } from '@/utils/openIM';
+import { debounce } from '@/utils';
+import { accountCheck } from '@/api/app/user/userList.js';
+import { findChatLogsByUserIdAndCustomerId, editUserChatLogs, } from "@/api/app/userChatLogs";
+import userDetails from './CustomerDetail.vue';
+import {mapGetters, mapState} from "vuex";
+// import AiSipCallDialog from '@/components/AiSipCallDialog.vue';
+export default {
+  name: "UrgentClass",
+  components: { CusRoleList, userDetails
+    // , AiSipCallDialog
+  },
+  data() {
+    return {
+      hasBindReadyEvent : false,
+      imLoading: false,
+      isSDKReady: false,
+      OpenIM: null,
+      tokenCache: {},
+      loadingRowId: null,
+      actName: "0",
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      companyUserOptionsParams: {
+        name: undefined,
+        hasNextPage: false,
+        pageNum: 1,
+        pageSize: 10
+      },
+      isSM: false,//是否色盲模式
+      companyUserOptionsLoading: false,
+      companyUserOptions: [],
+      myQwUserList: [],
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 提醒任务看板表格数据
+      urgentClassTaskList: [],
+      // 状态 0 待处理 1 已处理 3 过期字典
+      statusOptions: [],
+      // 类别 1先导 2 科普 3 节点 4 转人工字典
+      typeOptions: [],
+      trackTypeOptions: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        fsUserId: null,
+        customerRoles: null,
+        crtTime: this.formatDateToYMD(new Date()),
+        status: 0,
+        type: null,
+        title: null,
+        score: null,
+        sopId: null,
+      },
+      customerRoleSearch: {
+        visible: false,
+        selected: [],
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {},
+      userDetails: {
+        visible: false,
+        rowInfo: null,
+        count: 0,
+      },
+      // AI外呼相关
+      callDialogVisible: false,
+      currentCallPhone: '',
+      currentDialMode: 'encrypted', // plaintext-明文, encrypted-密文
+      currentRecordId: '', // 当前外呼的记录ID
+      quickReplyDialog: {
+        visible: false,
+        rowInfo: null,
+        loading: false,
+        chatInfo: {},
+        chatLogs: [],
+        title: '',
+        replyContent: '',
+      },
+    };
+  },
+  computed: {
+    ...mapState({
+      userID: state => state.imuser.userID,
+      applicationUnreadCount: state => state.friend.unreadCount,
+      fsUserId: state => state.imuser.fsUserId,
+      currentConversation: state => state.conversation.currentConversation
+    }),
+  },
+  watch: {
+    '$store.state.imuser.isLogin'(val) {
+      if (!val) this.hasBindReadyEvent = false; // 退出后允许下次登录重新绑定 OnConnectSuccess
+    }
+  },
+  created() {
+
+    this.getDicts("app_urgent_class_task_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    this.getDicts("app_urgent_class_task_type").then(response => {
+      this.typeOptions = response.data;
+    });
+    this.getDicts("app_urgent_class_task_track_type").then(response => {
+      this.trackTypeOptions = response.data;
+    });
+    this.getList();
+    this.OpenIM = getOpenIM();
+    this.initListener();
+    // 防抖:收到新消息时避免频繁调用 getOneConversation,减少 SDK 日志与请求
+    this._debouncedRefreshConversation = debounce(() => {
+      if (this.fsUserId) {
+        this.OpenIM.getOneConversation({ sourceID: this.fsUserId, sessionType: 1 })
+          .then(({ data }) => this.$store.commit('updateConversationList', data))
+          .catch(() => {})
+      }
+    }, 500)
+
+  },
+  methods: {
+    handleCloseCus(cus) {
+      this.customerRoleSearch.selected = this.customerRoleSearch.selected.filter(item => item.id != cus.id);
+    },
+    loginIm(row) {
+      this.login(row)
+    },
+    initListener() {
+      // 登录成功后会触发 SDK_READY 事件,该事件触发后,可正常使用 SDK 接口
+      this.OpenIM.on(getCbEvents().OnConnectSuccess, () => {
+        console.log("OnConnectSuccess 事件触发!"); // 调试日志
+        this.$store.commit('toggleIsSDKReady', true);
+      });
+      this.OpenIM.on(getCbEvents().OnConnectFailed, this.onError);
+      this.OpenIM.on(getCbEvents().OnKickedOffline, this.onKickOut);
+      this.OpenIM.on(getCbEvents().OnSelfInfoUpdated, this.onSelfInfoUpdated);
+      /*this.OpenIM.on(getCbEvents().OnRecvNewMessage, (message) => {
+        console.log("收到单条消息", message);
+        this.onReceiveMessage({data: [message]}); // 包装成数组形式
+      });*/
+
+      this.OpenIM.on(getCbEvents().OnRecvNewMessages, (data) => {
+        console.log("收到多条消息", data);
+        const msgList = []
+        data.data.forEach(msg =>{
+          if (msg.contentType!==113){
+            msgList.push(msg)
+          }
+        })
+        if (msgList.length>0){
+          this.onReceiveMessage({data: msgList});
+
+        }
+      });
+    },
+    login(row) {
+      this.imLoading = true;
+      this.loadingRowId = row.customerRoleId;
+      const cacheKey = 'C' + row.customerRoleId;
+      const cached = this.tokenCache?.[cacheKey];
+      const tokenPromise = cached && (Date.now() - cached.ts) < 5 * 60 * 1000
+        ? Promise.resolve({ token: cached.token })
+        : accountCheck(cacheKey).then(res => {
+          if (!this.tokenCache) this.tokenCache = {};
+          this.tokenCache[cacheKey] = { token: res.token, ts: Date.now() };
+          return res;
+        });
+      tokenPromise.then(response => {
+        const userToken = response.token;
+        const config = {
+          userID: cacheKey,
+          token:userToken,
+          logLevel: 6,
+          platformID: 5,
+          apiAddr: 'https://web.im.cdwjyyh.com/api',
+          wsAddr: 'wss://web.im.cdwjyyh.com/msg_gateway',
+          dataDir: '/imdata'
+        };
+        this.checkSDKReadyState(row.fsUserId);
+        this.doLogin(config, row);
+      }).catch(err => {
+        this.imLoading = false;
+        this.loadingRowId = null;
+        this.$store.commit('showMessage', { message: `获取 IM Token 失败:${err?.message || '未知错误'}`, type: 'error' });
+      });
+    },
+    doLogin(config, row, isRetry = false) {
+      // 登录前先设置 fsUserId、cacheFsUserName,确保 OnConnectSuccess 回调时能拿到当前用户
+      this.$store.commit('setUserId', 'C'+row.customerRoleId);
+      this.$store.commit('setFsUserId', 'U'+row.fsUserId);
+      this.$store.commit('setCacheFsUserName', row.userName || '');
+      this.OpenIM.login(config).then(() => {
+        this.imLoading = false;
+        this.loadingRowId = null;
+        this.$store.commit('setImChatVisible', true)
+        this.$store.commit('toggleIsLogin', true);
+        this.$store.commit('startComputeCurrent');
+        this.$store.commit('showMessage', { type: 'success', message: 'IM 登录成功' });
+      }).catch(async (error) => {
+        const is10102 = error?.errCode === 10102 || String(error?.errMsg || '').includes('10102')
+        if (is10102 && !isRetry) {
+          try {
+            await this.OpenIM.logout()
+            await new Promise(r => setTimeout(r, 300))
+            this.doLogin(config, row, true)
+          } catch (e) {
+            this.imLoading = false;
+            this.loadingRowId = null;
+            this.$store.commit('showMessage', { message: `IM 登录失败:${error.message || error.errMsg || '未知错误'}`, type: 'error' })
+          }
+        } else {
+          this.imLoading = false;
+          this.loadingRowId = null;
+          console.error('登录失败:', error)
+          this.$store.commit('showMessage', {
+            message: `IM 登录失败:${error.message || error.errMsg || '未知错误'}`,
+            type: 'error',
+          })
+        }
+      })
+    },
+    // 添加SDK就绪状态检查
+    checkSDKReadyState(fsUserId) {
+      if (this.hasBindReadyEvent) return;
+      this.hasBindReadyEvent = true;
+
+      let isReady = false;
+
+      const timeout = setTimeout(() => {
+        if (!isReady) {
+          this.imLoading = false;
+          this.loadingRowId = null;
+          this.$store.commit('toggleIsSDKReady', false);
+          // this.$store.commit('showMessage', { message: 'SDK初始化超时', type: 'error' });
+        }
+      }, 10000);
+
+      this.OpenIM.on(getCbEvents().OnConnectSuccess, () => {
+        console.log("WebSocket连接成功");
+        clearTimeout(timeout);
+        isReady = true;
+        this.OpenIM.getSelfUserInfo().then(({ data }) => {
+          console.log("当前登录用户基本信息",data)
+          this.$store.commit('updateCurrentUserProfile', data)
+        })
+        this.$store.commit('toggleIsSDKReady', true);
+        this.isSDKReady = true
+        // 已关闭 SDK 初始化成功提示,避免重复弹窗
+        // 触发获取用户信息等操作,使用 store 中的 fsUserId(避免切换用户时 closure 仍为旧值)
+        console.log("开始加载用户数据,获取会话列表")
+        const currentFsUserId = this.$store.state.imuser.fsUserId;
+        if (currentFsUserId) {
+          const fsUserIdRaw = String(currentFsUserId).replace(/^U/, '');
+          this.loadUserData(fsUserIdRaw);
+        }
+        console.log("结束加载用户数据,获取会话列表")
+      });
+
+      this.OpenIM.on(getCbEvents().OnConnectFailed, (error) => {
+        clearTimeout(timeout);
+        this.imLoading = false;
+        this.loadingRowId = null;
+        this.$store.commit('toggleIsSDKReady', false);
+        this.$store.commit('showMessage', { message: `SDK 连接失败: ${error.message}`, type: 'error' });
+      });
+    },
+    onKickOut(event) {
+      // event.data.type 是踢出原因
+      //const reason = this.kickedOutReason(event.data.type);
+
+      // 弹窗提示用户
+      this.$alert(`即时通账号在其他设备登录,请重新登录。`, '提示', {
+        confirmButtonText: '确定',
+        type: 'error',
+        callback: () => {
+          // 用户确认后执行退出操作
+          this.$store.commit('toggleIsLogin', false);
+          this.$store.commit('reset');
+        }
+      });
+    },
+
+    // 返回踢出原因
+    kickedOutReason(type) {
+      switch(type) {
+        case 1: return '被管理员踢出';
+        case 2: return '账号在其他设备登录';
+        default: return '未知原因';
+      }
+    },
+    onReceiveMessage({ data: messageList }) {
+      const fsUserId = this.$store.state.imuser.fsUserId
+      const isCurrentSessionMsg = fsUserId && messageList.some(
+        m => m.sendID === fsUserId || m.recvID === fsUserId
+      )
+      if (isCurrentSessionMsg) {
+        this._debouncedRefreshConversation?.()
+      }
+      this.$store.commit('pushCurrentMessageList', messageList)
+      // this.$store.commit('pushAvChatRoomMessageList', messageList)
+    },
+
+    // 添加加载用户数据方法(获取单个会话)
+    loadUserData(fsUserId) {
+      if (this._loadUserDataGuard === fsUserId) return
+      this._loadUserDataGuard = fsUserId
+      // 切换用户时先清空会话列表,避免展示上一用户的数据
+      this.$store.commit('updateConversationList', [])
+      this.$store.commit('setConversationListLoading', true)
+      this.OpenIM.getOneConversation({
+        sourceID: 'U' + fsUserId,
+        sessionType: 1,
+      })
+        .then(({ data }) => {
+          console.log("获取到会话", data)
+          this.conversationList = data ? [data] : []
+          this.$store.commit('updateConversationList', data)
+          if (data) {
+            this.$store.dispatch('checkoutConversation', {
+              conversation: data,
+              skipRefreshConversationList: true
+            })
+          }
+        })
+        .catch(({ errCode, errMsg }) => {
+          // 调用失败
+        })
+        .finally(() => {
+          this.$store.commit('setConversationListLoading', false)
+          this._loadUserDataGuard = null
+        })
+      //查询好友列表
+      // this.OpenIM.getFriendListPage({ offset:0, count:100 })
+      //   .then(({ data }) => {
+      //     // 调用成功
+      //     console.log("获取到好友列表",data)
+      //     //this.conversationList= data
+      //     this.$store.commit('updateFriendList', data)
+      //   })
+      //   .catch(({ errCode, errMsg }) => {
+      //     // 调用失败
+      //   })
+      // //查询群列表
+      // this.OpenIM.getJoinedGroupList()
+      //   .then(({ data }) => {
+      //     // 调用成功
+      //     console.log("获取到群聊列表",data)
+      //     //this.conversationList= data
+      //     this.$store.commit('updateGroupList', data)
+      //   })
+      //   .catch(({ errCode, errMsg }) => {
+      //     // 调用失败
+      //   });
+    },
+    /**
+     * 分数对应背景颜色
+     * @param score
+     */
+    getScoreStyle(score) {
+      let backgroundColor = '';
+      if (score >= 15) {
+        backgroundColor = '#ff4d4f';    // 红色
+      } else if (score >= 9) {
+        backgroundColor = '#ff7d45';    // 橘红
+      } else if (score >= 4) {
+        backgroundColor = '#ffec3d';    // 黄色
+      } else {
+        backgroundColor = '#ffffff';    // 白色
+      }
+      return {
+        'background-color': backgroundColor,
+        'padding': '5px 10px',
+        'border-radius': '4px'
+      };
+    },
+    openCusSearchPage() {
+      this.customerRoleSearch.visible = true;
+    },
+    customerSelectedConfirmCall() {
+      this.customerRoleSearch.visible = false;
+    },
+    /**
+     * 选中元素改变回调
+     * @param selected
+     */
+    customerSelectionChangeCall(config) {
+      let selected = config.selected;
+      //单行操作
+      if (config.mode === 0) {
+        let row = config.row;
+        if (selected) {
+          this.customerRoleSearch.selected.push(row)
+        } else {
+          this.customerRoleSearch.selected = this.customerRoleSearch.selected.filter(m => m.id != row.id);
+        }
+      }
+      //操作当前页所有行
+      else {
+        let list = config.rows;
+        if (selected) {
+          list.map(l => this.customerRoleSearch.selected.push(l));
+        } else {
+          list.map(l => {
+            for (let i = 0; i < this.customerRoleSearch.selected.length; i++) {
+              if (l.id == this.customerRoleSearch.selected[i].id) {
+                this.customerRoleSearch.selected.splice(i, 1);
+              }
+            }
+          })
+        }
+      }
+    },
+    formatTime(timeStr) {
+      if (!timeStr && timeStr !== 0) return '';
+      // 处理数字和字符串输入
+      const str = String(timeStr).padStart(4, '0');
+      // 提取有效部分
+      const hours = str.substring(0, 2);
+      const minutes = str.substring(2, 4);
+      // 简单验证
+      if (hours > 23 || minutes > 59) return '无效时间';
+      return `${hours}:${minutes}`;
+    },
+    /**
+     * 查询任务看板列表
+     */
+    async getList() {
+      this.loading = true;
+      let rp = JSON.parse(JSON.stringify(this.queryParams));
+      rp['customerRoleIds'] = this.customerRoleSearch.selected.map(s => s.id);
+      let res = await listUrgentClassTask(rp);
+      // console.log(res.rows)
+      this.urgentClassTaskList = res.rows;
+      this.total = res.total;
+      this.loading = false;
+    },
+    formatDateToYMD(date) {
+      const year = date.getFullYear(); // 年(4位,如 2026)
+      const month = date.getMonth() + 1; // 月(+1 修正 0 开始的问题)
+      const day = date.getDate(); // 日
+      const formattedMonth = month.toString().padStart(2, '0');
+      const formattedDay = day.toString().padStart(2, '0');
+      return `${year}-${formattedMonth}-${formattedDay}`;
+    },
+    /**
+     * 切换选项卡
+     * @param tab
+     * @param event
+     */
+    toggleTab(tab, event) {
+      this.queryParams.status = tab.name;
+      this.handleQuery();
+    },
+    /**
+     * 搜索按钮操作
+     */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /**
+     * 重置按钮操作
+     */
+    resetQuery() {
+      this.customerRoleSearch.selected = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /**
+     * 切换状态
+     * @param row
+     */
+    toggleStatus(row, status) {
+      let ids = [row.id];
+      this.$confirm('是否确认切换状态?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(async () => {
+        let res = await setStatus({ids, status});
+        this.msgSuccess('切换状态成功!');
+        this.getList();
+      }).catch(() => { });
+    },
+    /**
+     * 快速回复
+     */
+    async quickReply(row) {
+      this.quickReplyDialog.visible = true;
+      this.quickReplyDialog.loading = true;
+      this.quickReplyDialog.rowInfo = row;
+      if (!(row.customerRoleId && row.fsUserId)) {
+        this.quickReplyDialog.loading = false;
+        return this.$message.error('当前记录不满足快速回复条件,用户或客服为空!');
+      }
+      this.quickReplyDialog.title = `对话详情(${row.fsUserId}-${row.customerRoleId})`;
+      let info = await findChatLogsByUserIdAndCustomerId({
+        userId: row.fsUserId,
+        customerId: row.customerRoleId,
+      });
+      if (!this.isEmpty(info.data)) {
+        this.quickReplyDialog.chatInfo = info.data;
+        let data = info.data;
+        let chatLogs = [];
+        chatLogs.push({
+          userType: 0,
+          sendTime: data.userAskTime,
+          sendContent: !this.isEmpty(data.userAskFullContent) ? data.userAskFullContent.replace(/\n+/g, '\n').replace(/\n/g, '<br>') : '--',
+        });
+        chatLogs.push({
+          userType: 1,
+          sendTime: data.customerReplyTime,
+          sendContent: !this.isEmpty(data.customerReplyFullContent) ? data.customerReplyFullContent.replace(/\n+/g, '\n').replace(/\n/g, '<br>') : '--',
+        });
+        if (!this.isEmpty(data.extend)) {
+          let extend = JSON.parse(data.extend);
+          if (!this.isEmpty(extend.chats)) {
+            chatLogs.push(...extend.chats);
+          }
+        }
+        this.quickReplyDialog.chatLogs = chatLogs;
+      }
+      this.quickReplyDialog.loading = false;
+    },
+    isEmpty(value) {
+      return value === null || value === undefined || value.length === 0;
+    },
+    /**
+     * 关闭快速回复对话框
+     */
+    closeQuickReplyDialog() {
+      this.quickReplyDialog.visible = false;
+      this.quickReplyDialog.rowInfo = null;
+      this.quickReplyDialog.loading = false;
+      this.quickReplyDialog.chatInfo = false;
+      this.quickReplyDialog.title = '';
+      this.quickReplyDialog.chatLogs = [];
+      this.quickReplyDialog.replyContent = '';
+    },
+    handledetails(row) {
+      this.userDetails.visible = true;
+      this.userDetails.rowInfo = row;
+      this.userDetails.count++;
+    },
+
+    /**
+     * 外呼电话
+     */
+    handleCallPhone(row) {
+      if (!row.fsUserId) {
+        this.$message.warning('该记录无fsUserId');
+        return;
+      }
+      getFsUserPhone(row.fsUserId).then(response => {
+        if(response.code === 200){
+          // 设置当前拨打的电话号码
+          this.currentCallPhone = response.data;
+          // 默认使用明文模式plaintext,如果需要密文模式可以改为 'encrypted'
+          this.currentDialMode = 'encrypted';
+          // 保存当前外呼的记录ID
+          this.currentRecordId = row.id;
+
+          // 显示外呼对话框,会自动拨号
+          this.callDialogVisible = true;
+        }else{
+          this.$message.warning(response.message);
+        }
+      });
+    },
+    /**
+     * 外呼对话框关闭处理
+     */
+    handleCallDialogClose() {
+      console.log('[外呼] 对话框已关闭');
+      this.callDialogVisible = false;
+      this.currentCallPhone = '';
+      this.currentRecordId = '';
+    },
+    /**
+     * 外呼结束处理
+     */
+    handleCallEnded(recordId) {
+
+      if (!recordId) {
+        console.warn('[外呼] 未收到recordId,无法更新状态');
+        return;
+      }
+
+      // 查找当前列表中对应的记录
+      const row = this.urgentClassTaskList.find(item => item.id === recordId);
+
+      if (!row) {
+        console.warn('[外呼] 未找到对应的记录,recordId:', recordId);
+        return;
+      }
+
+      // 只有当status为0时才更新为1
+      if (row.status === 0) {
+
+        let ids = [row.id];
+        setStatus({ids, status: 1}).then(res => {
+          this.msgSuccess('外呼结束,状态已更新为已处理!');
+          this.getList(); // 刷新列表
+        }).catch(err => {
+          this.$message.error('状态更新失败:' + (err.message || '未知错误'));
+        });
+      } else {
+        this.msgSuccess('外呼结束,当前状态无需更新!');
+      }
+    },
+    handleKeyDown(e) {
+       // 只处理回车键
+      if (e.keyCode !== 13) {
+        return;
+      }
+      if (e.ctrlKey || e.metaKey) {
+        e.preventDefault();
+        // 手动在光标位置插入换行符
+        let textarea = e.target;
+        let start = textarea.selectionStart;
+        let end = textarea.selectionEnd;
+        // 插入换行
+        this.quickReplyDialog.replyContent =
+          this.quickReplyDialog.replyContent.substring(0, start) +
+          "\n" +
+          this.quickReplyDialog.replyContent.substring(end);
+
+        // 光标定位到换行后
+        this.$nextTick(() => {
+          textarea.selectionStart = textarea.selectionEnd = start + 1;
+        });
+        return;
+      }
+      e.preventDefault();
+      this.sendReplyContent();
+    },
+    /**
+     * 发送回复内容
+     */
+    async sendReplyContent() {
+      this.quickReplyDialog.loading = true;
+      if (this.isEmpty(this.quickReplyDialog.replyContent.trim())) {
+        this.quickReplyDialog.loading = false;
+        return this.$message.error('请输入回复内容!');
+      }
+      let res = await editUserChatLogs({
+        id: this.quickReplyDialog.chatInfo.id,
+        replyContent: this.quickReplyDialog.replyContent,
+      });
+      if (res.code == 200) {
+        this.quickReplyDialog.chatLogs.push({
+          userType: 1,
+          sendTime: res.data,
+          sendContent: this.quickReplyDialog.replyContent,
+        });
+        this.quickReplyDialog.replyContent = '';
+        const el = this.$refs.scrollWrap;
+        if (el) {
+          el.scrollTo({
+            top: el.scrollHeight + 50,
+            behavior: 'auto'
+          });
+        }
+        this.$message.success('发送成功!');
+      } else {
+        this.$message.error('发送失败:' + res.msg);
+      }
+      this.quickReplyDialog.loading = false;
+    },
+    closeQuickReply() {
+      this.quickReplyDialog.visible = false;
+      this.quickReplyDialog.rowInfo = null;
+      this.quickReplyDialog.loading = false;
+      this.quickReplyDialog.chatInfo = false;
+      this.quickReplyDialog.chatLogs = [];
+      this.quickReplyDialog.title = '';
+      this.quickReplyDialog.replyContent = '';
+    },
+  }
+};
+</script>
+<style scoped lang="scss">
+::v-deep .el-card .is-always-shadow {
+    padding: 8px;
+}
+
+.no-copy {
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+
+::v-deep  .quick-reply {
+  height: 115px;
+  border: 1px solid #DCDFE6;
+  padding: 5px;
+  border-radius: 10px;
+
+   .el-textarea__inner {
+    border: none !important;
+  }
+}
+</style>

+ 274 - 0
src/views/app/user/CusRoleList.vue

@@ -0,0 +1,274 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px"
+      @submit.prevent="handleQuery">
+
+      <el-form-item label="销售名称" prop="memberName">
+        <el-input v-model="queryParams.memberName" placeholder="请输入销售名称" clearable size="small"
+          @keydown.enter.native="handleQueryEnter" />
+      </el-form-item>
+      <el-form-item label="所属公司" prop="roleName">
+        <el-input v-model="queryParams.roleName" placeholder="请输入所属公司" clearable size="small"
+          @keydown.enter.native="handleQueryEnter" />
+      </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="roleList"
+      ref="roleList"
+      :row-key="row => String(row.id)"
+      :class="{ 'cus-role-single': !multiple }"
+      @select="singleSelectHandler"
+      @select-all="allSelectHandler"
+     >
+      <el-table-column type="selection" width="55" align="center"/>
+      <el-table-column label="销售id" align="center" prop="id" min-width="100"/>
+      <el-table-column label="销售名称" align="center" prop="memberName" min-width="140" show-overflow-tooltip/>
+      <el-table-column label="所属公司" align="center" prop="roleName" min-width="140" show-overflow-tooltip/>
+      <el-table-column label="备注" align="center" prop="remark" min-width="160" show-overflow-tooltip/>
+    </el-table>
+
+    <div style="margin-top: 30px;display: flex;justify-content: center">
+      <el-button type="warning" icon="el-icon-search" @click="confirmSelect">确定选择</el-button>
+    </div>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import { listRole } from "@/api/app/user/userList";
+export default {
+  name: "SelectedCustomer",
+  props: {
+    defaultPageNum: {
+      type: Number,
+      default: 1
+    },
+    defaultPageSize: {
+      type: Number,
+      default: 10
+    },
+    selected: {
+      type: Array,
+      default: () => [],
+    },
+    // 是否允许多选,false 时只能勾选一个
+    multiple: {
+      type: Boolean,
+      default: true,
+    },
+    // 排除的销售id
+    ignoreIds: {
+      type: Array,
+      default: () => [],
+    },
+    // 仅查询指定销售id
+    ids: {
+      type: Array,
+      default: () => [],
+    },
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 总条数
+      total: 0,
+      // 客服成员表格数据
+      roleList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        memberName: null,
+        roleName: null
+      },
+    };
+  },
+  created() {
+    this.queryParams.pageNum = this.defaultPageNum;
+    this.queryParams.pageSize = this.defaultPageSize;
+    this.getList();
+  },
+  watch: {
+    selected: {
+      immediate: true,
+      handler(nv) {
+        this.setSelectionStatus();
+      }
+    }
+  },
+  methods: {
+    /**
+     * 查询客服成员列表
+     */
+    getList() {
+      this.loading = true;
+      let qp = this.queryParams;
+      if (this.ignoreIds && this.ignoreIds.length) {
+        qp['ignoreIds'] = this.ignoreIds;
+      } else {
+        delete qp.ignoreIds;
+      }
+      if (this.ids && this.ids.length) {
+        qp['ids'] = this.ids;
+      } else {
+        delete qp.ids;
+      }
+      listRole(qp).then(response => {
+        this.roleList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+        this.setSelectionStatus();
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.reset();
+      try {
+        this.$emit("cancel", this.selected);
+      } catch (e) {
+        console.warn('暂未指定确认选择回调');
+      }
+    },
+    setSelectionStatus() {
+      if (this.roleList.length == 0) {
+        return;
+      }
+      this.$nextTick(() => {
+        this.roleList.forEach(row => {
+          // 判断当前行ID是否在全局选中集合中
+          const isSelected = this.selected.some(si => row.id == si.id);
+          this.$refs.roleList.toggleRowSelection(row, isSelected);
+        });
+      });
+    },
+    /**
+     * 表单重置
+     */
+    reset() {
+      this.queryParams = {
+        pageNum: this.defaultPageNum,
+        pageSize: this.defaultPageSize,
+        memberName: null,
+        roleName: null,
+      }
+      this.resetForm("form");
+    },
+    /**
+     * 确定选择
+     */
+    confirmSelect() {
+      try {
+        this.$emit("confirm", this.selected);
+      } catch (e) {
+        console.warn('暂未指定确认选择回调');
+      }
+    },
+    /**
+     * 搜索按钮操作
+     */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    handleQueryEnter(event) {
+      // 确保事件对象存在
+      if (event && event.preventDefault) {
+        event.preventDefault(); // 阻止默认提交行为
+      }
+      this.handleQuery();
+    },
+    /**
+     * 重置按钮操作
+     */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /**
+     * 单选
+     * @param selection
+     * @param row
+     */
+    singleSelectHandler(selection, row) {
+      // 单选模式:只保留当前行
+      if (!this.multiple) {
+        const isSelected = selection.some(l => l.id == row.id)
+        this.$refs.roleList.clearSelection()
+        if (isSelected) {
+          this.$nextTick(() => {
+            this.$refs.roleList.toggleRowSelection(row, true)
+          })
+        }
+        try {
+          this.$emit('selectionChange', {
+            mode: 0,
+            row,
+            selected: isSelected
+          })
+        } catch (e) {
+          console.warn('当前组件暂未指定selection回调')
+        }
+        return
+      }
+      let pm = {
+        mode: 0,//0-单行选,1-全选
+        row,//操作的行
+      }
+      //列表中找不到当前行数据,则说明是取消选中
+      if (!selection.some(l => l.id == row.id)) {
+        pm['selected'] = false;//设置取消选中
+      } else {
+        pm['selected'] = true;//设置选中
+      }
+      try {
+        this.$emit("selectionChange", pm);
+      } catch (e) {
+        console.warn('当前组件暂未指定selection回调');
+      }
+    },
+    /**
+     * 全选
+     * @param selection
+     */
+    allSelectHandler(selection) {
+      // 单选模式不允许全选
+      if (!this.multiple) {
+        this.$refs.roleList.clearSelection()
+        return
+      }
+      let pm = {
+        mode: 1,//0-单行选,1-全选
+        rows: this.roleList,//操作的所有行
+        selected: selection.length > 0 ? true : false,
+      }
+      try {
+        this.$emit("selectionChange", pm);
+      } catch (e) {
+        console.warn('当前组件暂未指定selection回调');
+      }
+    },
+  }
+};
+</script>
+
+<style scoped>
+/* 单选模式隐藏表头全选勾选框 */
+.cus-role-single ::v-deep .el-table__header-wrapper .el-checkbox {
+  display: none;
+}
+</style>

+ 150 - 0
src/views/app/user/UserList.vue

@@ -0,0 +1,150 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px"
+      @submit.prevent="handleQuery">
+
+      <el-form-item label="用户昵称" prop="nickName">
+        <el-input v-model="queryParams.nickName" placeholder="请输入用户昵称" clearable size="small"
+          @keydown.enter.native="handleQueryEnter" />
+      </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="userList" ref="userList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center"/>
+      <el-table-column label="用户id" align="center" prop="userId"/>
+      <!-- <el-table-column label="用户账户" align="center" prop="username"/> -->
+      <el-table-column label="用户昵称" align="center" prop="nickName"/>
+
+    </el-table>
+    <div style="margin-top: 30px;display: flex;justify-content: center">
+      <el-button type="warning" icon="el-icon-search" @click="confirmSelect">确定选择</el-button>
+    </div>
+
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.startIndex" :limit.sync="queryParams.pageLimit"
+      @pagination="handlePaginationChange" />
+  </div>
+</template>
+
+<script>
+import { listUser } from "@/api/app/user/userList";
+export default {
+  name: "userList",
+  data() {
+    return {
+      type: null,
+      sendType: null,
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      selectUsers: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 企微用户表格数据
+      userList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        startIndex: 1,
+        pageLimit: 5,
+        nickName: null
+      },
+
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询用户列表 */
+    getList() {
+      this.loading = true;
+      listUser(this.queryParams).then(response => {
+        // 如果 companyUserId 为 null,移除列
+        this.userList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    handlePaginationChange(row) {
+      this.queryParams.startIndex = row.page;
+      this.queryParams.pageLimit = row.limit;
+      this.getList();
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.queryParams = {
+        startIndex: 1,
+        pageLimit: 5,
+        nickName: null,
+      }
+      this.resetForm("form");
+    },
+    //确定选择
+    confirmSelect() {
+      this.$emit("selectUserList", this.selectUsers);
+      this.resetSelect();
+    },
+    //重置选择
+    resetSelect() {
+      this.$refs.userList.clearSelection();
+      this.selectUsers = [];
+      this.reset();
+      this.getList();
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.startIndex = 1;
+      this.getList();
+    },
+    handleQueryEnter(event) {
+      // 确保事件对象存在
+      if (event && event.preventDefault) {
+        event.preventDefault(); // 阻止默认提交行为
+      }
+      this.handleQuery();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      // 保存当前页的选中项
+      const currentPageSelections = selection.map(item => item.userId);
+      // 合并选中项
+      this.selectUsers = this.selectUsers.filter(item =>
+        this.userList.some(tag => tag.userId === item.id) ? currentPageSelections.includes(item.userId) : true
+      ).concat(selection.filter(item => !this.selectUsers.some(selected => selected.userId === item.userId)));
+      // 更新 single 和 multiple
+      this.single = this.selectUsers.length !== 1;
+      this.multiple = !this.selectUsers.length;
+    },
+  }
+};
+</script>

+ 1362 - 0
src/views/app/user/index.vue

@@ -0,0 +1,1362 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="会员ID" prop="userId">
+        <el-input
+          v-model="queryParams.userId"
+          placeholder="请输入会员ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="用户昵称" prop="nickName">
+        <el-input
+          v-model="queryParams.nickName"
+          placeholder="请输入用户昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="加密号码" prop="phone">
+        <el-input
+          v-model="queryParams.phone"
+          placeholder="请输入手机号码"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="邀请码" prop="invitationCode">
+        <el-input
+          v-model="queryParams.invitationCode"
+          placeholder="请输入邀请码"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="所属部门" prop="deptId">
+        <treeselect
+          v-model="queryParams.deptId"
+          :options="deptOptions"
+          :normalizer="normalizer"
+          placeholder="选择部门"
+          style="width: 250px"
+        />
+      </el-form-item>
+
+      <el-form-item label="邀请日期" prop="inviteDate">
+        <el-date-picker clearable size="small" v-model="queryParams.inviteDate" type="daterange" value-format="yyyy-MM-dd"
+          placeholder="选择邀请日期">
+        </el-date-picker>
+      </el-form-item>
+
+      <el-form-item label="标签" prop="tagIds">
+        <div @click="openTagSearchPage"
+             style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
+          <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+            <el-tag type="success"
+                closable
+                :disable-transitions="false"
+                v-for="list in this.tagSearchConfig.selected"
+                :key="list.tagId"
+                @close="handleCloseTags(list)"
+                style="margin: 3px;"
+            >{{ list.tagName }}
+            </el-tag>
+          </div>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="客服" prop="customerRoles">
+        <div @click="openCusSearchPage"
+             style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
+          <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+            <el-tag type="success"
+                closable
+                :disable-transitions="false"
+                v-for="list in this.bindAISearch.selected"
+                :key="list.id"
+                @close="handleCloseCus(list)"
+                style="margin: 3px;"
+            >{{ list.roleName }}
+            </el-tag>
+          </div>
+        </div>
+      </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="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          v-hasPermi="['app:user:export']"
+          @click="handleExport"
+        >导出</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:bindTag']"
+          @click="openBindTagPage(false, 0)"
+        > 批量添加标签
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:unbindTag']"
+          @click="openUnbindTagPage(false, 1)"
+        >批量移除标签
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:bindCustomer']"
+          @click="openbindAIPage(false, 0)"
+        >批量换绑销售
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:batchBindTag']"
+          @click="openBindTagPage(true, 0)"
+        >批量添加标签(筛选条件)
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:batchUnbindTag']"
+          @click="openUnbindTagPage(true, 1)"
+        >批量移除标签(筛选条件)
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:batchBindCustomer']"
+          @click="openbindAIPage(true, 0)"
+        >批量换绑销售(筛选条件)
+        </el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table height="660" v-loading="loading" border :data="userList" @selection-change="handleSelectionChange" >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="用户ID" align="center" prop="userId" width="150px"/>
+      <el-table-column label="用户昵称" align="center" prop="nickName" width="150px"/>
+      <el-table-column label="用户头像" align="center" prop="avatar" >
+         <template slot-scope="scope">
+            <el-image v-if="scope.row.avatar!=null"
+                style="width: 50px;"
+                :src="scope.row.avatar"
+              >
+            </el-image>
+         </template>
+      </el-table-column>
+      <el-table-column label="手机号码" align="center" prop="phone" min-width="150px"/>
+      <el-table-column label="部门 / 公司" align="center" prop="deptName" min-width="150px"/>
+      <el-table-column label="用户状态" align="center" prop="status">
+         <template slot-scope="scope">
+              <dict-tag :options="userOptions" :value="scope.row.status"/>
+              <p v-if="scope.row.status == 0 && scope.row.remark">{{scope.row.remark}}</p>
+         </template>
+      </el-table-column>
+      <el-table-column label="注册时间" align="center" prop="createTime" min-width="150px"/>
+       <el-table-column label="邀请码" align="center" prop="invitationCode" min-width="150px"/>
+       <el-table-column label="邀请时间" align="center" prop="inviteTime" min-width="150px"/>
+       <el-table-column label="客服" align="center" min-width="200px" prop="customerRoles">
+        <template slot-scope="scope">
+          <div v-for="(item, idx) in (scope.row.customerRoles ? scope.row.customerRoles.split(',') : [])" :key="idx" style="display: inline;">
+            <el-tag type="primary" style="margin: 3px;">{{item}}</el-tag>
+          </div>
+        </template>
+       </el-table-column>
+       <el-table-column label="标签" align="center" min-width="200px" prop="tagsName">
+        <template slot-scope="scope">
+          <div v-for="(item, idx) in (scope.row.tagsName ? scope.row.tagsName.split(',') : [])" :key="idx" style="display: inline;">
+            <el-tag type="success" style="margin: 3px;">{{item}}</el-tag>
+          </div>
+        </template>
+       </el-table-column>
+
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="190px">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleAIUserInfo(scope.row)"
+          >AI获取用户信息</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            @click="handledetails(scope.row)"
+          >详情</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            @click="handleDelete(scope.row)"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.startIndex"
+      :limit.sync="queryParams.pageLimit"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改用户对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="用户昵称" prop="nickName">
+          <el-input v-model="form.nickName" placeholder="请输入用户昵称" />
+        </el-form-item>
+        <el-form-item label="用户状态" prop="status">
+            <el-select v-model="form.status" placeholder="请选择状态" clearable size="small">
+                  <el-option
+                    v-for="dict in userOptions"
+                    :key="dict.dictValue"
+                    :label="dict.dictLabel"
+                    :value="dict.dictValue"
+                  />
+                </el-select>
+        </el-form-item>
+        <el-form-item label="用户备注" prop="remark" >
+          <el-input v-model="form.remark" placeholder="请输入用户备注" type="textarea"/>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog :title="tagConfig.title" :visible.sync="tagConfig.visible" width="800px" append-to-body>
+      <div>搜索标签:
+        <el-input v-model="tagConfig.queryParams.groupName" placeholder="请输入标签名称" clearable size="small"
+          style="width: 200px;margin-right: 10px"/>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleSearchTags(false)">搜索</el-button>
+        <el-button type="primary" icon="el-icon-refresh" size="mini" @click="cancelSearchTags(false)">重置</el-button>
+      </div>
+      <div class="dialog-body">
+        <div v-for="groupItem in tagConfig.tagGroupList" :key="groupItem.id">
+          <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+            <span class="name-background">{{ groupItem.name }}</span>
+          </div>
+          <div class="tag-container">
+            <a
+              v-for="tagItem in groupItem.tags"
+              class="tag-box"
+              @click="tagSelection(tagItem)"
+              :class="{ 'tag-selected': tagItem.isSelected }"
+            >
+              {{ tagItem.name }}
+            </a>
+          </div>
+        </div>
+      </div>
+      <pagination
+        v-show="tagConfig.total > 0"
+        :total="tagConfig.total"
+        :page.sync="tagConfig.queryParams.pageNum"
+        :limit.sync="tagConfig.queryParams.pageSize"
+        @pagination="getTagGroupList"
+      />
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="bindOrUnbindTagSubmit">确 定</el-button>
+        <el-button @click="bindTagCancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="搜索标签" :visible.sync="tagSearchConfig.visible" width="800px" append-to-body>
+      <div>搜索标签:
+        <el-input v-model="tagSearchConfig.queryParams.groupName" placeholder="请输入标签名称" clearable size="small"
+          style="width: 200px;margin-right: 10px"/>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleSearchTags(true)">搜索</el-button>
+        <el-button type="primary" icon="el-icon-refresh" size="mini" @click="cancelSearchTags(true)">重置</el-button>
+      </div>
+      <div class="dialog-body">
+        <div v-for="groupItem in tagSearchConfig.tagGroupList" :key="groupItem.id">
+          <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+            <span class="name-background">{{ groupItem.name }}</span>
+          </div>
+          <div class="tag-container">
+            <a
+              v-for="tagItem in groupItem.tags"
+              class="tag-box"
+              @click="searchTagSelection(tagItem)"
+              :class="{ 'tag-selected': tagItem.isSelected }"
+            >
+              {{ tagItem.name }}
+            </a>
+          </div>
+        </div>
+      </div>
+      <pagination
+        v-show="tagSearchConfig.total > 0"
+        :total="tagSearchConfig.total"
+        :page.sync="tagSearchConfig.queryParams.pageNum"
+        :limit.sync="tagSearchConfig.queryParams.pageSize"
+        @pagination="getSearchTagGroupList"
+      />
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="searchTagSubmit">确 定</el-button>
+        <el-button @click="searchTagCancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-drawer
+      :with-header="false"
+      size="75%"
+      :title="show.title"
+      :visible.sync="show.open"
+    >
+      <userDetails  ref="userDetails" />
+    </el-drawer>
+
+      <!-- 选择app客服角色 -->
+    <el-dialog :title="bindAIConfig.title" :visible.sync="bindAIConfig.visible" width="1000px" append-to-body :close-on-click-modal="false">
+      <CusRoleList
+        ref="customerBindRef"
+        :multiple="false"
+        @confirm="customerSelectedConfirmCallForBind"
+        @selectionChange="customerSelectionChangeCallForBind"
+        :selected="bindAIConfig.selected"
+      />
+    </el-dialog>
+
+    <!-- app客服搜索 -->
+    <el-dialog title="客服搜索" :visible.sync="bindAISearch.visible" width="1000px" append-to-body>
+      <CusRoleList ref="customerSearchRef"
+        @confirm="customerSelectedConfirmCall"
+        @selectionChange="customerSelectionChangeCall"
+        :selected="bindAISearch.selected"
+      />
+    </el-dialog>
+
+    <!-- ai获取用户信息 -->
+    <el-dialog title="AI获取用户信息" :visible.sync="aiUserInfoDialog.visible" width="700px" append-to-body :close-on-click-modal="false">
+      <el-form ref="form" :model="aiUserInfoDialog.userInfo" :rules="aiUserInfoDialog.rules" label-width="140px" v-loading="aiUserInfoDialog.loading">
+        <div style="height: 500px; overflow: auto;" class="ai-user-info">
+          <el-form-item label="姓名" prop="name">
+            <el-input v-model="aiUserInfoDialog.userInfo.name" placeholder="请输入姓名" />
+          </el-form-item>
+          <el-form-item label="性别" prop="sex">
+            <el-input v-model="aiUserInfoDialog.userInfo.sex" placeholder="请输入性别" />
+          </el-form-item>
+          <el-form-item label="年龄" prop="age">
+            <el-input v-model="aiUserInfoDialog.userInfo.age" placeholder="请输入年龄" />
+          </el-form-item>
+          <el-form-item label="地区" prop="address">
+            <el-input v-model="aiUserInfoDialog.userInfo.address" placeholder="请输入地区" />
+          </el-form-item>
+          <el-form-item label="行为习惯" prop="habits">
+            <el-input v-model="aiUserInfoDialog.userInfo.habits" placeholder="请输入行为习惯" />
+          </el-form-item>
+          <el-form-item label="患病时间" prop="illnessTime">
+            <el-input v-model="aiUserInfoDialog.userInfo.illnessTime" placeholder="请输入患病时间" />
+          </el-form-item>
+          <el-form-item label="身体状态" prop="body">
+            <el-input v-model="aiUserInfoDialog.userInfo.body" placeholder="请输入身体状态" />
+          </el-form-item>
+          <el-form-item label="疾病" prop="disease">
+            <el-input v-model="aiUserInfoDialog.userInfo.disease" placeholder="请输入疾病" />
+          </el-form-item>
+          <el-form-item label="提及的家人" prop="family">
+            <el-input v-model="aiUserInfoDialog.userInfo.family" placeholder="请输入提及的家人" />
+          </el-form-item>
+          <el-form-item label="是否线下就诊" prop="isLine">
+            <el-input v-model="aiUserInfoDialog.userInfo.isLine" placeholder="是否线下就诊" />
+          </el-form-item>
+          <el-form-item label="家人的疾病" prop="familyDisease">
+            <el-input v-model="aiUserInfoDialog.userInfo.familyDisease" placeholder="请输入家人的疾病" />
+          </el-form-item>
+          <el-form-item label="用户分类" prop="userType">
+          <el-input v-model="aiUserInfoDialog.userInfo.userType" placeholder="用户分类" />
+          </el-form-item>
+          <el-form-item label="是否本人会诊" prop="isSelf">
+            <el-input v-model="aiUserInfoDialog.userInfo.isSelf" placeholder="请输入是否本人会诊" />
+          </el-form-item>
+          <el-form-item label="什么情况加重或缓解" prop="intensify">
+            <el-input v-model="aiUserInfoDialog.userInfo.intensify" placeholder="请输入什么情况加重或缓解" />
+          </el-form-item>
+          <el-form-item label="是否怕热或者怕冷" prop="isCold">
+            <el-input v-model="aiUserInfoDialog.userInfo.isCold" placeholder="请输入是否怕热或者怕冷" />
+          </el-form-item>
+          <el-form-item label="怕冷或怕热的部位" prop="coldBody">
+            <el-input v-model="aiUserInfoDialog.userInfo.coldBody" placeholder="请输入怕冷或怕热的部位" />
+          </el-form-item>
+          <el-form-item label="出汗情况" prop="sweat">
+            <el-input v-model="aiUserInfoDialog.userInfo.sweat" placeholder="请输入出汗情况" />
+          </el-form-item>
+          <el-form-item label="其他情况" prop="other">
+            <el-input v-model="aiUserInfoDialog.userInfo.other" placeholder="请输入其他情况" />
+          </el-form-item>
+          <el-form-item label="大小便情况" prop="toilet">
+            <el-input v-model="aiUserInfoDialog.userInfo.toilet" placeholder="请输入大小便情况" />
+          </el-form-item>
+          <el-form-item label="饮食情况" prop="eat">
+            <el-input v-model="aiUserInfoDialog.userInfo.eat" placeholder="请输入饮食情况" />
+          </el-form-item>
+          <el-form-item label="经期如何" prop="menses">
+            <el-input v-model="aiUserInfoDialog.userInfo.menses" placeholder="请输入经期如何 女 55岁以下" />
+          </el-form-item>
+          <el-form-item label="现在使用的药品" prop="medicine">
+            <el-input v-model="aiUserInfoDialog.userInfo.medicine" placeholder="请输入现在使用的药品" />
+          </el-form-item>
+          <el-form-item label="体质" prop="constitution">
+            <el-input v-model="aiUserInfoDialog.userInfo.constitution" placeholder="请输入体质" />
+          </el-form-item>
+          <el-form-item label="推荐用药" prop="recommendMedicine">
+            <el-input v-model="aiUserInfoDialog.userInfo.recommendMedicine" placeholder="请输入推荐用药" />
+          </el-form-item>
+          <el-form-item label="咨询产品" prop="consultProduct">
+            <el-input v-model="aiUserInfoDialog.userInfo.consultProduct" placeholder="请输入咨询产品" />
+          </el-form-item>
+          <el-form-item label="是否已经购买产品" prop="isBuy">
+            <el-input v-model="aiUserInfoDialog.userInfo.isBuy" placeholder="请输入是否已经购买产品" />
+          </el-form-item>
+          <el-form-item label="已经购买的产品" prop="buyProduct">
+            <el-input v-model="aiUserInfoDialog.userInfo.buyProduct" placeholder="请输入已经购买的产品" />
+          </el-form-item>
+          <el-form-item label="产品交流" prop="productTalk">
+            <el-input v-model="aiUserInfoDialog.userInfo.productTalk" placeholder="请输入产品交流信息" />
+          </el-form-item>
+          <el-form-item label="疾病交流" prop="diseaseTalk">
+            <el-input v-model="aiUserInfoDialog.userInfo.diseaseTalk" placeholder="请输入疾病交流信息" />
+          </el-form-item>
+          <el-form-item label="渠道类型" prop="channelType">
+            <el-input v-model="aiUserInfoDialog.userInfo.channelType" placeholder="请输入渠道类型信息" />
+          </el-form-item>
+        </div>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitAiUserInfo" :disabled="aiUserInfoDialog.loading">确 定</el-button>
+        <el-button @click="closeAIUserInfoDialog">取 消</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { getUser, delUser, addUser, updateUser } from "@/api/store/user";
+import {
+  bindUserCusByIdsV2,
+  unbindUserCusByIdsV2,
+  listAppUser,
+  exportAppUser
+} from '@/api/app/user';
+import { listTagGroupForUserBindTag } from "@/api/app/tag/tagGroup";
+import {
+  bindTagV2,
+  unbindTagV2,
+} from "@/api/app/tag/bindTag";
+import { getAIUserInfoByUserId, saveAiUserInfo, } from '@/api/app/user/userInfo';
+import { listDept, } from '@/api/system/appDept';
+
+import CusRoleList from '@/views/app/user/CusRoleList.vue';
+import userDetails from '@/views/store/components/userDetails.vue';
+import Treeselect from '@riophae/vue-treeselect'
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+export default {
+  name: "UserTag",
+  components: { Treeselect, userDetails, CusRoleList},
+  data() {
+    return {
+      show:{
+        title:"用户详情",
+        open:false,
+      },
+      userOptions: [],
+      // 遮罩层
+      loading: false,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      orOptions:[],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 用户表格数据
+      userList: [],
+      // app部门下拉选项
+      deptOptions: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      createTime:null,
+      firstLoginTime:null,
+      // 查询参数
+      queryParams: {
+        startIndex: 1,
+        pageLimit: 10,
+        nickName: null,
+        phone: null,
+        phoneMk: null,
+        status: null,
+        invitationCode: null,
+        deptId: null,
+        inviteDate: null,
+        customerRoles: [],
+        tagIds: [],
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        nickName: [
+          { required: true, message: "昵称不能为空", trigger: "blur" }
+        ],
+        status: [
+          { required: true, message: "用户状态不能为空", trigger: "blur" }
+        ],
+        integral: [
+          { required: true, message: "用户积分不能为空", trigger: "blur" }
+        ],
+      },
+      tagConfig: {
+        visible: false,//对话框是否展示
+        enableFilter: false,//是否开启过滤模式,开启则是引用外部条件,针对外部条件过滤的所有数据做 绑定/解绑 标签,否则根据针对外部勾选的用户做 绑定/解绑 标签操作
+        title: null,
+        opType: 0,//0-添加,1-移除,2-搜索
+        queryParams: {
+          groupName: null,
+          pageNum: 1,
+          pageSize: 10,
+        },
+        total: 0,
+        tagGroupList: [],
+        selected: [],
+      },
+      tagSearchConfig: {
+        visible: false,//对话框是否展示
+        queryParams: {
+          groupName: null,
+          pageNum: 1,
+          pageSize: 10,
+        },
+        total: 0,
+        tagGroupList: [],
+        selected: [],
+      },
+      bindAIConfig: {
+        title: null,
+        type: null,
+        visible: false,
+        enableFilter: false,
+        selected: [],
+      },
+      bindAISearch: {
+        visible: false,
+        selected: [],
+        // 缓存子组件实例
+        cusRoleListRef: null,
+      },
+      aiUserInfoDialog: {
+        visible: false,
+        loading: false,
+        rules: {},
+        userId: null,
+        userInfo: {
+          id: null,
+          name: null,
+          sex: null,
+          age: null,
+          address: null,
+          habits: null,
+          illnessTime: null,
+          body: null,
+          family: null, 
+          familyDisease: null,
+          disease: null,
+          isLine: null,
+          userType: null,
+          isSelf: null,
+          intensify: null,
+          isCold: null,
+          coldBody: null,
+          sweat: null,
+          other: null,
+          toilet: null,
+          eat: null,
+          menses: null,
+          medicine: null,
+          constitution: null,
+          recommendMedicine: null,
+          consultProduct: null,
+          isBuy: null,
+          buyProduct: null,
+          productTalk: null,
+          diseaseTalk: null,
+          channelType: null,
+        },
+      },
+      userDetails: {
+        visible: false,
+        rowInfo: null,
+        count: 0,
+      },
+    };
+  },
+  created() {
+    this.getDicts("sys_user_status").then(response => {
+      this.userOptions = response.data;
+    });
+    this.getDicts("sys_company_or").then(response => {
+      this.orOptions = response.data;
+    });
+    this.getDeptList();
+  },
+  methods: {
+    getDeptList() {
+      //app个人部门
+      listDept().then(response => {
+        if (response.code === 200) {
+          this.deptOptions = this.handleTree(response.data, "deptId");
+        } else {
+          this.deptOptions = [];
+        }
+      }).catch(() => {
+        this.deptOptions = [];
+      });
+    },
+    normalizer(node) {
+      if (node.children && !node.children.length) {
+        delete node.children;
+      }
+      return {
+        id: node.deptId,
+        label: node.deptName,
+        children: node.children
+      };
+    },
+    handleTree(data, id, parentId, children) {
+      id = id || 'id';
+      parentId = parentId || 'parentId';
+      children = children || 'children';
+      var tree = [];
+      var map = {};
+      data.forEach(function(item) {
+        map[item[id]] = item;
+      });
+      data.forEach(function(item) {
+        var parent = map[item[parentId]];
+        if (parent) {
+          (parent[children] || (parent[children] = [])).push(item);
+        } else {
+          tree.push(item);
+        }
+      });
+      return tree;
+    },
+    change() {
+      if (this.createTime != null) {
+        this.queryParams.sTime = this.createTime[0];
+        this.queryParams.eTime = this.createTime[1];
+      } else {
+        this.queryParams.sTime = null;
+        this.queryParams.eTime = null;
+      }
+    },
+    changeLoginTime() {
+      if (this.firstLoginTime != null) {
+        this.queryParams.startLoginTime = this.firstLoginTime[0];
+        this.queryParams.endLoginTime = this.firstLoginTime[1];
+      } else {
+        this.queryParams.startLoginTime = null;
+        this.queryParams.endLoginTime = null;
+      }
+    },
+    handledetails(row) {
+      this.show.open = true;
+      setTimeout(() => {
+        this.$refs.userDetails.getDetails(row.userId);
+      }, 1);
+    },
+    /** 查询用户列表 */
+    getList() {
+      this.loading = true;
+      let query = JSON.parse(JSON.stringify(this.queryParams));
+      query['tagIds'] = this.tagSearchConfig.selected.map(item => item.tagId);
+      query['customerRoles'] = this.bindAISearch.selected.map(item => item.id);
+      if (!this.isEmpty(query.inviteDate)) {
+        let dateRange = query.inviteDate;
+        query['inviteStartDate'] = dateRange[0] + ' 00:00:00';
+        query['inviteEndDate'] = dateRange[1] + ' 23:59:59';
+        delete query['inviteDate']
+      }
+      listAppUser(query).then(response => {
+        this.userList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    /**
+     * 确认选中回调
+     * @param selected
+     */
+    customerSelectedConfirmCall(selected) {
+      this.bindAISearch.visible = false;
+    },
+    /**
+     * 选中元素改变回调
+     * @param selected
+     */
+    customerSelectionChangeCall(config) {
+      let selected = config.selected;
+      //单行操作
+      if (config.mode === 0) {
+        let row = config.row;
+        if (selected) {
+          this.bindAISearch.selected.push(row)
+        } else {
+          this.bindAISearch.selected = this.bindAISearch.selected.filter(m => m.id != row.id);
+        }
+      }
+      //操作当前页所有行
+      else {
+        let list = config.rows;
+        if (selected) {
+          list.map(l => this.bindAISearch.selected.push(l));
+        } else {
+          list.map(l => {
+            for (let i = 0; i < this.bindAISearch.selected.length; i++) {
+              if (l.id == this.bindAISearch.selected[i].id) {
+                this.bindAISearch.selected.splice(i, 1);
+              }
+            }
+          })
+        }
+      }
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        userId: null,
+        nickName: null,
+        avatar: null,
+        phone: null,
+        integral: null,
+        status: null,
+        tuiUserId: null,
+        tuiTime: null,
+        tuiUserCount: null,
+        maOpenId: null,
+        mpOpenId: null,
+        unionId: null,
+        isDel: null,
+        userCode: null,
+        remark: null,
+        createTime: null,
+        updateTime: null,
+        lastIp: null,
+        balance: null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.startIndex = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    async resetQuery() {
+      this.resetForm("queryForm");
+      this.resetTagSearchConfig();
+      this.resetCusSearch();
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.userId)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加用户";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const userId = row.userId || this.ids
+      getUser(userId).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改用户";
+        this.form.status = String(this.form.status)
+      });
+    },
+    /**
+     * ai获取用户信息
+     */
+    handleAIUserInfo(row) {
+      this.aiUserInfoDialog.visible = true;
+      this.aiUserInfoDialog.userId = row.userId;
+      this.getAIUserInfo(row.userId);
+    },
+    closeAIUserInfoDialog() {
+      this.aiUserInfoDialog.visible = false;
+      this.resetAIUserInfo();
+    },
+    /**
+     * 重置AI用户信息
+     */
+    resetAIUserInfo() {
+      this.aiUserInfoDialog.userInfo = {
+          name: null,
+          sex: null,
+          age: null,
+          address: null,
+          habits: null,
+          illnessTime: null,
+          body: null,
+          familyDisease: null,
+          disease: null,
+          isLine: null,
+          userType: null,
+          isSelf: null,
+          intensify: null,
+          isCold: null,
+          coldBody: null,
+          sweat: null,
+          other: null,
+          toilet: null,
+          eat: null,
+          menses: null,
+          medicine: null,
+          constitution: null,
+          recommendMedicine: null,
+          consultProduct: null,
+          isBuy: null,
+          buyProduct: null,
+          productTalk: null,
+          diseaseTalk: null,
+          channelType: null,
+        }
+    },
+    /**
+     * 获取ai用户信息
+     */
+    async getAIUserInfo(userId) {
+      this.aiUserInfoDialog.loading = true;
+      let userInfo = await getAIUserInfoByUserId(userId);
+      let userInfoData = userInfo.data;
+      if (userInfoData) {
+        this.aiUserInfoDialog.userInfo = userInfoData;
+      }
+      this.aiUserInfoDialog.loading = false;
+    },
+    /**
+     * 提交ai用户信息
+     */
+    async submitAiUserInfo() {
+      this.aiUserInfoDialog.loading = true;
+      let realInfo = JSON.parse(JSON.stringify(this.aiUserInfoDialog.userInfo));
+      try {
+        for (let key in realInfo) {
+          let source = realInfo[key];
+          realInfo[key] = this.isEmpty(source) ? null : this.isString(source) ? source.trim() : source;
+        }
+        realInfo['fsUserId'] = this.aiUserInfoDialog.userId;
+        let save = await saveAiUserInfo(realInfo);
+        if (save.data) {
+          this.msgSuccess("修改成功");
+          this.aiUserInfoDialog.visible = false;
+          this.resetAIUserInfo();
+        } else {
+          this.msgError("修改失败");
+        }
+      } catch (e) {
+        console.warn(e);
+        this.msgError("修改失败");
+      } finally {
+        this.aiUserInfoDialog.loading = false;
+      }
+    },
+    isEmpty(value) {
+      return value === null || value === undefined || value.length === 0;
+    },
+    isString(val) {
+      return Object.prototype.toString.call(val) === '[object String]';
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.userId != null) {
+            updateUser(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addUser(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const userIds = row.userId || this.ids;
+      this.$confirm('是否确认删除用户编号为"' + userIds + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return delUser(userIds);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => { });
+    },
+    /** 导出按钮操作 */
+    /** 导出按钮操作 */
+    handleExport() {
+      // 空值保护:深拷贝前先判断
+      if (!this.queryParams) return this.$message.warning("查询参数异常");
+
+      const queryParams = JSON.parse(JSON.stringify(this.queryParams));
+      delete queryParams['startIndex'];
+      delete queryParams['pageLimit'];
+
+      // 空值判断:避免map空数组报错
+      queryParams['tagIds'] = this.tagSearchConfig.selected?.map(item => item.tagId) || [];
+      queryParams['customerRoles'] = this.bindAISearch.selected?.map(item => item.id) || [];
+
+      // 日期参数空值保护
+      if (!this.isEmpty(queryParams.inviteDate)) {
+        let dateRange = queryParams.inviteDate;
+        queryParams['inviteStartDate'] = dateRange[0] + ' 00:00:00';
+        queryParams['inviteEndDate'] = dateRange[1] + ' 23:59:59';
+        delete queryParams['inviteDate'];
+      } else {
+        delete queryParams['inviteDate']; // 空值时直接删除
+      }
+
+      this.$confirm('是否确认导出所有用户数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportAppUser(queryParams);
+      }).then(response => {
+        // 接口返回码判断(适配常见的后端返回格式)
+        if (response.code === 200) {
+          this.download(response.msg);
+          this.msgSuccess("导出成功");
+        } else {
+          this.msgError("导出失败:" + response.msg);
+        }
+        this.exportLoading = false;
+      }).catch(() => {
+        this.exportLoading = false; // 取消/失败时重置加载状态
+        this.msgError("导出取消或失败");
+      });
+    },
+    openTagSearchPage() {
+      this.getSearchTagGroupList();
+      this.tagSearchConfig.visible = true;
+    },
+    openCusSearchPage() {
+      this.bindAISearch.visible = true;
+    },
+    searchTagSubmit() {
+      this.tagSearchConfig.visible = false;
+    },
+    searchTagCancel() {
+      this.tagSearchConfig.visible = false;
+    },
+    async resetTagSearchConfig() {
+      this.tagSearchConfig = {
+        visible: false,//对话框是否展示
+        queryParams: {
+          groupName: null,
+          pageNum: 1,
+          pageSize: 10,
+        },
+        total: 0,
+        tagGroupList: [],
+        selected: [],
+      }
+    },
+    async resetCusSearch() {
+      this.bindAISearch = {
+        visible: false,
+        selected: [],
+      }
+    },
+    /**
+     * 用户绑定标签页面
+     */
+    openBindTagPage(enableFilter = false, opType = 0) {
+      this.tagConfig.enableFilter = enableFilter;
+      this.tagConfig.opType = opType;
+      if (!enableFilter && (this.ids === null || this.ids.length === 0)) {
+        this.$message.closeAll();
+        return this.$message('请选择需要添加标签的用户');
+      }
+      this.getTagGroupList();
+      this.tagConfig.title = '批量添加标签' + (enableFilter ? '(筛选条件)' : '');
+      this.tagConfig.visible = true;
+    },
+    /**
+     * 用户解绑标签页面
+     */
+    openUnbindTagPage(enableFilter = false, opType = 1) {
+      this.tagConfig.enableFilter = enableFilter;
+      this.tagConfig.opType = opType;
+      if (!enableFilter && (this.ids === null || this.ids.length === 0)) {
+        this.$message.closeAll();
+        return this.$message('请选择需要移除标签的用户');
+      }
+      this.getTagGroupList();
+      this.tagConfig.title = '批量移除标签' + (enableFilter ? '(筛选条件)' : '');
+      this.tagConfig.visible = true;
+    },
+    handleCloseTags(tag) {
+      this.tagSearchConfig.selected = this.tagSearchConfig.selected.filter(item => item.tagId != tag.tagId);
+    },
+    handleCloseCus(cus) {
+      this.bindAISearch.selected = this.bindAISearch.selected.filter(item => item.id != cus.id);
+    },
+    /**
+     * 绑定/解绑 标签页面搜索
+     */
+    handleSearchTags(isSearch = false) {
+      if (isSearch) {
+        this.tagSearchConfig.queryParams.pageNum = 1;
+        this.getSearchTagGroupList();
+      } else {
+        this.tagConfig.queryParams.pageNum = 1;
+        this.getTagGroupList();
+      }
+    },
+    /**
+     * 绑定/解绑 标签页面重置
+     */
+    cancelSearchTags(isSearch = false) {
+      if (isSearch) {
+        this.tagSearchConfig.queryParams = {
+          groupName: null,
+          pagenum: 1,
+          pagesize: 10,
+        }
+        this.getSearchTagGroupList();
+      } else {
+        this.tagConfig.queryParams = {
+          groupName: null,
+          pagenum: 1,
+          pagesize: 10,
+        }
+        this.getTagGroupList();
+      }
+    },
+    /**
+     * 切换选中状态
+     * @param row
+     */
+    tagSelection(row) {
+      row.isSelected = !row.isSelected;
+      if (row.isSelected) {
+        this.tagConfig.selected.push({ tagId: row.id, tagName: row.name });
+      }
+      //
+      else {
+        this.tagConfig.selected = this.tagConfig.selected.filter(item => item.tagId !== row.id);
+      }
+      this.$forceUpdate();
+    },
+    searchTagSelection(row) {
+      row.isSelected = !row.isSelected;
+      if (row.isSelected) {
+        this.tagSearchConfig.selected.push({ tagId: row.id, tagName: row.name });
+      }
+      //
+      else {
+        this.tagSearchConfig.selected = this.tagSearchConfig.selected.filter(item => item.tagId !== row.id);
+      }
+      this.$forceUpdate();
+    },
+    /**
+     * 重置 绑定/解绑 标签参数
+     */
+    resetBindTag() {
+      this.tagConfig = {
+        visible: false,
+        enableFilter: false,
+        title: null,
+        opType: 0,
+        queryParams: {
+          groupName: null,
+          pageNum: 1,
+          pagesize: 10,
+        },
+        total: 0,
+        tagGroupList: [],
+        selected: [],
+      }
+    },
+    /**
+     * 绑定/解绑 标签-提交
+     */
+    bindOrUnbindTagSubmit() {
+      let opType = this.tagConfig.opType;
+      let tmpParam = {};
+      if (!this.tagConfig.selected || this.tagConfig.selected.length === 0) {
+        return this.$message('请选择标签');
+      }
+      tmpParam['enableFilter'] = this.tagConfig.enableFilter;
+      tmpParam['tagId'] = this.tagConfig.selected.map(item => item.tagId);
+      tmpParam['userId'] = this.ids;
+      const queryParams = JSON.parse(JSON.stringify(this.queryParams));
+      delete queryParams['startIndex'];
+      delete queryParams['pageLimit'];
+      queryParams['tagIds'] = this.tagSearchConfig.selected.map(item => item.tagId);
+      queryParams['customerRoles'] = this.bindAISearch.selected.map(item => item.id);
+      if (queryParams.inviteDate) {
+        let dateRange = queryParams.inviteDate;
+        queryParams['inviteStartDate'] = dateRange[0] + ' 00:00:00';
+        queryParams['inviteEndDate'] = dateRange[1] + ' 23:59:59';
+        delete queryParams['inviteDate']
+      }
+      tmpParam['fsUserParam'] = queryParams;
+      //绑定标签
+      if (opType === 0) {
+        bindTagV2(tmpParam)
+          .then(res => {
+            this.tagConfig.visible = false;
+            this.resetBindTag();
+            this.msgSuccess("正在添加中!");
+            setTimeout(() => {
+              this.getList();
+            }, 500);
+          })
+          .catch(err => {
+            this.msgError("添加标签失败!");
+          })
+      }
+      //解绑标签
+      else {
+        unbindTagV2(tmpParam)
+          .then(res => {
+            this.tagConfig.visible = false;
+            this.msgSuccess("正在移除中!");
+            this.resetBindTag();
+            setTimeout(() => {
+              this.getList();
+            }, 500);
+          })
+          .catch(err => {
+            this.msgError("移除标签失败!");
+          })
+      }
+    },
+    /**
+     * 绑定/解绑 标签-取消
+     */
+    bindTagCancel() {
+      this.resetBindTag();
+    },
+    /**
+     * 查询标签列表
+     */
+    getTagGroupList() {
+      listTagGroupForUserBindTag(this.tagConfig.queryParams).then(response => {
+        this.tagConfig.tagGroupList = response.rows;
+        this.tagConfig.total = response.total;
+      });
+    },
+    getSearchTagGroupList() {
+      listTagGroupForUserBindTag(this.tagSearchConfig.queryParams).then(response => {
+        let tRows = response.rows;
+        if (tRows && tRows.length > 0) {
+          tRows.forEach(group => {
+            group.tags.forEach(tag => {
+              if (this.tagSearchConfig.selected.some(item => item.tagId === tag.id)) {
+                tag.isSelected = true;
+              } else {
+                tag.isSelected = false;
+              }
+            })
+          });
+        }
+        this.tagSearchConfig.tagGroupList = tRows;
+        this.tagSearchConfig.total = response.total;
+      });
+    },
+    /**
+     * 打开绑定客服页面
+     * @param enableFilter 是否开启过滤模式
+     * @param type 0-绑定,1-解绑
+     */
+    openbindAIPage(enableFilter = false, type = 1) {
+      this.bindAIConfig.enableFilter = enableFilter;
+      if (!enableFilter && (this.ids === null || this.ids.length === 0)) {
+        this.$message.closeAll();
+        return this.$message('请选择需要设置客服的用户');
+      }
+      this.bindAIConfig.title = type == 0 ? '换绑销售' : '解绑客服';
+      this.bindAIConfig.type = type;
+      this.bindAIConfig.selected = [];
+      this.bindAIConfig.visible = true;
+    },
+    /**
+     * 绑定客服页面-选中客服
+     */
+    customerSelectionChangeCallForBind(config) {
+      let selected = config.selected;
+      // 单选模式:最多只保留一个客服
+      if (config.mode === 0) {
+        let row = config.row;
+        if (selected) {
+          this.bindAIConfig.selected = [row];
+        } else {
+          this.bindAIConfig.selected = this.bindAIConfig.selected.filter(m => m.id != row.id);
+        }
+        return;
+      }
+      // 全选(单选模式下 CusRoleList 已拦截,此处兜底忽略)
+    },
+    /**
+     * 保存绑定的客服
+     */
+    async customerSelectedConfirmCallForBind() {
+      if (!this.bindAIConfig.selected || this.bindAIConfig.selected.length !== 1) {
+        this.$message.closeAll();
+        return this.$message.warning('请选择一个客服');
+      }
+      let tmpParam = {};
+      tmpParam['enableFilter'] = this.bindAIConfig.enableFilter;
+      tmpParam['roleId'] = this.bindAIConfig.selected.map(item => item.id) || [];
+      tmpParam['userId'] = this.ids;
+      const queryParams = JSON.parse(JSON.stringify(this.queryParams));
+      delete queryParams['startIndex'];
+      delete queryParams['pageLimit'];
+      queryParams['tagIds'] = this.tagSearchConfig.selected.map(item => item.tagId);
+      queryParams['customerRoles'] = this.bindAISearch.selected.map(item => item.id);
+      if (queryParams.inviteDate) {
+        let dateRange = queryParams.inviteDate;
+        queryParams['inviteStartDate'] = dateRange[0] + ' 00:00:00';
+        queryParams['inviteEndDate'] = dateRange[1] + ' 23:59:59';
+        delete queryParams['inviteDate']
+      }
+      tmpParam['fsUserParam'] = queryParams;
+      let res;
+      if (this.bindAIConfig.type == 0) {
+        res = await bindUserCusByIdsV2(tmpParam);
+        this.msgSuccess("正在绑定中!");
+      } else {
+        res = await unbindUserCusByIdsV2(tmpParam);
+        this.msgSuccess("正在解绑中!");
+      }
+      this.getList();
+      this.bindAIConfig.visible = false;
+      this.bindAIConfig.selected = [];
+    },
+  }
+};
+</script>
+<style lang="scss" scoped>
+.dialog-body {
+  height: 400px;
+  overflow: auto;
+}
+
+.tag-container {
+  display: flex;
+  flex-wrap: wrap; /* 超出宽度时自动换行 */
+  gap: 8px; /* 设置标签之间的间距 */
+}
+
+.name-background {
+  display: inline-block;
+  background-color: #abece6; /* 背景颜色 */
+  padding: 4px 8px; /* 调整内边距,让背景包裹文字 */
+  border-radius: 4px; /* 可选:设置圆角 */
+}
+
+.tag-box {
+  padding: 8px 12px;
+  border: 1px solid #989797;
+  border-radius: 4px;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.tag-selected {
+  background-color: #00bc98;
+  color: #fff;
+  border-color: #00bc98;
+}
+
+.el-tag + .el-tag {
+  margin-left: 10px;
+}
+
+::v-deep  .ai-user-info .el-form-item__label {
+  text-align: right !important;
+}
+</style>

+ 248 - 0
src/views/app/userChatLogs/index.vue

@@ -0,0 +1,248 @@
+<template>
+    <div class="app-container">
+        <el-form
+            :model="queryParams"
+            ref="queryForm"
+            :inline="true"
+            v-show="showSearch"
+            label-width="120px"
+        >
+            <el-form-item label="会员ID" prop="userId">
+                <el-input
+                    v-model="queryParams.userId"
+                    placeholder="请输入会员ID"
+                    clearable
+                    size="small"
+                    @keyup.enter.native="handleQuery"
+                    @input="queryParams.userId = queryParams.userId.replace(/[^0-9]/g, '')"
+                />
+            </el-form-item>
+            <el-form-item label="发送日期" prop="userAskDate">
+                <el-date-picker
+                    clearable
+                    size="small"
+                    v-model="queryParams.userAskDate"
+                    type="date"
+                    value-format="yyyy-MM-dd"
+                    placeholder="请选择发送日期"
+                    @change="handleQuery"
+                />
+            </el-form-item>
+            <el-form-item label="客服ID" prop="appCustomerId">
+                <el-input
+                    v-model="queryParams.appCustomerId"
+                    placeholder="请输入客服ID"
+                    clearable
+                    size="small"
+                    @keyup.enter.native="handleQuery"
+                    @input="queryParams.appCustomerId = queryParams.appCustomerId.replace(/[^0-9]/g, '')"
+                />
+            </el-form-item>
+            <el-form-item label="回复日期" prop="customerReplyDate">
+                <el-date-picker
+                    clearable
+                    size="small"
+                    v-model="queryParams.customerReplyDate"
+                    type="date"
+                    value-format="yyyy-MM-dd"
+                    placeholder="请选择回复日期"
+                    @change="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">
+            <right-toolbar :showSearch.sync="showSearch" @queryTable="getFindList"/>
+        </el-row>
+
+        <el-table v-loading="loading" :data="userChatLogs" border>
+            <el-table-column type="index" label="序号" align="center" width="80"/>
+            <el-table-column label="会员ID" align="center" prop="userId" min-width="130"/>
+            <el-table-column label="会员名称" align="left" prop="userName" min-width="100"/>
+            <el-table-column label="客服ID" align="left" prop="appCustomerId" min-width="100"/>
+            <el-table-column label="客服名称" align="center" prop="appCustomerName" min-width="150">
+                <template slot-scope="scope">
+                    <el-tag
+                        v-if="scope.row.appCustomerName"
+                        v-text="scope.row.appCustomerName"
+                    />
+                </template>
+            </el-table-column>
+            <el-table-column label="会员对话" align="left" prop="userAskShortContent" min-width="250"/>
+            <el-table-column label="对话时间" align="center" prop="userAskTime" min-width="120"/>
+            <el-table-column label="客服回复" align="left" prop="customerReplyShortContent" min-width="250">
+                <template slot-scope="scope">
+                    {{ scope.row.customerReplyShortContent || '--' }}
+                </template>
+            </el-table-column>
+            <el-table-column label="回复时间" align="center" prop="customerReplyTime" min-width="120"/>
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width" min-width="180"
+                fixed="right">
+                <template slot-scope="scope">
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-edit"
+                        @click="openChatPage(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="getFindList" />
+
+        <!-- 查看详情 -->
+        <el-dialog 
+            title="对话详情"
+            :visible.sync="dialog.visible"
+            v-loading="dialog.loading"
+            width="1000px"
+            append-to-body
+            :close-on-click-modal="false"
+        >
+            <div style="height: 600px; overflow: auto;">
+                <el-timeline>
+                    <el-timeline-item 
+                        :timestamp="dialog.chatInfo.userAskTime"
+                        placement="top"
+                        type="success"
+                    >
+                        <el-card shadow="hover" :body-style="{'padding': '8px', 'background': '#9df29f', 'color': '#161616'}">
+                            <div
+                                class="no-copy1"
+                                v-html="dialog.chatInfo.userAskFullContentFmt || '--'"
+                            >
+                            </div>
+                        </el-card>
+                    </el-timeline-item>
+                    <el-timeline-item
+                        :timestamp="dialog.chatInfo.customerReplyTime"
+                        placement="top"
+                        type="primary"
+                    >
+                        <el-card shadow="hover" :body-style="{'padding': '8px', 'background': '#eeeef0', 'color': '#161616'}">
+                            <div
+                                class="no-copy1"
+                                v-html="dialog.chatInfo.customerReplyFullContentFmt || '--'"
+                                oncontextmenu="return false"
+                                @selectstart.prevent
+                                @contextmenu.prevent
+                            >
+                            </div>
+                        </el-card>
+                    </el-timeline-item>
+                </el-timeline>
+            </div>
+
+            <div slot="footer" class="dialog-footer" style="text-align: center">
+                <el-button @click="cancel">关 闭</el-button>
+            </div>
+        </el-dialog>
+        
+    </div>
+</template>
+
+<script>
+import { findList, } from "@/api/app/userChatLogs";
+
+export default {
+    name: "UserChatLogs",
+    data() {
+        return {
+            loading: true,
+            showSearch: true,
+            userChatLogs: [],
+            total: 0,
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                userId: null,
+                userAskDate: null,
+                appCustomerId: null,
+                customerReplyDate: null,
+            },
+            dialog: {
+                visible: false,
+                loading: false,
+                chatInfo: {},
+            },
+        };
+    },
+    created() {
+        this.getFindList();
+    },
+    methods: {
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getFindList();
+        },
+        resetQuery() {
+            this.resetForm("queryForm");
+            this.getFindList();
+        },
+        async getFindList() {
+            this.loading = true;
+            let tableInfo = await findList(this.queryParams);
+            this.userChatLogs = tableInfo.rows;
+            this.total = tableInfo.total;
+            this.loading = false;
+        },
+        /**
+         * 打开详情页面
+         */
+        openChatPage(row) {
+            this.dialog.visible = true;
+            this.dialog.loading = true;
+            this.dialog.chatInfo = row;
+            let userAskFullContentFmt = !this.isEmpty(this.dialog.chatInfo.userAskFullContent) ? this.dialog.chatInfo.userAskFullContent.replace(/\n+/g, '\n').replace(/\n/g, '<br>') : '--';
+            this.dialog.chatInfo['userAskFullContentFmt'] = userAskFullContentFmt;
+            let customerReplyFullContentFmt = !this.isEmpty(this.dialog.chatInfo.customerReplyFullContent) ? this.dialog.chatInfo.customerReplyFullContent.replace(/\n+/g, '\n').replace(/\n/g, '<br>') : '--';
+            this.dialog.chatInfo['customerReplyFullContentFmt'] = customerReplyFullContentFmt;
+            this.dialog.loading = false;
+        },
+        cancel() {
+            this.dialog.visible = false;
+        },
+        isEmpty(value) {
+            return value === null || value === undefined || value.length === 0;
+        },
+        /**
+         * 删除
+         * @param row 
+         */
+        handleDelete(row) {
+            const ids = row.id || this.ids;
+            this.$confirm('是否确认删除欢迎语:编号为"' + ids + '"的数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(function () {
+                return deleteByIds(ids);
+            }).then(() => {
+                this.getList();
+                this.msgSuccess("删除成功");
+            }).catch(() => { });
+        },
+    }
+};
+</script>
+
+<style scoped>
+::v-deep .el-card .is-always-shadow {
+    padding: 8px;
+}
+
+.no-copy {
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+}
+</style>

+ 545 - 0
src/views/app/userPortrait/index.vue

@@ -0,0 +1,545 @@
+<template>
+  <div class="app-container portrait-container">
+
+    <!-- 搜索栏 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="用户ID" prop="userId">
+        <el-input
+          v-model="queryParams.userId"
+          placeholder="请输入用户ID"
+          clearable
+          size="small"
+          style="width: 160px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="手机号" prop="phone">
+        <el-input
+          v-model="queryParams.phone"
+          placeholder="请输入手机号"
+          clearable
+          size="small"
+          style="width: 160px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickName">
+        <el-input
+          v-model="queryParams.nickName"
+          placeholder="请输入用户昵称"
+          clearable
+          size="small"
+          style="width: 160px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="消费等级" prop="consumptionLevel">
+        <el-select
+          v-model="queryParams.consumptionLevel"
+          placeholder="请选择消费等级"
+          clearable
+          size="small"
+          style="width: 130px"
+        >
+          <el-option label="S+" value="S+" />
+          <el-option label="S"  value="S" />
+          <el-option label="A"  value="A" />
+          <el-option label="B"  value="B" />
+          <el-option label="C"  value="C" />
+          <el-option label="D"  value="D" />
+          <el-option label="E"  value="E" />
+          <el-option label="代客下单"  value="F" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="活跃度" prop="activityLevel">
+        <el-select
+          v-model="queryParams.activityLevel"
+          placeholder="请选择活跃度"
+          clearable
+          size="small"
+          style="width: 130px"
+        >
+          <el-option label="高活跃" value="高活跃" />
+          <el-option label="中活跃" value="中活跃" />
+          <el-option label="低活跃" value="低活跃" />
+          <el-option label="沉默"   value="沉默" />
+          <el-option label="流失"   value="流失" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="portraitList" border stripe style="width: 100%">
+      <el-table-column label="用户ID" align="center" prop="userId" min-width="130" />
+
+      <el-table-column label="头像" align="center" width="80">
+        <template slot-scope="scope">
+          <el-avatar
+            v-if="scope.row.avatar"
+            :src="scope.row.avatar"
+            :size="40"
+          />
+          <el-avatar v-else icon="el-icon-user-solid" :size="40" style="background:#c0c4cc" />
+        </template>
+      </el-table-column>
+
+      <el-table-column label="昵称" align="center" prop="nickName" min-width="120" show-overflow-tooltip />
+      <el-table-column label="手机号" align="center" prop="phone" min-width="160" />
+
+      <el-table-column label="活跃度" align="center" prop="activityLevel" width="110">
+        <template slot-scope="scope">
+          <el-tag
+            v-if="scope.row.activityLevel"
+            :type="activityTagType(scope.row.activityLevel)"
+            size="small"
+          >{{ scope.row.activityLevel }}</el-tag>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="消费等级" align="center" prop="consumptionLevel" width="100">
+        <template slot-scope="scope">
+          <span v-if="scope.row.consumptionLevel" :class="consumptionClass(scope.row.consumptionLevel)" class="level-badge">
+            {{ comsumptionLevelMapper[scope.row.consumptionLevel] }}
+          </span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="兴趣标签" align="center" prop="interestTags" min-width="230">
+        <template slot-scope="scope">
+          <template v-if="scope.row.interestTags">
+            <el-tag
+              v-for="(tag, idx) in splitTags(scope.row.interestTags)"
+              :key="idx"
+              type="success"
+              size="mini"
+              style="margin: 2px;"
+            >{{ tag }}</el-tag>
+          </template>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="城市" align="center" prop="location" width="110" show-overflow-tooltip />
+
+      <el-table-column label="疗法周期" align="center" min-width="160">
+        <template slot-scope="scope">
+          <template v-if="scope.row.treatmentCycle">
+            <el-button
+              v-for="(cycle, idx) in splitTags(scope.row.treatmentCycle)"
+              :key="idx"
+              size="mini"
+              type="primary"
+              plain
+              style="margin: 2px;"
+              @click="openTreatmentDialog(scope.row, cycle)"
+            >{{ cycle }}</el-button>
+          </template>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" fixed="right" min-width="160">
+        <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="text"
+              icon="el-icon-edit"
+              plain
+              style="margin: 2px;"
+              @click="openEditDialog(scope.row)"
+            />
+        </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="疗法周期详情"
+      :visible.sync="treatmentDialog.visible"
+      width="620px"
+      append-to-body
+      custom-class="treatment-dialog"
+      @close="closeTreatmentDialog"
+    >
+      <div v-loading="treatmentDialog.loading" class="treatment-content">
+        <!-- 用户信息头部 -->
+        <div class="treatment-user-header">
+          <el-avatar
+            v-if="treatmentDialog.row && treatmentDialog.row.avatar"
+            :src="treatmentDialog.row && treatmentDialog.row.avatar"
+            :size="48"
+          />
+          <el-avatar v-else icon="el-icon-user-solid" :size="48" style="background:#909399" />
+          <div class="treatment-user-info">
+            <div class="user-name">{{ treatmentDialog.row && (treatmentDialog.row.nickName || '—') }}</div>
+            <div class="user-phone">{{ treatmentDialog.row && (treatmentDialog.row.phone || '—') }}</div>
+          </div>
+          <el-tag v-if="treatmentDialog.cycleName" type="warning" style="margin-left: auto;">
+            {{ treatmentDialog.cycleName }}
+          </el-tag>
+        </div>
+
+        <!-- 详情字段 -->
+        <el-descriptions
+          class="treatment-desc"
+          :column="2"
+          border
+          size="small"
+        >
+          <el-descriptions-item label="购买时间">
+            <span>{{ treatmentDialog.detail.buyTime || '—' }}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="购买量">
+            <span>{{ treatmentDialog.detail.buyQuantity || '—' }}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="建议单日服用量">
+            <span>{{ treatmentDialog.detail.dailyDosage || '—' }}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="回访时间">
+            <span>{{ treatmentDialog.detail.visitTime || '—' }}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="提醒复购时间">
+            <span>{{ treatmentDialog.detail.repurchaseRemindTime || '—' }}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="推测余量">
+            <span>{{ treatmentDialog.detail.estimatedRemaining || '—' }}</span>
+          </el-descriptions-item>
+        </el-descriptions>
+
+        <!-- 空状态 -->
+        <div v-if="!treatmentDialog.loading && !hasTreatmentData" class="treatment-empty">
+          <i class="el-icon-warning-outline" style="font-size:36px;color:#c0c4cc"></i>
+          <p>暂无疗法周期数据</p>
+        </div>
+      </div>
+
+      <div slot="footer">
+        <el-button @click="treatmentDialog.visible = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog
+      title="编辑画像"
+      :visible.sync="editDialog.visible"
+      width="350px"
+      append-to-body
+      :close-on-click-modal="false"
+      custom-class="user-portrait-dialog"
+    >
+      <div v-loading="editDialog.loading">
+        <el-form :model="editDialog.editForm" ref="editForm" :rules="editDialog.rules" :inline="true" label-width="80px">
+          <el-row>
+            <el-form-item label="用户ID">
+              <el-input v-model="editDialog.editForm.userId" :disabled="true"/>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="昵称">
+              <el-input v-model="editDialog.editForm.nickName" :disabled="true"/>
+            </el-form-item>
+          </el-row>
+          <el-row>
+            <el-form-item label="消费等级" prop="consumptionLevel">
+              <el-select
+                v-model="editDialog.editForm.consumptionLevel"
+                placeholder="请选择消费等级"
+                clearable
+                style="width: 205px;"
+              >
+                <el-option label="S+" value="S+" />
+                <el-option label="S"  value="S" />
+                <el-option label="A"  value="A" />
+                <el-option label="B"  value="B" />
+                <el-option label="C"  value="C" />
+                <el-option label="D"  value="D" />
+                <el-option label="E"  value="E" />
+                <el-option label="代客下单"  value="F" />
+              </el-select>
+            </el-form-item>
+          </el-row>
+        </el-form>
+      </div>
+      <div slot="footer">
+        <el-button type="primary" size="small" :disabled="editDialog.loading" @click="submitEditInfo">确 认</el-button>
+        <el-button size="small" @click="editDialog.visible = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { listUserPortrait, getUserTreatmentCycle, saveEdit, } from '@/api/app/user/portrait'
+
+export default {
+  name: 'UserPortrait',
+  data() {
+    return {
+      loading: false,
+      showSearch: true,
+      total: 0,
+      portraitList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: null,
+        phone: null,
+        nickName: null,
+        consumptionLevel: null,
+        activityLevel: null
+      },
+      treatmentDialog: {
+        visible: false,
+        loading: false,
+        row: null,
+        cycleName: null,
+        detail: {
+          buyTime: null,
+          buyQuantity: null,
+          dailyDosage: null,
+          visitTime: null,
+          repurchaseRemindTime: null,
+          estimatedRemaining: null
+        }
+      },
+      comsumptionLevelMapper: {
+        'S+': 'S+',
+        'S': 'S',
+        'A': 'A',
+        'B': 'B',
+        'C': 'C',
+        'D': 'D',
+        'E': 'E',
+        'F': '代客下单'
+      },
+      editDialog: {
+        loading: false,
+        visible: false,
+        id: null,
+        editForm: {
+          id: null,
+          userId: null,
+          nickName: null,
+          consumptionLevel: null,
+        },
+        rules: {
+          consumptionLevel: [
+            { required: true, message: '请选择消费等级', trigger: 'change' }
+          ]
+        },
+      },
+    }
+  },
+  computed: {
+    hasTreatmentData() {
+      const d = this.treatmentDialog.detail
+      return Object.values(d).some(v => v !== null && v !== '' && v !== undefined)
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    /** 获取列表 */
+    getList() {
+      this.loading = true
+      const params = {}
+      if (this.queryParams.pageNum) params.pageNum = this.queryParams.pageNum
+      if (this.queryParams.pageSize) params.pageSize = this.queryParams.pageSize
+      if (this.queryParams.userId) params.userId = this.queryParams.userId
+      if (this.queryParams.phone) params.phone = this.queryParams.phone
+      if (this.queryParams.nickName) params.nickName = this.queryParams.nickName
+      if (this.queryParams.consumptionLevel) params.consumptionLevel = this.queryParams.consumptionLevel
+      if (this.queryParams.activityLevel) params.activityLevel = this.queryParams.activityLevel
+      listUserPortrait(params).then(response => {
+        this.portraitList = response.rows || []
+        this.total = response.total || 0
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    /** 搜索 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    /** 重置 */
+    resetQuery() {
+      this.$refs.queryForm.resetFields()
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    /** 拆分兴趣标签 */
+    splitTags(str) {
+      if (!str) return []
+      return str.split(/[,,]/).map(t => t.trim()).filter(t => t)
+    },
+    /** 活跃度 tag 颜色 */
+    activityTagType(level) {
+      const map = {
+        '高活跃': 'success',
+        '中活跃': '',
+        '低活跃': 'warning',
+        '沉默': 'info',
+        '流失': 'danger'
+      }
+      for (const key in map) {
+        if (level && level.includes(key)) return map[key]
+      }
+      return 'info'
+    },
+    /** 消费等级样式 */
+    consumptionClass(level) {
+      const map = { 'S+': 'level-splus', 'S': 'level-s', 'A': 'level-a', 'F': 'level-a', 'B': 'level-b', 'C': 'level-c' }
+      return map[level] || 'level-default'
+    },
+    /** 打开疗法周期详情(row: 当前行数据,cycleName: 点击的具体周期名称) */
+    openTreatmentDialog(row, cycleName) {
+      this.treatmentDialog.visible = true
+      this.treatmentDialog.row = row
+      this.treatmentDialog.cycleName = cycleName || null
+      this.treatmentDialog.detail = {
+        buyTime: null,
+        buyQuantity: null,
+        dailyDosage: null,
+        visitTime: null,
+        repurchaseRemindTime: null,
+        estimatedRemaining: null
+      }
+      if (row.userId) {
+        this.treatmentDialog.loading = true
+        getUserTreatmentCycle(row.userId, cycleName).then(res => {
+          const data = res.data || {}
+          this.treatmentDialog.detail = {
+            buyTime: data.buyTime || null,
+            buyQuantity: data.buyQuantity || null,
+            dailyDosage: data.dailyDosage || null,
+            visitTime: data.visitTime || null,
+            repurchaseRemindTime: data.repurchaseRemindTime || null,
+            estimatedRemaining: data.estimatedRemaining || null
+          }
+        }).catch(() => {
+          // 接口暂未对接时不报错
+        }).finally(() => {
+          this.treatmentDialog.loading = false
+        })
+      }
+    },
+    /** 关闭疗法周期弹窗 */
+    closeTreatmentDialog() {
+      this.treatmentDialog.row = null
+      this.treatmentDialog.cycleName = null
+    },
+    openEditDialog(row) {
+      this.editDialog.visible = true;
+      this.editDialog.id = row.id;
+      this.editDialog.loading = true;
+      this.editDialog.editForm['id'] = row.id;
+      this.editDialog.editForm['userId'] = row.userId;
+      this.editDialog.editForm['nickName'] = row.nickName;
+      this.editDialog.editForm['consumptionLevel'] = row.consumptionLevel;
+      this.editDialog.loading = false;
+    },
+    submitEditInfo() {
+      this.$refs["editForm"].validate(async (valid) => {
+        this.editDialog.visible = true;
+        if (valid) {
+          await saveEdit(this.editDialog.editForm);
+          this.$message.success("修改成功");
+          this.$refs['editForm'].resetFields();
+          this.$refs['editForm'].clearValidate();
+          this.editDialog.visible = false;
+          this.getList();
+        }
+        this.editDialog.visible = false;
+      });
+    },
+  },
+}
+</script>
+
+<style scoped lang="scss">
+.portrait-container {
+  padding: 20px;
+}
+
+/* 消费等级徽标 */
+.level-badge {
+  display: inline-block;
+  padding: 2px 10px;
+  border-radius: 12px;
+  font-size: 12px;
+  font-weight: 700;
+  letter-spacing: 1px;
+}
+.level-splus { background: #8b0000; color: #fff; }
+.level-s     { background: #c0392b; color: #fff; }
+.level-a     { background: #e05555; color: #fff; }
+.level-b     { background: #f0908a; color: #fff; }
+.level-c     { background: #fad4d1; color: #c0392b; }
+.level-default { background: #f0f0f0; color: #606266; }
+
+/* 疗法周期弹窗 */
+.treatment-content {
+  min-height: 120px;
+}
+.treatment-user-header {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 12px 16px;
+  background: #f8f9fa;
+  border-radius: 8px;
+  margin-bottom: 18px;
+  border: 1px solid #ebeef5;
+}
+.treatment-user-info .user-name {
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+}
+.treatment-user-info .user-phone {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+.treatment-desc {
+  margin-top: 4px;
+}
+.treatment-empty {
+  text-align: center;
+  padding: 30px 0 10px;
+  color: #c0c4cc;
+}
+.treatment-empty p {
+  margin-top: 8px;
+  font-size: 13px;
+}
+
+::v-deep  .user-portrait-dialog {
+
+  .el-form-item__label {
+    text-align: right !important;
+  }
+
+}
+</style>

+ 1079 - 0
src/views/app/welcome/index.vue

@@ -0,0 +1,1079 @@
+<template>
+    <div class="app-container">
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="120px">
+            <el-form-item label="客服" prop="customerRoles">
+                <div @click="openCustomerPage(0)"
+                    style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
+                    <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+                        <el-tag type="success"
+                            closable
+                            :disable-transitions="false"
+                            v-for="list in appCustomerSearchConfig.selected"
+                            :key="list.id"
+                            @close="handleCloseCustomer(list, 0)"
+                            style="margin: 3px;"
+                        >{{ list.memberName }}
+                        </el-tag>
+                    </div>
+                </div>
+            </el-form-item>
+            <el-form-item label="创建时间" prop="createTime">
+                <el-date-picker clearable size="small" v-model="queryParams.createTime" type="date"
+                    value-format="yyyy-MM-dd" placeholder="选择创建时间">
+                </el-date-picker>
+            </el-form-item>
+            <el-form-item label="是否发送欢迎语" prop="sendStatus">
+                <el-select v-model="queryParams.sendStatus" placeholder="请选择" clearable size="small">
+                    <el-option label="是" :value="1"/>
+                    <el-option label="否" :value="0"/>
+                </el-select>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+                <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+                <el-button
+                    type="primary"
+                    plain
+                    size="mini"
+                    icon="el-icon-plus"
+                    @click="handleAdd"
+                    v-hasPermi="['app:welcome:add']"
+                >
+                    新增
+                </el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-row :gutter="10" class="mb8">
+            <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+
+        <el-table v-loading="loading" :data="welcomeList" border>
+
+            <el-table-column label="消息内容" align="left" prop="welcomeText" min-width="200">
+                <template slot-scope="scope">
+                    <el-tooltip class="item" effect="dark" :content="scope.row.welcomeText" placement="top">
+                        <div
+                            style="display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; text-overflow: ellipsis;">
+                            <span>{{ scope.row.welcomeText }}</span>
+                        </div>
+                    </el-tooltip>
+                </template>
+            </el-table-column>
+
+            <el-table-column label="使用客服" align="center" prop="customerRoles" min-width="250">
+                <template slot-scope="scope">
+                    <div v-for="customerRole in (scope.row.customerRoles ? scope.row.customerRoles.split(',') : [])" :key="customerRole" style="display: inline;"
+                        class="text-container">
+                        <el-tag type="success" style="margin: 3px;">{{ customerRole }}</el-tag>
+                    </div>
+                </template>
+            </el-table-column>
+
+            <el-table-column label="发送状态" align="center" prop="sendStatus" min-width="120">
+                <template slot-scope="scope">
+                    <el-switch v-model="scope.row.sendStatus" active-color="#13ce66" inactive-color="#ff4949"
+                        :active-value="1" :inactive-value="0" active-text="开启" inactive-text="关闭" @change="toggleSendStatus($event, scope.row)">
+                    </el-switch>
+                </template>
+            </el-table-column>
+
+            <el-table-column label="创建时间" align="center" prop="createTime" min-width="180" />
+            <el-table-column label="更新时间" align="center" prop="updateTime" min-width="180" />
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width" min-width="180"
+                fixed="right">
+                <template slot-scope="scope">
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-edit"
+                        @click="handleUpdate(scope.row)"
+                        v-hasPermi="['app:welcome:edit']"
+                    >
+                        修改
+                    </el-button>
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-delete"
+                        @click="handleDelete(scope.row)"
+                        v-hasPermi="['app:welcome:delete']"
+                    >
+                        删除
+                    </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="addForm.title" :visible.sync="addForm.visible" v-loading="loading" width="1700px" append-to-body :close-on-click-modal="false">
+            <div style="width: 1130px" class="app-container">
+                <div>
+                    <span style="font-size: 15px">基础信息</span>
+                    <el-divider></el-divider>
+                    <el-alert title="同一个客服,如果绑定了多个欢迎语,则以最新绑定的那一条为准,本次保存涉及的客服,那么当前的欢迎语就是最新绑定的!!" type="info" :closable="false" show-icon>
+                    </el-alert>
+                </div>
+                <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+                    <el-form-item label="选择使用客服:" prop="customerRoles" style="margin-top: 2%">
+                        <div>
+                            <el-button size="medium" icon="el-icon-circle-plus-outline" plain
+                                @click="openCustomerPage(1)">请选择使用客服</el-button>
+                        </div>
+                        <div>
+                            <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in appCustomerBindConfig.selected" closable
+                                :disable-transitions="false" @close="handleCloseCustomer(s, 1)">
+                                <span>{{ s.memberName }}</span>
+                            </el-tag>
+                        </div>
+                    </el-form-item>
+                    <el-form-item label="发送状态" prop="sendStatus">
+                        <el-switch v-model="form.sendStatus" active-color="#13ce66" inactive-color="#ff4949"
+                            :active-value="1" :inactive-value="0" active-text="开启" inactive-text="关闭">
+                        </el-switch>
+                    </el-form-item>
+                    <div>
+                        <span style="font-size: 15px">欢迎语</span>
+                        <el-divider></el-divider>
+                    </div>
+                    <el-form-item label="默认欢迎语:" prop="welcomeText">
+                        <el-input v-model="form.welcomeText" type="textarea" :rows="12" maxlength="1300" show-word-limit
+                            placeholder="请输入消息内容" />
+                        <!-- 附件和链接列表 -->
+                        <el-row>
+                            <el-col>
+                                <div v-for="(item, index) in form.attachments" :key="index"
+                                    style="background-color: #f5f7fa;padding: 5px;border: 1px solid #d9d9d9;">
+                                    <div slot="header"
+                                        style="display: flex;justify-content: space-between;align-items: center;">
+                                        <div style="flex: 1;">
+                                            <span v-if="item.contentType == '2'">【图片】: {{ item.image.imgUrl }}</span>
+                                            <span v-else-if="item.contentType == '9'">【科普】: {{ item.course.courseTitle }}</span>
+                                            <span v-else-if="item.contentType == '20'">【疗法】: {{ item.pkg.packageName }}</span>
+                                            <span v-else-if="item.contentType == '21'">【直播】: {{ item.live.liveTitle }}</span>
+                                        </div>
+                                        <div style="display: flex;gap: 10px;">
+                                            <el-button size="mini" type="text" icon="el-icon-edit" style="float: left;"
+                                                @click="editFileItem(item, index)">修改</el-button>
+                                            <el-button size="mini" type="text" icon="el-icon-delete"
+                                                style="float: right;"
+                                                @click="removeFileItem(index)">删除</el-button>
+                                        </div>
+                                    </div>
+                                </div>
+                            </el-col>
+                        </el-row>
+
+                        <el-dropdown @command="(command) => handleCommand(command)" trigger="click"
+                            placement="top-start">
+                            <el-dropdown-menu slot="dropdown" style="width: 120px;">
+                                <!-- 和模板那边的消息类型一致,文本在上面输入框输入,故此不显示在此处 -->
+                                <el-dropdown-item
+                                    v-for="ct in contentTypeOptions"
+                                    :key="ct.dictValue"
+                                    :command="ct.dictValue"
+                                    v-show="ct.dictValue != 11"
+                                >
+                                    <i class="el-icon-link" style="margin-right: 10px;"></i>{{ ct.dictLabel }}
+                                </el-dropdown-item>
+                            </el-dropdown-menu>
+                            <span class="el-dropdown-link">
+                                <el-link icon="el-icon-paperclip" type="text" style="color: rgb(24, 144, 255)">
+                                    添加附件(最多9个)
+                                </el-link>
+                            </span>
+                        </el-dropdown>
+                    </el-form-item>
+                </el-form>
+            </div>
+
+            <div slot="footer" class="dialog-footer" style="text-align: center">
+                <el-button type="primary" @click="submitForm">确定</el-button>
+                <el-button @click="cancel">取 消</el-button>
+            </div>
+        </el-dialog>
+
+        <!-- 列表搜索条件:选择app客服角色 -->
+        <el-dialog title="选择客服" :visible.sync="appCustomerSearchConfig.visible" width="1000px" append-to-body :close-on-click-modal="false">
+            <CusRoleList
+                ref="customerSearchRef"
+                @confirm="customerSelectedConfirmCallForSearch"
+                @selectionChange="customerSelectionChangeCallForSearch"
+                :selected="appCustomerSearchConfig.selected"
+            />
+        </el-dialog>
+
+        <!-- 新增编辑:选择app客服角色 -->
+        <el-dialog title="选择客服" :visible.sync="appCustomerBindConfig.visible" width="1000px" append-to-body :close-on-click-modal="false">
+            <CusRoleList
+                ref="customerBindRef"
+                @confirm="customerSelectedConfirmCallForBind"
+                @selectionChange="customerSelectionChangeCallForBind"
+                :selected="appCustomerBindConfig.selected"
+            />
+        </el-dialog>
+
+        <!-- 新增/编辑页面内,欢迎语类型 -->
+        <el-dialog :title="welcomeItem.title" :visible.sync="welcomeItem.visible" style="width: 1300px;height: 100%; margin-top: 10vh;"
+            append-to-body :close-on-click-modal="false">
+            <!-- 科普 -->
+            <el-form  v-if="welcomeItem.contentType == '9'" ref="courseForm" :model="courseForm" :rules="courseFormRule" label-width="110px">
+                <div>
+                    <el-form-item label="选择科普" prop="videoId">
+                        <el-select
+                            v-model="courseForm.courseId"
+                            placeholder="默认50条,可输入关键字检索"
+                            style="margin-right: 10px;"
+                            size="mini"
+                            filterable
+                            :loading="welcomeItem.courseLoading"
+                            @change="courseChange(courseForm)"
+                            remote
+                            :remote-method="(keyword) => handleCourseRemoteSearch(keyword, courseForm)"
+                        >
+                            <el-option
+                                v-for="dict in courseList"
+                                :key="dict.dictValue"
+                                :label="dict.dictLabel"
+                                :value="parseInt(dict.dictValue)"
+                            />
+                        </el-select>
+                        <el-select
+                            v-model="courseForm.videoId"
+                            size="mini"
+                            placeholder="请选择小节"
+                            style="margin-right: 10px;"
+                            @change="videoIdChange(courseForm, welcomeItem.contentType)"
+                        >
+                            <el-option
+                                v-for="dict in videoList"
+                                :key="dict.dictValue"
+                                :label="dict.dictLabel"
+                                :value="parseInt(dict.dictValue)"
+                            >
+                                <div :style="{ color: dict.isPause == 1 ? '#f56c6c' : 'inherit', display: 'flex', alignItems: 'center' }">
+                                    <span>{{ dict.dictLabel }}</span>
+                                    <span v-if="dict.isPause == 1" style="font-size: 12px; margin-left: 8px; color: #f56c6c;">
+                                        (该科普已被关闭,无法正常发送)
+                                    </span>
+                                </div>
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="科普标题:" prop="courseTitle">
+                        <el-input
+                            v-model="courseForm.courseTitle"
+                            :rows="2"
+                            maxlength="64"
+                            placeholder="请输入科普标题,最长为64字节"
+                            @input="checkByteLength(courseForm, 'courseTitle')"
+                        />
+                    </el-form-item>
+                    <div v-if="courseForm.videoId != null ">
+                        <el-form-item label="课节链接:" label-width="100px">
+                            <el-tag type="warning">选择的科普小节 即为卡片链接地址</el-tag>
+                        </el-form-item>
+                    </div>
+                    <div v-if="courseForm.videoId != null">
+                        <el-form-item label="课节过期时间" style="margin-top: 1%" required label-width="110px">
+                            <el-row>
+                                <el-input-number v-model="courseForm.expireDays" :min="1" :max="9999"></el-input-number>
+                                (天)
+                            </el-row>
+                            <el-row>
+                                <span class="tip">默认为30天</span>
+                            </el-row>
+                        </el-form-item>
+                    </div>
+                </div>
+            </el-form>
+            <!-- 图片 -->
+            <el-form v-else-if="welcomeItem.contentType == '2'" ref="imageForm" :model="imageForm" :rules="imageFormRule" label-width="110px">
+                <div>
+                    <el-form-item label="图片:" prop="imgUrl">
+                        <ImageUpload
+                            v-model="imageForm.imgUrl"
+                            type="image"
+                            :num="10"
+                            :width="150"
+                            :height="150"
+                        />
+                    </el-form-item>
+                </div>
+            </el-form>
+            <!--语音: welcomeItem.contentType == '12' -->
+            <!--视频: welcomeItem.contentType == '6' -->
+            <!-- 疗法: welcomeItem.contentType == '20' -->
+            <el-form v-else-if="welcomeItem.contentType == '20'" ref="pkgForm" :model="pkgForm" :rules="pkgFormRule" label-width="110px">
+                <div>
+                    <el-form-item
+                        label="选择疗法:"
+                        label-width="100px"
+                        required
+                    >
+                        <el-select
+                            v-model="pkgForm.packageId"
+                            placeholder="默认50条,可输入关键字检索"
+                            style=" margin-right: 10px; width: 230px;"
+                            size="mini"
+                            filterable
+                            clearable
+                            :loading="pkgForm.pkgLoading"
+                            remote
+                            :remote-method="(keyword) => handlePkgRemoteSearch(keyword)"
+                            @change="pkgChangeUpdate"
+                        >
+                        <el-option
+                            v-for="dict in pkgList"
+                            :key="dict.packageId"
+                            :label="`${dict.packageName}(${dict.packageId})`"
+                            :value="parseInt(dict.packageId)"
+                        />
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item
+                        label="疗法名称:"
+                        label-width="100px"
+                    >
+                        <el-input
+                            disabled
+                            :rows="3"
+                            v-model="pkgForm.packageName"
+                            style="width: 90%;margin-top: 1%;"
+                        />
+                    </el-form-item>
+                    <el-form-item
+                        label="疗法封面:"
+                        label-width="100px"
+                    >
+                        <ImageUpload
+                            :disabled="true"
+                            v-model="pkgForm.packageImgUrl"
+                            type="image"
+                            :num="1"
+                            :file-size="1"
+                            :width="150"
+                            :height="150"
+                            style="margin-top: 1%;"
+                        />
+                    </el-form-item>
+                </div>
+            </el-form>
+            <!-- 直播 -->
+<!--            <el-form v-else-if="welcomeItem.contentType == '21'" ref="liveForm" :model="liveForm" :rules="liveFormRule" label-width="110px">-->
+<!--                <div>-->
+<!--                    <el-card class="box-card">-->
+<!--                        <el-form-item label="直播间"-->
+<!--                            required-->
+<!--                            style="margin-bottom: 5px;"-->
+<!--                        >-->
+<!--                            <el-select-->
+<!--                                v-model="liveForm.liveId"-->
+<!--                                placeholder="请选择直播间"-->
+<!--                                size="mini"-->
+<!--                                filterable-->
+<!--                                @change="liveChange()"-->
+<!--                            >-->
+<!--                            <el-option-->
+<!--                                v-for="dict in liveList"-->
+<!--                                :key="dict.liveId"-->
+<!--                                :label="dict.liveName"-->
+<!--                                :value="parseInt(dict.liveId)"-->
+<!--                            />-->
+<!--                            </el-select>-->
+<!--                        </el-form-item>-->
+<!--                        <el-form-item label="标题"-->
+<!--                            prop="liveTitle"-->
+<!--                            style="margin-bottom: 5px;"-->
+<!--                        >-->
+<!--                            <el-input-->
+<!--                                v-model="liveForm.liveTitle"-->
+<!--                                placeholder="请输入消息标题,最长为64字节"-->
+<!--                                :rows="2"-->
+<!--                                maxlength="64"-->
+<!--                                type="textarea"-->
+<!--                                disabled-->
+<!--                                @input="checkByteLength(liveForm, 'liveTitle')"-->
+<!--                            />-->
+<!--                        </el-form-item>-->
+<!--                        <el-form-item label="封面"-->
+<!--                            prop="liveImgUrl"-->
+<!--                        >-->
+<!--                            <ImageUpload-->
+<!--                                v-model="liveForm.liveImgUrl"-->
+<!--                                type="image"-->
+<!--                                :num="10"-->
+<!--                                :width="150"-->
+<!--                                :height="150"-->
+<!--                                disabled-->
+<!--                            />-->
+<!--                        </el-form-item>-->
+<!--                    </el-card>-->
+<!--                </div>-->
+<!--            </el-form>-->
+            <div slot="footer" class="dialog-footer" style="text-align: center">
+                <el-button type="primary" @click="confirmUpload">确定</el-button>
+                <el-button @click="cancelUpload">取消</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import { list, getById, deleteByIds, save, setSendStatus } from "@/api/app/welcome";
+import {listRole} from '@/api/app/user/userList';
+import ImageUpload from "@/views/qw/sop/ImageUpload";
+import CusRoleList from '@/views/app/user/CusRoleList.vue';
+import {courseList, videoList} from "@/api/app/course";
+import { getPackageOptions,} from "@/api/app/pkg";
+// import { getLiveOptions,} from "@/api/app/live";
+
+export default {
+    name: "AppWelcome",
+    components: { ImageUpload, CusRoleList },
+    data() {
+        return {
+            loading: true,
+            exportLoading: false,
+            ids: [],
+            appCustomerSearchConfig: {
+                type: null,
+                visible: false,
+                enableFilter: false,
+                selected: [],
+            },
+            appCustomerBindConfig: {
+                type: null,
+                visible: false,
+                enableFilter: false,
+                selected: [],
+            },
+            addForm: {
+                visible: false,
+                title: "",
+            },
+            contentTypeOptions: [],
+            courseForm: {
+                courseId: null,//科普-科普id
+                videoId: null,//科普-科普小结id
+                expireDays: 30,//科普-科普小节过期时间(天)
+                courseTitle: null,//科普-科普小结标题
+                courseLoading: false,
+            },
+            courseFormRule: {
+                courseId: [
+                    { required: true, message: "请选择科普信息", trigger: "blur" }
+                ],
+                videoId: [
+                    { required: true, message: "请选择科普信息", trigger: "blur" }
+                ],
+                courseTitle: [
+                    { required: true, message: "请输入科普标题", trigger: "blur" }
+                ]
+            },
+            imageForm: {
+                imgUrl: null,//图片url
+            },
+            pkgList: [],
+            pkgForm: {
+                packageId: null,
+                packageName: null,
+                packageImgUrl: null,
+                loading: false,
+            },
+            pkgFormRule: {},
+            imageFormRule: {},
+            liveList: [],
+            liveForm: {
+                liveId: null,
+                liveTitle: null,
+                liveImgUrl: null,
+            },
+            liveFormRule: {},
+            courseList: [],
+            videoList: [],
+            welcomeItem: {
+                visible: false,//控制添加附件对话框是否显示
+                title: null,//对话框标题
+                contentType: null,//附件类型
+                index: -1,//新增,还是编辑,-1即为新增,>-1即为编辑
+            },
+            showSearch: true,
+            total: 0,
+            welcomeList: [],
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                customerRoles: [],
+                createTime: null,
+                sendStatus: null,
+            },
+            form: {},
+            rules: {
+                welcomeText: [
+                    { required: true, message: "消息内容不能为空", trigger: "submit" }
+                ],
+            },
+        };
+    },
+    async created() {
+        let defaultCourse = await this.getCourseList(null, null);
+        this.courseList = defaultCourse.list;
+        let contentTypeOptions = await this.getDicts("app_sop_plugin_settingType");
+        this.contentTypeOptions = contentTypeOptions.data;
+        this.getList();
+        let defaultPkg = await this.getPkgList(null, null);
+        this.pkgList = defaultPkg.data;
+        // let defaultLive = await this.getLiveList();
+        // this.liveList = defaultLive.data;
+    },
+    methods: {
+        /**
+         * 获取科普信息
+         */
+        async getCourseList(keyword, selectedId) {
+            let data = {
+                keyword: keyword,
+                selectedId: selectedId,
+                pageNum: 1,
+                pageSize: 50,
+            };
+            return await courseList(data);
+        },
+        /**
+         * 获取疗信息
+         */
+        async getPkgList(keyword, selectedId) {
+            let data = {
+                keyword: keyword,
+                metaId: selectedId,
+                limit: 50
+            };
+            return await getPackageOptions(data);
+        },
+        /**
+         * 获取直播间信息
+         */
+        // async getLiveList() {
+        //     return await getLiveOptions();
+        // },
+        /**
+         * 疗法下拉搜索
+         * @param keyword
+         */
+        async handlePkgRemoteSearch(keyword) {
+            this.pkgForm.pkgLoading = true;
+            // 模拟远程接口请求(替换为你的真实接口)
+            let cl = await this.getPkgList(keyword ? null : keyword, null);
+            this.pkgForm.pkgLoading = false;
+            this.pkgList = cl.data;
+        },
+        /**
+         * 疗法变动处理
+         */
+        async pkgChangeUpdate() {
+            this.pkgForm.packageName = null;
+            this.pkgForm.packageImgUrl = null;
+            if (this.pkgForm.packageId) {
+                // 查找选中疗法的信息
+                const selectedPkg = this.pkgList.find(pkg => parseInt(pkg.packageId) === this.pkgForm.packageId);
+                this.pkgForm.packageName = selectedPkg.packageName;
+                this.pkgForm.packageImgUrl = selectedPkg.imgUrl;
+            }
+        },
+        // /**
+        //  * 直播间切换
+        //  */
+        // liveChange() {
+        //     this.liveForm.liveTitle = null;
+        //     this.liveForm.liveImgUrl = null;
+        //     const selectedLive = this.liveList.find(live => live.liveId === this.liveForm.liveId);
+        //     if (selectedLive) {
+        //         this.liveForm.liveTitle = selectedLive.liveName; // 自动填充标题
+        //         this.liveForm.liveImgUrl = selectedLive.liveImgUrl; // 自动填充封面
+        //     }
+        // },
+        /**
+         * 搜索条件:客服选中确认回调
+         */
+        customerSelectedConfirmCallForSearch() {
+            this.appCustomerSearchConfig.visible = false;
+        },
+        /**
+         * 新增/编辑:客服选中确认回调
+         */
+        customerSelectedConfirmCallForBind() {
+            this.appCustomerBindConfig.visible = false;
+        },
+        /**
+         * 搜索条件:客服选中变更回调
+         * @param config
+         */
+        customerSelectionChangeCallForSearch(config) {
+            let selected = config.selected;
+            //单行操作
+            if (config.mode === 0) {
+                let row = config.row;
+                if (selected) {
+                    if (!this.appCustomerSearchConfig.selected.some(s => s.id == row.id)) {
+                        this.appCustomerSearchConfig.selected.push(row);
+                    }
+                } else {
+                    this.appCustomerSearchConfig.selected = this.appCustomerSearchConfig.selected.filter(m => m.id != row.id);
+                }
+            }
+            //操作当前页所有行
+            else {
+                let list = config.rows;
+                if (selected) {
+                    list.map(l => {
+                        if (!this.appCustomerSearchConfig.selected.some(s => s.id == l.id)) {
+                            this.appCustomerSearchConfig.selected.push(l);
+                        }
+                    });
+                } else {
+                    list.map(l => {
+                        for (let i = 0; i < this.appCustomerSearchConfig.selected.length; i++) {
+                            if (l.id == this.appCustomerSearchConfig.selected[i].id) {
+                                this.appCustomerSearchConfig.selected.splice(i, 1);
+                            }
+                        }
+                    })
+                }
+            }
+        },
+        /**
+         * 新增/编辑:客服选中变更回调
+         * @param config
+         */
+        customerSelectionChangeCallForBind(config) {
+            let selected = config.selected;
+            //单行操作
+            if (config.mode === 0) {
+                let row = config.row;
+                if (selected) {
+                    if (!this.appCustomerBindConfig.selected.some(s => s.id == row.id)) {
+                        this.appCustomerBindConfig.selected.push(row)
+                    }
+                } else {
+                    this.appCustomerBindConfig.selected = this.appCustomerBindConfig.selected.filter(m => m.id != row.id);
+                }
+            }
+            //操作当前页所有行
+            else {
+                let list = config.rows;
+                if (selected) {
+                    list.map(l => {
+                        if (!this.appCustomerBindConfig.selected.some(s => s.id == l.id)) {
+                            this.appCustomerBindConfig.selected.push(l)
+                        }
+                    });
+                } else {
+                    list.map(l => {
+                        for (let i = 0; i < this.appCustomerBindConfig.selected.length; i++) {
+                            if (l.id == this.appCustomerBindConfig.selected[i].id) {
+                                this.appCustomerBindConfig.selected.splice(i, 1);
+                            }
+                        }
+                    })
+                }
+            }
+        },
+        /**
+         * 打开选择客服页面
+         * @param mode 0-搜索,1-新增/编辑
+         */
+        openCustomerPage(mode) {
+            if (mode == 0) {
+                this.appCustomerSearchConfig.visible = true;
+            } else {
+                this.appCustomerBindConfig.visible = true;
+            }
+        },
+        /**
+         * 删除客服
+         * @param cus
+         * @param mode
+         */
+        handleCloseCustomer(cus, mode) {
+            if (mode == 0) {
+                this.appCustomerSearchConfig.selected = this.appCustomerSearchConfig.selected.filter(item => item.id != cus.id);
+            } else {
+                this.appCustomerBindConfig.selected = this.appCustomerBindConfig.selected.filter(item => item.id != cus.id);
+            }
+        },
+        getList() {
+            this.loading = true;
+            list(this.queryParams).then(response => {
+                this.welcomeList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        /**
+         * 检查长度
+         * @param formInfo
+         * @param key
+         */
+        checkByteLength(formInfo, key) {
+            const text = formInfo[key];
+            const byteLength = this.getByteLength(text);
+            if (byteLength > 64) {
+                this.$set(formInfo, key, this.truncateTextByByteLength(text, 64));
+            }
+        },
+        /**
+         * 获取字节长度
+         * @param text
+         */
+        getByteLength(text) {
+            return new Blob([text]).size;
+        },
+        /**
+         * 截断字节长度
+         * @param text
+         * @param maxByteLength
+         */
+        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;
+        },
+        /**
+         * 点击具体的欢迎语选项进行新增附件
+         * @param command
+         */
+        handleCommand(command) {
+            if (this.form.attachments.length >= 9) {
+                return this.$message.error('附件数量已达上限,无法添加更多附件');
+            }
+            this.welcomeItem = {
+                visible: true,
+                title: '添加' + this.getTitleByCommand(command),
+                contentType: command,
+                index: -1,
+            };
+        },
+        /**
+         * 根据消息类型编码获取消息类型名称
+         * @param command
+         */
+        getTitleByCommand(command) {
+            let option = this.contentTypeOptions.find(cto => cto.dictValue == command);
+            return option.dictLabel;
+        },
+        /**
+         *
+         */
+        async handleCourseRemoteSearch(keyword, courseForm) {
+            this.welcomeItem.courseLoading = true;
+            // 模拟远程接口请求(替换为你的真实接口)
+            let cl = await this.getCourseList((keyword ? keyword : null), courseForm.courseId);
+            this.courseList = cl.list;
+            this.welcomeItem.courseLoading = false;
+        },
+        /**
+         * 科普变化
+         * @param form
+         */
+        courseChange(form) {
+            this.$set(form, 'videoId', null);
+            this.$set(form, 'courseTitle', null);
+            this.videoList = [];
+            if (form.courseId != null) {
+                videoList({courseId: form.courseId}).then(response => {
+                    this.videoList = response.list;
+                });
+            }
+        },
+        /**
+         * 科普小节改变
+         * @param form
+         * @param type
+         */
+        videoIdChange(form, type) {
+            if (form.videoId != null) {
+                let selectedVideo = this.videoList.find(course => course.dictValue == form.videoId);
+                //科普
+                if (selectedVideo && type == '9') {
+                    this.$set(form, 'courseTitle', this.truncateTextByByteLength(selectedVideo.dictLabel, 64));
+                    this.$set(form, 'expireDays', 30);
+                }
+            }
+        },
+        /**
+         * 编辑附件信息
+         * @param item
+         */
+        async editFileItem(item, index) {
+            this.welcomeItem = {
+                visible: true,
+                title: '编辑' + this.getTitleByCommand(item.contentType),
+                contentType: item.contentType,
+                index,
+            };
+            //图片
+             if (item.contentType == '2') {
+                this.imageForm = JSON.parse(JSON.stringify(item.image));//深拷贝
+            }
+            // 科普
+            else if (item.contentType == '9') {
+                this.courseForm = JSON.parse(JSON.stringify(item.course));//深拷贝
+                if (item.course.courseId) {
+                    let cl = await this.getCourseList(null, this.courseForm.courseId);
+                    this.courseList = cl.list;
+                    videoList({courseId: item.course.courseId}).then(response => {
+                        this.videoList = response.list;
+                    });
+                }
+            }
+        },
+        /**
+         * 移除欢迎语附件
+         * @param index
+         */
+        removeFileItem(index) {
+            this.form.attachments.splice(index, 1);
+        },
+        /**
+         * 确认添加附件
+         */
+        confirmUpload() {
+            const { contentType, index } = this.welcomeItem;
+            //科普
+            if (contentType == '9') {
+                this.$refs["courseForm"].validate(valid => {
+                    if (valid) {
+                        this.addFile(index, {
+                            contentType: 9,
+                            course: {...this.courseForm},
+                        });
+                    }
+                });
+            }
+            //图片
+            else if (contentType == '2') {
+                this.$refs["imageForm"].validate(valid => {
+                    if (valid) {
+                        this.addFile(index, {
+                            contentType: 2,
+                            image: {...this.imageForm},
+                        });
+                    }
+                });
+            }
+            //疗法
+            else if (contentType == '20') {
+                this.$refs["pkgForm"].validate(valid => {
+                    if (valid) {
+                        this.addFile(index, {
+                            contentType: 20,
+                            pkg: {...this.pkgForm},
+                        });
+                    }
+                });
+            }
+            //直播
+            else if (contentType == '21') {
+                this.$refs["liveForm"].validate(valid => {
+                    if (valid) {
+                        this.addFile(index, {
+                            contentType: 21,
+                            live: {...this.liveForm},
+                        });
+                    }
+                });
+            }
+        },
+        /**
+         * 添加附件
+         * @param index
+         * @param attachment
+         */
+        addFile(index, attachment) {
+            //编辑模式:执行替换
+            if (index > -1) {
+                this.form.attachments.splice(index, 1, attachment);
+            }
+            //新增模式:直接追加
+            else {
+                this.form.attachments.push(attachment);
+            }
+            this.resetFileForm();
+            this.welcomeItem.visible = false;
+        },
+        /**
+         * 取消添加附件
+         */
+        cancelUpload() {
+            this.resetFileForm();
+            this.welcomeItem.visible = false;
+        },
+        /**
+         * 取消新增欢迎语
+         */
+        cancel() {
+            this.addForm.visible = false;
+            this.reset();
+        },
+        /**
+         * 重置附件表单
+         */
+        resetFileForm() {
+            //清空科普表单信息
+            for (let key in this.courseForm) {
+                this.courseForm[key] = null;
+            }
+            //清空图片表单信息
+            for (let key in this.imageForm) {
+                this.imageForm[key] = null;
+            }
+            this.welcomeItem = {
+                visible: false,
+                title: null,
+                contentType: null,
+                index: -1,
+            };
+            this.videoList = [];
+        },
+        /**
+         * 切换发送状态
+         * @param val
+         */
+        async toggleSendStatus(val, row) {
+            console.log(row)
+            let activeName = val == 0 ? '关闭' : '开启';
+            this.$confirm(`是否确认${activeName}发送?`, "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(async res => {
+                await setSendStatus({id: row.id, sendStatus: val});
+                this.msgSuccess(`${activeName}成功!`);
+                this.getList();
+            }).catch(() => {
+                this.getList();
+            });
+        },
+        /**
+         * 重置新增欢迎语表单
+         */
+        reset() {
+            this.form = {
+                id: null,
+                attachments: [],
+                sendStatus: 1,
+                welcomeText: null,
+            };
+            this.appCustomerBindConfig.selected = [];
+            this.resetForm("form");
+            this.resetFileForm();
+        },
+        /**
+         * 执行查询
+         */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.queryParams.customerRoles = this.appCustomerSearchConfig.selected.map(s => s.id) || null;
+            this.getList();
+        },
+        /**
+         * 重置查询
+         */
+        resetQuery() {
+            this.resetForm("queryForm");
+            this.appCustomerSearchConfig.selected = [];
+            this.handleQuery();
+        },
+        /**
+         * 新增欢迎语
+         */
+        handleAdd() {
+            this.reset();
+            this.addForm.visible = true;
+            this.addForm.title = "新建欢迎语";
+        },
+        /**
+         * 编辑欢迎语
+         * @param row
+         */
+        async handleUpdate(row) {
+            this.reset();
+            const id = row.id;
+            let info = await getById(id);
+            let data = info.data;
+            if (data.attachments) {
+                data.attachments = JSON.parse(data.attachments);
+            }
+            if (data.customerRoles) {
+                let roleList = await listRole({ids: data.customerRoles.split(',')});
+                this.appCustomerBindConfig.selected = roleList.rows;
+            }
+            this.form = data;
+            this.addForm.visible = true;
+            this.addForm.title = "修改欢迎语";
+        },
+        /**
+         * 提交欢迎语表单
+         */
+        submitForm() {
+            this.loading = true;
+            this.$refs["form"].validate(valid => {
+                if (valid) {
+                    this.commitForm();
+                }
+            });
+        },
+        /**
+         * 提交表单
+         */
+        async commitForm() {
+            const requestData = { ...this.form };
+            requestData['customerRoles'] = this.appCustomerBindConfig.selected.map(s => s.id);
+            requestData.attachments = JSON.stringify(this.form.attachments);
+            await save(requestData);
+            this.reset();
+            this.addForm.visible = false;
+            this.getList();
+        },
+        /**
+         * 删除
+         * @param row
+         */
+        handleDelete(row) {
+            const ids = row.id || this.ids;
+            this.$confirm('是否确认删除欢迎语:编号为"' + ids + '"的数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(function () {
+                return deleteByIds(ids);
+            }).then(() => {
+                this.getList();
+                this.msgSuccess("删除成功");
+            }).catch(() => { });
+        },
+    }
+};
+</script>
+
+<style>
+.text-container {
+    max-height: 7.5em;
+    overflow-y: auto;
+    line-height: 1.5em;
+}
+</style>

+ 864 - 0
src/views/course/courseWatchLog/indexApp.vue

@@ -0,0 +1,864 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="会员ID" prop="userId">
+        <el-input
+          v-model="queryParams.userId"
+          placeholder="请输入会员ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="会员昵称" prop="nickName">
+        <el-input
+          v-model="queryParams.nickName"
+          placeholder="请输入会员昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="所属客服" prop="appCustomerId">
+        <el-select v-model="queryParams.appCustomerId" clearable filterable remote
+                   placeholder="请输入关键词" :remote-method="loadCompanyUserOptions"
+                   v-select-load-more="loadMoreCompanyUserOptions"
+                   :loading="companyUserOptionsLoading">
+          <el-option
+            v-for="item in companyUserOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="科普" prop="courseId">
+        <el-select filterable  v-model="queryParams.courseId" placeholder="请选择科普"  clearable size="small" @change="courseChange(queryParams.courseId)">
+          <el-option
+            v-for="dict in courseLists"
+            :key="dict.dictValue"
+            :label="dict.remark"
+            :value="dict.dictValue"
+          >
+          <!-- <el-tooltip
+        v-if="dict.remark"
+        :content="dict.remark"
+        placement="right"
+        effect="dark"
+        :disabled="!dict.remark"
+      >
+        <span>{{ dict.dictLabel }}</span>
+      </el-tooltip>
+      <span v-else>{{ dict.dictLabel }}</span> -->
+        </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="小节" prop="videoId">
+        <el-select filterable  v-model="queryParams.videoId" placeholder="请选择小节"  clearable size="small">
+          <el-option
+            v-for="dict in videoList"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <!-- <el-form-item label="是否为会员" prop="isVip">
+        <el-select
+          filterable
+          v-model="queryParams.isVip"
+          placeholder="请选择是否为会员"
+          clearable size="small">
+          <el-option
+            v-for="dict in isVipList"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item> -->
+      
+      <el-form-item label="营期时间" prop="scheduleTime">
+        <el-date-picker
+          v-model="scheduleTime"
+          type="daterange"
+          size="small"
+          style="width: 240px"
+          value-format="yyyy-MM-dd"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          @change="handleScheduleTimeChange">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker v-model="createTime" size="small" style="width: 220px" value-format="yyyy-MM-dd" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" @change="createChange"></el-date-picker>
+      </el-form-item>
+      <el-form-item label="最新更新时间" prop="updateTime">
+        <el-date-picker v-model="updateTime" size="small" style="width: 220px" value-format="yyyy-MM-dd" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" @change="updateChange"></el-date-picker>
+      </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="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['course:courseWatchLog:export']"
+        >导出</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:bindTag']"
+          @click="openBindTagPage(false, 0)"
+        >批量添加标签
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:unbindTag']"
+          @click="openUnbindTagPage(false, 1)"
+        >批量移除标签
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:batchBindTag']"
+          @click="openBindTagPage(true, 0)"
+        >批量添加标签(筛选条件)
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:batchUnbindTag']"
+          @click="openUnbindTagPage(true, 1)"
+        >批量移除标签(筛选条件)
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          size="mini"
+          @click="openRemarkPage(false)"
+        >批量修改备注
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          size="mini"
+          @click="openRemarkPage(true)"
+        >批量修改备注(筛选条件)
+        </el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-tabs type="card" v-model="activeName" @tab-click="handleClickX">
+      <el-tab-pane label="全部" name="00"></el-tab-pane>
+      <el-tab-pane v-for="(item,index) in logTypeOptions" :label="item.dictLabel" :name="item.dictValue"></el-tab-pane>
+    </el-tabs>
+    <el-table border v-loading="loading" :data="courseWatchLogList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="记录编号" align="center" prop="logId" />
+      <el-table-column label="客户备注" align="center" prop="remarkName"/>
+      <el-table-column label="会员ID" align="center" prop="userId" />
+      <el-table-column label="会员昵称" align="center" prop="fsNickName">
+        <template slot-scope="scope">
+          <div style="display: flex;white-space: nowrap">
+            <div style="margin: auto">
+              {{scope.row.fsNickName}}
+            </div>
+            <el-popover
+              placement="right"
+              title=""
+              trigger="hover">
+              <img slot="reference" :src="scope.row.fsAvatar" style="width: 30px;height: 30px">
+              <img :src="scope.row.fsAvatar" style="max-width: 200px;max-height: 200px">
+            </el-popover>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="科普名称" align="center" prop="courseName" />
+      <el-table-column label="小节名称" align="center" prop="videoName" />
+      <el-table-column label="记录类型" align="center" prop="logType">
+        <template slot-scope="scope">
+          <dict-tag :options="logTypeOptions" :value="scope.row.logType"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="播放时长" align="center" prop="duration" />
+      <el-table-column label="所属客服" align="center" prop="companyUserName" />
+      <el-table-column label="创建时间" align="center" prop="createTime" />
+      <el-table-column label="更新时间" align="center" prop="updateTime" />
+      <el-table-column label="完课时间" align="center" prop="finishTime" />
+      
+      <el-table-column label="营期时间" align="center" prop="campPeriodTime" />
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-dialog :title="tagConfig.title" :visible.sync="tagConfig.visible" width="800px" append-to-body>
+      <div>搜索标签:
+        <el-input v-model="tagConfig.queryParams.groupName" placeholder="请输入标签名称" clearable size="small"
+          style="width: 200px;margin-right: 10px"/>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleSearchTags">搜索</el-button>
+        <el-button type="primary" icon="el-icon-refresh" size="mini" @click="cancelSearchTags">重置</el-button>
+      </div>
+      <div class="dialog-body">
+        <div v-for="groupItem in tagConfig.tagGroupList" :key="groupItem.id">
+          <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+            <span class="name-background">{{ groupItem.name }}</span>
+          </div>
+          <div class="tag-container">
+            <a
+              v-for="tagItem in groupItem.tags"
+              :key="tagItem.id"
+              class="tag-box"
+              @click="tagSelection(tagItem)"
+              :class="{ 'tag-selected': tagItem.isSelected }"
+            >
+              {{ tagItem.name }}
+            </a>
+          </div>
+        </div>
+      </div>
+      <pagination
+        v-show="tagConfig.total > 0"
+        :total="tagConfig.total"
+        :page.sync="tagConfig.queryParams.pageNum"
+        :limit.sync="tagConfig.queryParams.pageSize"
+        @pagination="getTagGroupList"
+      />
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="bindOrUnbindTagSubmit">确 定</el-button>
+        <el-button @click="bindTagCancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog :title="remarkConfig.title" :visible.sync="remarkConfig.visible" width="600px" append-to-body>
+      <el-form label-width="90px">
+        <el-form-item label="备注类型">
+          <el-radio-group v-model="remarkConfig.remarkType">
+            <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="备注内容">
+          <el-input
+            v-model="remarkConfig.remark"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入备注信息"
+            maxlength="30"
+            show-word-limit
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" :loading="remarkConfig.loading" @click="submitRemark">确 定</el-button>
+        <el-button @click="cancelRemark">取 消</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { listAppCourseWatchLog, exportAppCourseWatchLog, getCustomerListLikeName, batchUpdateFriendRemark } from "@/api/course/courseWatchLog";
+import { courseList,videoList } from '../../../api/course/courseRedPacketLog'
+import {getTask} from "@/api/common";
+import { listTagGroupForUserBindTag } from "@/api/app/tag/tagGroup";
+import {
+  bindTagV2,
+  unbindTagV2,
+} from "@/api/app/tag/bindTag";
+export default {
+  name: "CourseWatchLog",
+  data() {
+    return {
+      activeName:"00",
+      createTime:null,
+      updateTime:null,
+      courseLists:[],
+      videoList:[],
+      logTypeOptions:[],
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组(会员userId)
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 短链科普学习记录表格数据
+      courseWatchLogList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      isVipList: [
+        { dictLabel: '是', dictValue: 1 },
+        { dictLabel: '否', dictValue: 0 }
+      ],
+      watchTypeList: [
+        { dictLabel: 'app', dictValue: 1 },
+        { dictLabel: '小程序', dictValue: 2 }
+      ],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: null,
+        nickName: null,
+        videoId: null,
+        logType: null,
+        qwExternalContactId: null,
+        externalUserName:null,
+        duration: null,
+        qwUserId: null,
+        qwUserName: null, //企微名称
+        qwUserUserId: null, //企微id
+        deptName: null, //部门名称
+        companyUserId: null,
+        companyId: null,
+        courseId: null,
+        sTime:null,
+        eTime:null,
+        upSTime:null,
+        upETime:null,
+        scheduleStartTime: null,
+        scheduleEndTime: null,
+        isVip: null,
+        watchType:null,
+        appCustomerId: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      },
+      scheduleTime: null,
+      // 员工选项列表
+      companyUserOptionsParams: {
+        name: undefined,
+        hasNextPage: false,
+        pageNum: 1,
+        pageSize: 10
+      },
+      companyUserOptionsLoading: false,
+      companyUserOptions: [],
+      tagConfig: {
+        visible: false,
+        enableFilter: false,
+        title: null,
+        opType: 0,//0-添加,1-移除
+        queryParams: {
+          groupName: null,
+          pageNum: 1,
+          pageSize: 10,
+        },
+        total: 0,
+        tagGroupList: [],
+        selected: [],
+      },
+      remarkConfig: {
+        visible: false,
+        enableFilter: false,
+        title: '批量修改备注',
+        remarkType: 1,
+        remark: '',
+        loading: false,
+      },
+    };
+  },
+  created() {
+    courseList().then(response => {
+      this.courseLists = response.list;
+    });
+    this.getList();
+    this.getDicts("sys_course_watch_log_type").then(response => {
+      this.logTypeOptions = response.data;
+    });
+  },
+  methods: {
+    courseChange(row){
+      this.queryParams.videoId=null;
+      if(row === ''){
+        this.videoList=[];
+        return
+      }
+      videoList(row).then(response => {
+        this.videoList=response.list
+      });
+    },
+    createChange() {
+      if (this.createTime != null) {
+        this.queryParams.sTime = this.createTime[0];
+        this.queryParams.eTime = this.createTime[1];
+      } else {
+        this.queryParams.sTime = null;
+        this.queryParams.eTime = null;
+      }
+    },
+
+    updateChange(){
+      if (this.updateTime != null) {
+        this.queryParams.upSTime = this.updateTime[0];
+        this.queryParams.upETime = this.updateTime[1];
+      } else {
+        this.queryParams.upSTime = null;
+        this.queryParams.upETime = null;
+      }
+    },
+    handleClickX(tab,event){
+      this.activeName=tab.name;
+      if(tab.name=="00"){
+        this.queryParams.logType=null;
+      }else{
+        this.queryParams.logType=tab.name;
+      }
+      this.getList()
+    },
+    /** 查询短链科普学习记录列表 */
+    getList() {
+      this.loading = true;
+      if(this.queryParams.logType == "10"){
+        this.queryParams.logType = null;
+      }
+
+      listAppCourseWatchLog(this.queryParams).then(response => {
+        this.courseWatchLogList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        logId: null,
+        userId: null,
+        videoId: null,
+        logType: null,
+        createTime: null,
+        updateTime: null,
+        qwExternalContactId: null,
+        externalUserName:null,
+        duration: null,
+        qwUserId: null,
+        companyUserId: null,
+        companyId: null,
+        courseId: null,
+        scheduleStartTime: null,
+        scheduleEndTime: null,
+      };
+      this.scheduleTime=null;
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.createTime = null;
+      this.scheduleTime = null;
+      this.queryParams.sTime = null;
+      this.queryParams.eTime = null;
+      this.queryParams.upSTime = null;
+      this.queryParams.upETime = null;
+      this.queryParams.scheduleStartTime = null;
+      this.queryParams.scheduleEndTime = null;
+      this.scheduleTime=null;
+      this.updateTime=null;
+      this.handleQuery();
+    },
+    // 多选框选中数据(按会员userId去重,用于批量打标签)
+    handleSelectionChange(selection) {
+      this.ids = [...new Set(selection.map(item => item.userId).filter(id => id != null))]
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /**
+     * 用户绑定标签页面
+     */
+    openBindTagPage(enableFilter = false, opType = 0) {
+      this.tagConfig.enableFilter = enableFilter;
+      this.tagConfig.opType = opType;
+      if (!enableFilter && (this.ids === null || this.ids.length === 0)) {
+        this.$message.closeAll();
+        return this.$message('请选择需要添加标签的用户');
+      }
+      this.getTagGroupList();
+      this.tagConfig.title = '批量添加标签' + (enableFilter ? '(筛选条件)' : '');
+      this.tagConfig.visible = true;
+    },
+    /**
+     * 用户解绑标签页面
+     */
+    openUnbindTagPage(enableFilter = false, opType = 1) {
+      this.tagConfig.enableFilter = enableFilter;
+      this.tagConfig.opType = opType;
+      if (!enableFilter && (this.ids === null || this.ids.length === 0)) {
+        this.$message.closeAll();
+        return this.$message('请选择需要移除标签的用户');
+      }
+      this.getTagGroupList();
+      this.tagConfig.title = '批量移除标签' + (enableFilter ? '(筛选条件)' : '');
+      this.tagConfig.visible = true;
+    },
+    /**
+     * 绑定/解绑 标签页面搜索
+     */
+    handleSearchTags() {
+      this.tagConfig.queryParams.pageNum = 1;
+      this.getTagGroupList();
+    },
+    /**
+     * 绑定/解绑 标签页面重置
+     */
+    cancelSearchTags() {
+      this.tagConfig.queryParams = {
+        groupName: null,
+        pageNum: 1,
+        pageSize: 10,
+      }
+      this.getTagGroupList();
+    },
+    /**
+     * 切换选中状态
+     * @param row
+     */
+    tagSelection(row) {
+      row.isSelected = !row.isSelected;
+      if (row.isSelected) {
+        this.tagConfig.selected.push({ tagId: row.id, tagName: row.name });
+      } else {
+        this.tagConfig.selected = this.tagConfig.selected.filter(item => item.tagId !== row.id);
+      }
+      this.$forceUpdate();
+    },
+    /**
+     * 重置 绑定/解绑 标签参数
+     */
+    resetBindTag() {
+      this.tagConfig = {
+        visible: false,
+        enableFilter: false,
+        title: null,
+        opType: 0,
+        queryParams: {
+          groupName: null,
+          pageNum: 1,
+          pageSize: 10,
+        },
+        total: 0,
+        tagGroupList: [],
+        selected: [],
+      }
+    },
+    /**
+     * 绑定/解绑 标签-提交
+     */
+    bindOrUnbindTagSubmit() {
+      let opType = this.tagConfig.opType;
+      let tmpParam = {};
+      if (!this.tagConfig.selected || this.tagConfig.selected.length === 0) {
+        return this.$message('请选择标签');
+      }
+      tmpParam['enableFilter'] = this.tagConfig.enableFilter;
+      tmpParam['tagId'] = this.tagConfig.selected.map(item => item.tagId);
+      tmpParam['userId'] = this.ids;
+      const queryParams = JSON.parse(JSON.stringify(this.queryParams));
+      delete queryParams['startIndex'];
+      delete queryParams['pageLimit'];
+      tmpParam['fsUserParam'] = queryParams;
+      //绑定标签
+      if (opType === 0) {
+        bindTagV2(tmpParam)
+          .then(res => {
+            this.tagConfig.visible = false;
+            this.resetBindTag();
+            this.msgSuccess("正在添加中!");
+            setTimeout(() => {
+              this.getList();
+            }, 500);
+          })
+          .catch(err => {
+            this.msgError("添加标签失败!");
+          })
+      }
+      //解绑标签
+      else {
+        unbindTagV2(tmpParam)
+          .then(res => {
+            this.tagConfig.visible = false;
+            this.msgSuccess("正在移除中!");
+            this.resetBindTag();
+            setTimeout(() => {
+              this.getList();
+            }, 500);
+          })
+          .catch(err => {
+            this.msgError("移除标签失败!");
+          })
+      }
+    },
+    /**
+     * 绑定/解绑 标签-取消
+     */
+    bindTagCancel() {
+      this.resetBindTag();
+    },
+    /**
+     * 查询标签列表
+     */
+    getTagGroupList() {
+      listTagGroupForUserBindTag(this.tagConfig.queryParams).then(response => {
+        this.tagConfig.tagGroupList = response.rows;
+        this.tagConfig.total = response.total;
+      });
+    },
+    /**
+     * 打开批量修改备注弹窗
+     * @param enableFilter 是否按筛选条件
+     */
+    openRemarkPage(enableFilter = false) {
+      if (!enableFilter && (!this.ids || this.ids.length === 0)) {
+        this.$message.closeAll();
+        return this.$message('请选择需要修改备注的用户');
+      }
+      if (enableFilter && (!this.total || this.total <= 0)) {
+        this.$message.closeAll();
+        return this.$message('当前筛选条件下没有数据');
+      }
+      this.remarkConfig.enableFilter = enableFilter;
+      this.remarkConfig.remarkType = 1;
+      this.remarkConfig.remark = '';
+      this.remarkConfig.loading = false;
+      this.remarkConfig.title = '批量修改备注' + (enableFilter ? '(筛选条件)' : '');
+      this.remarkConfig.visible = true;
+    },
+    cancelRemark() {
+      this.remarkConfig.visible = false;
+      this.remarkConfig.enableFilter = false;
+      this.remarkConfig.remarkType = 1;
+      this.remarkConfig.remark = '';
+      this.remarkConfig.loading = false;
+    },
+    /**
+     * 提交批量修改备注
+     */
+    async submitRemark() {
+      const remark = (this.remarkConfig.remark || '').trim();
+      if (!remark) {
+        return this.$message.error('请输入备注内容');
+      }
+      if (remark.length > 30) {
+        return this.$message.error('备注内容不能超过30个字符');
+      }
+      this.remarkConfig.loading = true;
+      try {
+        let userIds = [];
+        if (this.remarkConfig.enableFilter) {
+          // 按筛选条件拉取全部匹配记录的 userId
+          const queryParams = JSON.parse(JSON.stringify(this.queryParams));
+          queryParams.pageNum = 1;
+          queryParams.pageSize = this.total > 0 ? this.total : 10000;
+          if (queryParams.logType == "10") {
+            queryParams.logType = null;
+          }
+          const response = await listAppCourseWatchLog(queryParams);
+          const rows = response.rows || [];
+          userIds = [...new Set(rows.map(item => item.userId).filter(id => id != null))];
+        } else {
+          userIds = this.ids;
+        }
+        if (!userIds || userIds.length === 0) {
+          this.remarkConfig.loading = false;
+          return this.$message.error('没有可修改备注的用户');
+        }
+        await batchUpdateFriendRemark({
+          remarkType: this.remarkConfig.remarkType,
+          remark,
+          userId: userIds,
+        });
+        this.msgSuccess('正在修改备注中!');
+        this.cancelRemark();
+        setTimeout(() => {
+          this.getList();
+        }, 500);
+      } catch (e) {
+        this.msgError('修改备注失败!');
+      } finally {
+        this.remarkConfig.loading = false;
+      }
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加短链科普学习记录";
+    },
+   
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.logId != null) {
+            updateCourseWatchLog(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addCourseWatchLog(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    
+    /** 导出按钮操作 */
+    handleExport() {
+      const that = this
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有短链科普学习记录数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportAppCourseWatchLog(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    },
+    handleScheduleTimeChange(val) {
+      if (val) {
+        this.queryParams.scheduleStartTime = val[0];
+        this.queryParams.scheduleEndTime = val[1];
+      } else {
+        this.queryParams.scheduleStartTime = null;
+        this.queryParams.scheduleEndTime = null;
+      }
+    },
+    /**
+     * 根据名称模糊查询用户列表
+     * @param query 参数
+     */
+    loadCompanyUserOptions(query) {
+      this.companyUserOptions = [];
+      if (query === '') {
+        return;
+      }
+
+      this.companyUserOptionsParams.pageNum = 1
+      this.companyUserOptionsParams.name = query
+      this.companyUserOptionsLoading = true;
+      this.getCompanyUserListLikeName()
+    },
+    /**
+     * 获取员工列表
+     */
+    getCompanyUserListLikeName() {
+      getCustomerListLikeName(this.companyUserOptionsParams).then(response => {
+        this.companyUserOptions = [...this.companyUserOptions, ...response.data.list]
+        this.companyUserOptionsParams.hasNextPage = response.data.hasNextPage
+        this.companyUserOptionsLoading = false;
+      });
+    },
+    /**
+     * 加载更多员工选项
+     */
+    loadMoreCompanyUserOptions() {
+      if (!this.companyUserOptionsParams.hasNextPage) {
+        return;
+      }
+
+      this.companyUserOptionsParams.pageNum += 1
+      this.getCompanyUserListLikeName()
+    },
+  }
+};
+</script>
+<style lang="scss" scoped>
+.dialog-body {
+  height: 400px;
+  overflow: auto;
+}
+
+.tag-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.name-background {
+  display: inline-block;
+  background-color: #abece6;
+  padding: 4px 8px;
+  border-radius: 4px;
+}
+
+.tag-box {
+  padding: 8px 12px;
+  border: 1px solid #989797;
+  border-radius: 4px;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.tag-selected {
+  background-color: #00bc98;
+  color: #fff;
+  border-color: #00bc98;
+}
+</style>

+ 8 - 8
src/views/index.vue

@@ -1459,7 +1459,7 @@ export default {
           }
           redPackageOption.series[0].data = rewardMoneyList;
 
-          this.answerRedPackViewerChart.setOption(redPackageOption)
+          this.answerRedPackViewerChart && this.answerRedPackViewerChart.setOption(redPackageOption)
         }
       })
     },
@@ -1472,7 +1472,7 @@ export default {
           let option = data.map(e=>[e.x,e.rewardMoney])
           lineChartOption.series[0].data = option;
 
-          this.answerRedPackMoneyViewerChart.setOption(lineChartOption)
+          this.answerRedPackMoneyViewerChart && this.answerRedPackMoneyViewerChart.setOption(lineChartOption)
         }
       })
     },
@@ -1492,7 +1492,7 @@ export default {
           courseWatchOption.series[1].data = completedUserCountList;
           courseWatchOption.series[2].data = answerUserCountList;
           courseWatchOption.series[3].data = correctUserCountList;
-          this.courseWatchChart.setOption(courseWatchOption)
+          this.courseWatchChart && this.courseWatchChart.setOption(courseWatchOption)
         }
       })
     },
@@ -1508,7 +1508,7 @@ export default {
           dealerOption.yAxis.data = companyNameList;
           dealerOption.series[0].data = watchUserList;
 
-          this.dealerChart.setOption(dealerOption)
+          this.dealerChart && this.dealerChart.setOption(dealerOption)
         }
       })
 
@@ -1530,7 +1530,7 @@ export default {
           dealerOptionNew.series[1].data = completedUserCountList;
           dealerOptionNew.xAxis.data = xAxis;
 
-          this.dealerChartNew.setOption(dealerOptionNew);
+          this.dealerChartNew && this.dealerChartNew.setOption(dealerOptionNew);
 
         }
       })
@@ -1553,7 +1553,7 @@ export default {
           thisMonthOrderCountOption.series[1].data = payPrice;
           thisMonthOrderCountOption.xAxis.data = dates;
 
-          this.thisMonthOrderChart.setOption(thisMonthOrderCountOption)
+          this.thisMonthOrderChart && this.thisMonthOrderChart.setOption(thisMonthOrderCountOption)
         }
       })
     },
@@ -1567,7 +1567,7 @@ export default {
           thisMonthRecvCountOption.series[0].data = orderCount;
           thisMonthRecvCountOption.series[1].data = payMoney;
           thisMonthRecvCountOption.xAxis.data = dates;
-          this.thisMonthRecvChart.setOption(thisMonthRecvCountOption)
+          this.thisMonthRecvChart && this.thisMonthRecvChart.setOption(thisMonthRecvCountOption)
         }
       })
     },
@@ -1584,7 +1584,7 @@ export default {
           viewCharOption.series[1].data = completedUserCountList;
           viewCharOption.xAxis.data = xAxis;
 
-          this.viewerChart.setOption(viewCharOption);
+          this.viewerChart && this.viewerChart.setOption(viewCharOption);
         }
       })