瀏覽代碼

cid外呼

wangxy 6 天之前
父節點
當前提交
91f00ce3a5
共有 56 個文件被更改,包括 18680 次插入2883 次删除
  1. 44 0
      src/api/company/companyAccount.js
  2. 33 0
      src/api/company/companyUser.js
  3. 44 0
      src/api/company/companyVoiceRobotic.js
  4. 15 0
      src/api/company/companyWorkflow.js
  5. 16 0
      src/api/crm/customer.js
  6. 10 0
      src/api/crm/customerAnalyze.js
  7. 10 0
      src/api/crm/customerCallLog.js
  8. 29 29
      src/api/crm/customerLogs.js
  9. 9 0
      src/api/crm/customerProperty.js
  10. 62 0
      src/api/sensitive/word.js
  11. 15 0
      src/api/wx/sopUserLogsWx.js
  12. 71 0
      src/api/wx/wxSop.js
  13. 71 0
      src/api/wx/wxSopLogs.js
  14. 53 0
      src/api/wx/wxSopUser.js
  15. 53 0
      src/api/wx/wxSopUserInfo.js
  16. 456 0
      src/views/company/companyClient/indexOld.vue
  17. 182 6
      src/views/company/companyConfig/index.vue
  18. 43 4
      src/views/company/companyUser/index.vue
  19. 120 192
      src/views/company/companyVoiceLogs/index.vue
  20. 307 0
      src/views/company/companyVoiceRobotic/handleManualAnswered.vue
  21. 1071 973
      src/views/company/companyVoiceRobotic/index.vue
  22. 2406 0
      src/views/company/companyVoiceRobotic/myIndex.vue
  23. 139 15
      src/views/company/companyWorkflow/design.vue
  24. 8 6
      src/views/company/companyWorkflow/index.vue
  25. 657 0
      src/views/company/companyWorkflow/myIndex.vue
  26. 60 20
      src/views/company/wxAccount/index.vue
  27. 304 0
      src/views/company/wxAccount/myIndex.vue
  28. 80 0
      src/views/crm/components/AiTagPanel.vue
  29. 607 0
      src/views/crm/components/AppendCustomerSelect.vue
  30. 623 437
      src/views/crm/components/CustomerSelect.vue
  31. 571 0
      src/views/crm/components/CustomerSelectOlde.vue
  32. 63 0
      src/views/crm/components/aiAddWxLog.vue
  33. 63 0
      src/views/crm/components/aiCallVoiceLog.vue
  34. 66 0
      src/views/crm/components/aiSendMsgLog.vue
  35. 848 415
      src/views/crm/components/customerDetails.vue
  36. 773 0
      src/views/crm/components/customerDetailsOld.vue
  37. 2159 0
      src/views/crm/customer/customerDetailOld.vue
  38. 797 664
      src/views/crm/customer/line.vue
  39. 851 0
      src/views/crm/customer/lineOlde.vue
  40. 17 13
      src/views/store/externalOrder/index.vue
  41. 11 7
      src/views/store/financeOrder/index.vue
  42. 42 16
      src/views/store/rejectNoReturn/index.vue
  43. 52 19
      src/views/store/rejectReport/index.vue
  44. 51 17
      src/views/store/returnReport/index.vue
  45. 19 5
      src/views/store/signDetailReport/index.vue
  46. 42 15
      src/views/store/signReport/index.vue
  47. 856 0
      src/views/taskStatistics/callLog/callLogDetail.vue
  48. 697 28
      src/views/taskStatistics/callLog/index.vue
  49. 2 2
      src/views/taskStatistics/wxClient/index.vue
  50. 188 0
      src/views/wx/sopUserLogsWx/detail.vue
  51. 351 0
      src/views/wx/sopUserLogsWx/sopUserLogsScheduleWx.vue
  52. 916 0
      src/views/wx/wxSop/index.vue
  53. 404 0
      src/views/wx/wxSop/sopLogsList.vue
  54. 481 0
      src/views/wx/wxSopLogs/index.vue
  55. 350 0
      src/views/wx/wxSopUser/index.vue
  56. 412 0
      src/views/wx/wxSopUserInfo/index.vue

+ 44 - 0
src/api/company/companyAccount.js

@@ -124,3 +124,47 @@ export function syncWx(query) {
     params: query
   })
 }
+
+// 查询我的个微账号列表
+export function myListCompanyAccount(query) {
+  return request({
+    url: '/company/companyWx/myList',
+    method: 'get',
+    params: query
+  })
+}
+
+// 新增我的个微账号
+export function addMyCompanyAccount(data) {
+  return request({
+    url: '/company/companyWx/myAdd',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改我的个微账号
+export function updateMyCompanyAccount(data) {
+  return request({
+    url: '/company/companyWx/myEdit',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除我的个微账号
+export function delMyCompanyAccount(id) {
+  return request({
+    url: '/company/companyWx/my/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出我的个微账号
+export function exportMyCompanyAccount(query) {
+  return request({
+    url: '/company/companyWx/myExport',
+    method: 'get',
+    params: query
+  })
+}

+ 33 - 0
src/api/company/companyUser.js

@@ -17,6 +17,14 @@ export function qwList(query) {
     params: query
   })
 }
+// 查询企微用户列表
+export function myQwList(query) {
+  return request({
+    url: '/company/user/myQwList',
+    method: 'get',
+    params: query
+  })
+}
 export function getList(query) {
   return request({
     url: '/company/user/getList',
@@ -367,3 +375,28 @@ export function updateCustomerAcquisitionLink(data) {
     data: data
   })
 }
+
+// 查询销售行为分析数据
+export function selectCompanyUserInfo(userId) {
+  return request({
+    url: '/company/user/selectCompanyUserInfo/' + userId,
+    method: 'get'
+  })
+}
+
+// 重新分析销售行为数据
+export function analyseCompanyUserInfo(userId) {
+  return request({
+    url: '/company/user/analyseCompanyUserInfo/' + userId,
+    method: 'get'
+  })
+}
+
+// 上传声纹
+export function addVoicePrintUrl(data) {
+  return request({
+    url: '/company/user/addVoicePrintUrl',
+    method: 'post',
+    data: data
+  })
+}

+ 44 - 0
src/api/company/companyVoiceRobotic.js

@@ -8,6 +8,14 @@ export function listRobotic(query) {
     params: query
   })
 }
+
+export function myListRobotic(query) {
+    return request({
+        url: '/company/companyVoiceRobotic/myList',
+        method: 'get',
+        params: query
+    })
+}
 // 查询机器人外呼任务列表
 export function listAll(query) {
   return request({
@@ -99,6 +107,24 @@ export function stopRobotic(taskId) {
     params: {taskId}
   })
 }
+
+// 外呼任务下拉(分页 + 名称搜索)
+export function listRoboticSelectOptions(query) {
+  return request({
+    url: '/company/companyVoiceRobotic/selectOptions',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出我的机器人外呼任务
+export function exportMyRobotic(query) {
+  return request({
+    url: '/company/companyVoiceRobotic/myExport',
+    method: 'get',
+    params: query
+  })
+}
 // 导出机器人外呼任务
 export function companyUserList(params) {
   return request({
@@ -170,3 +196,21 @@ export function getCurrentCompanyId() {
   })
 }
 
+// 暂停/继续任务
+export function pauseRoboticActive(data) {
+  return request({
+    url: '/company/companyVoiceRobotic/pauseRoboticActive',
+    method: 'post',
+    data: data
+  })
+}
+
+// 追加客户到运行中任务
+export function appendCustomers(data) {
+  return request({
+    url: '/company/companyVoiceRobotic/appendCustomers',
+    method: 'post',
+    data: data
+  })
+}
+

+ 15 - 0
src/api/company/companyWorkflow.js

@@ -9,6 +9,14 @@ export function listWorkflow(query) {
   })
 }
 
+export function myListWorkflow(query) {
+    return request({
+        url: '/company/companyWorkflow/myList',
+        method: 'get',
+        params: query
+    })
+}
+
 // 查询AI工作流详细
 export function getWorkflow(workflowId) {
   return request({
@@ -116,6 +124,13 @@ export function optionList() {
   })
 }
 
+export function getWorkflowNodeTypeCodes(workflowId) {
+  return request({
+    url: '/company/companyWorkflow/nodeTypeCodes/' + workflowId,
+    method: 'get'
+  })
+}
+
 //查看工作流历史版本
 export function getWorkflowVersionList(workflowId) {
     return request({

+ 16 - 0
src/api/crm/customer.js

@@ -22,6 +22,14 @@ export function listCustomerAll(query) {
     params: query
   })
 }
+export function listNoPage(query) {
+  return request({
+    url: '/crm/customer/listNoPage',
+    method: 'get',
+    params: query
+  })
+}
+
 export function getCustomerList(query) {
   return request({
     url: '/crm/customer/getCustomerList',
@@ -172,6 +180,14 @@ export function addMyCustomer(data) {
   })
 }
 
+export function queryPhone(customerId) {
+    return request({
+        url: '/crm/customer/queryPhone/' + customerId,
+        method: 'get'
+    })
+}
+
+
 
 // 修改客户
 export function updateCustomer(data) {

+ 10 - 0
src/api/crm/customerAnalyze.js

@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+// 查询客户分析列表
+export function listAnalyze(query) {
+  return request({
+    url: '/crm/customerAnalyze/list',
+    method: 'get',
+    params: query
+  })
+}

+ 10 - 0
src/api/crm/customerCallLog.js

@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+// 查询手动外呼记录列表
+export function listCustomerCallLog(query) {
+  return request({
+    url: '/company/crmCustomerCallLog/list',
+    method: 'get',
+    params: query
+  })
+}

+ 29 - 29
src/api/crm/customerLogs.js

@@ -2,52 +2,52 @@ import request from '@/utils/request'
 
 // 查询客户日志列表
 export function listCustomerLogs(query) {
-  return request({
-    url: '/crm/customerLogs/list',
-    method: 'get',
-    params: query
-  })
+    return request({
+        url: '/crm/customerLogs/list',
+        method: 'get',
+        params: query
+    })
 }
 
 // 查询客户日志详细
 export function getCustomerLogs(logsId) {
-  return request({
-    url: '/crm/customerLogs/' + logsId,
-    method: 'get'
-  })
+    return request({
+        url: '/crm/customerLogs/' + logsId,
+        method: 'get'
+    })
 }
 
 // 新增客户日志
 export function addCustomerLogs(data) {
-  return request({
-    url: '/crm/customerLogs',
-    method: 'post',
-    data: data
-  })
+    return request({
+        url: '/crm/customerLogs',
+        method: 'post',
+        data: data
+    })
 }
 
 // 修改客户日志
 export function updateCustomerLogs(data) {
-  return request({
-    url: '/crm/customerLogs',
-    method: 'put',
-    data: data
-  })
+    return request({
+        url: '/crm/customerLogs',
+        method: 'put',
+        data: data
+    })
 }
 
 // 删除客户日志
 export function delCustomerLogs(logsId) {
-  return request({
-    url: '/crm/customerLogs/' + logsId,
-    method: 'delete'
-  })
+    return request({
+        url: '/crm/customerLogs/' + logsId,
+        method: 'delete'
+    })
 }
 
 // 导出客户日志
 export function exportCustomerLogs(query) {
-  return request({
-    url: '/crm/customerLogs/export',
-    method: 'get',
-    params: query
-  })
-}
+    return request({
+        url: '/crm/customerLogs/export',
+        method: 'get',
+        params: query
+    })
+}

+ 9 - 0
src/api/crm/customerProperty.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 根据客户ID查询客户属性列表
+export function listByCustomerId(customerId) {
+  return request({
+    url: '/crm/customerProperty/listByCustomerId/' + customerId,
+    method: 'get'
+  })
+}

+ 62 - 0
src/api/sensitive/word.js

@@ -0,0 +1,62 @@
+import request from '@/utils/request'
+
+// 查询敏感词列表
+export function listSensitiveWord(query) {
+  return request({
+    url: '/sensitive/word/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询敏感词详细
+export function getSensitiveWord(wordId) {
+  return request({
+    url: '/sensitive/word/' + wordId,
+    method: 'get'
+  })
+}
+
+// 新增敏感词
+export function addSensitiveWord(data) {
+  return request({
+    url: '/sensitive/word',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改敏感词
+export function updateSensitiveWord(data) {
+  return request({
+    url: '/sensitive/word',
+    method: 'put',
+    data: data
+  })
+}
+
+// 启用/禁用
+export function changeSensitiveWordEnabled(wordId, enabled) {
+  return request({
+    url: '/sensitive/word/changeEnabled',
+    method: 'put',
+    data: { wordId, enabled }
+  })
+}
+
+// 删除敏感词
+export function delSensitiveWord(wordId) {
+  return request({
+    url: '/sensitive/word/' + wordId,
+    method: 'delete'
+  })
+}
+
+// 导出敏感词
+export function exportSensitiveWord(query) {
+  return request({
+    url: '/sensitive/word/export',
+    method: 'get',
+    params: query
+  })
+}

+ 15 - 0
src/api/wx/sopUserLogsWx.js

@@ -0,0 +1,15 @@
+import request from '@/utils/request'
+
+// 查询个微SOP营期详情(客户列表)
+// sopUserId: 营期编号;query: 查询参数(包含 sopId、wxContactId、fsUserId、grade、status 等)
+export function getSopUserLogsDetail(sopUserId, query) {
+  const params = Object.assign({}, query || {})
+  if (sopUserId) {
+    params.sopUserId = sopUserId
+  }
+  return request({
+    url: '/wx/wxSopUserInfo/list',
+    method: 'get',
+    params: params
+  })
+}

+ 71 - 0
src/api/wx/wxSop.js

@@ -0,0 +1,71 @@
+import request from '@/utils/request'
+
+// 查询个微SOP列表
+export function listWxSop(query) {
+  return request({
+    url: '/wx/wxSop/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询个微SOP详细
+export function getWxSop(id) {
+  return request({
+    url: '/wx/wxSop/' + id,
+    method: 'get'
+  })
+}
+
+// 新增个微SOP
+export function addWxSop(data) {
+  return request({
+    url: '/wx/wxSop',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改个微SOP
+export function updateWxSop(data) {
+  return request({
+    url: '/wx/wxSop',
+    method: 'put',
+    data: data
+  })
+}
+
+// 修改个微SOP状态
+export function updateWxStatus(data) {
+  return request({
+    url: '/wx/wxSop/updateWxStatus',
+    method: 'post',
+    data: data
+  })
+}
+
+// 个微SOP一键群发
+export function sendWxSopMsg(data) {
+  return request({
+    url: '/wx/wxSop/sendMsg',
+    method: 'post',
+    data: data
+  })
+}
+
+// 删除个微SOP
+export function delWxSop(id) {
+  return request({
+    url: '/wx/wxSop/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出个微SOP
+export function exportWxSop(query) {
+  return request({
+    url: '/wx/wxSop/export',
+    method: 'get',
+    params: query
+  })
+}

+ 71 - 0
src/api/wx/wxSopLogs.js

@@ -0,0 +1,71 @@
+import request from '@/utils/request'
+
+// 查询个微SOP执行记录列表(带关联信息)
+export function listWxSopLogsCVO(query) {
+  return request({
+    url: '/wx/wxSopLogs/listCVO',
+    method: 'get',
+    params: query
+  })
+}
+
+// 导出个微SOP执行记录列表
+export function exportWxSopLogsCVO(query) {
+  return request({
+    url: '/wx/wxSopLogs/exportCVO',
+    method: 'post',
+    params: query
+  })
+}
+
+// 查询个微发送记录列表
+export function listWxSopLogs(query) {
+  return request({
+    url: '/wx/wxSopLogs/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询个微发送记录详细
+export function getWxSopLogs(id) {
+  return request({
+    url: '/wx/wxSopLogs/' + id,
+    method: 'get'
+  })
+}
+
+// 新增个微发送记录
+export function addWxSopLogs(data) {
+  return request({
+    url: '/wx/wxSopLogs',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改个微发送记录
+export function updateWxSopLogs(data) {
+  return request({
+    url: '/wx/wxSopLogs',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除个微发送记录
+export function delWxSopLogs(id) {
+  return request({
+    url: '/wx/wxSopLogs/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出个微发送记录列表
+export function exportWxSopLogs(query) {
+  return request({
+    url: '/wx/wxSopLogs/export',
+    method: 'get',
+    params: query
+  })
+}

+ 53 - 0
src/api/wx/wxSopUser.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询个微营期列表
+export function listWxSopUser(query) {
+  return request({
+    url: '/wx/wxSopUser/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询个微营期详细
+export function getWxSopUser(id) {
+  return request({
+    url: '/wx/wxSopUser/' + id,
+    method: 'get'
+  })
+}
+
+// 新增个微营期
+export function addWxSopUser(data) {
+  return request({
+    url: '/wx/wxSopUser',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改个微营期
+export function updateWxSopUser(data) {
+  return request({
+    url: '/wx/wxSopUser',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除个微营期
+export function delWxSopUser(id) {
+  return request({
+    url: '/wx/wxSopUser/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出个微营期
+export function exportWxSopUser(query) {
+  return request({
+    url: '/wx/wxSopUser/export',
+    method: 'get',
+    params: query
+  })
+}

+ 53 - 0
src/api/wx/wxSopUserInfo.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询个微营期详情列表
+export function listWxSopUserInfo(query) {
+  return request({
+    url: '/wx/wxSopUserInfo/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询个微营期详情详细
+export function getWxSopUserInfo(id) {
+  return request({
+    url: '/wx/wxSopUserInfo/' + id,
+    method: 'get'
+  })
+}
+
+// 新增个微营期详情
+export function addWxSopUserInfo(data) {
+  return request({
+    url: '/wx/wxSopUserInfo',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改个微营期详情
+export function updateWxSopUserInfo(data) {
+  return request({
+    url: '/wx/wxSopUserInfo',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除个微营期详情
+export function delWxSopUserInfo(id) {
+  return request({
+    url: '/wx/wxSopUserInfo/' + id,
+    method: 'delete'
+  })
+}
+
+// 导出个微营期详情
+export function exportWxSopUserInfo(query) {
+  return request({
+    url: '/wx/wxSopUserInfo/export',
+    method: 'get',
+    params: query
+  })
+}

+ 456 - 0
src/views/company/companyClient/indexOld.vue

@@ -0,0 +1,456 @@
+<template>
+    <div class="app-container">
+        <el-row :gutter="24" class="baseInfo">
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card :bordered="false" dis-hover :padding="12">
+                    <div slot="header" class="acea-row row-between-wrapper">
+                        <span>总任务</span>
+                    </div>
+                    <div class="content" v-if="count">
+                        <span class="content-number spBlock mb15"><count-to :start-val="0" :end-val="count.totalNum"
+                                                                            :duration="2600"
+                                                                            class="card-panel-num"/></span>
+                    </div>
+                </el-card>
+            </el-col>
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card :bordered="false" dis-hover :padding="12">
+                    <div slot="header" class="acea-row row-between-wrapper">
+                        <span>今日加微数</span>
+                    </div>
+                    <div class="content" v-if="count">
+                        <span class="content-number spBlock mb15"><count-to :start-val="0" :end-val="count.toDayAddNum"
+                                                                            :duration="2600"
+                                                                            class="card-panel-num"/></span>
+                    </div>
+                </el-card>
+            </el-col>
+        </el-row>
+
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+            <el-form-item label="外呼任务" prop="roboticId">
+                <el-select v-model="queryParams.roboticId" filterable clearable>
+                    <el-option v-for="item in roboticList" :label="item.name" :value="item.id"/>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="话术" prop="dialogId">
+                <el-select v-model="queryParams.dialogId" filterable clearable>
+                    <el-option v-for="item in diaLogList" :label="item.name" :value="item.id"/>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="销售" prop="roboticWxId">
+                <el-select v-model="queryParams.roboticWxId" filterable clearable>
+                    <el-option v-for="item in accountList" :label="item.companyUserName + '-' + item.wxNickName"
+                               :value="item.userId"/>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="是否添加" prop="isAdd">
+                <el-select v-model="queryParams.isAdd" clearable>
+                    <el-option label="否" :value="0"/>
+                    <el-option label="待通过" :value="2"/>
+                    <el-option label="是" :value="1"/>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="加微时间" prop="time">
+                <el-date-picker
+                    v-model="queryParams.time"
+                    type="daterange"
+                    range-separator="至"
+                    value-format="yyyy-MM-dd"
+                    start-placeholder="开始日期"
+                    end-placeholder="结束日期">
+                </el-date-picker>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+                <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-row :gutter="10" class="mb8">
+            <el-col :span="1.5">
+                <el-button
+                    type="primary"
+                    icon="el-icon-plus"
+                    size="mini"
+                    @click="handleAdd"
+                    v-hasPermi="['company:companyClient:add']"
+                >新增
+                </el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    type="danger"
+                    icon="el-icon-delete"
+                    size="mini"
+                    :disabled="multiple"
+                    @click="handleDelete"
+                    v-hasPermi="['company:companyClient:remove']"
+                >删除
+                </el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    type="warning"
+                    icon="el-icon-download"
+                    size="mini"
+                    @click="handleExport"
+                    v-hasPermi="['company:companyClient:export']"
+                >导出
+                </el-button>
+            </el-col>
+            <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+
+        <el-table v-loading="loading" :data="companyClientList">
+            <el-table-column label="外呼任务" align="center" prop="roboticName"/>
+            <el-table-column label="销售" align="center" prop="wxNickName"/>
+            <el-table-column label="个微昵称" align="center" prop="nickName"/>
+            <el-table-column label="手机号" align="center" prop="phone"/>
+            <el-table-column label="话术" align="center" prop="dialogName"/>
+            <el-table-column label="客户意向度" align="center">
+                <template slot-scope="scope">
+                    <el-tag v-for="item in levelList" v-if="scope.row.intention == item.dictValue">{{item.dictLabel}}</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column label="是否添加" align="center" prop="isAdd">
+                <template slot-scope="scope">
+                    <el-tag type="danger" v-if="scope.row.isAdd == 0">否</el-tag>
+                    <el-tag type="warning" v-if="scope.row.isAdd == 2">待通过</el-tag>
+                    <el-tag type="success" v-if="scope.row.isAdd == 1">是</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column label="添加时间" align="center" prop="addTime" width="180">
+                <template slot-scope="scope">
+                    <span>{{ parseTime(scope.row.addTime, '{y}-{m}-{d}') }}</span>
+                </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+                <template slot-scope="scope">
+                    <!--          <el-button-->
+                    <!--            size="mini"-->
+                    <!--            type="text"-->
+                    <!--            icon="el-icon-edit"-->
+                    <!--            @click="handleUpdate(scope.row)"-->
+                    <!--            v-hasPermi="['company:companyClient:edit']"-->
+                    <!--          >修改</el-button>-->
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-delete"
+                        @click="handleDelete(scope.row)"
+                        v-hasPermi="['company:companyClient:remove']"
+                    >删除
+                    </el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <pagination
+            v-show="total>0"
+            :total="total"
+            :page.sync="queryParams.pageNum"
+            :limit.sync="queryParams.pageSize"
+            @pagination="getList"
+        />
+        <qw-user-select @success="selectQwUserFun" ref="qwUserSelect"/>
+        <!-- 添加或修改添加个微信账号对话框 -->
+        <el-dialog :title="title" :visible.sync="open" width="75%" append-to-body>
+            <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+                <el-form-item label="外呼任务" prop="roboticId">
+                    <el-select v-model="form.roboticId" filterable>
+                        <el-option v-for="item in roboticList" :label="item.name" :value="item.id"/>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="分配账号">
+                    <el-button @click="addQwUser">添加</el-button>
+                    <el-row :gutter="24" v-for="(item, index) in form.qwUser" style="margin-top: 5px">
+                        <el-col :span="5">
+                            <el-select v-model="item.intention" placeholder="意向等级" filterable clearable>
+                                <el-option v-for="item in levelList" :label="item.dictLabel" :value="item.dictValue"/>
+                            </el-select>
+                        </el-col>
+                        <el-col :span="5">
+                            <el-button @click="openQwUserSelect(index)">
+                                选择企微({{ item.qwUserId ? item.qwUserId.length : 0 }})
+                            </el-button>
+                        </el-col>
+                        <el-col :span="5">
+                            <el-select v-model="item.wxDialogId" placeholder="话术" filterable>
+                                <el-option v-for="item in diaLogList" :label="item.name" :value="item.id"/>
+                            </el-select>
+                        </el-col>
+                        <el-col :span="3">
+                            <el-button type="danger" icon="el-icon-delete" circle
+                                       @click="removeQwUser(index)"></el-button>
+                        </el-col>
+                    </el-row>
+                </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 {
+    listCompanyClient,
+    addWxClient,
+    addWxStatistics,
+    getCompanyClient,
+    delCompanyClient,
+    addCompanyClient,
+    updateCompanyClient,
+    exportCompanyClient
+} from "@/api/company/companyClient";
+import {listAll as roboticListAll} from "@/api/company/companyVoiceRobotic";
+import {listAll as dialogListAll} from '@/api/company/wxDialog';
+import {listAll as accountListAll} from "@/api/company/companyAccount";
+import {getDicts} from "@/api/system/dict/data";
+import qwUserSelect from "@/views/components/QwUserSelect.vue";
+import CountTo from "vue-count-to";
+
+export default {
+    name: "CompanyClient",
+    components: {qwUserSelect, CountTo},
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 添加个微信账号表格数据
+            companyClientList: [],
+            roboticList: [],
+            diaLogList: [],
+            accountList: [],
+            levelList: [],
+            thisQwUserIndex: 0,
+            selectQwUserList: [],
+            // 弹出层标题
+            title: "",
+            // 是否显示弹出层
+            open: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                roboticWxId: null,
+                nickAnme: null,
+                avatar: null,
+                phone: null,
+                wxNo: null,
+                isAdd: null,
+                addTime: null,
+                createUser: null
+            },
+            // 表单参数
+            form: {},
+            count: {},
+            // 表单校验
+            rules: {
+                roboticId: [
+                    {required: true, message: "请选择任务", trigger: "blur"}
+                ],
+            }
+        };
+    },
+    created() {
+        this.getList();
+        roboticListAll().then(e => {
+            this.roboticList = e.data;
+        })
+        dialogListAll().then(e => {
+            this.diaLogList = e.data;
+        })
+        accountListAll().then(e => {
+            this.accountList = e.data;
+        })
+        getDicts("customer_intention_level").then(e => {
+            this.levelList = e.data;
+        })
+        this.statis();
+    },
+    methods: {
+        /** 查询添加个微信账号列表 */
+        getList() {
+            this.loading = true;
+            listCompanyClient(this.getQueryData()).then(response => {
+                this.companyClientList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        statis() {
+            addWxStatistics(this.getQueryData()).then(e => {
+                this.count = e;
+            })
+        },
+        getQueryData() {
+            let form = JSON.parse(JSON.stringify(this.queryParams));
+            if (form.time && form.time.length === 2) {
+                form.beginTime = form.time[0];
+                form.endTime = form.time[1];
+            }
+            delete form.time;
+            return form;
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                id: null,
+                roboticWxId: null,
+                nickAnme: null,
+                avatar: null,
+                phone: null,
+                qwUser: [],
+                wxNo: null,
+                isAdd: null,
+                addTime: null,
+                remark: null,
+                createTime: null,
+                createUser: null
+            };
+            this.resetForm("form");
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.statis();
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm("queryForm");
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map(item => item.id)
+            this.single = selection.length !== 1
+            this.multiple = !selection.length
+        },
+        /** 新增按钮操作 */
+        handleAdd() {
+            this.reset();
+            this.open = true;
+            this.title = "添加添加个微信账号";
+        },
+        /** 修改按钮操作 */
+        handleUpdate(row) {
+            this.reset();
+            const id = row.id || this.ids
+            getCompanyClient(id).then(response => {
+                this.form = response.data;
+                this.open = true;
+                this.title = "修改添加个微信账号";
+            });
+        },
+        /** 提交按钮 */
+        submitForm() {
+            this.$refs["form"].validate(valid => {
+                if (valid) {
+                    let list = [];
+                    this.form.qwUser.forEach(l => {
+                        list = list.concat(l.qwUserId.map(e => {
+                            return {intention: l.intention, qwUserId: e, wxDialogId: l.wxDialogId}
+                        }))
+                    })
+                    this.form.qwUserList = list;
+                    if (!this.form.qwUserList || this.form.qwUserList.length === 0) {
+                        this.msgError("请选者加微方案");
+                        return;
+                    }
+                    addWxClient(this.form).then(response => {
+                        if (response.code === 200) {
+                            this.msgSuccess("修改成功");
+                            this.open = false;
+                            this.getList();
+                        }
+                    });
+                }
+            });
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const ids = row.id || this.ids;
+            this.$confirm('是否确认删除添加个微信账号编号为"' + ids + '"的数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(function () {
+                return delCompanyClient(ids);
+            }).then(() => {
+                this.getList();
+                this.msgSuccess("删除成功");
+            }).catch(function () {
+            });
+        },
+        /** 导出按钮操作 */
+        handleExport() {
+            const queryParams = this.queryParams;
+            this.$confirm('是否确认导出所有添加个微信账号数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(function () {
+                return exportCompanyClient(queryParams);
+            }).then(response => {
+                this.download(response.msg);
+            }).catch(function () {
+            });
+        },
+        addQwUser() {
+            this.form.qwUser.push({});
+        },
+        removeQwUser(index) {
+            this.form.qwUser.splice(index, 1)
+        },
+        openQwUserSelect(index) {
+            this.thisQwUserIndex = index;
+            this.$nextTick(() => {
+                this.$refs.qwUserSelect.setRows(this.selectQwUserList[index]);
+            })
+        },
+        selectQwUserFun(e) {
+            this.form.qwUser[this.thisQwUserIndex].qwUserId = e.ids;
+            this.selectQwUserList[this.thisQwUserIndex] = e.rows;
+            this.$forceUpdate()
+        },
+    }
+};
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+.baseInfo {
+    margin-bottom: 30px;
+}
+
+.content {
+    .card-panel-num {
+        display: inline-block;
+        font-size: 30px;
+    }
+
+    &-time {
+        font-size: 14px;
+        /*color: #8C8C8C;*/
+    }
+}
+</style>

+ 182 - 6
src/views/company/companyConfig/index.vue

@@ -43,12 +43,83 @@
               </el-form-item>
             </template>
 
-            <el-form-item label="是否允许重复客户导入" prop="allowRepeatCustomer">
+             <el-form-item label="是否允许重复客户导入" prop="allowRepeatCustomer">
               <el-switch v-model="cidConfig.allowRepeatCustomer"></el-switch>
             </el-form-item>
-            <el-form-item label="回调地址" prop="callbackUrl">
-              <el-input v-model="cidConfig.callbackUrl" style="width:800px"></el-input>
+            <el-form-item label="Ai外呼回调地址" prop="callbackUrl">
+               <el-input v-model="cidConfig.callbackUrl" style="width:800px"></el-input>
             </el-form-item>
+              <el-form-item label="人工外呼后是否填写意向度" prop="enableManualCallIntent">
+                  <el-switch v-model="cidConfig.enableManualCallIntent"></el-switch>
+              </el-form-item>
+
+            <el-divider content-position="left">外呼线路执行限制</el-divider>
+              <el-form-item label="启用外呼线路执行限制" prop="outboundLineLimitEnabled">
+                  <el-switch v-model="cidConfig.outboundLineLimitEnabled"></el-switch>
+                  <span class="tip-text">(开启后,系统将按以下规则限制各外呼线路的呼出次数)</span>
+              </el-form-item>
+
+              <template v-if="cidConfig.outboundLineLimitEnabled">
+                  <div style="margin-bottom: 12px; display: flex; align-items: center;">
+                      <el-button type="primary" size="small" icon="el-icon-plus" @click="addLimitRule">新增规则</el-button>
+                      <span class="tip-text" style="margin-left: 10px;">可配置多个时间粒度规则,同时生效</span>
+                  </div>
+
+                  <el-table :data="cidConfig.outboundLineRules" border stripe size="mini" style="width: 100%; margin-bottom: 15px;" empty-text="暂无规则,请点击上方按钮新增">
+                      <el-table-column label="指定线路" min-width="200" align="center">
+                          <template slot-scope="scope">
+                              <el-select v-model="scope.row.lineIds" multiple filterable collapse-tags clearable placeholder="不选则对所有线路生效" style="width: 100%;">
+                                  <el-option
+                                      v-for="item in gatewayList"
+                                      :key="item.id"
+                                      :label="item.gwDesc"
+                                      :value="item.id"
+                                  />
+                              </el-select>
+                          </template>
+                      </el-table-column>
+                      <el-table-column label="时间粒度" width="160" align="center">
+                          <template slot-scope="scope">
+                              <el-select v-model="scope.row.timeWindow" placeholder="选择粒度" style="width: 130px;">
+                                  <el-option label="每30分钟" :value="30"></el-option>
+                                  <el-option label="每1小时" :value="60"></el-option>
+                                  <el-option label="每2小时" :value="120"></el-option>
+                                  <el-option label="每6小时" :value="360"></el-option>
+                                  <el-option label="每天(24小时)" :value="1440"></el-option>
+                              </el-select>
+                          </template>
+                      </el-table-column>
+                      <el-table-column label="最大呼出次数" width="150" align="center">
+                          <template slot-scope="scope">
+                              <el-input-number v-model="scope.row.maxCalls" :min="1" :max="99999" :step="1" placeholder="次数" controls-position="right" size="mini" style="width: 100px;"></el-input-number>
+                          </template>
+                      </el-table-column>
+                      <el-table-column label="说明" min-width="220" align="left">
+                          <template slot-scope="scope">
+                    <span style="color: #909399; font-size: 12px; line-height: 1.6; white-space: normal; word-break: break-all;">
+                      {{ scope.row.lineIds && scope.row.lineIds.length > 0 ? '仅对指定线路生效' : '对所有外呼线路统一生效' }},
+                      每{{ formatTimeWindow(scope.row.timeWindow) }}最多呼出 {{ scope.row.maxCalls || 0 }} 次
+                    </span>
+                          </template>
+                      </el-table-column>
+                      <el-table-column label="操作" width="60" align="center" fixed="right">
+                          <template slot-scope="scope">
+                              <el-button type="text" icon="el-icon-delete" style="color: #F56C6C;" @click="removeLimitRule(scope.$index)"></el-button>
+                          </template>
+                      </el-table-column>
+                  </el-table>
+
+                  <div class="outbound-limit-tip">
+                      <strong>规则说明:</strong><br/>
+                      · 多条规则同时生效,取最严格的限制<br/>
+                      · 指定线路后,该规则仅对选中线路生效;不指定则对所有外呼线路生效<br/>
+                      · 例如:设置「线路A + 每30分钟=50次」+「每天=500次」,则线路A每30分钟最多50次,且全天累计不超过500次
+                  </div>
+              </template>
+
+             <!-- <el-form-item label="线路呼入回调地址" prop="inboundCallbackUrl">
+               <el-input v-model="cidConfig.inboundCallbackUrl" style="width:800px"></el-input>
+            </el-form-item> -->
             <div class="line"></div>
             <div style="float:right;margin-right:20px">
               <el-button type="primary" @click="onSubmitCidConfig">提交</el-button>
@@ -57,14 +128,30 @@
         </el-tab-pane>
           <el-tab-pane label="客户管理配置" name="sysConfig">
           <el-form ref="sysConfig" :model="sysConfig" label-width="120px">
-            <el-form-item label="公海回收规则">
+            <el-form-item label="领取无跟进公海回收规则">
                 <el-row>
                     <el-input-number  v-model="sysConfig.visitLimt"  :min="0" :max="100" ></el-input-number>
                 </el-row>
                 <el-row>
-                    <span class="tip">认领后N天自动回收</span>
+                    <span class="tip">领取无跟进N天自动回收</span>
                 </el-row>
             </el-form-item>
+            <el-form-item label="未跟进公海回收规则" label-width="170px">
+                  <el-row>
+                      <el-input-number  v-model="sysConfig.visitLimt1"  :min="0" :max="100" ></el-input-number>
+                  </el-row>
+                  <el-row>
+                      <span class="tip">无跟进记录N天自动回收</span>
+                  </el-row>
+            </el-form-item>
+            <el-form-item label="未成交公海回收规则" label-width="170px">
+                  <el-row>
+                      <el-input-number  v-model="sysConfig.visitLimt2"  :min="0" :max="356" ></el-input-number>
+                  </el-row>
+                  <el-row>
+                      <span class="tip">无商机N天自动回收</span>
+                  </el-row>
+            </el-form-item>
             <div class="line"></div>
             <div style="float:right;margin-right:20px">
               <el-button type="primary" @click="onSubmit1">提交</el-button>
@@ -236,6 +323,53 @@
             </div>
           </el-form>
         </el-tab-pane>
+        <el-tab-pane label="app提现配置" name="hisAppRedPacket" >
+              <el-form ref="hisAppRedPacket" :model="hisAppRedPacket"  label-width="150px">
+                  <el-form-item   label="红包接口类型" prop="isNew">
+                      <el-radio-group v-model="hisAppRedPacket.isNew">
+                          <el-radio label="0">商家转账到零钱(旧)</el-radio>
+                          <el-radio label="1">商家转账(新)</el-radio>
+                      </el-radio-group>
+                  </el-form-item>
+                  <el-form-item   label="公众号appid" prop="appId">
+                      <el-input   v-model="hisAppRedPacket.appId"  label="请输入appId"></el-input>
+                  </el-form-item>
+                  <el-form-item   label="小程序appid" prop="appId">
+                      <el-input   v-model="hisAppRedPacket.miniappId"  label="请输入appId"></el-input>
+                  </el-form-item>
+                  <el-form-item   label="商户号" prop="mchId">
+                      <el-input   v-model="hisAppRedPacket.mchId"  label="请输入mchId"></el-input>
+                  </el-form-item>
+                  <el-form-item   label="商户密钥" prop="mchKey">
+                      <el-input   v-model="hisAppRedPacket.mchKey"  label="mchKey"></el-input>
+                  </el-form-item>
+                  <el-form-item   label="p12证书路径" prop="keyPath">
+                      <el-input   v-model="hisAppRedPacket.keyPath"  label="请输入keyPath"></el-input>
+                  </el-form-item>
+                  <el-form-item   label="apiV3密钥" prop="apiV3Key">
+                      <el-input   v-model="hisAppRedPacket.apiV3Key"  label="请输入apiV3Key"></el-input>
+                  </el-form-item>
+                  <el-form-item   label="公钥ID" prop="publicKeyId">
+                      <el-input   v-model="hisAppRedPacket.publicKeyId"  label="请输入公钥ID"></el-input>
+                  </el-form-item>
+                  <el-form-item   label="公钥证书" prop="publicKeyPath">
+                      <el-input   v-model="hisAppRedPacket.publicKeyPath"  label="请输入publicKeyPath"></el-input>
+                  </el-form-item>
+                  <el-form-item   label="key路径" prop="privateKeyPath">
+                      <el-input   v-model="hisAppRedPacket.privateKeyPath"  label="请输入"></el-input>
+                  </el-form-item>
+                  <el-form-item   label="cert路径" prop="privateCertPath">
+                      <el-input   v-model="hisAppRedPacket.privateCertPath"  label="请输入"></el-input>
+                  </el-form-item>
+                  <el-form-item   label="回调地址" prop="notifyUrl">
+                      <el-input   v-model="hisAppRedPacket.notifyUrl"  label="请输入"></el-input>
+                  </el-form-item>
+
+                  <div style="float:right;margin-right:20px">
+                      <el-button type="primary" @click="onSubmit8">提交</el-button>
+                  </div>
+              </el-form>
+          </el-tab-pane>
         <el-tab-pane label="配置销售会员审核" name="companyUserConfig">
           <el-form ref="companyUserConfig" label-width="140px">
             <el-form-item label="会员是否默认黑名单">
@@ -294,6 +428,7 @@
 import { getCompanyInfo } from "@/api/company/company";
 import { getConfigKey,updateConfig,configUserCheck } from "@/api/company/companyConfig";
 import { listCustomerExt, getCustomerExt, delCustomerExt, addCustomerExt, updateCustomerExt } from "@/api/crm/customerExt";
+import { getGatewayList } from '@/api/company/easyCall'
 
 export default {
   name: "config",
@@ -342,6 +477,8 @@ export default {
           tel:"18900000000",
           noticeType:1,
           visitLimt:0,
+          visitLimt1:0,
+          visitLimt2:0,
           contractLimt:0
       },
       sysConfigForm:{},
@@ -360,8 +497,10 @@ export default {
       adminIsShowForm:{},
       userIsDefaultBlack: null,
       redPacketConfig:{},
-
+      hisAppRedPacket:{},
+      gatewayList: [],
       redPacketConfigForm:{},
+      hisAppRedPacketForm:{},
       cidConfig: {
         enablePhoneConfig: false,
         generateCount: 0,
@@ -385,6 +524,7 @@ export default {
       this.statusOptions = response.data;
     });
     this.getCustomerExt()
+    this.loadGatewayList()
   },
   mounted() {
 
@@ -404,6 +544,26 @@ export default {
         }
       });
     },
+      addLimitRule() {
+          if (!this.cidConfig.outboundLineRules) {
+              this.$set(this.cidConfig, 'outboundLineRules', []);
+          }
+          this.cidConfig.outboundLineRules.push({ lineIds: [], timeWindow: 60, maxCalls: 100 });
+      },
+
+      removeLimitRule(index) {
+          this.cidConfig.outboundLineRules.splice(index, 1);
+      },
+
+      loadGatewayList() {
+          getGatewayList().then(response => {
+              this.gatewayList = response.rows || response.data || [];
+          }).catch(() => {});
+      },
+      formatTimeWindow(minutes) {
+          const map = { 30: '30分钟', 60: '1小时', 120: '2小时', 360: '6小时', 1440: '天' };
+          return map[minutes] || (minutes + '分钟');
+      },
     /** 新增按钮操作 */
     handleCustomerExtAdd() {
       this.customerExt.open = true;
@@ -532,6 +692,12 @@ export default {
                 this.cidConfig = JSON.parse(response.data.configValue);
               }
             }
+            else if(key == "his:AppRedPacket"){
+                this.hisAppRedPacketForm = response.data;
+                if(response.data.configValue != null){
+                    this.hisAppRedPacket = JSON.parse(response.data.configValue);
+                }
+            }
         });
     },
     onSubmit1() {
@@ -607,6 +773,16 @@ export default {
         }
       })
     },
+
+    onSubmit8() {
+      this.hisAppRedPacketForm.configValue=JSON.stringify(this.hisAppRedPacket);
+      updateConfig(this.hisAppRedPacketForm).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess("修改成功");
+          this.getConfigKey("his:AppRedPacket");
+        }
+      });
+    },
     onSubmitCidConfig() {
       if (this.cidConfig.enablePhoneConfig) {
         if (!this.cidConfig.generateCount || this.cidConfig.generateCount < 1) {

+ 43 - 4
src/views/company/companyUser/index.vue

@@ -4,7 +4,7 @@
       <!--部门数据-->
       <el-col :span="2" :xs="24">
         <div class="head-container">
-          <el-input v-model="deptName" placeholder="请输入部门名称" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px" />
+          <el-input v-model="deptName" placeholder="请输入部门名称" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px" @blur="trimInput('deptName')" />
         </div>
         <div class="head-container">
           <el-tree :data="deptOptions" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" ref="tree"  @node-click="handleNodeClick" />
@@ -13,11 +13,14 @@
       <!--用户数据-->
       <el-col :span="22" :xs="24">
         <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" style="width: 240px" @keyup.enter.native="handleQuery" @blur="trimInput('userId', queryParams)" />
+          </el-form-item>
           <el-form-item label="员工后台昵称" prop="nickName">
-            <el-input v-model="queryParams.nickName" placeholder="请输入员工后台昵称" clearable size="small" style="width: 240px" @keyup.enter.native="handleQuery" />
+            <el-input v-model="queryParams.nickName" placeholder="请输入员工后台昵称" clearable size="small" style="width: 240px" @keyup.enter.native="handleQuery" @blur="trimInput('nickName', queryParams)" />
           </el-form-item>
           <el-form-item label="手机号码" prop="phonenumber">
-            <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable size="small" style="width: 240px" @keyup.enter.native="handleQuery" />
+            <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable size="small" style="width: 240px" @keyup.enter.native="handleQuery" @blur="trimInput('phonenumber', queryParams)" />
           </el-form-item>
           <el-form-item label="状态" prop="status">
             <el-select v-model="queryParams.status" placeholder="员工状态" clearable size="small" style="width: 240px" @change="handleQuery">
@@ -179,6 +182,13 @@
               <dict-tag :options="qwStatusOptions" :value="scope.row.qwStatus"/>
             </template>
           </el-table-column>
+            <el-table-column label="客服语音回复方式" align="center" v-if="projectFrom != 'hst'">
+                <template slot-scope="scope">
+                    <span v-if="scope.row.isCustomerVoice === 1" style="color: #67C23A;">客户语音销售回复语音</span>
+                    <span v-else-if="scope.row.isCustomerVoice === 2" style="color: #409EFF;">客户语音销售回复文本</span>
+                    <span v-else style="color: #67C23A;">客户语音销售回复语音</span>
+                </template>
+            </el-table-column>
           <el-table-column label="绑定的企微号" align="center">
             <template slot-scope="scope">
               <div v-if="scope.row.qwUsers && scope.row.qwUsers.length > 0">
@@ -286,7 +296,26 @@
                 @click="handleBindMember(scope.row)"
                 v-hasPermi="['company:user:bindUser']"
               >绑定会员</el-button>
-              <el-button size="mini" type="text" icon="el-icon-share" @click="handleGenInviteCode(scope.row)" v-hasPermi="['company:user:addCodeUrl']">招新邀请码</el-button>
+              <el-button
+              v-if="!!scope.row.cidServerId && projectFrom != 'hst'"
+                size="mini"
+                type="text"
+                icon="el-icon-s-platform"
+                plain
+                :loading="bindCidServerLoading"
+                @click="handleUnbindCidServer(scope.row)"
+              >取消绑定cid服务</el-button>
+              <el-button
+              v-else-if="!scope.row.cidServerId && projectFrom != 'hst'"
+                size="mini"
+                type="text"
+                icon="el-icon-s-platform"
+                plain
+                :loading="bindCidServerLoading"
+                @click="handleBindCidServer(scope.row)"
+              >绑定cid服务</el-button>
+
+
               <el-button v-if="scope.row.userType !== '00'" size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['company:user:edit']">修改</el-button>
               <el-button v-if="scope.row.userType !== '00'" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['company:user:remove']">删除</el-button>
               <el-button size="mini" type="text" icon="el-icon-key" @click="handleResetPwd(scope.row)" v-hasPermi="['company:user:resetPwd']">重置密码</el-button>
@@ -509,6 +538,16 @@
             </el-form-item>
           </el-col>
         </el-row>
+          <el-row>
+              <el-col :span="12">
+                  <el-form-item label="客服ai语音回复方式">
+                      <el-select v-model="form.isCustomerVoice" placeholder="请选择">
+                          <el-option label="客户语音销售回复语音" :value="1"></el-option>
+                          <el-option label="客户语音销售回复文本" :value="2"></el-option>
+                      </el-select>
+                  </el-form-item>
+              </el-col>
+          </el-row>
       </el-form>
       <div slot="footer" class="dialog-footer">
         <el-button type="primary" @click="submitForm">确 定</el-button>

+ 120 - 192
src/views/company/companyVoiceLogs/index.vue

@@ -1,63 +1,24 @@
 <template>
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="所属部门" prop="deptId">
-          <treeselect style="width:205.4px" v-model="queryParams.deptId" :options="deptOptions" :show-count="true" placeholder="请选择所属部门" />
-      </el-form-item>
-       <el-form-item label="员工姓名" prop="userNickName">
-        <el-input
-          v-model="queryParams.userNickName"
-          placeholder="请输入员工姓名"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="主叫" prop="callerPhone">
+      <el-form-item label="名称" prop="name">
         <el-input
-          v-model="queryParams.callerPhone"
-          placeholder="请输入主叫"
+          v-model="queryParams.name"
+          placeholder="请输入名称"
           clearable
           size="small"
           @keyup.enter.native="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="被叫" prop="calleePhone">
+      <el-form-item label="三方ID" prop="resultId">
         <el-input
-          v-model="queryParams.calleePhone"
-          placeholder="请输入被叫"
+          v-model="queryParams.resultId"
+          placeholder="请输入三方ID"
           clearable
           size="small"
           @keyup.enter.native="handleQuery"
         />
       </el-form-item>
-       <el-form-item label="客户来源" prop="source">
-            <el-select filterable v-model="queryParams.source" placeholder="请选择客户来源" clearable size="small">
-              <el-option
-                    v-for="item in sourceOptions"
-                    :key="item.dictValue"
-                    :label="item.dictLabel"
-                    :value="item.dictValue"
-                  />
-            </el-select>
-          </el-form-item>
-
-<!--        <el-form-item label="创建时间" prop="createTime">
-            <el-date-picker
-              style="width:205.4px"
-              clearable size="small"
-              v-model="dateRange"
-              type="daterange"
-              value-format="yyyy-MM-dd"
-              start-placeholder="开始日期"
-              end-placeholder="结束日期">
-            </el-date-picker>
-          </el-form-item> -->
-
-        <el-form-item label="开始时间" prop="startTime">
-          <el-date-picker v-model="dateRange" size="small" style="width: 240px" value-format="yyyy-MM-dd" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
-        </el-form-item>
-
       <el-form-item>
         <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@@ -65,71 +26,74 @@
     </el-form>
 
     <el-row :gutter="10" class="mb8">
-
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['system:companyVoiceDialog:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="single"
+          @click="handleUpdate"
+          v-hasPermi="['system:companyVoiceDialog:edit']"
+        >修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['system:companyVoiceDialog:remove']"
+        >删除</el-button>
+      </el-col>
       <el-col :span="1.5">
         <el-button
           type="warning"
           icon="el-icon-download"
           size="mini"
           @click="handleExport"
-          v-hasPermi="['company:companyVoiceLogs:export']"
+          v-hasPermi="['system:companyVoiceDialog:export']"
         >导出</el-button>
       </el-col>
 	  <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table  height="500" border v-loading="loading" :data="companyVoiceLogsList" @selection-change="handleSelectionChange">
+    <el-table v-loading="loading" :data="dialogList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="ID" align="center" prop="voiceId" />
-      <el-table-column label="员工姓名" align="center" prop="userNickName" />
-      <!-- <el-table-column label="通话音频" align="center" prop="voiceUrl" width="380">
-        <template slot-scope="scope">
-          <audio :src="scope.row.voiceUrl" controls  ></audio>
-        </template>
-      </el-table-column> -->
-       <el-table-column label="录制地址" align="center"  show-overflow-tooltip prop="voiceUrl" width="350">
-          <template slot-scope="scope">
-            <audio  v-if="scope.row.voiceUrl!=null"   controls :src="scope.row.voiceUrl"></audio>
-          </template>
-      </el-table-column>
-      <el-table-column label="开始时间" align="center" prop="startTime" width="180">
-        <template slot-scope="scope">
-          <span>{{ parseTime(scope.row.startTime) }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="结束时间" align="center" prop="finishTime" width="180">
-        <template slot-scope="scope">
-          <span>{{ parseTime(scope.row.finishTime) }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="主叫" align="center" prop="callerPhone" />
-      <el-table-column label="被叫" align="center" prop="calleePhone" />
-      <el-table-column label="时长" align="center" prop="times" width="180">
-        <template slot-scope="scope">
-          <span v-if="scope.row.voiceUrl!=null">{{  formatTime(scope.row.times) }} </span>
-        </template>
-      </el-table-column>
-      <el-table-column label="计费时长(分)" align="center" prop="billingTime" width="180">
-      </el-table-column>
-      <el-table-column label="备注"  show-overflow-tooltip  align="center" prop="remark" />
-      <el-table-column label="主叫显示号" align="center" prop="displayCallerNumber" />
-      <el-table-column label="被叫显示号" align="center" prop="displayCalleeNumber" />
-       <el-table-column  label="客户来源" align="center" prop="source">
-            <template slot-scope="scope">
-                <el-tag prop="status" v-for="(item, index) in sourceOptions"    v-if="scope.row.source==item.dictValue">{{item.dictLabel}}</el-tag>
-            </template>
-       </el-table-column>
-        <el-table-column label="创建时间" align="center" prop="createTime" width="180">
-        </el-table-column>
-
-       <el-table-column label="状态" align="center" prop="status" >
+      <el-table-column label="名称" align="center" prop="name" />
+      <el-table-column label="三方ID" align="center" prop="resultId" />
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
-              <el-tag prop="status" v-for="(item, index) in statusOptions"    v-if="scope.row.status==item.dictValue">{{item.dictLabel}}</el-tag>
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" fixed="right" align="center" prop="查看" >
-        <template slot-scope="scope">
-              <el-link  v-if="scope.row.voiceUrl!=null" :href="scope.row.voiceUrl" target="_blank" >下载录音</el-link>
+          <el-button
+            size="mini"
+            type="text"
+            @click="initUrl(scope.row.id)"
+            v-hasPermi="['system:companyVoiceDialog:list']"
+          >配置话术</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['system:companyVoiceDialog:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['system:companyVoiceDialog:remove']"
+          >删除</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -142,22 +106,31 @@
       @pagination="getList"
     />
 
-
+    <!-- 添加或修改AI外呼话术对话框 -->
+    <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="name">
+          <el-input v-model="form.name" placeholder="请输入名称" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" 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 { treeselect } from "@/api/company/companyDept";
-import Treeselect from "@riophae/vue-treeselect";
-import "@riophae/vue-treeselect/dist/vue-treeselect.css";
-import { listCompanyVoiceLogs, getCompanyVoiceLogs, delCompanyVoiceLogs, addCompanyVoiceLogs, updateCompanyVoiceLogs, exportCompanyVoiceLogs } from "@/api/company/companyVoiceLogs";
+import { listDialog, getDialog, delDialog, addDialog, updateDialog, exportDialog, getConfigUrl } from "@/api/company/companyVoiceDialog";
+
 export default {
-  name: "CompanyVoiceLogs",
-  components: { Treeselect },
+  name: "Dialog",
   data() {
     return {
-      statusOptions:[],
-      companys:[],
       // 遮罩层
       loading: true,
       // 选中数组
@@ -170,41 +143,22 @@ export default {
       showSearch: true,
       // 总条数
       total: 0,
-      // 通话记录表格数据
-      companyVoiceLogsList: [],
+      // AI外呼话术表格数据
+      dialogList: [],
       // 弹出层标题
       title: "",
-      dateRange: [],
       // 是否显示弹出层
       open: false,
       // 查询参数
       queryParams: {
         pageNum: 1,
         pageSize: 10,
-        companyId: null,
-        voiceTitle: null,
-        voiceUrl: null,
-        startTime: null,
-        endTime: null,
-        voiceType: null,
-        callerPhone: null,
-        calleePhone: null,
-        times: null,
-        moeny: null,
-        displayCallerNumber: null,
-        displayCalleeNumber: null,
-        source: null,
-      },
-       userSelect:{
-          title:"选择员工",
-          open:false,
+        name: null,
+        resultName: null,
+        resultId: null,
+        callNum: null,
+        createUser: null
       },
-      users:[],
-      // 部门树选项
-      deptOptions: undefined,
-      // 部门名称
-      deptName: undefined,
-      sourceOptions:[],
       // 表单参数
       form: {},
       // 表单校验
@@ -213,45 +167,18 @@ export default {
     };
   },
   created() {
-    this.getDicts("sys_company_voice_logs_status").then((response) => {
-      this.statusOptions = response.data;
-    });
-    this.getDicts("sys_crm_customer_source").then((response) => {
-      this.sourceOptions = response.data;
-    });
-    this.getTreeselect();
     this.getList();
   },
   methods: {
-    formatTime(value) {
-      const hours = Math.floor(value / 3600);
-      const minutes = Math.floor((value % 3600) / 60);
-      const seconds = value % 60;
-      if(hours == 0 && minutes == 0 ){
-        return `${seconds}秒`;
-      }else if(hours == 0 && minutes != 0 ){
-        return `${minutes}分钟${seconds}秒`;
-      }else{
-        return `${hours}小时${minutes}分钟${seconds}秒`;
-      }
-
-    },
-    /** 查询通话记录列表 */
+    /** 查询AI外呼话术列表 */
     getList() {
       this.loading = true;
-
-      listCompanyVoiceLogs(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
-        console.log(this.dateRange)
-        this.companyVoiceLogsList = response.rows;
+      listDialog(this.queryParams).then(response => {
+        this.dialogList = response.rows;
         this.total = response.total;
         this.loading = false;
       });
     },
-    getTreeselect() {
-      treeselect().then((response) => {
-        this.deptOptions = response.data;
-      });
-    },
     // 取消按钮
     cancel() {
       this.open = false;
@@ -260,20 +187,15 @@ export default {
     // 表单重置
     reset() {
       this.form = {
-        voiceId: null,
-        companyId: null,
-        voiceTitle: null,
-        voiceUrl: null,
-        startTime: null,
-        endTime: null,
-        voiceType: null,
+        id: null,
+        name: null,
+        resultName: null,
+        resultId: null,
+        callNum: null,
         remark: null,
-        callerPhone: null,
-        calleePhone: null,
-        times: null,
-        moeny: null,
-        displayCallerNumber: null,
-        displayCalleeNumber: null
+        dialogConfigUrl: null,
+        createTime: null,
+        createUser: null
       };
       this.resetForm("form");
     },
@@ -289,7 +211,7 @@ export default {
     },
     // 多选框选中数据
     handleSelectionChange(selection) {
-      this.ids = selection.map(item => item.voiceId)
+      this.ids = selection.map(item => item.id)
       this.single = selection.length!==1
       this.multiple = !selection.length
     },
@@ -297,24 +219,24 @@ export default {
     handleAdd() {
       this.reset();
       this.open = true;
-      this.title = "添加通话记录";
+      this.title = "添加AI外呼话术";
     },
     /** 修改按钮操作 */
     handleUpdate(row) {
       this.reset();
-      const voiceId = row.voiceId || this.ids
-      getCompanyVoiceLogs(voiceId).then(response => {
+      const id = row.id || this.ids
+      getDialog(id).then(response => {
         this.form = response.data;
         this.open = true;
-        this.title = "修改通话记录";
+        this.title = "修改AI外呼话术";
       });
     },
     /** 提交按钮 */
     submitForm() {
       this.$refs["form"].validate(valid => {
         if (valid) {
-          if (this.form.voiceId != null) {
-            updateCompanyVoiceLogs(this.form).then(response => {
+          if (this.form.id != null) {
+            updateDialog(this.form).then(response => {
               if (response.code === 200) {
                 this.msgSuccess("修改成功");
                 this.open = false;
@@ -322,7 +244,7 @@ export default {
               }
             });
           } else {
-            addCompanyVoiceLogs(this.form).then(response => {
+            addDialog(this.form).then(response => {
               if (response.code === 200) {
                 this.msgSuccess("新增成功");
                 this.open = false;
@@ -335,13 +257,13 @@ export default {
     },
     /** 删除按钮操作 */
     handleDelete(row) {
-      const voiceIds = row.voiceId || this.ids;
-      this.$confirm('是否确认删除通话记录编号为"' + voiceIds + '"的数据项?', "警告", {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除AI外呼话术编号为"' + ids + '"的数据项?', "警告", {
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning"
         }).then(function() {
-          return delCompanyVoiceLogs(voiceIds);
+          return delDialog(ids);
         }).then(() => {
           this.getList();
           this.msgSuccess("删除成功");
@@ -350,19 +272,25 @@ export default {
     /** 导出按钮操作 */
     handleExport() {
       const queryParams = this.queryParams;
-      this.$confirm('是否确认导出所有通话记录数据项?', "警告", {
+      this.$confirm('是否确认导出所有AI外呼话术数据项?', "警告", {
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning"
         }).then(function() {
-          return exportCompanyVoiceLogs(queryParams);
+          return exportDialog(queryParams);
         }).then(response => {
           this.download(response.msg);
         }).catch(function() {});
-    }
+    },
+    initUrl(id){
+      getConfigUrl(id).then(e => {
+        this.dialogConfigUrl = e.data;
+        this.openUrl();
+      })
+    },
+    openUrl(){
+      window.open(this.dialogConfigUrl, "_blank");
+    },
   }
 };
 </script>
-<style scoped>
-
-</style>

+ 307 - 0
src/views/company/companyVoiceRobotic/handleManualAnswered.vue

@@ -0,0 +1,307 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="外呼任务" prop="roboticId">
+        <el-select v-model="queryParams.roboticId" filterable clearable placeholder="请选择任务">
+          <el-option v-for="item in roboticList" :key="item.id" :label="item.name" :value="item.id"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="分机号" prop="answeredExtNum">
+        <el-input
+          v-model="queryParams.answeredExtNum"
+          placeholder="请输入分机号"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="list" border>
+      <el-table-column label="ID" align="center" prop="logId" width="80"/>
+      <el-table-column label="任务名称" align="center" prop="roboticName" show-overflow-tooltip/>
+      <el-table-column label="客户号码" align="center" prop="callerNum" width="130">
+        <template slot-scope="scope">
+          <span>{{ desensitizePhone(scope.row.callerNum) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="接听分机" align="center" prop="answeredExtNum" width="100"/>
+      <el-table-column label="销售姓名" align="center" prop="companyUserName" width="100"/>
+      <el-table-column label="AI通话时长(秒)" align="center" prop="callTime" width="120">
+        <template slot-scope="scope">
+          <span>{{ scope.row.callTime || 0 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="人工接听时长(秒)" align="center" prop="manualAnsweredTimeLen" width="130">
+        <template slot-scope="scope">
+          <span>{{ msToSeconds(scope.row.manualAnsweredTimeLen) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="呼入时间" align="center" prop="callCreateTime" width="160">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.callCreateTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="人工接听时间" align="center" prop="manualAnsweredTime" width="160">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.manualAnsweredTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="处理状态" align="center" prop="handleFlag" width="90">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.handleFlag === 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" width="120">
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.handleFlag !== 1"
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleProcess(scope.row)"
+            v-hasPermi="['company:handleManualAnswered:handle']"
+          >处理数据</el-button>
+          <el-button
+            v-else
+            size="mini"
+            type="text"
+            icon="el-icon-view"
+            @click="handleView(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="处理数据" :visible.sync="processDialogVisible" width="600px" append-to-body>
+      <el-form ref="processForm" :model="processForm" label-width="110px" v-loading="processLoading">
+        <el-form-item label="客户名称">
+          <span>{{ processForm.customerName }}</span>
+        </el-form-item>
+        <el-form-item label="客户号码">
+          <span>{{ desensitizePhone(processForm.mobile) }}</span>
+        </el-form-item>
+        <el-form-item label="有效客户" prop="effectiveCustomer">
+          <el-radio-group v-model="processForm.effectiveCustomer">
+            <el-radio :label="1">有效</el-radio>
+            <el-radio :label="0">无效</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="AI外呼备注" prop="aiCallRemark">
+          <el-input
+            v-model="processForm.aiCallRemark"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入AI外呼备注"
+            maxlength="500"
+            show-word-limit
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="processDialogVisible = false">取 消</el-button>
+        <el-tooltip placement="top">
+          <div slot="content">仅将此条外呼记录标记为已处理,<br/>不会修改客户的任何信息(有效客户、备注等)</div>
+          <el-button type="warning" @click="handleConfirmInfo" :loading="confirmLoading">确认信息</el-button>
+        </el-tooltip>
+        <el-button type="primary" @click="submitProcess" :loading="submitLoading">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 查看弹窗(已处理时使用,只读展示) -->
+    <el-dialog title="查看处理结果" :visible.sync="viewDialogVisible" width="600px" append-to-body>
+      <el-form :model="viewForm" label-width="110px" v-loading="viewLoading">
+        <el-form-item label="客户名称">
+          <span>{{ viewForm.customerName }}</span>
+        </el-form-item>
+        <el-form-item label="客户号码">
+          <span>{{ desensitizePhone(viewForm.mobile) }}</span>
+        </el-form-item>
+        <el-form-item label="有效客户">
+          <el-tag v-if="viewForm.effectiveCustomer === 1" type="success">有效</el-tag>
+          <el-tag v-else type="info">无效</el-tag>
+        </el-form-item>
+        <el-form-item label="AI外呼备注">
+          <span>{{ viewForm.aiCallRemark || '无' }}</span>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="viewDialogVisible = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listManualAnswered, getCrmCustomerByLogId, confirmManualAnswered, submitManualAnswered } from "@/api/company/callphone";
+import { listAll as roboticListAll } from "@/api/company/companyVoiceRobotic";
+
+export default {
+  name: "HandleManualAnswered",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 列表数据
+      list: [],
+      // 任务下拉列表
+      roboticList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        roboticId: null,
+        answeredExtNum: null
+      },
+      // 处理数据弹窗
+      processDialogVisible: false,
+      processLoading: false,
+      submitLoading: false,
+      confirmLoading: false,
+      processForm: {},
+      currentLogId: null,
+      // 查看弹窗
+      viewDialogVisible: false,
+      viewLoading: false,
+      viewForm: {}
+    };
+  },
+  created() {
+    this.getList();
+    this.getRoboticList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      listManualAnswered(this.queryParams).then(response => {
+        this.list = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    /** 获取任务下拉列表 */
+    getRoboticList() {
+      roboticListAll().then(response => {
+        this.roboticList = response.data || [];
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 处理数据按钮 */
+    handleProcess(row) {
+      this.currentLogId = row.logId;
+      this.processForm = {};
+      this.processDialogVisible = true;
+      this.processLoading = true;
+      getCrmCustomerByLogId(row.logId).then(response => {
+        if (response.code === 200 && response.data) {
+          this.processForm = response.data;
+        } else {
+          this.processForm = {};
+        }
+        this.processLoading = false;
+      }).catch(() => {
+        this.processForm = {};
+        this.processLoading = false;
+      });
+    },
+    /** 查看处理结果(已处理数据,只读展示) */
+    handleView(row) {
+      this.viewForm = {};
+      this.viewDialogVisible = true;
+      this.viewLoading = true;
+      getCrmCustomerByLogId(row.logId).then(response => {
+        if (response.code === 200 && response.data) {
+          this.viewForm = response.data;
+        } else {
+          this.viewForm = {};
+        }
+        this.viewLoading = false;
+      }).catch(() => {
+        this.viewForm = {};
+        this.viewLoading = false;
+      });
+    },
+    /** 确认信息(仅标记外呼记录为已处理,不更新客户信息) */
+    handleConfirmInfo() {
+      this.confirmLoading = true;
+      confirmManualAnswered({ logId: this.currentLogId }).then(response => {
+        if (response.code === 200) {
+          this.$message.success('已确认处理');
+          this.processDialogVisible = false;
+          this.getList();
+        }
+        this.confirmLoading = false;
+      }).catch(() => {
+        this.confirmLoading = false;
+      });
+    },
+    /** 提交处理(标记外呼记录为已处理 + 更新CRM客户信息) */
+    submitProcess() {
+      const submitData = {
+        customerId: this.processForm.customerId,
+        effectiveCustomer: this.processForm.effectiveCustomer,
+        aiCallRemark: this.processForm.aiCallRemark,
+        effectiveRecordPath: this.processForm.effectiveRecordPath,
+        lastEffectiveCallphoneLogId: this.currentLogId
+      };
+      this.submitLoading = true;
+      submitManualAnswered(submitData).then(response => {
+        if (response.code === 200) {
+          this.$message.success('处理成功');
+          this.processDialogVisible = false;
+          this.getList();
+        }
+        this.submitLoading = false;
+      }).catch(() => {
+        this.submitLoading = false;
+      });
+    },
+    /** 手机号脱敏:前3位 + **** + 后4位 */
+    desensitizePhone(phone) {
+      if (!phone) return '';
+      const str = String(phone);
+      if (str.length >= 7) {
+        return str.substring(0, 3) + '****' + str.substring(str.length - 4);
+      }
+      return str;
+    },
+    /** 毫秒转秒(向上取整) */
+    msToSeconds(ms) {
+      if (!ms || ms <= 0) return 0;
+      return Math.ceil(ms / 1000);
+    }
+  }
+};
+</script>

File diff suppressed because it is too large
+ 1071 - 973
src/views/company/companyVoiceRobotic/index.vue


+ 2406 - 0
src/views/company/companyVoiceRobotic/myIndex.vue

@@ -0,0 +1,2406 @@
+<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="robot">
+        <el-select v-model="queryParams.robot" filterable clearable>
+          <el-option v-for="item in robotList" :label="item.name + '('+item.num+')'" :value="item.id"/>
+        </el-select>
+      </el-form-item> -->
+      <el-form-item label="任务类型" prop="taskType">
+        <el-select v-model="queryParams.taskType" filterable clearable>
+          <el-option v-for="item in taskTypeList" :key="item.id" :label="item.name" :value="item.id"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['system:companyVoiceRobotic:add']"
+        >新增
+        </el-button>
+      </el-col>
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="danger"-->
+<!--          icon="el-icon-delete"-->
+<!--          size="mini"-->
+<!--          :disabled="multiple"-->
+<!--          @click="handleDelete"-->
+<!--          v-hasPermi="['system:companyVoiceRobotic:remove']"-->
+<!--        >删除-->
+<!--        </el-button>-->
+<!--      </el-col>-->
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExport"
+          v-hasPermi="['system:companyVoiceRobotic:export']"
+        >导出
+        </el-button>
+      </el-col>
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="success"-->
+<!--          icon="el-icon-refresh"-->
+<!--          size="mini"-->
+<!--          @click="updateStatusFun"-->
+<!--          v-hasPermi="['system:companyVoiceRobotic:list']"-->
+<!--        >更新状态-->
+<!--        </el-button>-->
+<!--      </el-col>-->
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="roboticList">
+      <el-table-column label="ID" align="center" prop="id"/>
+      <el-table-column label="任务名称" align="center" prop="name"/>
+      <el-table-column label="任务流程" align="center" prop="name">
+        <template slot-scope="scope">
+          <el-tag v-for="item in workflowList" v-if="scope.row.companyAiWorkflowId == item.value">{{item.label}}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="加微方式" align="center" prop="dialogId">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.addType == 0">平均</el-tag>
+          <el-tag v-if="scope.row.addType == 1">意向</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="添加类型" align="center" prop="isWeCom">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.isWeCom == 1">个微</el-tag>
+          <el-tag v-if="scope.row.isWeCom == 2">企微</el-tag>
+        </template>
+      </el-table-column>
+       <el-table-column label="任务类型" align="center" prop="taskType">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.taskType == 1">普通任务</el-tag>
+          <el-tag v-if="scope.row.taskType == 2">场景任务 - {{scope.row.sceneTypeName}}</el-tag>
+        </template>
+      </el-table-column>
+       <el-table-column label="生效时间段" align="center" prop="taskType" width="250">
+        <template slot-scope="scope">
+          <div v-if="scope.row.taskType == 1">
+            {{scope.row.runtimeRangeStart}} ~ {{scope.row.runtimeRangeEnd}}
+          </div>
+          <div v-if="scope.row.taskType == 2" >
+            <div v-if="!!scope.row.availableStartTime && !!scope.row.availableEndTime ">
+            适用:{{scope.row.availableStartTime}} ~ {{scope.row.availableEndTime}}
+            </div>
+            <div v-if="!!scope.row.runtimeRangeStart && !!scope.row.runtimeRangeEnd">
+            运行:{{scope.row.runtimeRangeStart}} ~ {{scope.row.runtimeRangeEnd}}
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="任务状态" align="center">
+        <template slot-scope="scope">
+            <el-tag v-if="scope.row.taskStatus == 0">未启动</el-tag>
+            <el-tag v-if="scope.row.taskStatus == 1" type="warning">执行中</el-tag>
+            <el-tag v-if="scope.row.taskStatus == 2" type="danger">任务暂停</el-tag>
+            <el-tag v-if="scope.row.taskStatus == 3" type="success">执行完成</el-tag>
+        </template>
+      </el-table-column>
+<!--      <el-table-column label="外呼状态" align="center">-->
+<!--        <template slot-scope="scope">-->
+<!--          <div v-loading="loadingStatus">-->
+<!--            <p v-if="statusObj.hasOwnProperty(scope.row.taskId)">-->
+<!--              <el-tag v-if="statusObj[scope.row.taskId].runningStatus == 0">未启动</el-tag>-->
+<!--              <el-tag v-if="statusObj[scope.row.taskId].runningStatus == 1">运行中</el-tag>-->
+<!--              <el-tag v-if="statusObj[scope.row.taskId].runningStatus == 2">已暂停</el-tag>-->
+<!--              <el-tag v-if="statusObj[scope.row.taskId].runningStatus == 3">已停止</el-tag>-->
+<!--            </p>-->
+<!--            <p v-if="!statusObj.hasOwnProperty(scope.row.taskId)"><el-tag>空</el-tag></p>-->
+<!--          </div>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            @click="calleesOpen(scope.row.id)"
+            v-hasPermi="['system:companyVoiceRobotic:list']"
+          >客户列表</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            @click="wxOpen(scope.row.id,scope.row.isWeCom)"
+            v-hasPermi="['system:companyVoiceRobotic:list']"
+          >加微管理</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-document"
+            @click="showExecLogs(scope.row)"
+            v-hasPermi="['system:companyVoiceRobotic:list']"
+          >执行日志</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            v-if="scope.row.taskStatus == 0"
+            @click="taskRunFun(scope.row.id)"
+          >启动任务</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            v-if="scope.row.taskStatus == 1"
+            @click="handlePauseTask(scope.row)"
+          >暂停任务</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            v-if="scope.row.taskStatus == 2"
+            @click="handleResumeTask(scope.row)"
+          >继续任务</el-button>
+          <!-- <el-button
+            size="mini"
+            type="text"
+            v-if="statusObj.hasOwnProperty(scope.row.taskId) && (statusObj[scope.row.taskId].runningStatus == 0 || statusObj[scope.row.taskId].runningStatus == 3)"
+            @click="startRoboticFun(scope.row.taskId)"
+            v-hasPermi="['system:companyVoiceRobotic:list']"
+          >开启外呼任务</el-button> -->
+          <el-button
+            size="mini"
+            type="text"
+            v-if="statusObj.hasOwnProperty(scope.row.taskId) && (statusObj[scope.row.taskId].runningStatus == 1 || statusObj[scope.row.taskId].runningStatus == 2)"
+            @click="stopRoboticFun(scope.row.taskId)"
+            v-hasPermi="['system:companyVoiceRobotic:list']"
+          >停止任务</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['system:companyVoiceRobotic:remove']"
+          >删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改机器人外呼任务对话框 -->
+    <el-drawer size="45%" :title="title" :visible.sync="open" width="500px" append-to-body class="task-form-drawer">
+      <div class="drawer-content">
+        <el-form ref="form" :model="form" :rules="rules" label-width="90px" class="task-form">
+            <div class="form-section" >
+               <div class="section-title">
+                <i class="el-icon-document"></i>
+                <span>任务类型</span>
+              </div>
+             <el-form-item label="任务类型" prop="taskType">
+              <el-select v-model="form.taskType" filterable placeholder="请选择任务类型" @change="taskTypeChange()">
+                <el-option v-for="item in taskTypeList" :key="item.id" :label="item.name" :value="item.id"/>
+              </el-select>
+            </el-form-item>
+             </div>
+
+          <div class="form-section" v-if="form.taskType === 1" >
+            <div class="section-title">
+              <i class="el-icon-document"></i>
+              <span>基本信息</span>
+            </div>
+            <el-form-item label="任务名称" prop="name">
+              <el-input v-model="form.name" placeholder="请输入任务名称" clearable/>
+            </el-form-item>
+            <el-form-item label="拨打客户" prop="userIds">
+              <el-button icon="el-icon-user" @click="openSelect">
+                选择客户
+                <el-tag v-if="form.userIds && form.userIds.length" type="primary" size="small" style="margin-left: 8px;">
+                  {{ form.userIds.length }}
+                </el-tag>
+              </el-button>
+            </el-form-item>
+            <el-form-item label="任务流程" prop="companyAiWorkflowId">
+              <el-select v-model="form.companyAiWorkflowId" filterable placeholder="请选择任务流程" @change="onWorkflowChange">
+                <el-option v-for="item in workflowList" :key="item.value" :label="item.label" :value="item.value"/>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="运行时间" required>
+                <el-col :span="11">
+                  <el-form-item prop="runtimeRangeStart">
+                     <el-time-select
+                  placeholder="任务运行开始时间"
+                   style="width: 100%;"
+                  v-model="form.runtimeRangeStart"
+                  key="runtimeRangeStart"
+                  :picker-options="{
+                    start: '07:00',
+                    step: '00:30',
+                    end: '22:00'
+                  }">
+                </el-time-select>
+                  </el-form-item>
+                </el-col>
+                <el-col class="line" :span="2" style="text-align: center">-</el-col>
+                <el-col :span="11">
+                  <el-form-item prop="runtimeRangeEnd">
+                     <el-time-select
+                      style="width: 100%;"
+                  placeholder="任务运行结束时间"
+                  v-model="form.runtimeRangeEnd"
+                  key="runtimeRangeEnd"
+                  :picker-options="{
+                    start: '07:00',
+                    step: '00:30',
+                    end: '22:00',
+                    minTime: form.runtimeRangeStart
+                  }">
+                </el-time-select>
+                  </el-form-item>
+                </el-col>
+              </el-form-item>
+          </div>
+          <!-- 场景任务 -->
+          <div class="form-section" v-if="form.taskType === 2" >
+            <div class="section-title">
+              <i class="el-icon-document"></i>
+              <span>基本信息</span>
+            </div>
+            <el-form-item label="任务名称" prop="name">
+              <el-input v-model="form.name" placeholder="请输入任务名称" clearable/>
+            </el-form-item>
+            <el-form-item label="任务流程" prop="companyAiWorkflowId">
+              <el-select v-model="form.companyAiWorkflowId" filterable placeholder="请选择任务流程" @change="onWorkflowChange">
+                <el-option v-for="item in workflowList" :key="item.value" :label="item.label" :value="item.value"/>
+              </el-select>
+            </el-form-item>
+             <el-form-item label="场景类型" prop="sceneType">
+              <el-select v-model="form.sceneType" filterable placeholder="请选择场景类型">
+                <el-option v-for="opt in sceneList" :key="opt.dictValue" :label="opt.dictLabel" :value="opt.dictValue"/>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="运行时间" required>
+                <el-col :span="11">
+                  <el-form-item prop="runtimeRangeStart">
+                     <el-time-select
+                  placeholder="任务运行开始时间"
+                   style="width: 100%;"
+                  v-model="form.runtimeRangeStart"
+                  key="runtimeRangeStart"
+                  :picker-options="{
+                    start: '07:00',
+                    step: '00:30',
+                    end: '22:00'
+                  }">
+                </el-time-select>
+                  </el-form-item>
+                </el-col>
+                <el-col class="line" :span="2" style="text-align: center">-</el-col>
+                <el-col :span="11">
+                  <el-form-item prop="runtimeRangeEnd">
+                     <el-time-select
+                      style="width: 100%;"
+                  placeholder="任务运行结束时间"
+                  v-model="form.runtimeRangeEnd"
+                  key="runtimeRangeEnd"
+                  :picker-options="{
+                    start: '07:00',
+                    step: '00:30',
+                    end: '22:00',
+                    minTime: form.runtimeRangeStart
+                  }">
+                </el-time-select>
+                  </el-form-item>
+                </el-col>
+              </el-form-item>
+            <el-form-item label="适用时间" required>
+                <el-col :span="11">
+                  <el-form-item prop="availableStartTime">
+                     <el-time-select
+                  placeholder="场景适用开始时间"
+                   style="width: 100%;"
+                  v-model="form.availableStartTime"
+                  key="availableStartTime"
+                  :picker-options="{
+                    start: '00:00',
+                    step: '00:30',
+                    end: '23:30'
+                  }">
+                </el-time-select>
+                  </el-form-item>
+                </el-col>
+                <el-col class="line" :span="2" style="text-align: center">-</el-col>
+                <el-col :span="11">
+                  <el-form-item prop="availableEndTime">
+                   <el-time-select
+                  style="width: 100%;"
+                  placeholder="场景适用结束时间"
+                  v-model="form.availableEndTime"
+                  key="availableEndTime"
+                  :picker-options="{
+                    start: '00:00',
+                    step: '00:30',
+                    end: '23:30'
+                  }">
+                </el-time-select>
+                  </el-form-item>
+                </el-col>
+              </el-form-item>
+
+          </div>
+
+          <div class="form-section" v-if="workflowHasAddWx">
+            <div class="section-title">
+              <i class="el-icon-setting"></i>
+              <span>加微设置</span>
+            </div>
+            <el-form-item label="加微方式" prop="addType">
+              <el-radio-group v-model="form.addType">
+                <el-radio :label="0" border>
+                  <i class="el-icon-pie-chart"></i>
+                  平均分配
+                </el-radio>
+                <el-radio :label="1" border>
+                  <i class="el-icon-star-on"></i>
+                  意向分配
+                </el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </div>
+
+          <div class="form-section account-section" v-if="workflowHasAddWx">
+            <div class="section-title">
+              <i class="el-icon-user-solid"></i>
+              <span>分配账号</span>
+            </div>
+            <el-form-item label="添加类型" prop="isWeCom">
+              <el-radio-group v-model="form.isWeCom">
+                <el-radio :label="1" border>
+                  <i class="el-icon-pie-chart"></i>
+                  个微
+                </el-radio>
+                <el-radio :label="2" border>
+                  <i class="el-icon-star-on"></i>
+                  企微
+                </el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-button type="primary" size="mini" icon="el-icon-plus" @click="addQwUser" plain>添加</el-button>
+
+            <div class="account-list" v-if="form.qwUser && form.qwUser.length">
+              <div v-for="(item, index) in form.qwUser" :key="index" class="account-item">
+                <el-row :gutter="12">
+                  <el-col :span="form.addType == 1 ? 6 : 0" v-if="form.addType == 1">
+                    <el-select v-model="item.intention" placeholder="意向等级" filterable clearable size="small">
+                      <el-option v-for="opt in levelList" :key="opt.dictValue" :label="opt.dictLabel" :value="opt.dictValue"/>
+                    </el-select>
+                  </el-col>
+                  <el-col :span="form.addType == 1 ? 7 : 10">
+                    <el-button icon="el-icon-user" @click="openQwUserSelect(index)" size="small" style="width: 100%;">
+                      选择账号
+                      <el-tag v-if="item.companyUserId && item.companyUserId.length" type="success" size="mini" style="margin-left: 6px;">
+                        {{ item.companyUserId.length }}
+                      </el-tag>
+                    </el-button>
+                  </el-col>
+                  <!-- <el-col :span="form.addType == 1 ? 9 : 12">
+                    <el-select v-model="item.wxDialogId" placeholder="选择话术" filterable size="small">
+                      <el-option v-for="dialog in wxDialogList" :key="dialog.id" :label="dialog.name" :value="dialog.id"/>
+                    </el-select>
+                  </el-col> -->
+                  <el-col :span="2">
+                    <el-button type="danger" icon="el-icon-delete" circle @click="removeQwUser(index)" size="small"></el-button>
+                  </el-col>
+                </el-row>
+              </div>
+            </div>
+            <el-empty v-else description="暂无分配账号" :image-size="80"></el-empty>
+          </div>
+          <!-- <el-form-item label="模式" prop="mode">
+            <el-select v-model="form.mode">
+              <el-option label="呼叫机器人后挂断" :value="7"/>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="呼叫倍率" prop="multiplier">
+            <el-select v-model="form.multiplier">
+              <el-option :value="1"/>
+              <el-option :value="2"/>
+              <el-option :value="3"/>
+              <el-option :value="4"/>
+              <el-option :value="5"/>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="自动重呼" prop="autoRecall">
+            <el-radio v-model="form.autoRecall" :label="0">否</el-radio>
+            <el-radio v-model="form.autoRecall" :label="1">是</el-radio>
+          </el-form-item>
+          <el-form-item label="重呼次数" prop="recallTimes" v-if="form.autoRecall == 1">
+            <el-select v-model="form.recallTimes">
+              <el-option label="不自动重呼" :value="0"/>
+              <el-option :value="1"/>
+              <el-option :value="2"/>
+              <el-option :value="3"/>
+              <el-option :value="4"/>
+              <el-option :value="5"/>
+            </el-select>
+          </el-form-item> -->
+        </el-form>
+        <div slot="footer" class="dialog-footer" style="text-align:right">
+          <el-button type="primary" :loading="submitFormLoading" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </div>
+    </el-drawer>
+    <customer-select @success="selectFun" ref="customer"/>
+    <component
+      :is="getCurrentComponent()"
+      @success="selectQwUserFun"
+      ref="dynamicQwUserSelect"
+    />
+
+    <el-drawer title="呼叫客户列表" size="60%" :visible.sync="callees.show" width="800px" append-to-body>
+      <el-table v-loading="callees.loading" :data="callees.list">
+        <el-table-column label="电话号码" align="center" prop="phone" width="150">
+          <template slot-scope="scope">
+            <span>{{ scope.row.phone }}</span>
+            <el-button
+              v-if="scope.row.userId"
+              icon="el-icon-search"
+              size="mini"
+              circle
+              style="margin-left: 5px;"
+              v-hasPermi="['crm:customer:queryPhone']"
+              @click="handleQueryPhone(scope.row)"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="客户名称" align="center" prop="userName"/>
+        <el-table-column label="客户ID" align="center" prop="userId"/>
+        <el-table-column label="客户意向度" align="center">
+          <template slot-scope="scope">
+            <el-tag v-for="item in levelList" v-if="scope.row.intention == item.dictValue">{{item.dictLabel}}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="呼出次数" align="right" prop="roboticCallOutCount" width="100">
+          <template slot-scope="scope">
+            {{ scope.row.roboticCallOutCount == null ? 0 : scope.row.roboticCallOutCount }}
+          </template>
+        </el-table-column>
+          <el-table-column label="AI标签" align="right" prop="customerId" width="250">
+              <template slot-scope="scope">
+                  <div v-if="scope.row.tagList && scope.row.tagList.length" class="ai-tags-container">
+                      <div v-for="tag in scope.row.tagList" :key="tag.id" class="ai-tag-item">
+                          <div class="tag-main-content">
+                              <span class="tag-property-name">{{ tag.propertyName }}</span>
+                              <span class="tag-property-value">
+                                  <span v-if="Array.isArray(tag.propertyValue)">{{ tag.propertyValue.join('、') }}</span>
+                                  <span v-else>{{ tag.propertyValue }}</span>
+                              </span>
+                          </div>
+                          <div v-if="tag.intention || (tag.likeRatio !== null && tag.likeRatio !== undefined)" class="tag-meta-info">
+                              <el-tag v-if="tag.intention" size="mini" effect="plain" class="meta-tag intention-tag">
+                                  <i class="el-icon-star-on"></i> {{ getIntentionText(tag.intention) }}
+                              </el-tag>
+                              <el-tag v-if="tag.likeRatio !== null && tag.likeRatio !== undefined" size="mini" type="success" effect="plain" class="meta-tag ratio-tag">
+                                  <i class="el-icon-trend-chart"></i> {{ tag.likeRatio }}%
+                              </el-tag>
+                          </div>
+                      </div>
+                  </div>
+                  <span v-else class="no-tags">暂无标签</span>
+              </template>
+          </el-table-column>
+          <el-table-column label="是否添加客服" align="center" prop="isAdd">
+              <template slot-scope="scope">
+                  <el-tag
+                      :type="scope.row.isAdd === 1 ? 'success' : 'info'"
+                      :style="{
+                  backgroundColor: scope.row.isAdd === 1 ? '#67C23A' : '#909399',
+                  color: '#fff',
+                  border: 'none'
+                }"
+                  >
+                      {{ scope.row.isAdd === 1 ? '已添加' : '未添加' }}
+                  </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"
+              @click="openCustomer(scope.row.userId,scope.row.idToString,scope.row.roboticId)"
+            >客户信息详情</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="callees.total>0"
+        :total="callees.total"
+        :page.sync="callees.queryParams.pageNum"
+        :limit.sync="callees.queryParams.pageSize"
+        @pagination="getCalleesList"
+      />
+    </el-drawer>
+    <el-drawer title="加个微详情" size="60%" :visible.sync="wx.show" append-to-body>
+      <el-table v-loading="wx.loading" :data="wx.list">
+        <el-table-column label="意向等级" align="center" prop="intention"/>
+        <el-table-column label="微信昵称" align="center" prop="wxNickName"/>
+        <el-table-column label="微信号" align="center" prop="wxNo"/>
+        <el-table-column label="手机号" align="center" prop="phone"/>
+        <el-table-column label="员工名称" align="center" prop="companyUserName"/>
+        <el-table-column label="话术" align="center" prop="dialogName"/>
+        <el-table-column label="分配数量" align="center" prop="num"/>
+        <el-table-column label="添加数量" align="center" prop="addNum"/>
+      </el-table>
+
+      <pagination
+        v-show="wx.total>0"
+        :total="wx.total"
+        :page.sync="wx.queryParams.pageNum"
+        :limit.sync="wx.queryParams.pageSize"
+        @pagination="getWxList"
+      />
+    </el-drawer>
+    <el-drawer title="加企微详情" size="60%" :visible.sync="qw.show" append-to-body>
+      <el-table v-loading="qw.loading" :data="qw.list">
+        <el-table-column label="意向等级" align="center" prop="intention"/>
+        <el-table-column label="企微昵称" align="center" prop="wxNickName"/>
+        <el-table-column label="企微id" align="center" prop="wxNo"/>
+        <el-table-column label="手机号" align="center" prop="phone"/>
+        <el-table-column label="话术" align="center" prop="dialogName"/>
+        <el-table-column label="分配数量" align="center" prop="num"/>
+        <el-table-column label="添加数量" align="center" prop="addNum"/>
+      </el-table>
+
+      <pagination
+        v-show="qw.total>0"
+        :total="qw.total"
+        :page.sync="qw.queryParams.pageNum"
+        :limit.sync="qw.queryParams.pageSize"
+        @pagination="getWxListQw"
+      />
+    </el-drawer>
+
+    <!-- 执行日志对话框 -->
+    <el-drawer
+      title="任务执行日志"
+      size="75%"
+      :visible.sync="execLogs.show"
+      append-to-body
+      class="exec-logs-drawer">
+      <div class="exec-logs-container" v-loading="execLogs.loading">
+        <el-card class="filter-card" shadow="never">
+          <el-form :inline="true" size="small" class="filter-form" style="margin-top: 20px;">
+            <el-form-item label="客户姓名">
+              <el-input
+                v-model="execLogs.customerName"
+                placeholder="请输入客户姓名"
+                clearable
+                @keyup.enter.native="handleSearch"
+                style="width: 180px">
+              </el-input>
+            </el-form-item>
+
+            <el-form-item label="手机号">
+              <el-input
+                v-model="execLogs.customerPhone"
+                placeholder="请输入手机号"
+                clearable
+                @keyup.enter.native="handleSearch"
+                style="width: 200px">
+              </el-input>
+            </el-form-item>
+
+            <el-form-item>
+              <span style="margin-right: 8px;">只看待执行外呼任务</span>
+              <el-checkbox v-model="execLogs.onlyCallNode"></el-checkbox>
+            </el-form-item>
+
+            <div style="margin-right:auto;">
+              <el-button type="primary" icon="el-icon-search" @click="handleSearch">
+                查询
+              </el-button>
+              <el-button icon="el-icon-refresh" @click="handleReset">
+                重置
+              </el-button>
+            </div>
+          </el-form>
+        </el-card>
+
+        <div v-if="execLogs.list.length === 0" class="empty-logs">
+          <i class="el-icon-document"></i>
+          <p>暂无执行记录</p>
+        </div>
+
+        <div v-else>
+          <!-- 统计信息卡片 -->
+          <el-card class="stats-card" shadow="never">
+            <div class="stats-grid">
+              <div class="stat-item">
+                <i class="el-icon-user" style="color: #1890ff;"></i>
+                <div class="stat-content">
+                  <div class="stat-label">客户总数</div>
+<!--                  <div class="stat-value">{{ execLogs.list.length }}</div>-->
+                  <div class="stat-value">{{ execLogs.total }}</div>
+                </div>
+              </div>
+              <div class="stat-item">
+                <i class="el-icon-phone" style="color: #52c41a;"></i>
+                <div class="stat-content">
+                  <div class="stat-label">已打电话</div>
+                  <div class="stat-value">{{ execLogs.stats.callDone }}</div>
+                </div>
+              </div>
+              <div class="stat-item">
+                <i class="el-icon-chat-line-square" style="color: #faad14;"></i>
+                <div class="stat-content">
+                  <div class="stat-label">已加微信</div>
+                  <div class="stat-value">{{ execLogs.stats.addWxDone }}</div>
+                </div>
+              </div>
+              <div class="stat-item">
+                <i class="el-icon-message" style="color: #722ed1;"></i>
+                <div class="stat-content">
+                  <div class="stat-label">已发短信</div>
+                  <div class="stat-value">{{ execLogs.stats.sendMsgDone }}</div>
+                </div>
+              </div>
+            </div>
+          </el-card>
+
+          <!-- 执行记录列表 -->
+          <div class="logs-list">
+            <el-collapse v-model="execLogs.activeNames" accordion>
+              <el-collapse-item
+                v-for="(record, index) in execLogs.list"
+                :key="index"
+                :name="index">
+                <template slot="title">
+                  <div class="record-header">
+                    <div class="record-left">
+                      <el-avatar :size="48" icon="el-icon-user-solid" :style="{background: getAvatarColor(index)}"></el-avatar>
+                      <div class="customer-detail">
+                        <div class="customer-name-row">
+                          <span class="customer-name">{{ record.customerName }}</span>
+                          <el-tag
+                            :type="getWorkflowStatusType(record.workflowStatus)"
+                            size="small"
+                            class="status-tag">
+                            {{ record.workflowStatusName }}
+                          </el-tag>
+                        </div>
+                        <div class="customer-meta">
+                          <span class="meta-item phone">
+                            <i class="el-icon-phone-outline"></i>
+                            {{ record.customerPhone }}
+                            <el-button
+                              v-if="record.customerId"
+                              icon="el-icon-search"
+                              size="mini"
+                              circle
+                              style="margin-left: 5px;"
+                              v-hasPermi="['crm:customer:queryPhone']"
+                              @click.stop="handleQueryPhone(record)"
+                            />
+                          </span>
+                          <span class="meta-divider">|</span>
+                          <span class="meta-item node">
+                            <i class="el-icon-s-operation"></i>
+                            当前节点:<strong>{{ record.currentNodeTypeName }}</strong>
+
+                            <!-- <el-button
+                              v-if="hasContent(record)"
+                              size="mini"
+                              type="primary"
+                              plain
+                              icon="el-icon-chat-dot-round"
+                              style="margin-left: 8px;"
+                              @click.stop="handleShowContent(record)">
+                                查看对话内容
+                            </el-button> -->
+                            <!-- 外呼未执行标识 -->
+                            <el-button
+                                v-if="record.waitCallNode"
+                                size="mini"
+                                type="primary"
+                                plain
+                                icon="el-icon-phone"
+                                style="margin-left: 8px;"
+                                @click.stop="handleManualCall(record)">
+                                开始人工外呼
+                            </el-button>
+                          </span>
+                        </div>
+                      </div>
+                    </div>
+                    <!-- <div class="record-right">
+                      <div class="progress-info">
+                        <span class="progress-label">执行进度</span>
+                        <span class="progress-value">{{ getProgress(record) }}%</span>
+                      </div>
+                      <el-progress
+                        :percentage="getProgress(record)"
+                        :color="getProgressColor(record)"
+                        :stroke-width="10"
+                        :show-text="false"></el-progress>
+                    </div> -->
+                  </div>
+                </template>
+
+                <!-- 节点执行日志 -->
+                <div class="node-logs">
+                  <el-timeline>
+                    <el-timeline-item
+                      v-for="(log, logIndex) in record.nodeLogs"
+                      :key="logIndex"
+                      :timestamp="log.startTime"
+                      :color="getNodeStatusColor(log.statusName)"
+                      placement="top">
+                      <el-card class="node-log-card">
+                        <div class="node-log-header">
+                          <div class="node-info">
+                            <i :class="getNodeIcon(log.nodeKey)" class="node-icon"></i>
+                            <span class="node-name">{{ log.nodeName }}</span>
+                            <el-button
+                              v-if="hasContent(log)"
+                              size="mini"
+                              type="primary"
+                              plain
+                              icon="el-icon-chat-dot-round"
+                              style="margin-left: 8px;"
+                              @click.stop="handleShowContent(record,log)">
+                                查看对话内容
+                            </el-button>
+                          </div>
+                          <el-tag
+                            :type="getStatusTagType(log.statusName)"
+                            size="mini">
+                            {{ log.statusName }}
+                          </el-tag>
+                        </div>
+                        <div class="node-log-body" v-if="log.errorMsg">
+                          <div class="error-msg">
+                            <i class="el-icon-warning"></i>
+                            {{ log.errorMsg }}
+                          </div>
+                        </div>
+                        <div class="node-log-footer">
+                          <span class="duration" v-if="log.duration">
+                            <i class="el-icon-time"></i>
+                            耗时: {{ formatDuration(log.duration) }}
+                          </span>
+                        </div>
+
+                      </el-card>
+                    </el-timeline-item>
+                  </el-timeline>
+                </div>
+              </el-collapse-item>
+            </el-collapse>
+          </div>
+          <!-- 分页 -->
+          <div class="pagination-wrapper" style="margin-top: 20px;">
+            <el-pagination
+              background
+              layout="total, sizes, prev, pager, next, jumper"
+              :total="execLogs.total"
+              :page-size="execLogs.pageSize"
+              :current-page.sync="execLogs.pageNum"
+              :page-sizes="[5, 10, 20, 30, 50]"
+              @current-change="handlePageChange"
+              @size-change="handleSizeChange">
+            </el-pagination>
+          </div>
+        </div>
+      </div>
+    </el-drawer>
+
+    <el-drawer size="75%" title="客户详情" :visible.sync="customerDetailShow" append-to-body>
+      <customer-details ref="customerDetails" />
+    </el-drawer>
+    <el-dialog
+      :title="manualCallDialog.title"
+      :visible.sync="manualCallDialog.visible"
+      width="1500px"
+      append-to-body
+      destroy-on-close
+      class="manual-call-dialog"
+      @close="handleManualCallDialogClose"
+    >
+      <call-center-phone-bar
+          :key="manualCallDialog.key"
+          ref="callCenterPhoneBar"
+          :init-phone-number="manualCallDialog.phone"
+          :robotic-id="manualCallDialog.roboticId"
+          :company-id="manualCallDialog.companyId"
+          :company-user-id="manualCallDialog.companyUserId"
+          :workflow-instance-id="manualCallDialog.workflowInstanceId"
+          :customer-id="manualCallDialog.customerId"
+      />
+    </el-dialog>
+
+    <el-dialog
+      title="对话内容"
+      :visible.sync="contentDialog.visible"
+      width="900px"
+      append-to-body
+      class="content-dialog"
+    >
+      <div class="content-dialog-wrapper">
+          <div class="content-dialog-header" >
+              <span>客户姓名:{{ contentDialog.customerName || '-' }}</span>
+              <span>手机号:{{ contentDialog.customerPhone || '-' }}</span>
+          </div>
+
+          <div v-if="!contentDialog.content" class="content-empty">
+              暂无对话内容
+          </div>
+
+          <div class="chat-container">
+              <div
+                  v-for="(msg, index) in parseContentList(contentDialog.content)"
+                  :key="index"
+                  :class="['chat-item', msg.role === 'user' ? 'chat-right' : 'chat-left']"
+              >
+                  <div class="chat-bubble-wrapper">
+                      <div class="chat-role">
+                          {{ msg.role === 'user' ? '客户' : '客服' }}
+                      </div>
+                      <div class="chat-bubble">
+                          {{ msg.content }}
+                      </div>
+                  </div>
+              </div>
+          </div>
+
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import {
+  listRobotic,
+  myListRobotic,
+  getRobotic,
+  delRobotic,
+  addRobotic,
+  updateRobotic,
+  exportRobotic,
+  exportMyRobotic,
+  calleesList,
+  statusList,
+  startRobotic,
+  stopRobotic,
+  companyUserList,
+  wxList,
+  taskRun,
+  // getTypes,
+  getSmsTempList,
+  // getCIDGroupList,
+  getExecRecords,
+  getCurrentCompanyId,
+  pauseRoboticActive
+} from "@/api/company/companyVoiceRobotic";
+import draggable from 'vuedraggable'
+// import { listAll } from '@/api/company/wxDialog';
+import customerSelect from '@/views/crm/components/CustomerSelect.vue';
+import qwUserSelect from '@/views/components/QwUserSelect.vue';
+import qwUserSelectTwo from '@/views/components/QwUserSelectTwo.vue';
+import customerDetails from "@/views/crm/components/customerDetails.vue";
+import { queryPhone } from "@/api/crm/customer";
+import {getDicts} from "@/api/system/dict/data";
+import { optionList, getWorkflowNodeTypeCodes } from '@/api/company/companyWorkflow'
+import {wxListQw} from "../../../api/company/companyVoiceRobotic";
+import CallCenterPhoneBar from '../../aiSipCall/aiSipCallManualOutbound.vue'
+import AiTagPanel from "../../crm/components/AiTagPanel.vue";
+
+export default {
+  name: "MyRobotic",
+  components: {AiTagPanel, draggable, customerDetails, customerSelect, qwUserSelect,qwUserSelectTwo,CallCenterPhoneBar},
+  data() {
+    return {
+      submitFormLoading:false,
+      taskType:1,
+      taskTypeList:[{id:1,name:"普通任务"},{id:2,name:"场景任务"}],
+      currentCompanyId:null,
+      workflowHasAddWx: false,
+      // 遮罩层
+      loading: true,
+      // CIDGroupList:[],
+      // 选中数组
+      ids: [],
+      weekList: [
+        {label: "星期一", value: 1},
+        {label: "星期二", value: 2},
+        {label: "星期三", value: 3},
+        {label: "星期四", value: 4},
+        {label: "星期五", value: 5},
+        {label: "星期六", value: 6},
+        {label: "星期日", value: 0},
+      ],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      loadingStatus: true,
+      // 总条数
+      total: 0,
+      // 机器人外呼任务表格数据
+      roboticList: [],
+      userTableList: [],
+      workflowList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        taskName: null,
+        taskId: null,
+        robot: null,
+        dialogId: null,
+        mode: null,
+        multiplier: null,
+        autoRecall: null,
+        recallTimes: null,
+        cidGroupId: null,
+        weekDay1: null,
+        startTime1: null,
+        endTime1: null,
+        weekDay2: null,
+        startTime2: null,
+        endTime2: null,
+        createUser: null,
+        taskType:null
+      },
+      // 表单参数
+      form: {
+        taskType:1
+      },
+      taskFlowList: [{key: "cellPhone", value: "外呼"}, {key: "qwAddWx", value: "企微加微"}, {key: "sendMsg", value: "发短信"}, {key: "addWx", value: "加微"}],
+      taskFlowMap: {cellPhone: "外呼", sendMsg: "发短信", addWx: "加微", qwAddWx: "企微加微"},
+      statusObj: {},
+      levelList: {},
+      robotList: [],
+      dialogList: [],
+      wxDialogList: [],
+      qwUserList: [],
+      selectQwUserList: [],
+      thisQwUserIndex: 0,
+      updateTime: null,
+      customer: {
+        show: false,
+      },
+      customerDetailShow: false,
+      callees: {
+        show: false,
+        list: [],
+        loading: false,
+        total: 0,
+        queryParams:{
+          id: null,
+          pageNum: 1,
+          pageSize: 10,
+        },
+      },
+      wx: {
+        show: false,
+        list: [],
+        loading: false,
+        total: 0,
+        queryParams:{
+          id: null,
+          pageNum: 1,
+          pageSize: 10,
+        },
+      },
+      qw: {
+        show: false,
+        list: [],
+        loading: false,
+        total: 0,
+        queryParams: {
+          id: null,
+          pageNum: 1,
+          pageSize: 10,
+        },
+      },
+      execLogs: {
+        show: false,
+        loading: false,
+        list: [],
+        activeNames: [],
+        stats: {
+          callDone: 0,
+          addWxDone: 0,
+          sendMsgDone: 0
+        },
+        pageNum: 1,
+        pageSize: 10,
+        total: 0,
+        currentTaskId: null,
+        customerName: '',
+        customerPhone: '',
+        onlyCallNode: false
+      },
+      manualCallDialog: {
+        visible: false,
+        phone: '',
+        title: "人工外呼",
+        record: null,
+        key: 0,
+        roboticId: null,
+        companyId: null,
+        companyUserId: null,
+        workflowInstanceId: null,
+        customerId: null
+      },
+      contentDialog: {
+        visible: false,
+        content: '',
+        customerName: '',
+        customerPhone: ''
+      },
+      // 表单校验
+      rules: {
+        taskType:[
+           { required: true, message: '请选择任务类型', trigger: 'change' },
+        ],
+        name: [
+            { required: true, message: '请输入任务名称', trigger: 'blur' },
+          ],
+        companyAiWorkflowId: [
+            { required: true, message: '请选择流程', trigger: 'change' }
+          ],
+        runtimeRangeStart:[
+             { required: true, message: '请选择任务运行开始时间', trigger: 'change' }
+          ],
+        runtimeRangeEnd:[
+             { required: true, message: '请选择任务运行结束时间', trigger: 'change' }
+          ]
+      },
+      smsTempList:[],
+      sceneList:[]
+    };
+  },
+  created() {
+    getCurrentCompanyId().then(res=>{
+        this.currentCompanyId = res.companyId;
+    }).catch(res=>{
+      console.log(res);
+    })
+    //getTypes().then(e => {
+      //this.robotList = e.robot;
+      //this.dialogList = e.dialog;
+    //})
+    // listAll().then(e => {
+    //   this.wxDialogList = e.data;
+    // })
+    companyUserList().then(e => {
+      this.qwUserList = e.data;
+    })
+    optionList().then(e => {
+      this.workflowList = e.data;
+    })
+    getDicts("customer_intention_level").then(e => {
+      this.levelList = e.data;
+    })
+    getDicts("task_scene_type").then(e => {
+      console.log(e);
+      this.sceneList = e.data;
+    })
+    this.getList();
+    this.getSmsTempDropList();
+
+    // getCIDGroupList().then(res=>{
+    //   console.log("----------------------")
+    //   console.log(res);
+    //   this.CIDGroupList = res.data;
+    // }).catch(res=>{
+    //   console.log("catch_____+++++++")
+    //   console.log(res);
+    // });
+  },
+  watch: {
+    // 监听添加类型的切换,清空选择器数据
+    'form.isWeCom': {
+      handler(newVal, oldVal) {
+        // 只有当值真正发生变化时才执行清空操作
+        if (newVal !== oldVal && oldVal !== undefined) {
+          // 清空 qwUser 数组中的选择数据
+          if (this.form.qwUser && this.form.qwUser.length > 0) {
+            this.form.qwUser.forEach(item => {
+              item.companyUserId = []; // 清空已选择的账号ID
+            });
+          }
+
+          // 清空选择器列表数据
+          this.selectQwUserList = [];
+
+          // 重置当前选择索引
+          this.thisQwUserIndex = 0;
+
+          // 强制更新视图
+          this.$forceUpdate();
+        }
+      },
+      immediate: false
+    },
+    'execLogs.currentTaskId': {
+      handler(newVal, oldVal) {
+        // 只有当任务ID真的变化时才重置搜索条件
+        if (newVal !== oldVal && oldVal !== undefined) {
+          // 清空搜索条件
+          this.execLogs.customerName = ''
+          this.execLogs.customerPhone = ''
+          this.execLogs.pageNum = 1
+          this.execLogs.onlyCallNode = false
+
+          // 可选:重置分页
+          this.execLogs.pageSize = 10
+
+          // 重新获取执行记录
+          this.getExecLogs()
+        }
+      },
+      immediate: false
+    }
+  },
+  methods: {
+
+      getIntentionText(intention) {
+          const intentionMap = {
+              high: "高意向",
+              medium: "中意向",
+              low: "低意向",
+              none: "无意向"
+          };
+          return intentionMap[intention] || intention;
+      },
+
+    getSmsTempDropList(){
+      getSmsTempList().then(res=>{
+        this.smsTempList = res.data;
+        console.log(this.smsTempList);
+      }).catch(res=>{
+        console.log(res);
+      })
+    },
+    /** 查询机器人外呼任务列表 */
+    getList() {
+      this.loading = true;
+      myListRobotic(this.queryParams).then(response => {
+        this.roboticList = response.rows;
+        this.roboticList.forEach(e => {
+          if(e.weekDay1){
+            e.weekDay = e.weekDay1.split(",")
+          }
+        })
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    updateStatusFun(){
+      if(!this.roboticList){
+        return;
+      }
+      this.loadingStatus = true;
+      statusList(this.roboticList.map(e => e.taskId).join()).then(e => {
+        this.loadingStatus = false;
+        this.statusObj = e.data;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        taskName: null,
+        taskId: null,
+        robot: null,
+        dialogId: null,
+        mode: 7,
+        multiplier: 3,
+        autoRecall: 0,
+        recallTimes: null,
+        cidGroupId: null,
+        weekDay1: null,
+        startTime1: null,
+        addType: 0,
+        endTime1: null,
+        weekDay2: null,
+        startTime2: null,
+        endTime2: null,
+        createTime: null,
+        qwUser: [],
+        createUser: null,
+        userIds: [],
+        userNames: [],
+        userTableList: [],
+        companyAiWorkflowId: null,
+        runtimeRangeStart:null,
+        runtimeRangeEnd:null,
+        isWeCom: 1,
+        taskType:1,
+        availableStartTime :null,
+        availableEndTime: null
+      };
+      this.workflowHasAddWx = false;
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      optionList().then(e => {
+      this.workflowList = e.data;
+      })
+      this.open = true;
+      this.title = "添加任务";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getRobotic(id).then(response => {
+        this.form = response.data;
+        if (this.form.companyAiWorkflowId) {
+          this.onWorkflowChange(this.form.companyAiWorkflowId);
+        }
+        this.open = true;
+        this.title = "修改机器人外呼任务";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.weekDay && this.form.weekDay.length > 0) {
+            this.form.weekDay1 = this.form.weekDay.join(",")
+          }
+
+          if (this.workflowHasAddWx) {
+            if(!this.form.qwUser || this.form.qwUser.length == 0){
+              this.msgError("请添加分配账号");
+              return;
+            }
+
+            for(let i = 0; i < this.form.qwUser.length; i++) {
+              const account = this.form.qwUser[i];
+              if(!account.companyUserId || account.companyUserId.length == 0) {
+                this.msgError(`第 ${i + 1} 个账号请选择企微`);
+                return;
+              }
+            }
+          }
+
+          let list = [];
+          if (this.workflowHasAddWx && this.form.qwUser) {
+            this.form.qwUser.forEach(l => {
+              list = list.concat(l.companyUserId.map(e => {return {intention: l.intention, companyUserId: e,wxDialogId: l.wxDialogId,smsTempId:l.smsTempId}}))
+            })
+          }
+          this.form.qwUserList = list;
+          console.log(this.form);
+          if(this.workflowHasAddWx && (!this.form.qwUserList || this.form.qwUserList.length == 0)){
+            this.msgError("请选者加微方案");
+            return;
+          }
+          this.form.taskFlow = this.taskFlowList.map(e => e.key).join();
+          this.submitFormLoading = true;
+          if (this.form.id != null) {
+            updateRobotic(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+              this.submitFormLoading = false;
+            }).catch(res=>{
+               this.submitFormLoading = false;
+            });
+          } else {
+            addRobotic(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                this.getList();
+              }
+              this.submitFormLoading = false;
+            }).catch(res=>{
+              this.submitFormLoading = false;
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除机器人外呼任务编号为"' + ids + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return delRobotic(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(function () {
+      });
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有我的机器人外呼任务数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return exportMyRobotic(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+      }).catch(function () {
+      });
+    },
+    openSelect() {
+      if(!!this.currentCompanyId){
+      this.$refs.customer.setRowsDesignatedCompany(this.form.userTableList || [],this.currentCompanyId);
+      }else{
+      this.$refs.customer.setRows(this.form.userTableList || []);
+      }
+    },
+    // openQwUserSelect(index) {
+    //   this.thisQwUserIndex = index;
+    //   this.$nextTick(() => {
+    //     this.$refs.qwUserSelect.setRows(this.selectQwUserList[index]);
+    //   })
+    // },
+    selectFun(e) {
+      this.form.userIds = e.ids;
+      this.form.userNames = e.names;
+      this.form.userTableList = e.rows;
+      this.$forceUpdate()
+    },
+    selectQwUserFun(e) {
+      this.form.qwUser[this.thisQwUserIndex].companyUserId = e.ids;
+      this.selectQwUserList[this.thisQwUserIndex] = e.rows;
+      this.$forceUpdate()
+    },
+    calleesOpen(id){
+      this.callees.show = true;
+      this.callees.queryParams.id = id;
+      this.getCalleesList();
+    },
+    getCalleesList() {
+      this.callees.loading = true;
+      calleesList(this.callees.queryParams).then(response => {
+        this.callees.list = response.rows;
+        this.callees.total = response.total;
+        this.callees.loading = false;
+      });
+    },
+    handleQueryPhone(row) {
+      const customerId = row.customerId || row.userId;
+      if (!customerId) {
+        this.$message.warning("客户ID不存在");
+        return;
+      }
+      queryPhone(customerId).then(response => {
+        this.$alert(response.mobile, "手机号", {
+          confirmButtonText: "确定"
+        });
+      });
+    },
+    wxOpen(id, isWeCom) {
+      console.log("isWeCom="+isWeCom)
+      if (isWeCom === 2) {
+        this.qw.show = true;
+        this.qw.queryParams.id = id;
+        this.getWxListQw();
+      } else {
+        this.wx.show = true;
+        this.wx.queryParams.id = id;
+        this.getWxList();
+      }
+    },
+    getWxListQw() {
+      this.qw.loading = true;
+      wxListQw(this.qw.queryParams).then(response => {
+        this.qw.list = response.rows;
+        this.qw.total = response.total;
+        this.qw.loading = false;
+      });
+    },
+    getWxList() {
+      this.wx.loading = true;
+      wxList(this.wx.queryParams).then(response => {
+        this.wx.list = response.rows;
+        this.wx.total = response.total;
+        this.wx.loading = false;
+      });
+    },
+    openCustomer(id,calleesId,roboticId) {
+      this.customerDetailShow=true;
+      this.$nextTick(() => {
+        this.$refs.customerDetails.getDetails(id,calleesId,roboticId);
+      })
+    },
+    startRoboticFun(id){
+      startRobotic(id).then(e => {
+        this.updateStatusFun();
+      })
+    },
+    taskRunFun(id){
+      taskRun({id}).then(e => {
+        this.getList();
+      })
+    },
+    // 暂停任务
+    handlePauseTask(row) {
+      this.$confirm('暂停后,该任务下所有工作流将停止执行,正在进行的节点将完成当前操作后停止。确认暂停?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return pauseRoboticActive({ taskId: row.id, activeType: 1 });
+      }).then(() => {
+        this.msgSuccess("暂停成功");
+        this.getList();
+      }).catch(() => {});
+    },
+    // 继续任务
+    handleResumeTask(row) {
+      this.$confirm('确认继续执行该任务?恢复后工作流将继续运行。', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return pauseRoboticActive({ taskId: row.id, activeType: 2 });
+      }).then(() => {
+        this.msgSuccess("继续成功");
+        this.getList();
+      }).catch(() => {});
+    },
+    stopRoboticFun(id){
+      stopRobotic(id).then(e => {
+        this.updateStatusFun();
+      })
+    },
+    addQwUser(){
+      this.form.qwUser.push({});
+    },
+    removeQwUser(index){
+      this.form.qwUser.splice(index, 1)
+    },
+    // 打开执行日志
+    async showExecLogs(row) {
+      this.execLogs.show = true;
+      // this.execLogs.loading = true;
+      // this.execLogs.list = [];
+      this.execLogs.pageNum = 1;
+      this.execLogs.pageSize = 10;
+      this.execLogs.currentTaskId = row.id;
+
+      try {
+        // const res = await getExecRecords(row.id);
+        // this.execLogs.list = res.data || [];
+        this.getExecLogs();
+
+        // 计算统计数据
+        // this.execLogs.stats = {
+        //   callDone: this.execLogs.list.reduce((sum, r) => sum + (r.callPhoneDone || 0), 0),
+        //   addWxDone: this.execLogs.list.reduce((sum, r) => sum + (r.addWxDone || 0), 0),
+        //   sendMsgDone: this.execLogs.list.reduce((sum, r) => sum + (r.sendMsgDone || 0), 0)
+        // };
+      } catch (error) {
+        console.error('获取执行日志失败:', error);
+        this.$message.error('获取执行日志失败');
+      } finally {
+        this.execLogs.loading = false;
+      }
+    },
+    // 获取工作流状态类型
+    getWorkflowStatusType(status) {
+      const typeMap = {
+        1: '',
+        2: 'success',
+        3: 'warning',
+        4: 'danger'
+      };
+      return typeMap[status] || 'info';
+    },
+    // 获取进度
+    getProgress(record) {
+      if (!record.nodeLogs || record.nodeLogs.length === 0) return 0;
+      const total = record.nodeLogs.length;
+      const completed = record.nodeLogs.filter(log => log.statusName === '执行成功').length;
+      return Math.round((completed / total) * 100);
+    },
+    // 获取进度条颜色
+    getProgressColor(record) {
+      const progress = this.getProgress(record);
+      if (progress === 100) return '#52c41a';
+      if (progress >= 50) return '#1890ff';
+      return '#faad14';
+    },
+    // 获取节点状态颜色
+    getNodeStatusColor(statusName) {
+      const colorMap = {
+        '执行成功': '#52c41a',
+        '执行中': '#1890ff',
+        '执行失败': '#f5222d',
+        '等待执行': '#d9d9d9',
+        '执行超时':'#f5222d'
+      };
+      return colorMap[statusName] || '#d9d9d9';
+    },
+    // 获取状态标签类型
+    getStatusTagType(statusName) {
+      const typeMap = {
+        '执行成功': 'success',
+        '执行中': 'warning',
+        '执行失败': 'danger',
+        '等待执行': 'info',
+        '执行超时':'danger'
+      };
+      return typeMap[statusName] || 'info';
+    },
+    // 获取节点图标
+    getNodeIcon(nodeKey) {
+      const iconMap = {
+        'node_start': 'el-icon-video-play',
+        'node_addwx': 'el-icon-chat-line-square',
+        'node_call': 'el-icon-phone',
+        'node_sms': 'el-icon-message',
+        'node_end': 'el-icon-circle-check'
+      };
+      return iconMap[nodeKey] || 'el-icon-document';
+    },
+    // 格式化时长
+    formatDuration(ms) {
+      if (!ms) return '-';
+      if (ms < 1000) return ms + 'ms';
+      const seconds = Math.floor(ms / 1000);
+      if (seconds < 60) return seconds + 's';
+      const minutes = Math.floor(seconds / 60);
+      const remainSeconds = seconds % 60;
+      return minutes + 'm' + remainSeconds + 's';
+    },
+    // 获取当前应该使用的组件
+    getCurrentComponent() {
+      return this.form.isWeCom === 2 ? 'qwUserSelectTwo' : 'qwUserSelect';
+    },
+
+    openQwUserSelect(index) {
+      this.thisQwUserIndex = index;
+      this.$nextTick(() => {
+        // 根据 isWeCom 的值选择对应的组件引用
+        const componentRef = this.form.isWeCom === 2 ? this.$refs.dynamicQwUserSelect : this.$refs.dynamicQwUserSelect;
+        if (componentRef && typeof componentRef.setRows === 'function') {
+          componentRef.setRows(this.selectQwUserList[index]);
+        }
+      })
+    },
+    // 获取头像颜色
+    getAvatarColor(index) {
+      const colors = ['#1890ff', '#52c41a', '#faad14', '#722ed1', '#eb2f96'];
+      return colors[index % colors.length];
+    },
+    /**
+     * 选择任务类型
+     */
+    taskTypeChange(){
+      console.log(this.form.taskType);
+      this.form.runtimeRangeStart=null;
+      this.form.runtimeRangeEnd= null;
+      this.form.userIds= [];
+      this.form.userNames= [];
+      this.form.userTableList= [];
+      this.form.companyAiWorkflowId = null;
+      this.form.name = null;
+      this.form.availableStartTime = null;
+      this.form.availableEndTime = null;
+      this.workflowHasAddWx = false;
+      this.$nextTick(() => {
+      this.$refs.form.clearValidate();
+      })
+    },
+    onWorkflowChange(workflowId) {
+      if (!workflowId) {
+        this.workflowHasAddWx = false;
+        return;
+      }
+      getWorkflowNodeTypeCodes(workflowId).then(res => {
+        const typeCodes = res.data || [];
+        this.workflowHasAddWx = typeCodes.includes('AI_ADD_WX_TASK_NEW') || typeCodes.includes('AI_QW_ADD_WX_TASK');
+        if (!this.workflowHasAddWx) {
+          this.form.addType = 0;
+          this.form.isWeCom = 1;
+          this.form.qwUser = [];
+        }
+      }).catch(() => {
+        this.workflowHasAddWx = false;
+      });
+    },
+    async getExecLogs() {
+      this.execLogs.loading = true
+      try {
+        const res = await getExecRecords({
+          roboticId: this.execLogs.currentTaskId,
+          pageNum: this.execLogs.pageNum,
+          pageSize: this.execLogs.pageSize,
+          customerName: this.execLogs.customerName,
+          customerPhone: this.execLogs.customerPhone,
+          onlyCallNode: this.execLogs.onlyCallNode
+        })
+        this.execLogs.list = (res.rows || []).map(item => ({
+          ...item,
+          contentList: item.contentList || ''
+        }))
+        this.execLogs.total = res.total || 0
+        // this.execLogs.stats = res.stats || {
+        //   callDone: 0,
+        //   addWxDone: 0,
+        //   sendMsgDone: 0
+        // }
+        // 前端计算统计数据
+        this.execLogs.stats = {
+          callDone: this.execLogs.list.reduce((sum, r) => sum + (Number(r.callPhoneDone) || 0), 0),
+          addWxDone: this.execLogs.list.reduce((sum, r) => sum + (Number(r.addWxDone) || 0), 0),
+          sendMsgDone: this.execLogs.list.reduce((sum, r) => sum + (Number(r.sendMsgDone) || 0), 0)
+        }
+
+      } catch (e) {
+        this.$message.error("获取执行日志失败")
+      } finally {
+        this.execLogs.loading = false
+      }
+    },
+
+    handlePageChange(page) {
+      this.execLogs.pageNum = page
+      this.getExecLogs()
+    },
+
+    handleSizeChange(size) {
+      this.execLogs.pageSize = size
+      this.execLogs.pageNum = 1
+      this.getExecLogs()
+    },
+    handleSearch() {
+      // 查询时回到第一页
+      this.execLogs.pageNum = 1
+      this.getExecLogs()
+    },
+
+    handleReset() {
+      this.execLogs.customerName = ''
+      this.execLogs.customerPhone = ''
+      this.execLogs.pageNum = 1
+      this.execLogs.onlyCallNode = false
+      this.getExecLogs()
+    },
+    handleManualCall(record) {
+      const phone = record.customerPhone || '';
+
+      if (!phone) {
+          this.$message.warning('当前客户没有可外呼的手机号');
+          return;
+      }
+
+      this.manualCallDialog.record = record;
+      this.manualCallDialog.phone = phone;
+      this.manualCallDialog.title = `人工外呼 - ${record.customerName || '未知客户'}`;
+
+      const currentTask = this.roboticList.find(
+        item => item.id === this.execLogs.currentTaskId
+      );
+      // 带过去的额外参数
+      this.manualCallDialog.roboticId = record.roboticId || null;
+      this.manualCallDialog.companyId = (currentTask && currentTask.companyId) || null;
+      this.manualCallDialog.companyUserId = (currentTask && currentTask.companyUserId) || null;
+      this.manualCallDialog.workflowInstanceId = record.workflowInstanceId || null;
+      this.manualCallDialog.customerId = record.customerId || null;
+      this.manualCallDialog.key += 1;
+      this.manualCallDialog.visible = true;
+    },
+    handleManualCallDialogClose() {
+      this.manualCallDialog.phone = '';
+      this.manualCallDialog.record = null;
+      this.manualCallDialog.roboticId = null;
+      this.manualCallDialog.companyId = null;
+      this.manualCallDialog.companyUserId = null;
+      this.manualCallDialog.workflowInstanceId = null;
+      this.manualCallDialog.customerId = null;
+    },
+    handleShowContent(record,log) {
+      this.contentDialog.customerName = record.customerName || '';
+      this.contentDialog.customerPhone = record.customerPhone || '';
+      this.contentDialog.content = log.nodeContentList || '';
+      this.contentDialog.visible = true;
+    },
+
+    parseContentList(content) {
+      if (!content) return []
+
+      try {
+          const parsed = typeof content === 'string' ? JSON.parse(content) : content
+          if (!Array.isArray(parsed)) return []
+
+          return parsed.filter(item => {
+              // 过滤 system 提示词
+              if (!item) return false
+              if (item.role === 'system') return false
+
+              // 过滤空内容
+              const text = item.content || ''
+              if (!String(text).trim()) return false
+
+              return true
+          })
+      } catch (e) {
+          console.error("解析 contentList 失败", e)
+          return []
+      }
+    },
+    hasContent(record) {
+      if (!record || !record.nodeContentList) return false
+
+      try {
+          const parsed = typeof record.nodeContentList === 'string'
+              ? JSON.parse(record.nodeContentList)
+              : record.nodeContentList
+
+          if (!Array.isArray(parsed)) return false
+
+          // 过滤 system 和空内容
+          const validList = parsed.filter(item => {
+              if (!item) return false
+              if (item.role === 'system') return false
+
+              const text = String(item.content || '').trim()
+              if (!text) return false
+
+              return true
+          })
+
+          return validList.length > 0
+      } catch (e) {
+          return false
+      }
+    }
+  }
+};
+</script>
+<style>
+.flow-parent{
+  display: flex;
+  flex-wrap: nowrap;
+}
+.flow-child{
+  position: relative;
+  width: 150px;
+  cursor: move;
+}
+.flow-child:last-child i{
+  display: none;
+}
+.flow-child:last-child::after{
+  display: none;
+}
+.flow-child i{
+  right: 20px;
+  top: 50%;
+  transform: translateY(-50%);
+  position: absolute;
+  font-weight: bold;
+}
+.flow-child::after{
+  content: "";
+  border-top: 1px solid #000;
+  height: 1px;
+  width: 50px;
+  right: 26px;
+  top: 50%;
+  transform: translateY(-50%);
+  position: absolute;
+}
+.sortable-ghost{
+  /* background: #FFF !important; */
+  background: rgb(217, 236, 255) !important;
+}
+
+/* 执行日志样式 */
+.exec-logs-container {
+  padding: 20px;
+  background: #f5f7fa;
+  min-height: calc(100vh - 60px);
+}
+
+.exec-logs-container ::v-deep .el-card {
+  border: none;
+  box-shadow: none;
+}
+
+.empty-logs {
+  text-align: center;
+  padding: 80px 20px;
+  color: #909399;
+}
+
+.empty-logs i {
+  font-size: 64px;
+  margin-bottom: 16px;
+  color: #dcdfe6;
+}
+
+.empty-logs p {
+  font-size: 16px;
+}
+
+/* 统计卡片 */
+.stats-card {
+  margin-bottom: 20px;
+  border-radius: 8px;
+  border: none;
+}
+
+.stats-card ::v-deep .el-card__body {
+  padding: 20px;
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 20px;
+}
+
+.stat-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px;
+  background: #f9fafb;
+  border-radius: 6px;
+}
+
+.stat-item i {
+  font-size: 32px;
+}
+
+.stat-content {
+  flex: 1;
+}
+
+.stat-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 4px;
+}
+
+.stat-value {
+  font-size: 24px;
+  font-weight: 600;
+  color: #303133;
+}
+
+/* 日志列表 */
+.logs-list {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px;
+  border: none;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+.el-collapse-item__header{
+  height: auto !important;
+}
+/* 记录头部 */
+.record-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  padding: 20px 16px;
+  gap: 32px;
+}
+
+.record-left {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  flex: 1;
+  min-width: 0;
+}
+
+.customer-detail {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.customer-name-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.customer-name {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.4;
+}
+
+.status-tag {
+  flex-shrink: 0;
+}
+
+.customer-meta {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.4;
+}
+
+.meta-item {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.meta-item i {
+  font-size: 14px;
+  color: #909399;
+}
+
+.meta-item.node strong {
+  color: #1890ff;
+  font-weight: 600;
+}
+
+.meta-divider {
+  color: #dcdfe6;
+}
+
+.record-right {
+  width: 200px;
+  flex-shrink: 0;
+}
+
+.progress-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.progress-label {
+  font-size: 13px;
+  color: #909399;
+}
+
+.progress-value {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+
+/* 节点日志 */
+.node-logs {
+  padding: 20px;
+  background: #fafafa;
+  border-radius: 6px;
+}
+
+.node-log-card {
+  margin-bottom: 0;
+}
+
+.node-log-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.node-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.node-icon {
+  font-size: 18px;
+  color: #1890ff;
+}
+
+.node-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.node-log-body {
+  margin: 12px 0;
+}
+
+.error-msg {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 12px;
+  background: #fff2e8;
+  border-left: 3px solid #faad14;
+  border-radius: 4px;
+  font-size: 13px;
+  color: #d46b08;
+}
+
+.error-msg i {
+  font-size: 16px;
+}
+
+.node-log-footer {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  font-size: 13px;
+  color: #909399;
+}
+
+.duration {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.duration i {
+  font-size: 14px;
+}
+
+/* 任务表单样式 */
+.task-form-drawer ::v-deep .el-drawer__body {
+  padding: 0;
+}
+
+.drawer-content {
+  padding: 16px;
+  background: #f5f7fa;
+  min-height: 100%;
+}
+
+.task-form {
+  max-width: 750px;
+  margin: 0 auto;
+}
+
+.form-section {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px 20px;
+  margin-bottom: 16px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.section-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 16px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #f0f2f5;
+}
+
+.section-title i {
+  font-size: 16px;
+  color: #1890ff;
+}
+
+.task-form ::v-deep .el-form-item {
+  margin-bottom: 16px;
+}
+
+.task-form ::v-deep .el-form-item:last-child {
+  margin-bottom: 0;
+}
+
+.task-form ::v-deep .el-input,
+.task-form ::v-deep .el-select {
+  width: 100%;
+}
+
+.task-form ::v-deep .el-radio-group {
+  width: 100%;
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12px;
+}
+
+.task-form ::v-deep .el-radio.is-bordered {
+  margin: 0;
+  padding: 10px 16px;
+  height: auto;
+}
+
+.task-form ::v-deep .el-radio.is-bordered i {
+  margin-right: 6px;
+  font-size: 16px;
+}
+
+.account-list {
+  margin-top: 12px;
+}
+
+.account-item {
+  margin-bottom: 10px;
+  padding: 12px;
+  background: #f9fafb;
+  border-radius: 6px;
+  border: 1px solid #e8e8e8;
+}
+
+.account-item:hover {
+  border-color: #1890ff;
+  background: #f0f9ff;
+}
+
+.account-item ::v-deep .el-select {
+  width: 100%;
+}
+.chat-container {
+    max-height: 600px;
+    overflow-y: auto;
+    padding: 16px;
+    background: #f5f7fa;
+    border-radius: 8px;
+}
+
+.chat-item {
+    display: flex;
+    margin-bottom: 14px;
+}
+
+.chat-left {
+    justify-content: flex-start;
+}
+
+.chat-right {
+    justify-content: flex-end;
+}
+
+.chat-bubble-wrapper {
+    max-width: 75%;
+}
+
+.chat-role {
+    font-size: 12px;
+    color: #909399;
+    margin-bottom: 4px;
+}
+
+.chat-left .chat-role {
+    text-align: left;
+}
+
+.chat-right .chat-role {
+    text-align: right;
+}
+
+.chat-bubble {
+    padding: 10px 14px;
+    border-radius: 12px;
+    font-size: 14px;
+    line-height: 1.6;
+    word-break: break-word;
+    white-space: pre-wrap;
+}
+
+.chat-left .chat-bubble {
+    background: #fff;
+    border: 1px solid #ebeef5;
+    color: #303133;
+}
+
+.chat-right .chat-bubble {
+    background: #409eff;
+    color: #fff;
+}
+
+.content-dialog-header {
+    margin-top: -12px;
+    margin-bottom: 24px;
+    padding-bottom: 8px;
+    border-bottom: 1px solid #ebeef5;
+    display: flex;
+    align-items: center;
+    gap: 20px;
+    font-size: 14px;
+    color: #606266;
+}
+
+/* AI标签样式优化 */
+.ai-tags-container {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    padding: 4px 0;
+    max-height: 270px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    scroll-behavior: smooth;
+}
+
+.ai-tags-container::-webkit-scrollbar {
+    width: 6px;
+}
+
+.ai-tags-container::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+}
+
+.ai-tags-container::-webkit-scrollbar-thumb {
+    background: #c0c4cc;
+    border-radius: 3px;
+    transition: background 0.3s ease;
+}
+
+.ai-tags-container::-webkit-scrollbar-thumb:hover {
+    background: #909399;
+}
+
+.ai-tag-item {
+    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
+    border: 1px solid #e4e7ed;
+    border-radius: 8px;
+    padding: 10px 12px;
+    transition: all 0.3s ease;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
+    flex-shrink: 0;
+}
+
+.ai-tag-item:hover {
+    border-color: #c0c4cc;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+    transform: translateY(-1px);
+}
+
+.tag-main-content {
+    display: flex;
+    align-items: baseline;
+    gap: 8px;
+    margin-bottom: 6px;
+}
+
+.tag-property-name {
+    font-size: 13px;
+    color: #909399;
+    font-weight: 500;
+    white-space: nowrap;
+    flex-shrink: 0;
+}
+
+.tag-property-value {
+    font-size: 13px;
+    color: #303133;
+    font-weight: 600;
+    flex: 1;
+    word-break: break-all;
+}
+
+.tag-meta-info {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding-top: 6px;
+    border-top: 1px dashed #e4e7ed;
+}
+
+.meta-tag {
+    font-size: 12px;
+    padding: 0 8px;
+    height: 22px;
+    line-height: 20px;
+    border-radius: 4px;
+    font-weight: 500;
+}
+
+.meta-tag i {
+    margin-right: 3px;
+    font-size: 12px;
+}
+
+.intention-tag {
+    background: linear-gradient(135deg, #fff1f0 0%, #ffffff 100%);
+    border-color: #ffa39e;
+    color: #cf1322;
+}
+
+.ratio-tag {
+    background: linear-gradient(135deg, #f6ffed 0%, #ffffff 100%);
+    border-color: #b7eb8f;
+    color: #389e0d;
+}
+
+.no-tags {
+    color: #c0c4cc;
+    font-size: 13px;
+    font-style: italic;
+}
+</style>

+ 139 - 15
src/views/company/companyWorkflow/design.vue

@@ -401,7 +401,7 @@
                   />
                 </el-select>
               </el-form-item>
-              <el-form-item label="转人工业务组">
+              <!-- <el-form-item label="转人工业务组">
                   <el-select
                       v-model="selectedNode.nodeConfig.busiGroupId"
                       filterable
@@ -415,7 +415,7 @@
                           :value="item.groupId"
                       />
                   </el-select>
-              </el-form-item>
+              </el-form-item> -->
               <el-form-item label="外呼线路">
                 <el-select
                   :disabled="editAiDisable"
@@ -448,6 +448,22 @@
                   />
                 </el-select>
               </el-form-item>
+              <el-form-item label="音色来源">
+                <el-select
+                    :disabled="editAiDisable"
+                    v-model="selectedNode.nodeConfig.voiceSource"
+                    placeholder="请选择音色来源"
+                    @change="handleConfigChange('handleVoiceSource')"
+                >
+                    <el-option
+                        v-for="item in voiceSourceOptions"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value"
+                    />
+                </el-select>
+              </el-form-item>
+
               <el-form-item label="音色">
                 <el-select
                   :disabled="editAiDisable"
@@ -464,6 +480,23 @@
                   />
                 </el-select>
               </el-form-item>
+              <el-form-item label="转人工分机" v-if="selectedNode.nodeConfig.callMode === 2">
+                <el-select
+                  v-model="selectedNode.nodeConfig.extensionIds"
+                  multiple
+                  filterable
+                  placeholder="请选择转人工分机(最多50个)"
+                  @change="handleExtensionChange"
+                >
+                  <el-option
+                    v-for="item in easyCallExtensionList"
+                    :key="item.extId"
+                    :label="item._label"
+                    :value="item.extId"
+                    :disabled="selectedNode.nodeConfig.extensionIds && selectedNode.nodeConfig.extensionIds.length >= 50 && !selectedNode.nodeConfig.extensionIds.includes(item.extId)"
+                  />
+                </el-select>
+              </el-form-item>
               <!-- <el-form-item label="技能组">
                 <el-select
                   v-model="selectedNode.nodeConfig.busiGroupId"
@@ -536,10 +569,10 @@
             </div>
 
             <!-- 条件判断 -->
-            <div v-if="edgeSourceNode.nodeType == 'AI_CALL_TASK' || edgeSourceNode.nodeType == 'AI_SEND_MSG_TASK' || edgeSourceNode.nodeType == 'AI_ADD_WX_TASK' || edgeSourceNode.nodeType == 'AI_QW_ADD_WX_TASK'"  class="property-section">
+            <div v-if="edgeSourceNode.nodeType == 'AI_CALL_TASK' || edgeSourceNode.nodeType == 'AI_ADD_WX_TASK_NEW' || edgeSourceNode.nodeType == 'AI_QW_ADD_WX_TASK'"  class="property-section">
               <div class="section-title">
                 <i class="el-icon-set-up"></i>条件判断
-                <el-button type="success" size="mini" icon="el-icon-plus" @click="addCondition" class="add-condition-btn">新增条件</el-button>
+                <el-button v-if="!(edgeSourceNode.nodeType == 'AI_CALL_TASK' && selectedEdge.conditionExprObj && selectedEdge.conditionExprObj.length >= 1)" type="success" size="mini" icon="el-icon-plus" @click="addCondition" class="add-condition-btn">新增条件</el-button>
               </div>
 
               <!-- AI外呼任务条件 -->
@@ -556,13 +589,19 @@
                         <el-option :value="true" label="是" />
                       </el-select>
                     </el-form-item>
+                    <!-- todo 注释现在完成了一半代码 联调的时候打开 -->
+                    <!-- <el-form-item label="未拨通类型" v-if="item.callConnected == false">
+                      <el-select v-model="item.hangupType" placeholder="请选择未拨通类型" filterable clearable multiple>
+                        <el-option v-for="dict in hangupTypeList" :key="dict.dictValue" :label="dict.dictLabel" :value="dict.dictValue"/>
+                      </el-select>
+                    </el-form-item> -->
                     <el-form-item label="延迟时间" v-if="item.callConnected == false">
                       <el-input v-model="item.callTime" placeholder="请输入时间">
                         <template slot="append">分钟</template>
                       </el-input>
                     </el-form-item>
                     <el-form-item label="意向度" v-if="!!item.callConnected">
-                      <el-select v-model="item.intention" placeholder="请选择意向等级" filterable clearable>
+                      <el-select v-model="item.intention" placeholder="请选择意向等级" filterable clearable multiple>
                         <el-option v-for="level in levelList" :key="level.dictValue" :label="level.dictLabel" :value="level.dictValue"/>
                       </el-select>
                     </el-form-item>
@@ -575,7 +614,7 @@
               </div>
 
               <!-- 加微信任务条件 -->
-              <div v-if="edgeSourceNode.nodeType == 'AI_ADD_WX_TASK' || edgeSourceNode.nodeType == 'AI_QW_ADD_WX_TASK'" class="conditions-container">
+              <div v-if="edgeSourceNode.nodeType == 'AI_ADD_WX_TASK_NEW' || edgeSourceNode.nodeType == 'AI_QW_ADD_WX_TASK'" class="conditions-container">
                 <div v-for="(item, index) in selectedEdge.conditionExprObj" :key="index" class="condition-item">
                   <div class="condition-header">
                     <span class="condition-number">条件 {{ index + 1 }}</span>
@@ -646,8 +685,8 @@ import {
 } from "@/api/company/companyVoiceRobotic";
 import { getWorkflow, addWorkflow, updateWorkflow, getNodeTypes, getWorkflowVersionDetail } from '@/api/company/companyWorkflow'
 import {getDicts} from "@/api/system/dict/data";
-import { getGatewayList, getLlmAccountList, getVoiceCodeList, getBusiGroupList } from '@/api/company/easyCall'
-// import { listAll } from '@/api/company/wxDialog';
+import { getGatewayList, getLlmAccountList, getVoiceCodeList, getBusiGroupList, getExtensionList } from '@/api/company/easyCall'
+import { listAll } from '@/api/company/wxDialog';
 export default {
   name: 'WorkflowDesign',
   data() {
@@ -682,6 +721,7 @@ export default {
       // 节点类型列表
       nodeTypes: [],
       levelList: [],
+      hangupTypeList: [],
       // 节点分类
       nodeCategories: [],
       // 选中的节点
@@ -729,7 +769,8 @@ export default {
       conditionExprTemp:{
         callConnected: false,
         callTime: '',
-        intention: '',
+        intention: [],
+        hangupType: [],
         isAdd: false,
         addTime: '',
         sendStatus: false,
@@ -743,7 +784,15 @@ export default {
       easyCallGatewayList: [],     // 外呼线路(网关)列表
       easyCallLlmAccountList: [],  // 大模型底座列表
       easyCallVoiceCodeList: [],   // 音色列表
+      allEasyCallVoiceCodeList: [],   // 接口原始数据
       easyCallBusiGroupList: [],   // tts厂商(技能组)列表
+      easyCallExtensionList: [],   // 转人工分机列表
+      voiceSourceOptions: [
+        { label: '阿里云TTS', value: 'aliyun_tts' },
+        { label: '豆包语音', value: 'doubao_vcl_tts' },
+        { label: '电信语音', value: 'chinatelecom_tts' },
+        { label: '科大讯飞', value: 'xf_tts' }
+      ],
     }
   },
   created() {
@@ -766,11 +815,24 @@ export default {
     //   console.log("------")
     //   console.log(this.wxDialogList)
     // })
+    listAll().then(e => {
+      this.wxDialogList = e.data;
+      console.log("------")
+      console.log(this.wxDialogList)
+    })
 
     this.getDicts("sys_qw_qw_wx_add_way").then(response => {
       this.qwWxAddWayOptions = response.data;
     });
 
+    // 预加载转人工分机列表(用于保存时ID→完整信息映射)
+    getExtensionList().then(res => {
+      this.easyCallExtensionList = (res.data || []).map(item => ({
+        ...item,
+        _label: item.companyUserName ? `${item.extensionNum}-${item.companyUserName}` : item.extensionNum
+      }))
+    })
+
     // listAll().then(e => {
     //   this.wxDialogList = e.data;
     // })
@@ -800,11 +862,36 @@ export default {
       this.$refs.canvasContainer.focus()
     },
 
-    // 处理配置变化,强制更新视图
+    /** 转人工分机选择变化,限制最多50个 */
+    handleExtensionChange(val) {
+      if (val.length > 50) {
+        this.$message.warning('转人工分机最多只能选择50个')
+        this.selectedNode.nodeConfig.extensionIds = val.slice(0, 50)
+      }
+      this.handleConfigChange()
+    },
+    /** 根据音色来源过滤音色列表,未选择来源时展示全部 */
+    applyVoiceSourceFilter() {
+      const voiceSource = this.selectedNode && this.selectedNode.nodeConfig
+        ? this.selectedNode.nodeConfig.voiceSource
+        : null
+      if (voiceSource) {
+        this.easyCallVoiceCodeList = this.allEasyCallVoiceCodeList.filter(e => e.voiceSource == voiceSource)
+      } else {
+        this.easyCallVoiceCodeList = this.allEasyCallVoiceCodeList
+      }
+    },
+    /** 处理配置变化,强制更新视图 */
     handleConfigChange(v) {
+      if( !!v && v === "handleVoiceSource" ){
+        this.applyVoiceSourceFilter()
+        this.selectedNode.nodeConfig.voiceCode = null;
+        this.selectedNode.nodeConfig.ttsModels = null;
+      }
       if( !!v && v === "handleVoice" ){
        let voice =  this.easyCallVoiceCodeList.filter(e=>e.voiceCode == this.selectedNode.nodeConfig.voiceCode);
        this.selectedNode.nodeConfig.voiceSource = voice[0].voiceSource;
+       this.selectedNode.nodeConfig.ttsModels =  voice[0].ttsModels;
       }
       else if( !!v && v === "handleGateway" ){
        let gateway =  this.easyCallGatewayList.filter(e=>e.id == this.selectedNode.nodeConfig.gatewayId);
@@ -852,14 +939,21 @@ export default {
         if (!this.selectedNode.nodeConfig.multiplier) {
           this.$set(this.selectedNode.nodeConfig, 'multiplier', 1)
         }
-          if (this.selectedNode.nodeConfig.busiGroupId === undefined) {
-              this.$set(this.selectedNode.nodeConfig, 'busiGroupId', null)
-          }
         if (this.selectedNode.nodeConfig.autoRecall === undefined) {
           this.$set(this.selectedNode.nodeConfig, 'autoRecall', 0)
         }
         if (!this.selectedNode.nodeConfig.recallTimes && this.selectedNode.nodeConfig.recallTimes !== 0) {
           this.$set(this.selectedNode.nodeConfig, 'recallTimes', 0)
+        }
+          // if (this.selectedNode.nodeConfig.busiGroupId === undefined) {
+          //     this.$set(this.selectedNode.nodeConfig, 'busiGroupId', null)
+          // }
+        // 从保存的extensionList还原extensionIds(完整信息→ID数组,供下拉回显)
+        if (this.selectedNode.nodeConfig.extensionList && this.selectedNode.nodeConfig.extensionList.length > 0) {
+          this.$set(this.selectedNode.nodeConfig, 'extensionIds',
+            this.selectedNode.nodeConfig.extensionList.map(item => item.extId))
+        } else if (this.selectedNode.nodeConfig.extensionIds === undefined) {
+          this.$set(this.selectedNode.nodeConfig, 'extensionIds', [])
         }
         // 初始化外呼模式,默认为人工外呼(1)
         if (!this.selectedNode.nodeConfig.callMode) {
@@ -885,11 +979,19 @@ export default {
           this.easyCallLlmAccountList = res.data || []
         })
         getVoiceCodeList().then(res => {
-          this.easyCallVoiceCodeList = res.data || []
+          this.allEasyCallVoiceCodeList = res.data || []
+          this.applyVoiceSourceFilter()
         })
         getBusiGroupList().then(res => {
           this.easyCallBusiGroupList = res.data || []
         })
+        // 刷新分机列表(预加载已在created完成,此处刷新最新数据)
+        getExtensionList().then(res => {
+          this.easyCallExtensionList = (res.data || []).map(item => ({
+            ...item,
+            _label: item.companyUserName ? `${item.extensionNum}-${item.companyUserName}` : item.extensionNum
+          }))
+        })
       }
       if (this.selectedNode.nodeType === 'AI_SEND_MSG_TASK' || this.selectedNode.nodeType === 'AI_QW_ADD_WX_TASK') {
         getSmsTempList().then(res => {
@@ -914,6 +1016,11 @@ export default {
             this.levelList = e.data;
           })
         }
+        if(this.hangupTypeList.length == 0) {
+          getDicts("ai_call_hangup_type").then(e => {
+            this.hangupTypeList = e.data;
+          })
+        }
       }
       this.selectedNode = null
       this.focusCanvasContainer()
@@ -1009,7 +1116,7 @@ export default {
         end: '#ff4d4f',
         condition: '#faad14',
         AI_CALL_TASK: '#1890ff',
-        AI_ADD_WX_TASK: '#722ed1',
+        AI_ADD_WX_TASK_NEW: '#722ed1',
         AI_SEND_MSG_TASK: '#eb2f96',
         DELAY_TASK: '#13c2c2'
       }
@@ -1620,6 +1727,10 @@ export default {
       if (!this.selectedEdge.conditionExprObj) {
         this.$set(this.selectedEdge, 'conditionExprObj', [])
       }
+      if (this.edgeSourceNode && this.edgeSourceNode.nodeType == 'AI_CALL_TASK' && this.selectedEdge.conditionExprObj.length >= 1) {
+        this.msgWarning('外呼节点连线条件只能新增一条')
+        return
+      }
       this.selectedEdge.conditionExprObj.push(JSON.parse(JSON.stringify(this.conditionExprTemp)));
     },
     removeCondition(index) {
@@ -1725,6 +1836,19 @@ export default {
         edges.conditionExpr = JSON.stringify(edges.conditionExprObj);
       })
       nodes.filter(node => typeof node.nodeConfig == 'object').forEach(node => {
+        // 隐藏转人工业务组字段,不保存busiGroupId
+        if (node.nodeType === 'AI_CALL_TASK' && node.nodeConfig.busiGroupId !== undefined) {
+          delete node.nodeConfig.busiGroupId;
+        }
+        // 转人工分机:将ID数组转换为完整分机信息列表供后端持久化
+        if (node.nodeType === 'AI_CALL_TASK' && node.nodeConfig.extensionIds && node.nodeConfig.extensionIds.length > 0) {
+          node.nodeConfig.extensionList = this.easyCallExtensionList
+            .filter(item => node.nodeConfig.extensionIds.includes(item.extId))
+            .map(({ _label, ...item }) => item)
+        } else if (node.nodeType === 'AI_CALL_TASK') {
+          delete node.nodeConfig.extensionList
+        }
+        delete node.nodeConfig.extensionIds;
         node.nodeConfig = JSON.stringify(node.nodeConfig);
       })
 

+ 8 - 6
src/views/company/companyWorkflow/index.vue

@@ -10,7 +10,7 @@
           @keyup.enter.native="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="工作流类型" prop="workflowType">
+      <!-- <el-form-item label="工作流类型" prop="workflowType">
         <el-select v-model="queryParams.workflowType" placeholder="请选择类型" clearable size="small">
           <el-option
             v-for="dict in workflowTypeOptions"
@@ -19,8 +19,8 @@
             :value="dict.dictValue"
           />
         </el-select>
-      </el-form-item>
-      <el-form-item label="状态" prop="status">
+      </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"
@@ -29,7 +29,7 @@
             :value="dict.dictValue"
           />
         </el-select>
-      </el-form-item>
+      </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>
@@ -89,6 +89,8 @@
           />
         </template>
       </el-table-column>
+      <el-table-column label="创建人" align="center" prop="createByName"/>
+      <el-table-column label="创建部门" align="center" prop="createByDeptName"/>
       <el-table-column label="版本" align="center" prop="version" width="60" />
       <el-table-column label="创建时间" align="center" prop="createTime" width="160">
         <template slot-scope="scope">
@@ -103,12 +105,12 @@
             icon="el-icon-edit"
             @click="handleEdit(scope.row)"
           >设计</el-button>
-          <el-button
+          <!-- <el-button
             size="mini"
             type="text"
             icon="el-icon-user"
             @click="handleBindSales(scope.row)"
-          >绑定销售</el-button>
+          >绑定销售</el-button> -->
           <el-button
             size="mini"
             type="text"

+ 657 - 0
src/views/company/companyWorkflow/myIndex.vue

@@ -0,0 +1,657 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="90px">
+      <el-form-item label="工作流名称" prop="workflowName">
+        <el-input
+          v-model="queryParams.workflowName"
+          placeholder="请输入工作流名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="工作流类型" prop="workflowType">
+        <el-select v-model="queryParams.workflowType" placeholder="请选择类型" clearable size="small">
+          <el-option
+            v-for="dict in workflowTypeOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option
+            v-for="dict in statusOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <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"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" border :data="workflowList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="ID" align="center" prop="workflowId" width="80" />
+      <el-table-column label="工作流名称" align="center" prop="workflowName" min-width="150" show-overflow-tooltip />
+      <el-table-column label="描述" align="center" prop="workflowDesc" min-width="200" show-overflow-tooltip />
+      <el-table-column label="类型" align="center" prop="workflowType" width="100">
+        <template slot-scope="scope">
+          <dict-tag :options="workflowTypeOptions" :value="scope.row.workflowType"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status" width="80">
+        <template slot-scope="scope">
+          <el-switch
+            v-model="scope.row.status"
+            :active-value="1"
+            :inactive-value="0"
+            @change="handleStatusChange(scope.row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="创建人" align="center" prop="createByName"/>
+      <el-table-column label="创建部门" align="center" prop="createByDeptName"/>
+      <el-table-column label="版本" align="center" prop="version" width="60" />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="200">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleEdit(scope.row)"
+          >设计</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-user"
+            @click="handleBindSales(scope.row)"
+          >绑定销售</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-document-copy"
+            @click="handleCopy(scope.row)"
+          >复制</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+          >删除</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleVersion(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="绑定销售" :visible.sync="bindSalesOpen" width="500px" append-to-body>
+      <!-- 当前绑定的销售 -->
+      <div class="current-sales">
+        <div class="section-title">当前绑定销售</div>
+        <div v-if="currentBindUserList && currentBindUserList.length > 0" class="bind-info">
+          <el-tag
+            v-for="(user, index) in currentBindUserList"
+            :key="index"
+            type="success"
+            style="margin-right: 8px; margin-bottom: 8px;"
+          >
+            {{ user.userName || user.user_name }}({{ user.nickName || user.nick_name }})
+          </el-tag>
+        </div>
+        <div v-else class="no-bind">
+          <el-tag type="info">暂未绑定销售</el-tag>
+        </div>
+      </div>
+
+      <!-- 可选销售列表 -->
+      <div class="sales-list">
+        <div class="section-title">选择销售</div>
+        <el-input
+          v-model="salesSearchKey"
+          placeholder="搜索昵称"
+          size="small"
+          prefix-icon="el-icon-search"
+          clearable
+          style="margin-bottom: 10px"
+        />
+        <el-table
+          :data="filteredCompanyUserList"
+          v-loading="salesLoading"
+          border
+          height="300"
+          :row-key="getSalesRowKey"
+          :row-class-name="salesRowClassName"
+          @selection-change="handleSalesSelectionChange"
+          ref="salesTable"
+        >
+          <el-table-column type="selection" width="55" align="center" :selectable="checkSelectable" :reserve-selection="true" />
+          <el-table-column label="用户名" align="center" prop="userName" />
+          <el-table-column label="昵称" align="center" prop="nickName" />
+        </el-table>
+      </div>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="handleBindSalesCancel">取 消</el-button>
+        <el-button type="primary" :disabled="selectedSalesList.length === 0" @click="confirmBindSales">确 定</el-button>
+      </div>
+    </el-dialog>
+
+
+    <!-- 历史版本弹窗 -->
+    <el-dialog
+      title="历史版本"
+      :visible.sync="versionOpen"
+      width="60%"
+      custom-class="version-dialog"
+      append-to-body
+    >
+      <el-table
+          v-loading="versionLoading"
+          :data="versionList"
+          border
+          size="small"
+      >
+          <el-table-column label="版本号" align="center" width="120">
+              <template slot-scope="scope">
+                  <el-tag v-if="scope.row.versionNo === currentVersionNo" type="warning">
+                      v{{ scope.row.versionNo }}(当前)
+                  </el-tag>
+                  <el-tag v-else type="success">
+                      v{{ scope.row.versionNo }}
+                  </el-tag>
+              </template>
+          </el-table-column>
+
+          <el-table-column label="工作流名称" align="center" prop="workflowName" min-width="60" />
+
+          <el-table-column label="快照时间" align="center" width="170">
+              <template slot-scope="scope">
+                  <span>{{scope.row.snapshotTime }}</span>
+              </template>
+          </el-table-column>
+
+          <el-table-column label="操作人" align="center" prop="snapshotBy" width="120" />
+
+          <el-table-column label="操作" align="center" width="180">
+              <template slot-scope="scope">
+                  <el-button
+                      type="text"
+                      size="mini"
+                      @click="handleViewVersion(scope.row)"
+                  >查看</el-button>
+                  <el-button
+                      v-if="versionList.length > 1 && scope.row.versionNo !== currentVersionNo"
+                      type="text"
+                      size="mini"
+                      style="color: #f56c6c"
+                      @click="handleRollbackVersion(scope.row)"
+                  >回退</el-button>
+              </template>
+          </el-table-column>
+      </el-table>
+
+      <div slot="footer" class="dialog-footer">
+          <el-button @click="versionOpen = false">关闭</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { myListWorkflow, delWorkflow, updateWorkflowStatus, copyWorkflow, exportWorkflow, getBindCompanyUserByWorkflowId,
+         listCompanyUser, updateWorkflowBindCompanyUser, getWorkflowVersionList, getWorkflowVersionDetail, rollbackWorkflowVersion} from '@/api/company/companyWorkflow'
+
+export default {
+  name: 'myCompanyWorkflow',
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 工作流表格数据
+      workflowList: [],
+      // 工作流类型字典
+      workflowTypeOptions: [
+        { dictValue: '1', dictLabel: '对话流程' },
+        { dictValue: '2', dictLabel: '任务流程' },
+        { dictValue: '3', dictLabel: '审批流程' }
+      ],
+      // 状态字典
+      statusOptions: [
+        { dictValue: '1', dictLabel: '启用' },
+        { dictValue: '0', dictLabel: '禁用' }
+      ],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        workflowName: null,
+        workflowType: null,
+        status: null
+      },
+      // 绑定销售弹窗
+      bindSalesOpen: false,
+      // 销售列表加载
+      salesLoading: false,
+      // 当前操作的工作流
+      currentWorkflow: null,
+      // 当前绑定的用户列表
+      currentBindUserList: [],
+      // 销售列表
+      companyUserList: [],
+      // 选中的销售列表
+      selectedSalesList: [],
+      // 销售搜索关键字
+      salesSearchKey: '',
+      // 历史版本弹窗
+      versionOpen: false,
+      // 版本列表
+      versionList: [],
+      // 加载状态
+      versionLoading: false,
+      // 当前版本详情弹窗
+      versionDetailOpen: false,
+      // 当前版本详情加载
+      versionDetailLoading: false,
+      // 当前版本详情
+      versionDetail: {
+        workflowVersion: {},
+        nodes: [],
+        edges: []
+      },
+      currentVersionNo: null,
+    }
+  },
+  computed: {
+    // 过滤后的销售列表
+    filteredCompanyUserList() {
+      if (!this.salesSearchKey) {
+        return this.companyUserList
+      }
+      const key = this.salesSearchKey.toLowerCase()
+      return this.companyUserList.filter(item => {
+        return (item.nickName && item.nickName.toLowerCase().includes(key))
+      })
+    }
+  },
+  created() {
+    this.getList()
+    // 如果有字典配置,可以使用以下方式获取
+    // this.getDicts("ai_workflow_type").then(response => {
+    //   this.workflowTypeOptions = response.data
+    // })
+  },
+  watch: {
+    '$route.query.openVersionWorkflowId': {
+        handler(val) {
+            if (val) {
+                this.$nextTick(() => {
+                    this.autoOpenVersionDialog(val)
+                })
+            }
+        },
+        immediate: true
+    }
+  },
+  methods: {
+    /** 查询工作流列表 */
+    getList() {
+        this.loading = true
+        myListWorkflow(this.queryParams).then(response => {
+            this.workflowList = response.rows
+            this.total = response.total
+            this.loading = false
+
+            const workflowId = this.$route.query.openVersionWorkflowId
+            if (workflowId) {
+                this.$nextTick(() => {
+                    this.autoOpenVersionDialog(workflowId)
+                })
+            }
+        }).catch(() => {
+            this.loading = false
+        })
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.workflowId)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.$router.push('/companyWx/companyWorkflow/design')
+    },
+    /** 设计按钮操作 */
+    handleEdit(row) {
+      this.$router.push('/companyWx/companyWorkflow/design/' + row.workflowId)
+    },
+    /** 复制按钮操作 */
+    handleCopy(row) {
+      this.$confirm('是否确认复制该工作流?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return copyWorkflow(row.workflowId)
+      }).then(() => {
+        this.getList()
+        this.msgSuccess('复制成功')
+      }).catch(() => {})
+    },
+    /** 状态修改 */
+    handleStatusChange(row) {
+      const text = row.status === 1 ? '启用' : '停用'
+      this.$confirm('确认要"' + text + '"该工作流吗?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return updateWorkflowStatus(row.workflowId, row.status)
+      }).then(() => {
+        this.msgSuccess(text + '成功')
+      }).catch(() => {
+        row.status = row.status === 1 ? 0 : 1
+      })
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const workflowIds = row.workflowId || this.ids
+      this.$confirm('是否确认删除工作流编号为"' + workflowIds + '"的数据项?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        return delWorkflow(workflowIds)
+      }).then(() => {
+        this.getList()
+        this.msgSuccess('删除成功')
+      }).catch(() => {})
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams
+      this.$confirm('是否确认导出所有工作流数据项?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.exportLoading = true
+        return exportWorkflow(queryParams)
+      }).then(response => {
+        this.download(response.msg)
+        this.exportLoading = false
+      }).catch(() => {})
+    },
+    /** 绑定销售按钮操作 */
+    handleBindSales(row) {
+      this.currentWorkflow = row
+      this.currentBindUserList = []
+      this.selectedSalesList = []
+      this.salesSearchKey = ''
+      this.bindSalesOpen = true
+      this.salesLoading = true
+      console.log(11111)
+      // 获取当前绑定的销售列表
+      getBindCompanyUserByWorkflowId(row.workflowId).then(res => {
+        this.currentBindUserList = res.data.data || []
+      }).catch(() => {})
+      console.log(this.currentBindUserList)
+      // 获取销售列表
+      listCompanyUser().then(res => {
+        this.companyUserList = res.data || res.rows || []
+        this.salesLoading = false
+      }).catch(() => {
+        this.salesLoading = false
+      })
+    },
+    /** 销售勾选变化 */
+    handleSalesSelectionChange(selection) {
+      this.selectedSalesList = selection
+    },
+    /** 获取销售行key */
+    getSalesRowKey(row) {
+      return row.userId || row.companyUserId || row.id
+    },
+    /** 取消绑定销售弹窗 */
+    handleBindSalesCancel() {
+      this.bindSalesOpen = false
+      this.selectedSalesList = []
+      this.$refs.salesTable && this.$refs.salesTable.clearSelection()
+    },
+    /** 销售行样式 */
+    salesRowClassName({ row }) {
+      if (this.isCurrentBindUser(row)) {
+        return 'disabled-row'
+      }
+      return ''
+    },
+    /** 判断是否为当前绑定用户 */
+    isCurrentBindUser(row) {
+      if (!this.currentBindUserList || this.currentBindUserList.length === 0) {
+        return false
+      }
+      const rowUserId = row.userId || row.companyUserId || row.user_id
+      return this.currentBindUserList.some(user => {
+        const bindUserId = user.userId || user.companyUserId || user.user_id
+        return bindUserId == rowUserId
+      })
+    },
+    /** 检查是否可选 */
+    checkSelectable(row) {
+      return !this.isCurrentBindUser(row)
+    },
+    /** 确认绑定销售 */
+    confirmBindSales() {
+      if (this.selectedSalesList.length === 0) {
+        this.msgWarning('请选择要绑定的销售')
+        return
+      }
+
+      const workflowId = this.currentWorkflow.workflowId
+      const companyUserIds = this.selectedSalesList.map(item => item.userId || item.companyUserId)
+
+      this.doBindSales(workflowId, companyUserIds)
+    },
+    /** 执行绑定销售 */
+    doBindSales(workflowId, companyUserIds) {
+      updateWorkflowBindCompanyUser({
+        workflowId: workflowId,
+        companyUserIds: companyUserIds
+      }).then(() => {
+        this.msgSuccess('绑定成功')
+        this.bindSalesOpen = false
+        this.getList()
+      })
+    },
+    /** 打开历史版本弹窗 */
+    handleVersion(row) {
+      this.currentWorkflow = row
+      this.currentVersionNo = row.version
+      this.versionOpen = true
+      this.versionLoading = true
+      this.versionList = []
+
+      getWorkflowVersionList(row.workflowId).then(res => {
+          this.versionList = res.data || []
+          this.versionLoading = false
+      }).catch(() => {
+          this.versionLoading = false
+      })
+    },
+
+    /** 查看版本详情 */
+    handleViewVersion(row) {
+        this.$router.push({
+            path: '/companyWorkflow/version-preview/' + row.versionId,
+            query: {
+                workflowId: this.currentWorkflow.workflowId
+            }
+        })
+    },
+
+    /** 一键回退 */
+    handleRollbackVersion(row) {
+      this.$confirm('是否确认回退到版本 v' + row.versionNo + ' ?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+      }).then(() => {
+          return rollbackWorkflowVersion(row.versionId)
+      }).then(() => {
+          this.msgSuccess('回退成功')
+          this.versionOpen = false
+          this.versionDetailOpen = false
+          this.getList()
+      }).catch(() => {})
+    },
+    /** 自动打开历史版本弹窗 */
+    autoOpenVersionDialog(workflowId) {
+      const row = this.workflowList.find(item => String(item.workflowId) === String(workflowId))
+      if (!row) {
+          return
+      }
+      this.handleVersion(row)
+
+      // 清掉 query,避免重复触发
+      const query = { ...this.$route.query }
+      delete query.openVersionWorkflowId
+      this.$router.replace({
+          path: this.$route.path,
+          query
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.current-sales {
+  margin-bottom: 20px;
+  padding-bottom: 15px;
+  border-bottom: 1px solid #eee;
+
+  .section-title {
+    font-weight: 600;
+    margin-bottom: 10px;
+    color: #333;
+  }
+
+  .bind-info {
+    .el-tag {
+      font-size: 14px;
+      padding: 8px 15px;
+      height: auto;
+      line-height: 1.5;
+      white-space: normal;
+    }
+  }
+
+  .no-bind {
+    .el-tag {
+      font-size: 14px;
+      padding: 8px 15px;
+      height: auto;
+    }
+  }
+}
+
+.sales-list {
+  .section-title {
+    font-weight: 600;
+    margin-bottom: 10px;
+    color: #333;
+  }
+}
+</style>
+
+<style lang="scss">
+.el-table .disabled-row {
+  background-color: #f5f5f5;
+  color: #999;
+
+  &:hover > td {
+    background-color: #f5f5f5 !important;
+  }
+}
+</style>

+ 60 - 20
src/views/company/wxAccount/index.vue

@@ -89,6 +89,7 @@
       <el-table-column label="微信号" align="center" prop="wxNo" />
       <el-table-column label="手机号" align="center" prop="phone" />
       <el-table-column label="员工" align="center" prop="companyUserName" />
+      <el-table-column label="微信备注前缀" align="center" prop="wxRemark" />
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button
@@ -105,6 +106,13 @@
             @click="handleDelete(scope.row)"
             v-hasPermi="['company:companyWx:remove']"
           >删除</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-refresh"
+            @click="handleSyncCustomer(scope.row)"
+            v-hasPermi="['company:companyWx:edit']"
+          >同步客户</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -118,8 +126,8 @@
     />
 
     <!-- 添加或修改个微账号对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+    <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
         <el-form-item label="微信昵称" prop="wxNickName">
           <el-input v-model="form.wxNickName" placeholder="请输入微信昵称" />
         </el-form-item>
@@ -144,7 +152,7 @@
 </template>
 
 <script>
-import { listCompanyAccount, getCompanyAccount, delCompanyAccount, addCompanyAccount, updateCompanyAccount, exportCompanyAccount, companyListAll } from "@/api/company/companyAccount";
+import { listCompanyAccount, getCompanyAccount, delCompanyAccount, addCompanyAccount, updateCompanyAccount, exportCompanyAccount, companyListAll, syncWx } from "@/api/company/companyAccount";
 import {getAllUserlist} from "@/api/company/companyUser";
 
 
@@ -184,6 +192,24 @@ export default {
       form: {},
       // 表单校验
       rules: {
+        wxNickName: [
+          { required: true, message: '请输入微信昵称', trigger: 'blur' }
+        ],
+        wxNo: [
+          { required: true, message: '请输入微信号', trigger: 'blur' }
+        ],
+        phone: [
+          { required: true, message: '请输入手机号', trigger: 'blur' },
+          { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
+        ],
+        companyUserId: [
+          { required: true, message: '请选择员工', trigger: 'change' }
+        ],
+        wxRemark: [
+          { required: true, message: '请输入微信备注前缀', trigger: 'blur' },
+          { len: 6, message: '微信备注前缀必须为6位', trigger: 'blur' },
+          { pattern: /^[0-9a-zA-Z\u4e00-\u9fa5]{6}$/, message: '微信备注前缀只能输入6位数字、字母或中文', trigger: 'blur' }
+        ]
       }
     };
   },
@@ -280,28 +306,42 @@ export default {
     handleDelete(row) {
       const ids = row.id || this.ids;
       this.$confirm('是否确认删除个微账号编号为"' + ids + '"的数据项?', "警告", {
-        confirmButtonText: "确定",
-        cancelButtonText: "取消",
-        type: "warning"
-      }).then(function() {
-        return delCompanyAccount(ids);
-      }).then(() => {
-        this.getList();
-        this.msgSuccess("删除成功");
-      }).catch(function() {});
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delCompanyAccount(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(function() {});
     },
     /** 导出按钮操作 */
     handleExport() {
       const queryParams = this.queryParams;
       this.$confirm('是否确认导出所有个微账号数据项?', "警告", {
-        confirmButtonText: "确定",
-        cancelButtonText: "取消",
-        type: "warning"
-      }).then(function() {
-        return exportCompanyAccount(queryParams);
-      }).then(response => {
-        this.download(response.msg);
-      }).catch(function() {});
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return exportCompanyAccount(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+        }).catch(function() {});
+    },
+    /** 同步客户按钮操作 */
+    handleSyncCustomer(row) {
+      this.$confirm('是否确认同步该个微账号的客户数据?', "提示", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          return syncWx({ accountId: row.id });
+        }).then(response => {
+          if (response.code === 200) {
+            this.msgSuccess("同步客户成功");
+          }
+        }).catch(function() {});
     },
   }
 };

+ 304 - 0
src/views/company/wxAccount/myIndex.vue

@@ -0,0 +1,304 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="微信昵称" prop="wxNickName">
+        <el-input
+          v-model="queryParams.wxNickName"
+          placeholder="请输入微信昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="微信号" prop="wxNo">
+        <el-input
+          v-model="queryParams.wxNo"
+          placeholder="请输入微信号"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="手机号" prop="phone">
+        <el-input
+          :maxlength="11"
+          v-model="queryParams.phone"
+          placeholder="请输入手机号"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['company:companyWx:myAdd']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="single"
+          @click="handleUpdate"
+          v-hasPermi="['company:companyWx:myEdit']"
+        >修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['company:companyWx:myRemove']"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExport"
+          v-hasPermi="['company:companyWx:myExport']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="companyAccountList" @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="wxNickName" />
+      <el-table-column label="微信号" align="center" prop="wxNo" />
+      <el-table-column label="手机号" align="center" prop="phone" />
+      <el-table-column label="微信备注前缀" align="center" prop="wxRemark" />
+      <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="['company:companyWx:myEdit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['company:companyWx:myRemove']"
+          >删除</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-refresh"
+            @click="handleSyncCustomer(scope.row)"
+            v-hasPermi="['company:companyWx:myEdit']"
+          >同步客户</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="600px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+        <el-form-item label="微信昵称" prop="wxNickName">
+          <el-input v-model="form.wxNickName" placeholder="请输入微信昵称" />
+        </el-form-item>
+        <el-form-item label="微信号" prop="wxNo">
+          <el-input v-model="form.wxNo" placeholder="请输入微信号" />
+        </el-form-item>
+        <el-form-item label="手机号" prop="phone">
+          <el-input v-model="form.phone" :maxlength="11" 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 {
+  myListCompanyAccount,
+  getCompanyAccount,
+  delMyCompanyAccount,
+  addMyCompanyAccount,
+  updateMyCompanyAccount,
+  exportMyCompanyAccount,
+  syncWx
+} from "@/api/company/companyAccount";
+
+export default {
+  name: "MyCompanyAccount",
+  data() {
+    return {
+      loading: true,
+      ids: [],
+      single: true,
+      multiple: true,
+      showSearch: true,
+      total: 0,
+      companyAccountList: [],
+      title: "",
+      open: false,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        wxNickName: null,
+        wxNo: null,
+        phone: null
+      },
+      form: {},
+      rules: {
+        wxNickName: [
+          { required: true, message: '请输入微信昵称', trigger: 'blur' }
+        ],
+        wxNo: [
+          { required: true, message: '请输入微信号', trigger: 'blur' }
+        ],
+        phone: [
+          { required: true, message: '请输入手机号', trigger: 'blur' },
+          { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    getList() {
+      this.loading = true;
+      myListCompanyAccount(this.queryParams).then(response => {
+        this.companyAccountList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    reset() {
+      this.form = {
+        id: null,
+        wxNickName: null,
+        wxNo: null,
+        phone: null
+      };
+      this.resetForm("form");
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加个微账号";
+    },
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getCompanyAccount(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改个微账号";
+      });
+    },
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateMyCompanyAccount(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          } else {
+            addMyCompanyAccount(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          }
+        }
+      });
+    },
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除个微账号编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delMyCompanyAccount(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(function() {});
+    },
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出我的个微账号数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return exportMyCompanyAccount(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+        }).catch(function() {});
+    },
+    handleSyncCustomer(row) {
+      this.$confirm('是否确认同步该个微账号的客户数据?', "提示", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          return syncWx({ accountId: row.id });
+        }).then(response => {
+          if (response.code === 200) {
+            this.msgSuccess("同步客户成功");
+          }
+        }).catch(function() {});
+    }
+  }
+};
+</script>

+ 80 - 0
src/views/crm/components/AiTagPanel.vue

@@ -0,0 +1,80 @@
+<template>
+  <div class="ai-tag-panel">
+    <div v-loading="loading" class="tag-container">
+      <el-tag
+        v-for="tag in list"
+        :key="tag.id"
+        type="info"
+        class="ai-tag-item"
+        effect="plain"
+      >
+        {{ tag.name }}
+      </el-tag>
+      <span v-if="list.length === 0" class="no-data">暂无AI标签</span>
+    </div>
+  </div>
+</template>
+
+<script>
+import { listTag } from "@/api/tag/api";
+
+export default {
+  name: "AiTagPanel",
+  props: {
+    customerId: {
+      type: [Number, String],
+      default: null
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      list: []
+    };
+  },
+  watch: {
+    customerId: {
+      immediate: true,
+      handler(val) {
+        if (val) {
+          this.getList();
+        }
+      }
+    }
+  },
+  methods: {
+    getList() {
+      this.loading = true;
+      listTag({ customerId: this.customerId }).then(response => {
+        this.list = response.rows || response.data || [];
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+    handleTagChange() {
+      this.$emit("tag-change");
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.ai-tag-panel {
+  .tag-container {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    min-height: 24px;
+
+    .ai-tag-item {
+      margin: 0;
+    }
+
+    .no-data {
+      color: #909399;
+      font-size: 13px;
+    }
+  }
+}
+</style>

+ 607 - 0
src/views/crm/components/AppendCustomerSelect.vue

@@ -0,0 +1,607 @@
+<template>
+  <el-drawer size="75%" title="追加客户选择" :visible.sync="shows" append-to-body>
+    <div class="app-container">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="82px">
+        <el-form-item label="公司名" prop="companyId">
+          <el-select filterable v-model="queryParams.companyId" :disabled="designatedCompany" placeholder="请选择公司名" @change="companyChange" clearable size="small">
+            <el-option
+              v-for="item in companys"
+              :key="item.companyId"
+              :label="item.companyName"
+              :value="item.companyId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <treeselect style="width: 220px" :clearable="false" v-model="queryParams.deptId" :options="deptOptions" :show-count="true" placeholder="请选择归属部门" />
+        </el-form-item>
+        <el-form-item label="认领人" prop="companyUserNickName">
+          <el-input
+            v-model="queryParams.companyUserNickName"
+            placeholder="请输入认领人"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="是否可追加" prop="taskCustomerFilter">
+          <el-select v-model="taskCustomerFilter" placeholder="请选择是否可追加" clearable size="small" @change="handleQuery">
+            <el-option label="不在任务(可追加)" value="notInTask" />
+            <el-option label="已在任务(不可追加)" value="inTask" />
+          </el-select>
+        </el-form-item>
+        <!-- <el-form-item label="客户状态" prop="status">
+          <el-select v-model="queryParams.status" placeholder="请选择客户状态" clearable size="small">
+            <el-option
+              v-for="item in statusOptions"
+              :key="'status'+item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="客户类型" prop="customerType">
+          <el-select multiple v-model="ctsTypeArr" placeholder="请选择客户类型" clearable size="small">
+            <el-option
+              v-for="(item, index) in typeOptions"
+              :key="`${item.dictValue}-${index}`"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item> -->
+        <el-form-item label="创建时间" prop="createTime">
+          <el-date-picker clearable size="small" style="width: 205.4px"
+                          v-model="dateRange"
+                          type="daterange"
+                          value-format="yyyy-MM-dd"
+                          start-placeholder="开始日期" end-placeholder="结束日期">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="领取时间" prop="receiveTimeRange">
+          <el-date-picker
+            style="width:205.4px"
+            clearable size="small"
+            v-model="receiveTimeRange"
+            type="daterange"
+            value-format="yyyy-MM-dd"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="客户来源" prop="source">
+          <el-select multiple v-model="sourceArr" placeholder="请选择客户来源" clearable size="small">
+            <el-option
+              v-for="item in sourceOptions"
+              :key="'source'+item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="客户标签" prop="tags">
+          <el-select multiple v-model="tagIds" placeholder="请选择客户标签" clearable size="small">
+            <el-option
+              v-for="(item, index) in tagsOptions"
+              :key="`${item.dictValue}-${index}`"
+              :label="item.dictLabel"
+              :value="item.dictLabel"
+            />
+          </el-select>
+        </el-form-item>
+                <el-form-item label="自定义标签" prop="customTag">
+          <el-input
+            v-model="customTag"
+            placeholder="输入标签关键词模糊搜索"
+            clearable
+            size="small"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+      <el-row :gutter="10" class="mb8" v-if="designatedCompany">
+        <el-col :span="1.5">
+          <el-button
+            type="primary"
+            plain
+            icon="el-icon-edit"
+            size="mini"
+            :loading="conditionLoading"
+            @click="checkWithCondition()"
+          >按筛选条件选中
+          </el-button>
+        </el-col>
+      </el-row>
+      <el-table border v-loading="loading" :data="customerList" :row-key="getRowKeys" @selection-change="handleSelectionChange" size="mini" ref="table" height="450" :selectable="canSelect">
+        <el-table-column type="selection" width="55" align="center" :reserve-selection="true" :selectable="canSelect" />
+        <el-table-column label="ID" align="center" prop="customerId" width="65" />
+        <el-table-column label="所属公司" align="center" prop="companyName" width="220"/>
+        <!-- <el-table-column label="客户编码" align="center" prop="customerCode" width="150"/> -->
+        <el-table-column label="客户名称" align="center" prop="customerName" width="110"/>
+        <el-table-column label="手机" align="center" prop="mobile" width="160">
+          <template slot-scope="scope">
+            <span>{{ scope.row.mobile }}</span>
+            <el-button
+              v-if="scope.row.customerId"
+              type="text"
+              v-hasPermi="['crm:customer:queryPhone']"
+              size="mini"
+              icon="el-icon-view"
+              @click="handleQueryPhone(scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column label="性别" align="center" prop="sex" width="55">
+          <template slot-scope="scope">
+            <el-tag prop="sex" v-for="(item, index) in sexOptions" :key="'sex'+index" v-if="scope.row.sex==item.dictValue">{{item.dictLabel}}</el-tag>
+          </template>
+        </el-table-column>
+        <!-- <el-table-column label="微信号" align="center" prop="weixin" width="95"/> -->
+        <!-- <el-table-column label="所在地" align="center" prop="address" /> -->
+        <el-table-column label="标签" align="center" prop="tags" width="100" :show-overflow-tooltip="true"/>
+        <el-table-column label="客户来源" align="center" prop="source" width="100">
+          <template slot-scope="scope">
+            <el-tag prop="source" v-for="(item, index) in sourceOptions" :key="'source'+index" v-if="scope.row.source==item.dictValue">{{item.dictLabel}}</el-tag>
+          </template>
+        </el-table-column>
+        <!-- <el-table-column label="客户类型" align="center" prop="customerType"> -->
+        <!--   <template slot-scope="scope"> -->
+        <!--     <el-tag prop="customerType" v-for="(item, index) in typeOptions" :key="'customerType'+index" v-if="scope.row.customerType==item.dictValue">{{item.dictLabel}}</el-tag> -->
+        <!--   </template> -->
+        <!-- </el-table-column> -->
+        <!-- <el-table-column label="客户状态" align="center" prop="status"> -->
+        <!--   <template slot-scope="scope"> -->
+        <!--     <el-tag prop="status" v-for="(item, index) in statusOptions" :key="'status'+index" v-if="scope.row.status==item.dictValue">{{item.dictLabel}}</el-tag> -->
+        <!--   </template> -->
+        <!-- </el-table-column> -->
+        <el-table-column label="认领人" align="center" prop="companyUserNickName" width="90"/>
+        <el-table-column align="center" width="120">
+          <template slot="header">
+            <div>今日手动外呼</div>
+            <div style="font-size:11px;color:#909399">(接通/总数)</div>
+          </template>
+          <template slot-scope="scope">
+            {{ (scope.row.todayManualConnectCount || 0) }}/{{ (scope.row.todayManualTotalCount || 0) }}
+          </template>
+        </el-table-column>
+        <el-table-column align="center" width="120">
+          <template slot="header">
+            <div>今日AI外呼</div>
+            <div style="font-size:11px;color:#909399">(接通/总数)</div>
+          </template>
+          <template slot-scope="scope">
+            {{ (scope.row.todayAiConnectCount || 0) }}/{{ (scope.row.todayAiTotalCount || 0) }}
+          </template>
+        </el-table-column>
+        <el-table-column align="center" width="120">
+          <template slot="header">
+            <div>累计手动外呼</div>
+            <div style="font-size:11px;color:#909399">(接通/总数)</div>
+          </template>
+          <template slot-scope="scope">
+            {{ (scope.row.totalManualConnectCount || 0) }}/{{ (scope.row.totalManualTotalCount || 0) }}
+          </template>
+        </el-table-column>
+        <el-table-column align="center" width="120">
+          <template slot="header">
+            <div>累计AI外呼</div>
+            <div style="font-size:11px;color:#909399">(接通/总数)</div>
+          </template>
+          <template slot-scope="scope">
+            {{ (scope.row.totalAiConnectCount || 0) }}/{{ (scope.row.totalAiTotalCount || 0) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" width="80">
+          <template slot-scope="scope">
+            <el-tag v-if="isExcluded(scope.row.customerId)" type="info" size="small">已添加</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" fixed="right" width="80px" align="center" class-name="small-padding fixed-width">
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="text"
+              @click="handleShow(scope.row)"
+              v-hasPermi="['crm:customer:query']"
+            >查看</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination
+        v-show="total>0"
+        :total="total"
+        :page.sync="queryParams.pageNum"
+        :limit.sync="queryParams.pageSize"
+        :page-sizes="pageSizes"
+        @pagination="getList"
+      />
+      <div slot="footer" class="dialog-footer" style="width: 100%;display: flex;flex-direction: row-reverse;margin-top: 20px;z-index: 9898989;padding-top: 5px">
+        <el-button type="primary" :loading="submitLoading" @click="submitForm" style="margin-bottom: 5px">确 定</el-button>
+      </div>
+      <el-drawer
+        size="75%"
+        :title="show.title" :visible.sync="show.open" append-to-body>
+        <customer-details ref="customerDetails" />
+      </el-drawer>
+    </div>
+  </el-drawer>
+</template>
+
+<script>
+import { listCustomerAll,listNoPage, queryPhone } from "@/api/crm/customer";
+import { getCompanyList } from "@/api/company/company";
+import customerDetails from '@/views/crm/components/customerDetails.vue';
+import {getCitys} from "@/api/store/city";
+import { treeselect } from "@/api/company/companyDept";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+
+export default {
+  name: "AppendCustomerSelect",
+  components: { customerDetails, Treeselect },
+  props: {
+    // 通过 setExcludeCustomerIds 方法动态设置,不建议直接通过 prop 传
+  },
+  data() {
+    return {
+      designatedCompany: false,
+      shows: false,
+      deptOptions: [],
+      receiveTimeRange: [],
+      tagId: null,
+      deptId: undefined,
+      companyId: undefined,
+      tagsOptions: [],
+      ctsTypeArr: [],
+      sourceArr: [],
+      dateRange: [],
+      cityIds: [],
+      citys: [],
+      tags: [],
+      tagIds: [],
+      customTag: null,
+      inputVisible: false,
+      inputValue: '',
+      statusOptions: [],
+      typeOptions: [],
+      sourceOptions: [],
+      sexOptions: [],
+      pageSizes: [10, 20, 30, 50, 100, 500],
+      submitLoading: false,
+      roboticId: null,
+      taskCustomerFilter: null,
+      _excludeCustomerIds: [],
+      show: {
+        title: "客户详情",
+        open: false,
+      },
+      companys: [],
+      loading: true,
+      rows: [],
+      ids: [],
+      names: [],
+      conditionLoading: false,
+      isSyncingSelection: false,
+      single: true,
+      multiple: true,
+      showSearch: true,
+      total: 0,
+      customerList: [],
+      title: "",
+      open: false,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        customerCode: null,
+        customerName: null,
+        mobile: null,
+        sex: null,
+        weixin: null,
+        userId: null,
+        createUserId: null,
+        receiveUserId: null,
+        customerUserId: null,
+        address: null,
+        location: null,
+        detailAddress: null,
+        lng: null,
+        lat: null,
+        status: null,
+        isReceive: null,
+        deptId: null,
+        isDel: null,
+        customerType: null,
+        receiveTime: null,
+        poolTime: null,
+        companyId: null,
+        isLine: null,
+        source: null,
+        tags: null,
+        companyUserNickName: null
+      },
+      form: {},
+      rules: {
+        mobile: [
+          { required: true, message: "手机号不能为空", trigger: "blur" }
+        ],
+        source: [
+          { required: true, message: "客户来源不能为空", trigger: "blur" }
+        ],
+      }
+    };
+  },
+  computed: {
+  },
+  created() {
+    this.getDicts("crm_customer_tag").then((response) => {
+      this.tagsOptions = response.data;
+    });
+    this.getDicts("crm_customer_source").then((response) => {
+      this.sourceOptions = response.data;
+    });
+    this.getDicts("common_sex").then((response) => {
+      this.sexOptions = response.data;
+    });
+    this.getDicts("crm_customer_status").then((response) => {
+      this.statusOptions = response.data;
+    });
+    this.getDicts("crm_customer_type").then((response) => {
+      this.typeOptions = response.data;
+    });
+    getCompanyList().then(response => {
+      this.companys = response.data;
+    });
+    this.getCitys();
+    this.getList();
+  },
+  methods: {
+    handleQueryPhone(row) {
+      queryPhone(row.customerId).then(response => {
+        this.$alert(response.mobile, '手机号', {
+          confirmButtonText: '确定',
+          callback: action => {}
+        });
+      });
+    },
+    isExcluded(customerId) {
+      return this._excludeCustomerIds.includes(customerId);
+    },
+    setExcludeCustomerIds(ids) {
+      this._excludeCustomerIds = ids || [];
+    },
+    setTaskId(roboticId) {
+      this.roboticId = roboticId;
+    },
+    canSelect(row) {
+      return !this.isExcluded(row.customerId);
+    },
+    setRows(rows) {
+      this.shows = true;
+      this.rows = rows || [];
+      this.ids = (rows || []).map(item => item.customerId);
+      this.names = (rows || []).map(item => item.customerName);
+      this.getList();
+    },
+    setRowsDesignatedCompany(rows, companyId) {
+      this.designatedCompany = true;
+      this.shows = true;
+      this.rows = rows || [];
+      this.ids = (rows || []).map(item => item.customerId);
+      this.names = (rows || []).map(item => item.customerName);
+      this.queryParams.companyId = companyId;
+      this.companyChange(companyId);
+      this.getList();
+    },
+    initSelect() {
+      this.syncTableSelection();
+    },
+    syncTableSelection() {
+      if (!this.$refs.table) {
+        return;
+      }
+      this.isSyncingSelection = true;
+      const selectedIds = new Set((this.rows || []).map(item => item.customerId));
+      this.$nextTick(() => {
+        this.customerList.forEach(row => {
+          if (this.isExcluded(row.customerId)) {
+            this.$refs.table.toggleRowSelection(row, false);
+          } else {
+            const shouldBeSelected = selectedIds.has(row.customerId);
+            this.$refs.table.toggleRowSelection(row, shouldBeSelected);
+          }
+        });
+        this.$nextTick(() => {
+          this.isSyncingSelection = false;
+        });
+      });
+    },
+    buildQueryParams(extraParams = {}) {
+      const params = Object.assign({}, this.queryParams, extraParams);
+      params.taskCustomerFilter = this.taskCustomerFilter || null;
+      params.roboticId = this.roboticId || null;
+      params.customTag = this.customTag || null;
+      if (this.receiveTimeRange != null && this.receiveTimeRange.length === 2) {
+        params.receiveTimeRange = this.receiveTimeRange[0] + "--" + this.receiveTimeRange[1];
+      } else {
+        params.receiveTimeRange = null;
+      }
+      if (this.ctsTypeArr.length > 0) {
+        params.customerType = this.ctsTypeArr.toString();
+      } else {
+        params.customerType = null;
+      }
+      if (this.sourceArr.length > 0) {
+        params.source = this.sourceArr.toString();
+      } else {
+        params.source = null;
+      }
+      if (this.tagIds.length > 0) {
+        params.tags = this.tagIds.toString();
+      } else {
+        params.tags = null;
+      }
+      return params;
+    },
+    getRowKeys(item) {
+      return item.customerId;
+    },
+    getCitys() {
+      getCitys().then(res => {
+        this.loading = false;
+        this.citys = res.data;
+      })
+    },
+    handleShow(row) {
+      var that = this;
+      that.show.open = true;
+      setTimeout(() => {
+        that.$refs.customerDetails.getDetails(row.customerId);
+      }, 200);
+    },
+    getList() {
+      this.loading = true;
+      const params = this.buildQueryParams();
+      listCustomerAll(this.addDateRange(params, this.dateRange)).then(response => {
+        this.customerList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+        this.initSelect();
+      });
+    },
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.dateRange = [];
+      this.receiveTimeRange = [];
+      this.ctsTypeArr = [];
+      this.sourceArr = [];
+      this.tagIds = [];
+      this.customTag = null;
+      this.taskCustomerFilter = null;
+      if (!this.designatedCompany) {
+        this.queryParams.companyId = null;
+      }
+      this.queryParams.deptId = undefined;
+      this.handleQuery();
+    },
+    handleSelectionChange(selection) {
+      if (this.isSyncingSelection) {
+        return;
+      }
+      const currentPageIds = new Set(this.customerList.map(item => item.customerId));
+      const otherPageRows = (this.rows || []).filter(item => !currentPageIds.has(item.customerId));
+      const rowMap = new Map();
+      otherPageRows.forEach(row => rowMap.set(row.customerId, row));
+      selection.forEach(row => {
+        if (!this.isExcluded(row.customerId)) {
+          rowMap.set(row.customerId, row);
+        }
+      });
+      this.rows = Array.from(rowMap.values());
+      this.ids = this.rows.map(item => item.customerId);
+      this.names = this.rows.map(item => item.customerName);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    getTreeselect() {
+      var that = this;
+      var param = { companyId: this.companyId }
+      treeselect(param).then((response) => {
+        this.deptOptions = response.data;
+      });
+    },
+    companyChange(val) {
+      this.companyId = val;
+      this.getTreeselect();
+    },
+    currDeptChange(val) {
+      this.queryParams.deptId = val;
+      this.getList();
+    },
+    submitForm() {
+      // 过滤掉已存在的客户(排除列表中的)
+      const filteredRows = (this.rows || []).filter(item => !this.isExcluded(item.customerId));
+      const filteredIds = filteredRows.map(item => item.customerId);
+      const filteredNames = filteredRows.map(item => item.customerName);
+
+      if (filteredRows.length === 0) {
+        this.$message.warning("请至少选择一位新客户(已添加的客户无法重复追加)");
+        return;
+      }
+
+      this.submitLoading = true;
+      this.$emit("success", { ids: filteredIds, names: filteredNames, rows: filteredRows });
+      this.shows = false;
+      this.$nextTick(() => {
+        if (this.$refs.table) {
+          this.isSyncingSelection = true;
+          this.$refs.table.clearSelection();
+          this.$nextTick(() => {
+            this.isSyncingSelection = false;
+          });
+        }
+        this.submitLoading = false;
+      });
+    },
+    checkWithCondition() {
+      this.conditionLoading = true;
+      const params = this.buildQueryParams();
+      listNoPage(this.addDateRange(params, this.dateRange)).then(response => {
+        const resList = (response.rows || []).filter(item => !this.isExcluded(item.customerId));
+        this.rows = resList;
+        this.ids = resList.map(item => item.customerId);
+        this.names = resList.map(item => item.customerName);
+        if (this.$refs.table) {
+          this.$refs.table.clearSelection();
+        }
+        this.syncTableSelection();
+        if (resList.length === 0) {
+          this.$message.warning("当前筛选条件下没有可追加的新客户");
+        } else {
+          this.$message.success(`已按筛选条件选中 ${resList.length} 位可追加客户,点击确定完成回传`);
+        }
+      }).finally(() => {
+        this.conditionLoading = false;
+      });
+    }
+  }
+};
+</script>
+<style scoped>
+  .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;
+  }
+  .el-dialog__wrapper{
+    z-index: 100000;
+  }
+  .app-container{padding: 0}
+  .dialog-footer{
+    position: absolute;
+    bottom: 0;
+    right: 20px;
+    background: #FFF;
+  }
+</style>

+ 623 - 437
src/views/crm/components/CustomerSelect.vue

@@ -1,159 +1,239 @@
 <template>
-  <el-drawer size="75%" title="客户选择" :visible.sync="shows" append-to-body>
-    <div class="app-container">
-      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
-        <el-form-item label="公司名" prop="companyId">
-          <el-select filterable  v-model="queryParams.companyId" placeholder="请选择公司名"  @change="companyChange" clearable size="small">
-            <el-option
-              v-for="item in companys"
-              :key="item.companyId"
-              :label="item.companyName"
-              :value="item.companyId"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item >
-          <treeselect style="width: 220px" :clearable="false"  v-model="queryParams.deptId"  :options="deptOptions" :show-count="true" placeholder="请选择归属部门" />
-        </el-form-item>
-        <el-form-item label="认领人" prop="companyUserNickName">
-          <el-input
-            v-model="queryParams.companyUserNickName"
-            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="item in statusOptions"
-              :key="'status'+item.dictValue"
-              :label="item.dictLabel"
-              :value="item.dictValue"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="客户类型" prop="customerType">
-          <el-select multiple v-model="ctsTypeArr" placeholder="请选择客户类型" clearable size="small">
-            <el-option
-              v-for="(item, index) in typeOptions"
-              :key="`${item.dictValue}-${index}`"
-              :label="item.dictLabel"
-              :value="item.dictValue"
-            />
-          </el-select>
-        </el-form-item>
+    <el-drawer size="75%" title="客户选择" :visible.sync="shows" append-to-body>
+        <div class="app-container">
+            <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="82px">
+                <el-form-item label="公司名" prop="companyId">
+                    <el-select filterable  v-model="queryParams.companyId" :disabled="designatedCompany" placeholder="请选择公司名"  @change="companyChange" clearable size="small">
+                        <el-option
+                            v-for="item in companys"
+                            :key="item.companyId"
+                            :label="item.companyName"
+                            :value="item.companyId"
+                        />
+                    </el-select>
+                </el-form-item>
+                <el-form-item >
+                    <treeselect style="width: 220px" :clearable="false"  v-model="queryParams.deptId"  :options="deptOptions" :show-count="true" placeholder="请选择归属部门" />
+                </el-form-item>
+                <el-form-item label="认领人" prop="companyUserNickName">
+                    <el-input
+                        v-model="queryParams.companyUserNickName"
+                        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="item in statusOptions"
+                      :key="'status'+item.dictValue"
+                      :label="item.dictLabel"
+                      :value="item.dictValue"
+                    />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="客户类型" prop="customerType">
+                  <el-select multiple v-model="ctsTypeArr" placeholder="请选择客户类型" clearable size="small">
+                    <el-option
+                      v-for="(item, index) in typeOptions"
+                      :key="`${item.dictValue}-${index}`"
+                      :label="item.dictLabel"
+                      :value="item.dictValue"
+                    />
+                  </el-select>
+                </el-form-item> -->
 
-        <el-form-item label="创建时间" prop="createTime">
-          <el-date-picker clearable size="small" style="width: 205.4px"
-                          v-model="dateRange"
-                          type="daterange"
-                          value-format="yyyy-MM-dd"
-                          start-placeholder="开始日期" end-placeholder="结束日期">
-          </el-date-picker>
-        </el-form-item>
-        <el-form-item label="领取时间" prop="receiveTimeRange">
-          <el-date-picker
-            style="width:205.4px"
-            clearable size="small"
-            v-model="receiveTimeRange"
-            type="daterange"
-            value-format="yyyy-MM-dd"
-            start-placeholder="开始日期"
-            end-placeholder="结束日期">
-          </el-date-picker>
-        </el-form-item>
-        <el-form-item label="客户来源" prop="source">
-          <el-select multiple v-model="sourceArr" placeholder="请选择客户来源" clearable size="small">
-            <el-option
-              v-for="item in sourceOptions"
-              :key="'source'+item.dictValue"
-              :label="item.dictLabel"
-              :value="item.dictValue"
+                <el-form-item label="创建时间" prop="createTime">
+                    <el-date-picker clearable size="small" style="width: 205.4px"
+                                    v-model="dateRange"
+                                    type="daterange"
+                                    value-format="yyyy-MM-dd"
+                                    start-placeholder="开始日期" end-placeholder="结束日期">
+                    </el-date-picker>
+                </el-form-item>
+                <el-form-item label="领取时间" prop="receiveTimeRange">
+                    <el-date-picker
+                        style="width:205.4px"
+                        clearable size="small"
+                        v-model="receiveTimeRange"
+                        type="daterange"
+                        value-format="yyyy-MM-dd"
+                        start-placeholder="开始日期"
+                        end-placeholder="结束日期">
+                    </el-date-picker>
+                </el-form-item>
+                <el-form-item label="客户来源" prop="source">
+                    <el-select multiple v-model="sourceArr" placeholder="请选择客户来源" clearable size="small">
+                        <el-option
+                            v-for="item in sourceOptions"
+                            :key="'source'+item.dictValue"
+                            :label="item.dictLabel"
+                            :value="item.dictValue"
+                        />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="客户标签" prop="tags">
+                    <el-select multiple v-model="tagIds" placeholder="请选择客户标签" clearable size="small">
+                        <el-option
+                            v-for="(item, index) in tagsOptions"
+                            :key="`${item.dictValue}-${index}`"
+                            :label="item.dictLabel"
+                            :value="item.dictLabel"
+                        />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="自定义标签" prop="customTag">
+                    <el-input
+                        v-model="customTag"
+                        placeholder="输入标签关键词模糊搜索"
+                        clearable
+                        size="small"
+                        @keyup.enter.native="handleQuery"
+                    />
+                </el-form-item>
+                <el-form-item label="AI接通筛选" prop="aiConnectFilter">
+                    <el-select v-model="aiConnectFilter" placeholder="请选择AI接通筛选" clearable size="small">
+                        <el-option label="今日AI未接通" value="todayNotConnected" />
+                        <el-option label="累计AI未接通" value="totalNotConnected" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+                    <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+                </el-form-item>
+            </el-form>
+            <el-row :gutter="10" class="mb8" v-if="designatedCompany">
+                <el-col :span="1.5">
+                    <el-button
+                        type="primary"
+                        plain
+                        icon="el-icon-edit"
+                        size="mini"
+                        :loading="conditionLoading"
+                        @click="checkWithCondition()"
+                    >按筛选条件选中
+                    </el-button>
+                </el-col>
+            </el-row>
+            <el-table border v-loading="loading" :data="customerList" :row-key="getRowKeys" @selection-change="handleSelectionChange" size="mini" ref="table" height="450">
+                <el-table-column type="selection" width="55" align="center" :reserve-selection="true" />
+                <el-table-column label="ID" align="center" prop="customerId" width="65" />
+                <el-table-column label="所属公司" align="center" prop="companyName" width="220"/>
+                <!-- <el-table-column label="客户编码" align="center" prop="customerCode" width="150"/> -->
+                <el-table-column label="客户名称" align="center" prop="customerName" width="110"/>
+                <el-table-column label="手机" align="center" prop="mobile" width="160">
+                    <template slot-scope="scope">
+                        <span>{{ scope.row.mobile }}</span>
+                        <el-button
+                            v-if="scope.row.customerId"
+                            type="text"
+                            v-hasPermi="['crm:customer:queryPhone']"
+                            size="mini"
+                            icon="el-icon-view"
+                            @click="handleQueryPhone(scope.row)"
+                        >查看</el-button>
+                    </template>
+                </el-table-column>
+                <el-table-column label="性别" align="center" prop="sex" width="55">
+                    <template slot-scope="scope">
+                        <el-tag prop="sex" v-for="(item, index) in sexOptions" :key="'sex'+index" v-if="scope.row.sex==item.dictValue">{{item.dictLabel}}</el-tag>
+                    </template>
+                </el-table-column>
+                <!-- <el-table-column label="微信号" align="center" prop="weixin" width="95"/> -->
+                <!-- <el-table-column label="所在地" align="center" prop="address" /> -->
+                <el-table-column label="标签" align="center" prop="tags" width="100" :show-overflow-tooltip="true"/>
+                <el-table-column label="客户来源" align="center" prop="source" width="100">
+                    <template slot-scope="scope">
+                        <el-tag prop="source" v-for="(item, index) in sourceOptions" :key="'source'+index" v-if="scope.row.source==item.dictValue">{{item.dictLabel}}</el-tag>
+                    </template>
+                </el-table-column>
+                <!-- <el-table-column label="客户类型" align="center" prop="customerType"> -->
+                <!--   <template slot-scope="scope"> -->
+                <!--     <el-tag prop="customerType" v-for="(item, index) in typeOptions" :key="'customerType'+index" v-if="scope.row.customerType==item.dictValue">{{item.dictLabel}}</el-tag> -->
+                <!--   </template> -->
+                <!-- </el-table-column> -->
+                <!-- <el-table-column label="客户状态" align="center" prop="status"> -->
+                <!--   <template slot-scope="scope"> -->
+                <!--     <el-tag prop="status" v-for="(item, index) in statusOptions" :key="'status'+index" v-if="scope.row.status==item.dictValue">{{item.dictLabel}}</el-tag> -->
+                <!--   </template> -->
+                <!-- </el-table-column> -->
+                <el-table-column label="认领人" align="center" prop="companyUserNickName" width="90"/>
+                <el-table-column align="center" width="120">
+                    <template slot="header">
+                        <div>今日手动外呼</div>
+                        <div style="font-size:11px;color:#909399">(接通/总数)</div>
+                    </template>
+                    <template slot-scope="scope">
+                        {{ (scope.row.todayManualConnectCount || 0) }}/{{ (scope.row.todayManualTotalCount || 0) }}
+                    </template>
+                </el-table-column>
+                <el-table-column align="center" width="120">
+                    <template slot="header">
+                        <div>今日AI外呼</div>
+                        <div style="font-size:11px;color:#909399">(接通/总数)</div>
+                    </template>
+                    <template slot-scope="scope">
+                        {{ (scope.row.todayAiConnectCount || 0) }}/{{ (scope.row.todayAiTotalCount || 0) }}
+                    </template>
+                </el-table-column>
+                <el-table-column align="center" width="120">
+                    <template slot="header">
+                        <div>累计手动外呼</div>
+                        <div style="font-size:11px;color:#909399">(接通/总数)</div>
+                    </template>
+                    <template slot-scope="scope">
+                        {{ (scope.row.totalManualConnectCount || 0) }}/{{ (scope.row.totalManualTotalCount || 0) }}
+                    </template>
+                </el-table-column>
+                <el-table-column align="center" width="120">
+                    <template slot="header">
+                        <div>累计AI外呼</div>
+                        <div style="font-size:11px;color:#909399">(接通/总数)</div>
+                    </template>
+                    <template slot-scope="scope">
+                        {{ (scope.row.totalAiConnectCount || 0) }}/{{ (scope.row.totalAiTotalCount || 0) }}
+                    </template>
+                </el-table-column>
+                <el-table-column label="领取时间" align="center" prop="receiveTime" width="145"/>
+                <el-table-column label="创建时间" align="center" prop="createTime" width="145"/>
+                <!-- <el-table-column label="最后一次跟进时间" align="center" prop="lastTime" width="180"> -->
+                <!-- </el-table-column> -->
+                <!-- <el-table-column label="入公海时间" align="center" prop="poolTime" width="180" /> -->
+                <el-table-column label="操作" fixed="right" width="80px" align="center" class-name="small-padding fixed-width">
+                    <template slot-scope="scope">
+                        <el-button
+                            size="mini"
+                            type="text"
+                            @click="handleShow(scope.row)"
+                            v-hasPermi="['crm:customer:query']"
+                        >查看</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+            <pagination
+                v-show="total>0"
+                :total="total"
+                :page.sync="queryParams.pageNum"
+                :limit.sync="queryParams.pageSize"
+                :page-sizes="pageSizes"
+                @pagination="getList"
             />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="客户标签" prop="tags">
-          <el-select multiple v-model="tagIds" placeholder="请选择客户标签" clearable size="small">
-            <el-option
-              v-for="(item, index) in tagsOptions"
-              :key="`${item.dictValue}-${index}`"
-              :label="item.dictLabel"
-              :value="item.dictLabel"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
-
-      <el-table border v-loading="loading" :data="customerList" :row-key="getRowKeys" @selection-change="handleSelectionChange" size="mini" ref="table" height="460">
-        <el-table-column type="selection" width="55" align="center" :reserve-selection="true" />
-        <el-table-column label="ID" align="center" prop="customerId" />
-        <el-table-column label="所属公司" align="center" prop="companyName" width="300"/>
-        <el-table-column label="客户编码" align="center" prop="customerCode" width="150"/>
-        <el-table-column label="客户名称" align="center" prop="customerName" />
-        <el-table-column label="手机" align="center" prop="mobile" width="90"/>
-        <el-table-column  label="性别" align="center" prop="sex" width="55">
-          <template slot-scope="scope">
-            <el-tag prop="sex" v-for="(item, index) in sexOptions" :key="'sex'+index"   v-if="scope.row.sex==item.dictValue">{{item.dictLabel}}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="微信号" align="center" prop="weixin" width="95"/>
-        <el-table-column label="所在地" align="center" prop="address" />
-        <el-table-column label="标签" align="center" prop="tags" width="100" show-overflow-tooltip="true"/>
-        <el-table-column  label="客户来源" align="center" prop="source">
-          <template slot-scope="scope">
-            <el-tag prop="source" v-for="(item, index) in sourceOptions"  :key="'source'+index"    v-if="scope.row.source==item.dictValue">{{item.dictLabel}}</el-tag>
-          </template>
-        </el-table-column>
-
-        <el-table-column  label="客户类型" align="center" prop="customerType">
-          <template slot-scope="scope">
-            <el-tag prop="customerType" v-for="(item, index) in typeOptions" :key="'customerType'+index"   v-if="scope.row.customerType==item.dictValue">{{item.dictLabel}}</el-tag>
-          </template>
-        </el-table-column>
-
-        <el-table-column  label="客户状态" align="center" prop="status">
-          <template slot-scope="scope">
-            <el-tag prop="status" v-for="(item, index) in statusOptions"  :key="'status'+index"  v-if="scope.row.status==item.dictValue">{{item.dictLabel}}</el-tag>
-          </template>
-        </el-table-column>
-
-        <el-table-column label="认领人" align="center" prop="companyUserNickName" />
-        <el-table-column label="领取时间" align="center" prop="receiveTime"  width="135"/>
-        <el-table-column label="创建时间" align="center" prop="createTime" width="135"/>
-        <el-table-column label="最后一次跟进时间" align="center" prop="lastTime" width="180">
-        </el-table-column>
-        <el-table-column label="入公海时间" align="center" prop="poolTime" width="180" />
-        <el-table-column label="操作" fixed="right" width="180px" align="center" class-name="small-padding fixed-width">
-          <template slot-scope="scope">
-            <el-button
-              size="mini"
-              type="text"
-              @click="handleShow(scope.row)"
-              v-hasPermi="['crm:customer:query']"
-            >查看</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <div slot="footer" class="dialog-footer" style="width: 100%;display: flex;flex-direction: row-reverse;margin-top: 20px;z-index: 9898989;padding-top: 5px">
-        <el-button type="primary" @click="submitForm" style="margin-bottom: 5px">确 定</el-button>
-      </div>
-      <el-drawer
-        size="75%"
-        :title="show.title" :visible.sync="show.open" append-to-body>
-        <customer-details  ref="customerDetails" />
-      </el-drawer>
-    </div>
-  </el-drawer>
+            <div slot="footer" class="dialog-footer" style="width: 100%;display: flex;flex-direction: row-reverse;margin-top: 20px;z-index: 9898989;padding-top: 5px">
+                <el-button type="primary" @click="submitForm" style="margin-bottom: 5px">确 定</el-button>
+            </div>
+            <el-drawer
+                size="75%"
+                :title="show.title" :visible.sync="show.open" append-to-body>
+                <customer-details  ref="customerDetails" />
+            </el-drawer>
+        </div>
+    </el-drawer>
 </template>
 
 <script>
-import { listCustomerAll  } from "@/api/crm/customer";
+import { listCustomerAll,listNoPage, queryPhone } from "@/api/crm/customer";
 import { getCompanyList } from "@/api/company/company";
 import customerDetails from '@/views/crm/components/customerDetails.vue';
 import editCustomerSource from '@/views/crm/components/editSource.vue';
@@ -164,311 +244,417 @@ import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 
 export default {
-  name: "CustomerSelect",
-  components: { customerDetails,editCustomerSource,Treeselect },
-  data() {
-    return {
-      source:{
-        title:"修改客户来源",
-        open:false,
-      },
-      shows:false,
-      deptOptions:[],
-      receiveTimeRange:[],
-      tagId:null,
-      deptId:undefined,
-      companyId:undefined,
-      tagsOptions:[],
-      ctsTypeArr:[],
-      sourceArr:[],
-      dateRange:[],
-      cityIds:[],
-      citys:[],
-      tags:[],
-      tagIds:[],
-      inputVisible: false,
-      inputValue: '',
-      statusOptions:[],
-      typeOptions:[],
-      sourceOptions:[],
-      sexOptions:[],
-      pageSizes: [10, 20, 30, 50,100,500 ],
-      show:{
-        title:"客户详情",
-        open:false,
-      },
-      companys:[],
-      // 遮罩层
-      loading: true,
-      // 选中数组
-      ids: [],
-      names: [],
-      userIds: [],
-      // 非单个禁用
-      single: true,
-      // 非多个禁用
-      multiple: true,
-      // 显示搜索条件
-      showSearch: true,
-      // 总条数
-      total: 0,
-      // 客户表格数据
-      customerList: [],
-      // 弹出层标题
-      title: "",
-      // 是否显示弹出层
-      open: false,
-      // 查询参数
-      queryParams: {
-        customerCode: null,
-        customerName: null,
-        mobile: null,
-        sex: null,
-        weixin: null,
-        userId: null,
-        createUserId: null,
-        receiveUserId: null,
-        customerUserId: null,
-        address: null,
-        location: null,
-        detailAddress: null,
-        lng: null,
-        lat: null,
-        status: null,
-        isReceive: null,
-        deptId: null,
-        isDel: null,
-        customerType: null,
-        receiveTime: null,
-        poolTime: null,
-        companyId: null,
-        isLine: null,
-        source: null,
-        tags: null
-      },
-      // 表单参数
-      form: {},
-      // 表单校验
-      rules: {
-        // customerName: [
-        //   { required: true, message: "客户名称不能为空", trigger: "blur" }
-        // ],
-        mobile: [
-          { required: true, message: "手机号不能为空", trigger: "blur" }
-        ],
-        // sex: [
-        //   { required: true, message: "性别不能为空", trigger: "blur" }
-        // ],
-        source: [
-          { required: true, message: "客户来源不能为空", trigger: "blur" }
-        ],
-      }
-    };
-  },
-  created() {
-    this.getDicts("crm_customer_tag").then((response) => {
-      this.tagsOptions = response.data;
-    });
-    this.getDicts("crm_customer_source").then((response) => {
-      this.sourceOptions = response.data;
-    });
-    this.getDicts("common_sex").then((response) => {
-      this.sexOptions = response.data;
-    });
-    this.getDicts("crm_customer_status").then((response) => {
-      this.statusOptions = response.data;
-    });
-    this.getDicts("crm_customer_type").then((response) => {
-      this.typeOptions = response.data;
-    });
-    getCompanyList().then(response => {
-      this.companys = response.data;
-    });
-    this.getCitys();
-    // this.getList();
-  },
-  methods: {
-    setRows(rows){
-      this.shows = true;
-      this.rows = rows;
-      this.initSelect();
-    },
-    initSelect(){
-      if(this.rows && this.rows.length > 0){
-        this.rows.forEach(row => {
-          this.$refs.table.toggleRowSelection(row);
-        })
-      }
-    },
-    getRowKeys(item){
-      return item.customerId;
-    },
-    handleEditSource(){
-      this.source.open=true;
-      var that=this;
-      setTimeout(() => {
-        that.$refs.editSource.handleEdit(that.ids);
-      }, 200);
-
-    },
-    closeSource(){
-      this.source.open=false;
-      this.getList();
-    },
-    tagsChange(e){
-      var item=this.tagsOptions.find(val => val.dictValue === e);
-      console.log(item);
-      this.tags.push(item.dictLabel);
-      this.form.tags=this.tags.toString();
-    },
-    handleCityChange(value) {
-      console.log(value);
-      var nodes=this.$refs.citySelect.getCheckedNodes();
-      this.form.address=nodes[0].pathLabels[0]+"-"+nodes[0].pathLabels[1]+"-"+nodes[0].pathLabels[2];
-      this.form.cityIds=value.toString();
-    },
-    getCitys(){
-        getCitys().then(res => {
-          this.loading = false;
-          this.citys=res.data;
-        })
-    },
-    handleClose(tag) {
-        this.tags.splice(this.tags.indexOf(tag), 1);
-        this.form.tags=this.tags.toString();
-    },
-    showInput() {
-      this.inputVisible = true;
-      this.$nextTick(_ => {
-        this.$refs.saveTagInput.$refs.input.focus();
-      });
-    },
-    handleInputConfirm() {
-      let inputValue = this.inputValue;
-      if (inputValue) {
-        this.tags.push(inputValue);
-      }
-      this.inputVisible = false;
-      this.inputValue = '';
-      this.form.tags=this.tags.toString();
-    },
-    handleShow(row){
-      var that=this;
-      that.show.open=true;
-      setTimeout(() => {
-          that.$refs.customerDetails.getDetails(row.customerId);
-      }, 200);
+    name: "CustomerSelect",
+    components: { customerDetails,editCustomerSource,Treeselect },
+    data() {
+        return {
+            designatedCompany:false,
+            source:{
+                title:"修改客户来源",
+                open:false,
+            },
+            shows:false,
+            deptOptions:[],
+            receiveTimeRange:[],
+            tagId:null,
+            deptId:undefined,
+            companyId:undefined,
+            tagsOptions:[],
+            ctsTypeArr:[],
+            sourceArr:[],
+            dateRange:[],
+            cityIds:[],
+            citys:[],
+            tags:[],
+            tagIds:[],
+            customTag: null,
+            aiConnectFilter: null,
+            inputVisible: false,
+            inputValue: '',
+            statusOptions:[],
+            typeOptions:[],
+            sourceOptions:[],
+            sexOptions:[],
+            pageSizes: [10, 20, 30, 50,100,500 ],
+            show:{
+                title:"客户详情",
+                open:false,
+            },
+            companys:[],
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            rows: [],
+            ids: [],
+            names: [],
+            userIds: [],
+            conditionLoading: false,
+            isSyncingSelection: false,
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 客户表格数据
+            customerList: [],
+            // 弹出层标题
+            title: "",
+            // 是否显示弹出层
+            open: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                customerCode: null,
+                customerName: null,
+                mobile: null,
+                sex: null,
+                weixin: null,
+                userId: null,
+                createUserId: null,
+                receiveUserId: null,
+                customerUserId: null,
+                address: null,
+                location: null,
+                detailAddress: null,
+                lng: null,
+                lat: null,
+                status: null,
+                isReceive: null,
+                deptId: null,
+                isDel: null,
+                customerType: null,
+                receiveTime: null,
+                poolTime: null,
+                companyId: null,
+                isLine: null,
+                source: null,
+                tags: null,
+                companyUserNickName: null
+            },
+            // 表单参数
+            form: {},
+            // 表单校验
+            rules: {
+                // customerName: [
+                //   { required: true, message: "客户名称不能为空", trigger: "blur" }
+                // ],
+                mobile: [
+                    { required: true, message: "手机号不能为空", trigger: "blur" }
+                ],
+                // sex: [
+                //   { required: true, message: "性别不能为空", trigger: "blur" }
+                // ],
+                source: [
+                    { required: true, message: "客户来源不能为空", trigger: "blur" }
+                ],
+            }
+        };
     },
-    /** 查询客户列表 */
-    getList() {
-      this.loading = true;
-      if(this.receiveTimeRange!=null&&this.receiveTimeRange.length==2){
-        this.queryParams.receiveTimeRange=this.receiveTimeRange[0]+"--"+this.receiveTimeRange[1]
-      }
-      else{
-        this.queryParams.receiveTimeRange=null;
-      }
-      if(this.ctsTypeArr.length>0){
-        this.queryParams.customerType=this.ctsTypeArr.toString();
-      }
-      else{
-        this.queryParams.customerType=null
-      }
-
-      if(this.sourceArr.length>0){
-        this.queryParams.source=this.sourceArr.toString();
-      }
-      else{
-        this.queryParams.source=null
-      }
-      if(this.tagIds.length>0){
-        this.queryParams.tags=this.tagIds.toString();
-      }
-      else{
-        this.queryParams.tags=null
-      }
-      listCustomerAll(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
-        this.customerList = response.rows;
-        this.total = response.total;
-        this.loading = false;
-        this.initSelect();
-      });
-    },
-    // 取消按钮
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    /** 搜索按钮操作 */
-    handleQuery() {
-      this.getList();
-    },
-    /** 重置按钮操作 */
-    resetQuery() {
-      this.resetForm("queryForm");
-    },
-    // 多选框选中数据
-    handleSelectionChange(selection) {
-      this.rows = selection;
-      this.ids = selection.map(item => item.customerId)
-      this.names = selection.map(item => item.customerName)
-      this.single = selection.length!==1
-      this.multiple = !selection.length
-    },
-    getTreeselect() {
-        var that=this;
-        var param={companyId:this.companyId}
-        treeselect(param).then((response) => {
-          this.deptOptions = response.data;
-          console.log(this.deptOptions)
-          if(response.data!=null&&response.data.length>0){
-            //this.queryParams.deptId=response.data[0].id;
-          }
+    created() {
+        this.getDicts("crm_customer_tag").then((response) => {
+            this.tagsOptions = response.data;
         });
+        this.getDicts("crm_customer_source").then((response) => {
+            this.sourceOptions = response.data;
+        });
+        this.getDicts("common_sex").then((response) => {
+            this.sexOptions = response.data;
+        });
+        this.getDicts("crm_customer_status").then((response) => {
+            this.statusOptions = response.data;
+        });
+        this.getDicts("crm_customer_type").then((response) => {
+            this.typeOptions = response.data;
+        });
+        getCompanyList().then(response => {
+            this.companys = response.data;
+        });
+        this.getCitys();
+        this.getList();
     },
-    companyChange(val){
-      console.log(val);
-      this.companyId=val;
-      this.getTreeselect();
-    },
-     currDeptChange(val){
-          this.queryParams.deptId=val;
-          this.getList();
-    },
-    submitForm(){
-      this.$nextTick(() => {
-        this.$emit("success", {ids: this.ids, names: this.names, rows: this.rows})
-        this.shows = false;
-        this.$refs.table.clearSelection();
-      })
-    },
-  }
+    methods: {
+        handleQueryPhone(row) {
+            queryPhone(row.customerId).then(response => {
+                this.$alert(response.mobile, '手机号', {
+                    confirmButtonText: '确定',
+                    callback: action => {}
+                });
+            });
+        },
+        setRows(rows){
+            this.shows = true;
+            this.rows = rows || [];
+            this.ids = (rows || []).map(item => item.customerId);
+            this.names = (rows || []).map(item => item.customerName);
+            this.getList();
+        },
+        setRowsDesignatedCompany(rows,companyId){
+            this.designatedCompany = true;
+            this.shows = true;
+            this.rows = rows || [];
+            this.ids = (rows || []).map(item => item.customerId);
+            this.names = (rows || []).map(item => item.customerName);
+            this.queryParams.companyId = companyId;
+            this.companyChange(companyId);
+            this.getList();
+        },
+        initSelect() {
+            this.syncTableSelection();
+        },
+        syncTableSelection() {
+            if (!this.$refs.table) {
+                return;
+            }
+            this.isSyncingSelection = true;
+            const selectedIds = new Set((this.rows || []).map(item => item.customerId));
+            this.$nextTick(() => {
+                this.customerList.forEach(row => {
+                    const shouldBeSelected = selectedIds.has(row.customerId);
+                    this.$refs.table.toggleRowSelection(row, shouldBeSelected);
+                });
+                this.$nextTick(() => {
+                    this.isSyncingSelection = false;
+                });
+            });
+        },
+        buildQueryParams(extraParams = {}) {
+            const params = Object.assign({}, this.queryParams, extraParams);
+            if (this.receiveTimeRange != null && this.receiveTimeRange.length === 2) {
+                params.receiveTimeRange = this.receiveTimeRange[0] + "--" + this.receiveTimeRange[1];
+            } else {
+                params.receiveTimeRange = null;
+            }
+            if (this.ctsTypeArr.length > 0) {
+                params.customerType = this.ctsTypeArr.toString();
+            } else {
+                params.customerType = null;
+            }
+            if (this.sourceArr.length > 0) {
+                params.source = this.sourceArr.toString();
+            } else {
+                params.source = null;
+            }
+            if (this.tagIds.length > 0) {
+                params.tags = this.tagIds.toString();
+            } else {
+                params.tags = null;
+            }
+            params.customTag = this.customTag || null;
+            params.aiConnectFilter = this.aiConnectFilter || null;
+            return params;
+        },
+        getRowKeys(item) {
+            return item.customerId;
+        },
+        handleEditSource() {
+            this.source.open = true;
+            var that = this;
+            setTimeout(() => {
+                that.$refs.editSource.handleEdit(that.ids);
+            }, 200);
+
+        },
+        closeSource() {
+            this.source.open = false;
+            this.getList();
+        },
+        tagsChange(e) {
+            var item = this.tagsOptions.find(val => val.dictValue === e);
+            console.log(item);
+            this.tags.push(item.dictLabel);
+            this.form.tags = this.tags.toString();
+        },
+        handleCityChange(value) {
+            console.log(value);
+            var nodes = this.$refs.citySelect.getCheckedNodes();
+            this.form.address = nodes[0].pathLabels[0] + "-" + nodes[0].pathLabels[1] + "-" + nodes[0].pathLabels[2];
+            this.form.cityIds = value.toString();
+        },
+        getCitys() {
+            getCitys().then(res => {
+                this.loading = false;
+                this.citys = res.data;
+            })
+        },
+        handleClose(tag) {
+            this.tags.splice(this.tags.indexOf(tag), 1);
+            this.form.tags = this.tags.toString();
+        },
+        showInput() {
+            this.inputVisible = true;
+            this.$nextTick(_ => {
+                this.$refs.saveTagInput.$refs.input.focus();
+            });
+        },
+        handleInputConfirm() {
+            let inputValue = this.inputValue;
+            if (inputValue) {
+                this.tags.push(inputValue);
+            }
+            this.inputVisible = false;
+            this.inputValue = '';
+            this.form.tags = this.tags.toString();
+        },
+        handleShow(row) {
+            var that = this;
+            that.show.open = true;
+            setTimeout(() => {
+                that.$refs.customerDetails.getDetails(row.customerId);
+            }, 200);
+        },
+        /** 查询客户列表 */
+        getList() {
+            this.loading = true;
+            const params = this.buildQueryParams();
+            listCustomerAll(this.addDateRange(params, this.dateRange)).then(response => {
+                this.customerList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+                this.initSelect();
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm("queryForm");
+            this.dateRange = [];
+            this.receiveTimeRange = [];
+            this.ctsTypeArr = [];
+            this.sourceArr = [];
+            this.tagIds = [];
+            this.customTag = null;
+            this.aiConnectFilter = null;
+            if (!this.designatedCompany) {
+                this.queryParams.companyId = null;
+            }
+            this.queryParams.deptId = undefined;
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            if (this.isSyncingSelection) {
+                return;
+            }
+            // 当前页所有行的ID集合
+            const currentPageIds = new Set(this.customerList.map(item => item.customerId));
+            // 保留非当前页的已选数据(这些行不在selection中是因为不在当前页,而非用户取消选中)
+            const otherPageRows = (this.rows || []).filter(item => !currentPageIds.has(item.customerId));
+            // 合并:其他页已选行 + 当前页selection(以selection为准反映用户实际操作)
+            const rowMap = new Map();
+            otherPageRows.forEach(row => rowMap.set(row.customerId, row));
+            selection.forEach(row => rowMap.set(row.customerId, row));
+            this.rows = Array.from(rowMap.values());
+            this.ids = this.rows.map(item => item.customerId);
+            this.names = this.rows.map(item => item.customerName);
+            this.single = selection.length !== 1;
+            this.multiple = !selection.length;
+        },
+        getTreeselect() {
+            var that = this;
+            var param = { companyId: this.companyId }
+            treeselect(param).then((response) => {
+                this.deptOptions = response.data;
+                console.log(this.deptOptions)
+                if (response.data != null && response.data.length > 0) {
+                    //this.queryParams.deptId=response.data[0].id;
+                }
+            });
+        },
+        companyChange(val) {
+            console.log(val);
+            this.companyId = val;
+            this.getTreeselect();
+        },
+        currDeptChange(val) {
+            this.queryParams.deptId = val;
+            this.getList();
+        },
+        submitForm() {
+            if (!this.rows || this.rows.length === 0) {
+                this.$message.warning("请至少选择一位客户");
+                return;
+            }
+            this.$emit("success", { ids: this.ids, names: this.names, rows: this.rows });
+            this.shows = false;
+            this.$nextTick(() => {
+                if (this.$refs.table) {
+                    this.isSyncingSelection = true;
+                    this.$refs.table.clearSelection();
+                    this.$nextTick(() => {
+                        this.isSyncingSelection = false;
+                    });
+                }
+            });
+        },
+        // 按照筛选条件选择客户
+        checkWithCondition() {
+            this.conditionLoading = true;
+            const params = this.buildQueryParams();
+            listNoPage(this.addDateRange(params, this.dateRange)).then(response => {
+                const resList = response.rows || [];
+                this.rows = resList;
+                this.ids = resList.map(item => item.customerId);
+                this.names = resList.map(item => item.customerName);
+                if (this.$refs.table) {
+                    this.$refs.table.clearSelection();
+                }
+                this.syncTableSelection();
+                if (resList.length === 0) {
+                    this.$message.warning("当前筛选条件下没有可选择的客户");
+                } else {
+                    this.$message.success(`已按筛选条件选中 ${resList.length} 位客户,点击确定完成回传`);
+                }
+            }).finally(() => {
+                this.conditionLoading = false;
+            });
+        }
+    }
 };
 </script>
 <style scoped>
-  .el-tag + .el-tag {
+.el-tag + .el-tag {
     margin-left: 10px;
-  }
-  .button-new-tag {
+}
+
+.button-new-tag {
     margin-left: 10px;
     height: 32px;
     line-height: 30px;
     padding-top: 0;
     padding-bottom: 0;
-  }
-  .input-new-tag {
+}
+
+.input-new-tag {
     width: 90px;
     margin-left: 10px;
     vertical-align: bottom;
-  }
-  .el-dialog__wrapper{
+}
+
+.el-dialog__wrapper {
     z-index: 100000;
-  }
-  .app-container{padding: 0}
-  .dialog-footer{
+}
+
+.app-container {
+    padding: 0
+}
+
+.dialog-footer {
     position: absolute;
     bottom: 0;
     right: 20px;
     background: #FFF;
-  }
+}
 </style>

+ 571 - 0
src/views/crm/components/CustomerSelectOlde.vue

@@ -0,0 +1,571 @@
+<template>
+  <el-drawer size="75%" title="客户选择" :visible.sync="shows" append-to-body>
+    <div class="app-container">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+        <el-form-item label="公司名" prop="companyId">
+          <el-select filterable  v-model="queryParams.companyId" :disabled="designatedCompany" placeholder="请选择公司名"  @change="companyChange" clearable size="small">
+            <el-option
+              v-for="item in companys"
+              :key="item.companyId"
+              :label="item.companyName"
+              :value="item.companyId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item >
+          <treeselect style="width: 220px" :clearable="false"  v-model="queryParams.deptId"  :options="deptOptions" :show-count="true" placeholder="请选择归属部门" />
+        </el-form-item>
+        <el-form-item label="认领人" prop="companyUserNickName">
+          <el-input
+            v-model="queryParams.companyUserNickName"
+            placeholder="请输入认领人"
+            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="客户电话" prop="mobile">
+              <el-input
+                  v-model="queryParams.mobile"
+                  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="item in statusOptions"
+              :key="'status'+item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="客户类型" prop="customerType">
+          <el-select multiple v-model="ctsTypeArr" placeholder="请选择客户类型" clearable size="small">
+            <el-option
+              v-for="(item, index) in typeOptions"
+              :key="`${item.dictValue}-${index}`"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="创建时间" prop="createTime">
+          <el-date-picker clearable size="small" style="width: 205.4px"
+                          v-model="dateRange"
+                          type="daterange"
+                          value-format="yyyy-MM-dd"
+                          start-placeholder="开始日期" end-placeholder="结束日期">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="领取时间" prop="receiveTimeRange">
+          <el-date-picker
+            style="width:205.4px"
+            clearable size="small"
+            v-model="receiveTimeRange"
+            type="daterange"
+            value-format="yyyy-MM-dd"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="客户来源" prop="source">
+          <el-select multiple v-model="sourceArr" placeholder="请选择客户来源" clearable size="small">
+            <el-option
+              v-for="item in sourceOptions"
+              :key="'source'+item.dictValue"
+              :label="item.dictLabel"
+              :value="item.dictValue"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="客户标签" prop="tags">
+          <el-select multiple v-model="tagIds" placeholder="请选择客户标签" clearable size="small">
+            <el-option
+              v-for="(item, index) in tagsOptions"
+              :key="`${item.dictValue}-${index}`"
+              :label="item.dictLabel"
+              :value="item.dictLabel"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+      <el-row :gutter="10" class="mb8" v-if="designatedCompany">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-edit"
+          size="mini"
+          @click="checkWithCondition()"
+        >按筛选条件选中
+        </el-button>
+      </el-col>
+    </el-row>
+      <el-table border v-loading="loading" :data="customerList" :row-key="getRowKeys" @selection-change="handleSelectionChange" size="mini" ref="table" height="450">
+        <el-table-column type="selection" width="55" align="center" :reserve-selection="true" />
+        <el-table-column label="ID" align="center" prop="customerId" />
+        <el-table-column label="所属公司" align="center" prop="companyName" width="300"/>
+        <el-table-column label="客户编码" align="center" prop="customerCode" width="150"/>
+        <el-table-column label="客户名称" align="center" prop="customerName" />
+        <el-table-column label="手机" align="center" prop="mobile" width="90"/>
+        <el-table-column  label="性别" align="center" prop="sex" width="55">
+          <template slot-scope="scope">
+            <el-tag prop="sex" v-for="(item, index) in sexOptions" :key="'sex'+index"   v-if="scope.row.sex==item.dictValue">{{item.dictLabel}}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="微信号" align="center" prop="weixin" width="95"/>
+        <el-table-column label="所在地" align="center" prop="address" />
+        <el-table-column label="标签" align="center" prop="tags" width="100" :show-overflow-tooltip="true"/>
+        <el-table-column  label="客户来源" align="center" prop="source">
+          <template slot-scope="scope">
+            <el-tag prop="source" v-for="(item, index) in sourceOptions"  :key="'source'+index"    v-if="scope.row.source==item.dictValue">{{item.dictLabel}}</el-tag>
+          </template>
+        </el-table-column>
+
+        <el-table-column  label="客户类型" align="center" prop="customerType">
+          <template slot-scope="scope">
+            <el-tag prop="customerType" v-for="(item, index) in typeOptions" :key="'customerType'+index"   v-if="scope.row.customerType==item.dictValue">{{item.dictLabel}}</el-tag>
+          </template>
+        </el-table-column>
+
+        <el-table-column  label="客户状态" align="center" prop="status">
+          <template slot-scope="scope">
+            <el-tag prop="status" v-for="(item, index) in statusOptions"  :key="'status'+index"  v-if="scope.row.status==item.dictValue">{{item.dictLabel}}</el-tag>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="认领人" align="center" prop="companyUserNickName" />
+        <el-table-column label="领取时间" align="center" prop="receiveTime"  width="135"/>
+        <el-table-column label="创建时间" align="center" prop="createTime" width="135"/>
+        <el-table-column label="最后一次跟进时间" align="center" prop="lastTime" width="180">
+        </el-table-column>
+        <el-table-column label="入公海时间" align="center" prop="poolTime" width="180" />
+        <el-table-column label="操作" fixed="right" width="180px" align="center" class-name="small-padding fixed-width">
+          <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="text"
+              @click="handleShow(scope.row)"
+              v-hasPermi="['crm:customer:query']"
+            >查看</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination
+        v-show="total>0"
+        :limit.sync="queryParams.pageSize"
+        :page.sync="queryParams.pageNum"
+        :total="total"
+        @pagination="getList"
+      />
+      <div slot="footer" class="dialog-footer" style="width: 100%;display: flex;flex-direction: row-reverse;margin-top: 20px;z-index: 9898989;padding-top: 5px">
+        <el-button type="primary" @click="submitForm" style="margin-bottom: 5px">确 定</el-button>
+      </div>
+      <el-drawer
+        size="75%"
+        :title="show.title" :visible.sync="show.open" append-to-body>
+        <customer-details  ref="customerDetails" />
+      </el-drawer>
+    </div>
+  </el-drawer>
+</template>
+
+<script>
+import { listCustomerAll,listNoPage } from "@/api/crm/customer";
+import { getCompanyList } from "@/api/company/company";
+import customerDetails from '@/views/crm/components/customerDetails.vue';
+import editCustomerSource from '@/views/crm/components/editSource.vue';
+import {getCitys} from "@/api/store/city";
+
+import { treeselect } from "@/api/company/companyDept";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+
+export default {
+  name: "CustomerSelect",
+  components: { customerDetails,editCustomerSource,Treeselect },
+  data() {
+    return {
+      designatedCompany:false,
+      source:{
+        title:"修改客户来源",
+        open:false,
+      },
+      shows:false,
+      deptOptions:[],
+      receiveTimeRange:[],
+      tagId:null,
+      deptId:undefined,
+      companyId:undefined,
+      tagsOptions:[],
+      ctsTypeArr:[],
+      sourceArr:[],
+      dateRange:[],
+      cityIds:[],
+      citys:[],
+      tags:[],
+      tagIds:[],
+      inputVisible: false,
+      inputValue: '',
+      statusOptions:[],
+      typeOptions:[],
+      sourceOptions:[],
+      sexOptions:[],
+      pageSizes: [10, 20, 30, 50,100,500 ],
+      show:{
+        title:"客户详情",
+        open:false,
+      },
+      companys:[],
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      names: [],
+      userIds: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 客户表格数据
+      customerList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        customerCode: null,
+        customerName: null,
+        mobile: null,
+        sex: null,
+        weixin: null,
+        userId: null,
+        createUserId: null,
+        receiveUserId: null,
+        customerUserId: null,
+        address: null,
+        location: null,
+        detailAddress: null,
+        lng: null,
+        lat: null,
+        status: null,
+        isReceive: null,
+        deptId: null,
+        isDel: null,
+        customerType: null,
+        receiveTime: null,
+        poolTime: null,
+        companyId: null,
+        isLine: null,
+        source: null,
+        tags: null
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        // customerName: [
+        //   { required: true, message: "客户名称不能为空", trigger: "blur" }
+        // ],
+        mobile: [
+          { required: true, message: "手机号不能为空", trigger: "blur" }
+        ],
+        // sex: [
+        //   { required: true, message: "性别不能为空", trigger: "blur" }
+        // ],
+        source: [
+          { required: true, message: "客户来源不能为空", trigger: "blur" }
+        ],
+      }
+    };
+  },
+  created() {
+    this.getDicts("crm_customer_tag").then((response) => {
+      this.tagsOptions = response.data;
+    });
+    this.getDicts("crm_customer_source").then((response) => {
+      this.sourceOptions = response.data;
+    });
+    this.getDicts("common_sex").then((response) => {
+      this.sexOptions = response.data;
+    });
+    this.getDicts("crm_customer_status").then((response) => {
+      this.statusOptions = response.data;
+    });
+    this.getDicts("crm_customer_type").then((response) => {
+      this.typeOptions = response.data;
+    });
+    getCompanyList().then(response => {
+      this.companys = response.data;
+    });
+    this.getCitys();
+    this.getList();
+  },
+  methods: {
+    setRows(rows){
+      this.shows = true;
+      this.rows = rows;
+      this.initSelect();
+    },
+    setRowsDesignatedCompany(rows,companyId){
+      this.designatedCompany = true;
+      this.shows = true;
+      this.rows = rows;
+      this.companyChange(companyId);
+      this.initSelect();
+      this.queryParams.companyId = companyId;
+    },
+    initSelect(){
+      if(this.rows && this.rows.length > 0){
+        this.rows.forEach(row => {
+          this.$refs.table.toggleRowSelection(row);
+        })
+      }
+    },
+    getRowKeys(item){
+      return item.customerId;
+    },
+    handleEditSource(){
+      this.source.open=true;
+      var that=this;
+      setTimeout(() => {
+        that.$refs.editSource.handleEdit(that.ids);
+      }, 200);
+
+    },
+    closeSource(){
+      this.source.open=false;
+      this.getList();
+    },
+    tagsChange(e){
+      var item=this.tagsOptions.find(val => val.dictValue === e);
+      console.log(item);
+      this.tags.push(item.dictLabel);
+      this.form.tags=this.tags.toString();
+    },
+    handleCityChange(value) {
+      console.log(value);
+      var nodes=this.$refs.citySelect.getCheckedNodes();
+      this.form.address=nodes[0].pathLabels[0]+"-"+nodes[0].pathLabels[1]+"-"+nodes[0].pathLabels[2];
+      this.form.cityIds=value.toString();
+    },
+    getCitys(){
+        getCitys().then(res => {
+          this.loading = false;
+          this.citys=res.data;
+        })
+    },
+    handleClose(tag) {
+        this.tags.splice(this.tags.indexOf(tag), 1);
+        this.form.tags=this.tags.toString();
+    },
+    showInput() {
+      this.inputVisible = true;
+      this.$nextTick(_ => {
+        this.$refs.saveTagInput.$refs.input.focus();
+      });
+    },
+    handleInputConfirm() {
+      let inputValue = this.inputValue;
+      if (inputValue) {
+        this.tags.push(inputValue);
+      }
+      this.inputVisible = false;
+      this.inputValue = '';
+      this.form.tags=this.tags.toString();
+    },
+    handleShow(row){
+      var that=this;
+      that.show.open=true;
+      setTimeout(() => {
+          that.$refs.customerDetails.getDetails(row.customerId);
+      }, 200);
+    },
+    /** 查询客户列表 */
+    getList() {
+      this.loading = true;
+      if(this.receiveTimeRange!=null&&this.receiveTimeRange.length==2){
+        this.queryParams.receiveTimeRange=this.receiveTimeRange[0]+"--"+this.receiveTimeRange[1]
+      }
+      else{
+        this.queryParams.receiveTimeRange=null;
+      }
+      if(this.ctsTypeArr.length>0){
+        this.queryParams.customerType=this.ctsTypeArr.toString();
+      }
+      else{
+        this.queryParams.customerType=null
+      }
+
+      if(this.sourceArr.length>0){
+        this.queryParams.source=this.sourceArr.toString();
+      }
+      else{
+        this.queryParams.source=null
+      }
+      if(this.tagIds.length>0){
+        this.queryParams.tags=this.tagIds.toString();
+      }
+      else{
+        this.queryParams.tags=null
+      }
+      listCustomerAll(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
+        this.customerList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+        this.initSelect();
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.getList();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.rows = selection;
+      this.ids = selection.map(item => item.customerId)
+      this.names = selection.map(item => item.customerName)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    getTreeselect() {
+        var that=this;
+        var param={companyId:this.companyId}
+        treeselect(param).then((response) => {
+          this.deptOptions = response.data;
+          console.log(this.deptOptions)
+          if(response.data!=null&&response.data.length>0){
+            //this.queryParams.deptId=response.data[0].id;
+          }
+        });
+    },
+    companyChange(val){
+      console.log(val);
+      this.companyId=val;
+      this.getTreeselect();
+    },
+     currDeptChange(val){
+          this.queryParams.deptId=val;
+          this.getList();
+    },
+    submitForm(){
+      console.log(this.ids);
+      this.$nextTick(() => {
+        this.$emit("success", {ids: this.ids, names: this.names, rows: this.rows})
+        this.shows = false;
+        this.$refs.table.clearSelection();
+      })
+    },
+    //按照筛选条件选择客户
+    checkWithCondition(){
+
+    let queryParamNoPage = {};
+    queryParamNoPage = this.queryParams;
+    if(this.receiveTimeRange!=null&&this.receiveTimeRange.length==2){
+        queryParamNoPage.receiveTimeRange=this.receiveTimeRange[0]+"--"+this.receiveTimeRange[1]
+      }
+      else{
+        queryParamNoPage.receiveTimeRange=null;
+      }
+      if(this.ctsTypeArr.length>0){
+        queryParamNoPage.customerType=this.ctsTypeArr.toString();
+      }
+      else{
+        queryParamNoPage.customerType=null
+      }
+
+      if(this.sourceArr.length>0){
+        queryParamNoPage.source=this.sourceArr.toString();
+      }
+      else{
+        queryParamNoPage.source=null
+      }
+      if(this.tagIds.length>0){
+        queryParamNoPage.tags=this.tagIds.toString();
+      }
+      else{
+        queryParamNoPage.tags=null
+      }
+      listNoPage(this.addDateRange(queryParamNoPage, this.dateRange)).then(response => {
+        let resList = response.rows;
+        this.ids = [];
+        this.names = [];
+        this.rows = [];
+        if(!!resList){
+          this.ids = resList.map(item => item.customerId);
+          this.names = resList.map(item => item.customerName);
+          this.rows = resList;
+        }
+        this.$nextTick(() => {
+        this.$emit("success", {ids: this.ids, names: this.names, rows: this.rows})
+        this.shows = false;
+        this.$refs.table.clearSelection();
+      })
+      });
+    }
+  }
+};
+</script>
+<style scoped>
+  .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;
+  }
+  .el-dialog__wrapper{
+    z-index: 100000;
+  }
+  .app-container{padding: 0}
+  .dialog-footer{
+    position: absolute;
+    bottom: 0;
+    right: 20px;
+    background: #FFF;
+  }
+</style>

+ 63 - 0
src/views/crm/components/aiAddWxLog.vue

@@ -0,0 +1,63 @@
+<template>
+  <div>
+    <el-table border v-loading="loading" :data="list">
+      <el-table-column label="ID" align="center" prop="logId" />
+      <el-table-column label="客户姓名" align="center" prop="customerName" />
+      <el-table-column label="手机号" align="center" prop="phone" />
+      <el-table-column label="话术名称" align="center" prop="roboticName" />
+      <el-table-column label="外呼号" align="center" prop="callerPhone" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.status == 1 ? 'success' : 'danger'">{{ scope.row.status == 1 ? '成功' : '失败' }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180" />
+    </el-table>
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import { listAddwx } from "@/api/company/addwx";
+
+export default {
+  name: "aiAddWxLog",
+  data() {
+    return {
+      loading: true,
+      total: 0,
+      list: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        customerId: null,
+        roboticId: null
+      }
+    };
+  },
+  methods: {
+    getData(customerId, roboticId) {
+      this.queryParams.customerId = customerId;
+      if (roboticId) {
+        this.queryParams.roboticId = roboticId;
+      }
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    getList() {
+      this.loading = true;
+      listAddwx(this.queryParams).then(response => {
+        this.list = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    }
+  }
+};
+</script>

+ 63 - 0
src/views/crm/components/aiCallVoiceLog.vue

@@ -0,0 +1,63 @@
+<template>
+  <div>
+    <el-table border v-loading="loading" :data="list">
+      <el-table-column label="ID" align="center" prop="voiceId" />
+      <el-table-column label="主叫号" align="center" prop="callerPhone" />
+      <el-table-column label="被叫号" align="center" prop="calleePhone" />
+      <el-table-column label="通话时长(秒)" align="center" prop="callTime" />
+      <el-table-column label="开始时间" align="center" prop="startTime" width="180" />
+      <el-table-column label="结束时间" align="center" prop="endTime" width="180" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.status == 1 ? 'success' : 'danger'">{{ scope.row.status == 1 ? '已接通' : '未接通' }}</el-tag>
+        </template>
+      </el-table-column>
+    </el-table>
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import { listCallphone } from "@/api/company/callphone";
+
+export default {
+  name: "aiCallVoiceLog",
+  data() {
+    return {
+      loading: true,
+      total: 0,
+      list: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        customerId: null,
+        calleesId: null
+      }
+    };
+  },
+  methods: {
+    getData(customerId, calleesId) {
+      this.queryParams.customerId = customerId;
+      if (calleesId) {
+        this.queryParams.calleesId = calleesId;
+      }
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    getList() {
+      this.loading = true;
+      listCallphone(this.queryParams).then(response => {
+        this.list = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    }
+  }
+};
+</script>

+ 66 - 0
src/views/crm/components/aiSendMsgLog.vue

@@ -0,0 +1,66 @@
+<template>
+  <div>
+    <el-table border v-loading="loading" :data="list">
+      <el-table-column label="ID" align="center" prop="logId" />
+      <el-table-column label="客户姓名" align="center" prop="customerName" />
+      <el-table-column label="手机号" align="center" prop="phone" />
+      <el-table-column label="短信内容" align="center" show-overflow-tooltip prop="content" />
+      <el-table-column label="发送时间" align="center" prop="sendTime" width="180" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.status == 1 ? 'success' : 'danger'">{{ scope.row.status == 1 ? '成功' : '失败' }}</el-tag>
+        </template>
+      </el-table-column>
+    </el-table>
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import { listSendmsg } from "@/api/company/sendmsg";
+
+export default {
+  name: "aiSendMsgLog",
+  data() {
+    return {
+      loading: true,
+      total: 0,
+      list: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        customerId: null,
+        calleesId: null,
+        roboticId: null
+      }
+    };
+  },
+  methods: {
+    getData(customerId, calleesId, roboticId) {
+      this.queryParams.customerId = customerId;
+      if (calleesId) {
+        this.queryParams.calleesId = calleesId;
+      }
+      if (roboticId) {
+        this.queryParams.roboticId = roboticId;
+      }
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    getList() {
+      this.loading = true;
+      listSendmsg(this.queryParams).then(response => {
+        this.list = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    }
+  }
+};
+</script>

+ 848 - 415
src/views/crm/components/customerDetails.vue

@@ -1,452 +1,885 @@
 <template>
-    <div class="contents" v-if="item!=null">
-        <div class="customer-title"  >
-            <div class="customer-name">
-                {{ showDuplicate?item.customerName+"[从]":item.customerName}}
-                 <el-button size="mini"  v-if="showDuplicate"  v-hasPermi="['crm:customer:lookDuplicate']"  @click=" getDetails" >主客户</el-button>
+    <div class="customer-detail-container" v-if="item!=null">
+        <!-- 顶部客户信息卡片 -->
+        <div class="customer-header-card">
+            <div class="customer-avatar">
+                <el-avatar :size="64" icon="el-icon-user-solid"></el-avatar>
+            </div>
+            <div class="customer-info">
+                <div class="customer-main-info">
+                    <h2 class="customer-name-title">
+                        {{ showDuplicate ? item.customerName + '[从]' : item.customerName }}
+                        <el-tag v-if="item.status" :type="getStatusType(item.status)" size="medium"
+                                style="margin-left: 12px;"
+                        >
+                            {{ getStatusText(item.status) }}
+                        </el-tag>
+                    </h2>
+                    <div class="customer-meta">
+                        <span class="meta-item meta-item-phone">
+                            <i class="el-icon-phone"></i>
+                            <span class="mobile-value" :title="item.mobile">{{ item.mobile || '未填写' }}</span>
+                            <el-button
+                                v-if="item.customerId && item.mobile"
+                                class="view-phone-btn"
+                                type="text"
+                                size="mini"
+                                icon="el-icon-view"
+                                v-hasPermi="['crm:customer:queryPhone']"
+                                :loading="phoneQueryLoading"
+                                @click="handleQueryPhone(item)"
+                            >查看</el-button>
+                            <!-- <el-button type="text" v-if="isReceive" size="mini"
+                                       @click="callNumber(item.customerId,null,null,null)" icon="el-icon-phone-outline"
+                            >拨打</el-button>
+                            <el-button type="text" v-if="isReceive" size="mini" @click="handleSms(item.mobile)"
+                                       icon="el-icon-message"
+                            >短信</el-button> -->
+                        </span>
+                        <span class="meta-item">
+                            <i class="el-icon-location"></i>
+                            {{ item.address || '未填写' }}
+                        </span>
+                        <span class="meta-item">
+                            <i class="el-icon-time"></i>
+                            创建于 {{ item.createTime }}
+                        </span>
+                    </div>
+                </div>
+                <div class="customer-actions">
+                    <el-button type="primary" size="small" icon="el-icon-edit" v-hasPermi="['crm:customer:edit']"
+                               @click="handleEdit()"
+                    >编辑客户
+                    </el-button>
+                    <el-button type="warning" size="small" icon="el-icon-price-tag" v-hasPermi="['crm:customer:addTag']"
+                               @click="handleAddTag()"
+                    >打标签
+                    </el-button>
+                    <el-button type="info" size="small" icon="el-icon-document" v-hasPermi="['crm:customer:addRemark']"
+                               @click="handleAddRemark()"
+                    >备注
+                    </el-button>
+                    <el-button type="success" size="small" icon="el-icon-chat-dot-round"
+                               @click="handleViewHistory()"
+                    >查看历史沟通
+                    </el-button>
+                    <el-button size="small" v-if="showDuplicate" v-hasPermi="['crm:customer:lookDuplicate']"
+                               @click="getDetails"
+                    >
+                        主客户
+                    </el-button>
+                </div>
+            </div>
+        </div>
+
+        <!-- 标签展示区 -->
+        <div class="customer-tags-section" v-if="item && item.tags">
+            <span class="tags-label">客户标签:</span>
+            <el-tag v-for="tag in item.tags.split(',')" :key="tag" size="small" effect="plain"
+                    style="margin-right: 8px;"
+            >
+                {{ tag }}
+            </el-tag>
+        </div>
+
+        <!-- 信息分组展示 -->
+        <div class="info-sections">
+            <!-- 基本信息 -->
+            <div class="info-section">
+                <div class="section-header">
+                    <span class="section-title"><i class="el-icon-user"></i> 基本信息</span>
+                </div>
+                <div class="section-content">
+                    <el-row :gutter="24">
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">客户编号</span>
+                                <span class="info-value">{{ item.customerCode || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">客户名称</span>
+                                <span class="info-value">{{ item.customerName || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">性别</span>
+                                <span class="info-value">
+                                    <el-tag v-for="dict in sexOptions" :key="dict.dictValue"
+                                            v-if="item.sex==dict.dictValue" size="mini"
+                                    >{{ dict.dictLabel }}</el-tag>
+                                    <span v-if="!item.sex">-</span>
+                                </span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">微信号</span>
+                                <span class="info-value">{{ item.weixin || '-' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                    <el-row :gutter="24">
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">客户来源</span>
+                                <span class="info-value">
+                                    <el-tag v-for="dict in sourceOptions" :key="dict.dictValue"
+                                            v-if="item.source==dict.dictValue" size="mini" type="info"
+                                    >{{ dict.dictLabel }}</el-tag>
+                                    <span v-if="!item.source">-</span>
+                                </span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">客户类型</span>
+                                <span class="info-value">
+                                    <el-tag v-for="dict in typeOptions" :key="dict.dictValue"
+                                            v-if="item.customerType==dict.dictValue" size="mini" type="warning"
+                                    >{{ dict.dictLabel }}</el-tag>
+                                    <span v-if="!item.customerType">-</span>
+                                </span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">最后一次跟进</span>
+                                <span class="info-value">{{ item.visitTime || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">入公海时间</span>
+                                <span class="info-value">{{ item.poolTime || '-' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                </div>
+            </div>
+
+            <!-- 购买信息 -->
+            <div class="info-section">
+                <div class="section-header">
+                    <span class="section-title"><i class="el-icon-shopping-cart-2"></i> 购买信息</span>
+                </div>
+                <div class="section-content">
+                    <el-row :gutter="24">
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">购买渠道</span>
+                                <span class="info-value">{{ item.platformName || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">购买商品</span>
+                                <span class="info-value">{{ item.goodsName || '-' }} {{
+                                        item.goodsSpecification || ''
+                                    }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">购买店铺</span>
+                                <span class="info-value">{{ item.shopName || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">消费金额</span>
+                                <span class="info-value highlight">¥{{ item.payMoney || '0' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                    <el-row :gutter="24">
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">购买次数</span>
+                                <span class="info-value">{{ item.buyCount || '0' }} 次</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">来源渠道编码</span>
+                                <span class="info-value">{{ item.sourceCode || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">推荐编码</span>
+                                <span class="info-value">{{ item.pushCode || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">推荐时间</span>
+                                <span class="info-value">{{ item.pushTime || '-' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                </div>
             </div>
-            <div>
-                <el-button size="mini"  v-hasPermi="['crm:customer:edit']"  @click=" handleEdit()">修改客户</el-button>
-                <el-button size="mini"  v-hasPermi="['crm:customer:addTag']"  @click=" handleAddTag()" >打标签</el-button>
-                <el-button size="mini"  v-hasPermi="['crm:customer:addRemark']"  @click=" handleAddRemark()" >修改备注</el-button>
 
+            <!-- 进线信息 -->
+            <div class="info-section">
+                <div class="section-header">
+                    <span class="section-title"><i class="el-icon-connection"></i> 进线信息</span>
+                </div>
+                <div class="section-content">
+                    <el-row :gutter="24">
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">进线日期</span>
+                                <span class="info-value">{{ item.registerDate || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">进线方式</span>
+                                <span class="info-value">{{ item.registerType || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">进线填写时间</span>
+                                <span class="info-value">{{ item.registerSubmitTime || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">进线链接</span>
+                                <span class="info-value text-ellipsis" :title="item.registerLinkUrl">{{
+                                        item.registerLinkUrl || '-'
+                                    }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                    <el-row :gutter="24">
+                        <el-col :span="24">
+                            <div class="info-item">
+                                <span class="info-label">进线客户详情</span>
+                                <span class="info-value">{{ item.registerDesc || '-' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                </div>
+            </div>
+
+            <!-- 扩展信息 -->
+            <div class="info-section" v-if="exts && exts.length > 0">
+                <div class="section-header">
+                    <span class="section-title"><i class="el-icon-tickets"></i> 扩展信息</span>
+                </div>
+                <div class="section-content">
+                    <el-row :gutter="24">
+                        <el-col :span="6" v-for="ext in exts" :key="ext.extId">
+                            <div class="info-item">
+                                <span class="info-label">{{ ext.name }}</span>
+                                <span class="info-value">{{ ext.value || '-' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                </div>
+            </div>
+
+            <!-- 备注信息 -->
+            <div class="info-section" v-if="item.remark">
+                <div class="section-header">
+                    <span class="section-title"><i class="el-icon-document"></i> 备注信息</span>
+                </div>
+                <div class="section-content">
+                    <div class="remark-content">{{ item.remark }}</div>
+                </div>
+            </div>
+
+            <!-- AI标签 -->
+            <div class="info-section">
+                <div class="ai-tag-wrapper" v-if="item && item.customerId">
+                    <ai-tag-panel ref="aiTagPanel" :customer-id="item.customerId" @tag-change="handleTagChange"
+                    ></ai-tag-panel>
+                </div>
             </div>
         </div>
-        <el-descriptions title="" :column="3" border>
-            <el-descriptions-item label="客户编号"  >
-                <span v-if="item!=null">{{item.customerCode}}</span>
-            </el-descriptions-item>
-            <el-descriptions-item label="客户名称" >
-                <span v-if="item!=null">{{item.customerName}}</span>
-            </el-descriptions-item>
-            <el-descriptions-item label="手机号" >
-                <span v-if="item!=null">{{item.mobile}}</span>
-                 <el-button type="text"  v-if="isReceive" size="mini" @click="callNumber(item.customerId,null,null,null)">拨号</el-button>
-                 <el-button type="text" v-if="isReceive" size="mini" @click="handleSms(item.mobile)">短信</el-button>
-                 <el-button icon="el-icon-search" size="mini" @click="handleMobile" style="margin-left: 20px;" circle v-hasPermi="['crm:customer:query2']"></el-button>
-            </el-descriptions-item>
-            <el-descriptions-item label="性别" >
-                <span v-if="item!=null">
-                    <el-tag  v-for="(dict, index) in sexOptions"    v-if="item.sex==dict.dictValue">{{dict.dictLabel}}</el-tag>
-                </span>
-            </el-descriptions-item>
-            <el-descriptions-item label="微信号" >
-                <span v-if="item!=null">
-                    {{item.weixin}}
-                </span>
-            </el-descriptions-item>
-            <el-descriptions-item label="所在地">
-                <span v-if="item!=null">
-                    {{item.address}}
-                </span>
-            </el-descriptions-item>
-            <el-descriptions-item label="客户来源" >
-                <span v-if="item!=null">
-                    <el-tag  v-for="(dict, index) in sourceOptions"    v-if="item.source==dict.dictValue">{{dict.dictLabel}}</el-tag>
-                </span>
-            </el-descriptions-item>
-            <el-descriptions-item label="客户类型" >
-                <span v-if="item!=null">
-                    <el-tag  v-for="(dict, index) in typeOptions"    v-if="item.customerType==dict.dictValue">{{dict.dictLabel}}</el-tag>
-                </span>
-            </el-descriptions-item>
-            <el-descriptions-item label="客户状态" >
-                <span v-if="item!=null">
-                    <el-tag  v-for="(dict, index) in statusOptions"    v-if="item.status==dict.dictValue">{{dict.dictLabel}}</el-tag>
-                </span>
-            </el-descriptions-item>
-            <el-descriptions-item label="创建时间" >
-                <span v-if="item!=null">
-                    {{item.createTime}}
-                </span>
-            </el-descriptions-item>
-            <el-descriptions-item label="最后一次跟进时间" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.visitTime}}
-                </span>
-            </el-descriptions-item>
-            <el-descriptions-item label="入公海时间" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.poolTime}}
-                </span>
-            </el-descriptions-item>
-
-            <el-descriptions-item label="标签" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.tags}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-
-            <el-descriptions-item label="进线日期" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.registerDate}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-            <el-descriptions-item label="进线链接" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.registerLinkUrl}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-            <el-descriptions-item label="进线客户详情" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.registerDesc}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-            <el-descriptions-item label="进线客户填写时间" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.registerSubmitTime}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-            <el-descriptions-item label="进线方式" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.registerType}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-            <el-descriptions-item label="消费金额" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.payMoney}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-            <el-descriptions-item label="购买次数" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.buyCount}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-            <el-descriptions-item label="来源渠道编码" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.sourceCode}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-            <el-descriptions-item label="推荐编码" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.pushCode}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-            <el-descriptions-item label="推荐时间" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.pushTime}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-
-            <el-descriptions-item :label="ext.name" v-for="ext in exts" label-class-name="my-label">
-                <span >
-                    {{ext.value}}
-                </span>
-            </el-descriptions-item>
-            <el-descriptions-item label="备注" label-class-name="my-label">
-                <span v-if="item!=null">
-                    {{item.remark}}
-                </span>
-                <!-- <el-button size="mini" icon="el-icon-edit"></el-button> -->
-            </el-descriptions-item>
-
-        </el-descriptions>
-
-        <el-tabs style="margin-top:15px;"  z-index = "99" type="border-card" v-model="activeName" @tab-click="handleClick">
-            <el-tab-pane label="跟进记录" name="visit">
-                <customer-visit-list ref="visit"></customer-visit-list>
-            </el-tab-pane>
-            <el-tab-pane label="联系人" name="contacts">
-                <customer-contacts ref="contacts"></customer-contacts>
-            </el-tab-pane>
-            <el-tab-pane label="订单记录" name="storeOrder">
-                <customer-store-order-list ref="storeOrder"></customer-store-order-list>
-            </el-tab-pane>
-
-            <el-tab-pane label="通话记录" name="voiceLogs">
-                <customer-voice-logs-list ref="voiceLogs"></customer-voice-logs-list>
-            </el-tab-pane>
-            <el-tab-pane label="短信记录" name="smsLogs">
-                <customer-sms-logs-list ref="smsLogs"></customer-sms-logs-list>
-            </el-tab-pane>
-            <el-tab-pane label="客户日志" name="logs">
-                <customer-logs-list ref="logs"></customer-logs-list>
-            </el-tab-pane>
-            <el-tab-pane label="历史订单" name="hisOrder">
-                <customer-his-order-list ref="hisOrder"></customer-his-order-list>
-            </el-tab-pane>
-        </el-tabs>
-
-        <el-dialog :title="addTag.title" :visible.sync="addTag.open" width="600px" append-to-body>
+
+        <!-- Tab 和 AI 标签区域 -->
+        <div class="content-area">
+            <el-tabs v-model="activeName" type="card" @tab-click="handleClick" class="customer-tabs">
+                <el-tab-pane label="跟进记录" name="visit">
+                    <customer-visit-list ref="visit"></customer-visit-list>
+                </el-tab-pane>
+                <el-tab-pane label="联系人" name="contacts">
+                    <customer-contacts ref="contacts"></customer-contacts>
+                </el-tab-pane>
+                <el-tab-pane label="订单记录" name="storeOrder">
+                    <customer-store-order-list ref="storeOrder"></customer-store-order-list>
+                </el-tab-pane>
+                <el-tab-pane label="通话记录" name="voiceLogs">
+                    <customer-voice-logs-list ref="voiceLogs"></customer-voice-logs-list>
+                </el-tab-pane>
+                <el-tab-pane label="短信记录" name="smsLogs">
+                    <customer-sms-logs-list ref="smsLogs"></customer-sms-logs-list>
+                </el-tab-pane>
+                <el-tab-pane label="客户日志" name="logs">
+                    <customer-logs-list ref="logs"></customer-logs-list>
+                </el-tab-pane>
+                <el-tab-pane label="历史订单" name="hisOrder">
+                    <customer-his-order-list ref="hisOrder"></customer-his-order-list>
+                </el-tab-pane>
+                <!--                <el-tab-pane label="AI 通话" name="aiVoiceLogs" v-if="checkPermi(['company:callphonelog:list'])">-->
+                <!--                    <ai-call-voice-log ref="aiVoiceRef"></ai-call-voice-log>-->
+                <!--                </el-tab-pane>     -->
+                <el-tab-pane label="AI 通话" name="aiVoiceLogs">
+                    <ai-call-voice-log ref="aiVoiceRef"></ai-call-voice-log>
+                </el-tab-pane>
+                <el-tab-pane label="AI 加微" name="aiAddWxLogs" v-if="checkPermi(['company:addwxlog:list'])">
+                    <ai-add-wx-log ref="aiAddWxRef"></ai-add-wx-log>
+                </el-tab-pane>
+                <el-tab-pane label="AI 短信" name="aiSendMsgLogs" v-if="checkPermi(['company:sendmsglog:list'])">
+                    <ai-send-msg-log ref="aiSendMsgRef"></ai-send-msg-log>
+                </el-tab-pane>
+                <el-tab-pane label="手动外呼记录" name="customerCallLogs" v-if="checkPermi(['crm:customerCallLog:list'])">
+                    <customer-call-log-list ref="customerCallLogRef"></customer-call-log-list>
+                </el-tab-pane>
+            </el-tabs>
+        </div>
+
+        <!-- 弹窗 -->
+        <el-dialog :title="addTag.title" :visible.sync="addTag.open" width="600px" append-to-body :show-close="true" class="customer-dialog">
             <add-tag ref="tag" @close="closeTag()"></add-tag>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="addTag.open = false">取 消</el-button>
+                <el-button type="primary" @click="$refs.tag.submitForm()">确 定</el-button>
+            </span>
         </el-dialog>
-        <el-dialog :title="addRemark.title" :visible.sync="addRemark.open" width="600px" append-to-body>
+        <el-dialog :title="addRemark.title" :visible.sync="addRemark.open" width="600px" append-to-body :show-close="true" class="customer-dialog">
             <add-remark ref="remark" @close="closeRemark()"></add-remark>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="addRemark.open = false">取 消</el-button>
+                <el-button type="primary" @click="$refs.remark.submitForm()">确 定</el-button>
+            </span>
         </el-dialog>
-        <el-dialog :title="addSms.title" :visible.sync="addSms.open" width="800px" append-to-body>
+        <el-dialog :title="addSms.title" :visible.sync="addSms.open" width="800px" append-to-body class="customer-dialog">
             <add-sms ref="sms" @close="closeSms()"></add-sms>
         </el-dialog>
-
-        <el-dialog :title="customer.title" :visible.sync="customer.open" width="1000px" append-to-body>
+        <el-dialog :title="customer.title" :visible.sync="customer.open" width="1000px" append-to-body :show-close="true" class="customer-dialog">
             <add-or-edit-customer ref="customer" @close="closeCustomer()"></add-or-edit-customer>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="customer.open = false">取 消</el-button>
+                <el-button type="primary" @click="$refs.customer.submitForm()">确 定</el-button>
+            </span>
         </el-dialog>
-
-        <!-- <el-dialog :title="duplicate.title" :visible.sync="duplicate.open" width="800px" append-to-body>
-            <duplicate-customer ref="duplicateCustomer" @close="closeDuplicate()"></duplicate-customer>
-        </el-dialog> -->
-
-       <el-drawer size="75%" :modal="false" :title="duplicate.title" :visible.sync="duplicate.open">
-            <duplicate-customer  ref="duplicateCustomer" />
+        <el-drawer size="75%" :modal="false" :title="duplicate.title" :visible.sync="duplicate.open">
+            <duplicate-customer ref="duplicateCustomer"/>
         </el-drawer>
 
+        <!-- 历史沟通弹窗(只读) -->
+        <el-dialog :title="historyDialog.title" :visible.sync="historyDialog.open" width="700px" append-to-body :show-close="true" class="customer-dialog">
+            <div class="history-communication-content">
+                <pre v-if="item && item.historicalCommunication" class="history-text">{{ item.historicalCommunication }}</pre>
+                <el-empty v-else description="暂无历史沟通记录"></el-empty>
+            </div>
+            <span slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="historyDialog.open = false">关 闭</el-button>
+            </span>
+        </el-dialog>
     </div>
 </template>
 
 <script>
-    import { listCustomerExt } from "@/api/crm/customerExt";
-    import customerVisitList from '../components/customerVisitList.vue';
-    import customerLogsList from '../components/customerLogsList.vue';
-    import customerSmsLogsList from '../components/customerSmsLogsList.vue';
-    import customerVoiceLogsList from '../components/customerVoiceLogsList.vue';
-    import customerStoreOrderList from '../components/customerStoreOrderList.vue';
-    import duplicateCustomer from '../components/duplicateCustomer.vue';
-    import customerContacts from './customerContacts.vue';
-    import customerHisOrderList from '../components/customerHisOrderList.vue';
-    import { getCustomerDetails1,updateCustomer,getCustomer1 } from "@/api/crm/customer";
-    import addTag from './addTag.vue';
-    import addRemark from './addRemark.vue';
-    import addSms from './addSms.vue';
-    import addOrEditCustomer from '../components/addOrEditCustomer.vue';
-    export default {
-        name: "customer",
-        components: {customerHisOrderList,addOrEditCustomer,addSms,addTag,addRemark, customerContacts,customerVisitList,customerLogsList,customerVoiceLogsList,customerStoreOrderList,customerSmsLogsList,duplicateCustomer },
-        data() {
-            return {
-                customer:{
-                    open:false,
-                    title:"修改客户"
-                },
-                isReceive:false,
-                tagId:null,
-                tagsOptions:[],
-                addSms:{
-                    open:false,
-                    title:"发短信"
-                },
-                addTag:{
-                    open:false,
-                    title:"打标签"
-                },
-                addRemark:{
-                    open:false,
-                    title:"客户备注"
-                },
-                duplicate:{
-                    open:false,
-                    title:"客户详情"
-                },
-                customerId:null,
-                 // 弹出层标题
-                title: "",
-                // 是否显示弹出层
-                open: false,
-                cityIds:[],
-                citys:[],
-                tags:[],
-                inputVisible: false,
-                inputValue: '',
-                receiveOptions:[],
-                statusOptions:[],
-                typeOptions:[],
-                sourceOptions:[],
-                sexOptions:[],
-                customerExts:[],
-                activeName:"",
-                item:null,
-                showDuplicate:false,
-                dCustomerId:null,
-
-            };
-        },
-        created() {
-            this.getDicts("crm_customer_source").then((response) => {
-                this.sourceOptions = response.data;
-            });
-            this.getDicts("sys_sex").then((response) => {
-                this.sexOptions = response.data;
-            });
-            this.getDicts("crm_customer_tag").then((response) => {
-                this.tagsOptions = response.data;
-            });
-            this.getDicts("crm_customer_status").then((response) => {
-                this.statusOptions = response.data;
-            });
-            this.getDicts("crm_customer_type").then((response) => {
-                this.typeOptions = response.data;
-            });
-            this.getDicts("crm_customer_is_receive").then((response) => {
-                this.receiveOptions = response.data;
-            });
+import { listCustomerExt } from '@/api/crm/customerExt'
+import customerVisitList from '../components/customerVisitList.vue'
+import customerLogsList from '../components/customerLogsList.vue'
+import customerSmsLogsList from '../components/customerSmsLogsList.vue'
+import customerVoiceLogsList from '../components/customerVoiceLogsList.vue'
+import customerStoreOrderList from '../components/customerStoreOrderList.vue'
+import duplicateCustomer from '../components/duplicateCustomer.vue'
+import customerContacts from './customerContacts.vue'
+import customerHisOrderList from '../components/customerHisOrderList.vue'
+import aiCallVoiceLog from './aiCallVoiceLog'
+import aiAddWxLog from './aiAddWxLog'
+import aiSendMsgLog from './aiSendMsgLog'
+//没迁没表
+// import customerCallLogList from '../components/customerCallLogList.vue'
+import AiTagPanel from './AiTagPanel.vue'
+import { getCustomerDetails1, updateCustomer, getCustomer1, queryPhone } from '@/api/crm/customer'
+// import { listCustomerCallLog } from '@/api/crm/customerCallLog'
+import { checkPermi } from '@/utils/permission'
+import addTag from './addTag.vue'
+import addRemark from './addRemark.vue'
+import addSms from './addSms.vue'
+import addOrEditCustomer from '../components/addOrEditCustomer.vue'
 
+export default {
+    name: 'customer',
+    components: {
+        customerHisOrderList,
+        addOrEditCustomer,
+        addSms,
+        addTag,
+        addRemark,
+        customerContacts,
+        customerVisitList,
+        customerLogsList,
+        customerVoiceLogsList,
+        customerStoreOrderList,
+        customerSmsLogsList,
+        duplicateCustomer,
+        aiCallVoiceLog,
+        aiAddWxLog,
+        aiSendMsgLog,
+        AiTagPanel
+    },
+    data() {
+        return {
+            calleesId: null,
+            roboticId: null,
+            customer: { open: false, title: '修改客户' },
+            isReceive: false,
+            tagId: null,
+            tagsOptions: [],
+            addSms: { open: false, title: '发短信' },
+            addTag: { open: false, title: '打标签' },
+            addRemark: { open: false, title: '客户备注' },
+            duplicate: { open: false, title: '客户详情' },
+            customerId: null,
+            title: '',
+            open: false,
+            cityIds: [],
+            citys: [],
+            tags: [],
+            inputVisible: false,
+            inputValue: '',
+            receiveOptions: [],
+            statusOptions: [],
+            typeOptions: [],
+            sourceOptions: [],
+            sexOptions: [],
+            customerExts: [],
+            activeName: '',
+            item: null,
+            showDuplicate: false,
+            dCustomerId: null,
+            phoneQueryLoading: false,
+            // 手动外呼记录总条数
+            callLogTotal: 0,
+            // 历史沟通弹窗
+            historyDialog: { open: false, title: '历史沟通' }
+        }
+    },
+    created() {
+        this.getDicts('crm_customer_source').then((response) => {
+            this.sourceOptions = response.data
+        })
+        this.getDicts('sys_sex').then((response) => {
+            this.sexOptions = response.data
+        })
+        this.getDicts('crm_customer_tag').then((response) => {
+            this.tagsOptions = response.data
+        })
+        this.getDicts('crm_customer_status').then((response) => {
+            this.statusOptions = response.data
+        })
+        this.getDicts('crm_customer_type').then((response) => {
+            this.typeOptions = response.data
+        })
+        this.getDicts('crm_customer_is_receive').then((response) => {
+            this.receiveOptions = response.data
+        })
+    },
+    methods: {
+        handleQueryPhone(item) {
+            if (this.phoneQueryLoading) {
+                return
+            }
+            this.phoneQueryLoading = true
+            queryPhone(item.customerId).then(response => {
+                this.$alert(response.mobile || '暂无手机号', '手机号明文', {
+                    confirmButtonText: '确定',
+                    callback: () => {}
+                })
+            }).finally(() => {
+                this.phoneQueryLoading = false
+            })
         },
-        mounted(){
-
+        checkPermi,
+        getStatusType(status) {
+            const statusMap = { '1': 'success', '2': 'warning', '3': 'danger', '4': 'info' }
+            return statusMap[status] || 'info'
+        },
+        getStatusText(status) {
+            const item = this.statusOptions.find(d => d.dictValue === status)
+            return item ? item.dictLabel : status
+        },
+        handleMobile() {
+            const customerId = this.item.customerId
+            getCustomer1(customerId).then(response => {
+                this.item.mobile = response.mobile
+            })
+        },
+        handleEdit() {
+            this.customer.open = true
+            var that = this
+            setTimeout(() => {
+                that.$refs.customer.handleUpdate(that.customerId)
+            }, 200)
         },
-        methods: {
-            handleMobile(){
-                const customerId = this.item.customerId;
-                getCustomer1(customerId).then(response =>{
-                    this.item.mobile = response.mobile;
+        closeCustomer() {
+            this.customer.open = false
+            this.getDetails(this.customerId)
+        },
+        tagsChange(e) {
+            var item = this.tagsOptions.find(val => val.dictValue === e)
+            this.tags.push(item.dictLabel)
+            this.form.tags = this.tags.toString()
+        },
+        closeSms() {
+            this.addSms.open = false
+            this.getDetails(this.customerId)
+        },
+        handleSms(mobile) {
+            this.addSms.open = true
+            var that = this
+            setTimeout(() => {
+                that.$refs.sms.reset(this.item.customerId, mobile, 1)
+            }, 500)
+        },
+        closeRemark() {
+            this.addRemark.open = false
+            this.getDetails(this.customerId)
+        },
+        handleAddRemark() {
+            this.addRemark.open = true
+            var that = this
+            setTimeout(() => {
+                that.$refs.remark.reset(this.item)
+            }, 500)
+        },
+        closeTag() {
+            this.addTag.open = false
+            this.getDetails(this.customerId)
+        },
+        handleAddTag() {
+            this.addTag.open = true
+            var that = this
+            setTimeout(() => {
+                that.$refs.tag.reset(this.item)
+            }, 500)
+        },
+        handleClick(tab, event) {
+            if (tab.name == 'contacts') {
+                this.$refs.contacts.getData(this.item.customerId)
+            }
+            if (tab.name == 'visit') {
+                this.$refs.visit.getData(this.item.customerId, this.isReceive)
+            }
+            if (tab.name == 'logs') {
+                this.$refs.logs.getData(this.item.customerId)
+            }
+            if (tab.name == 'voiceLogs') {
+                this.$refs.voiceLogs.getData(this.item.customerId)
+            }
+            if (tab.name == 'storeOrder') {
+                this.$refs.storeOrder.getData(this.item.customerId)
+            }
+            if (tab.name == 'smsLogs') {
+                this.$refs.smsLogs.getData(this.item.customerId)
+            }
+            if (tab.name == 'hisOrder') {
+                this.$refs.hisOrder.getData(this.item.customerId)
+            }
+            if (tab.name == 'aiVoiceLogs') {
+                this.$refs.aiVoiceRef.getData(this.item.customerId, this.calleesId)
+            } else if (tab.name == 'aiAddWxLogs') {
+                this.$refs.aiAddWxRef.getData(this.item.customerId, this.roboticId)
+            } else if (tab.name == 'aiSendMsgLogs') {
+                this.$refs.aiSendMsgRef.getData(this.item.customerId, this.calleesId, this.roboticId)
+            }
+            if (tab.name == 'customerCallLogs') {
+                this.$refs.customerCallLogRef.getData(this.item.customerId)
+            }
+        },
+        getDetails(customerId, calleesId, roboticId) {
+            if (!!calleesId) {
+                this.calleesId = calleesId
+            }
+            if (!!roboticId) {
+                this.roboticId = roboticId
+            }
+            this.phoneQueryLoading = false
+            this.callLogTotal = 0
+            var data = { customerId: customerId }
+            this.customerId = customerId
+            var that = this
+            this.exts = []
+            // 查询“已接通”的手动外呼记录总条数(minCallTime=0即 call_time>0),与主页列表“沟通次数”口径一致;无权限时不调用避免 403【20260625 没迁没表】
+            // if (checkPermi(['crm:customerCallLog:list'])) {
+            //     listCustomerCallLog({ customerId: customerId, minCallTime: 0, pageNum: 1, pageSize: 1 }).then(res => {
+            //         this.callLogTotal = res.total || 0
+            //     }).catch(() => {
+            //         this.callLogTotal = 0
+            //     })
+            // }
+            listCustomerExt(data).then(response => {
+                this.customerExts = response.data
+                this.customerExts.forEach(element => {
+                    var data = { extId: element.extId, name: element.name, value: '' }
+                    this.exts.push(data)
                 })
-            },
-            handleEdit() {
-                this.customer.open = true;
-                var that=this;
-                setTimeout(() => {
-                    that.$refs.customer.handleUpdate(that.customerId);
-                }, 200);
-            },
-            closeCustomer(){
-                this.customer.open=false;
-                this.getDetails(this.customerId)
-            },
-            tagsChange(e){
-                var item=this.tagsOptions.find(val => val.dictValue === e);
-                console.log(item);
-                this.tags.push(item.dictLabel);
-                this.form.tags=this.tags.toString();
-            },
-            closeSms(){
-                this.addSms.open=false;
-                this.getDetails(this.customerId)
-            },
-            handleSms(mobile){
-                this.addSms.open=true;
-                var that=this;
-                setTimeout(() => {
-                    that.$refs.sms.reset(this.item.customerId,mobile,1);
-                }, 500);
-
-            },
-            closeRemark(){
-                this.addRemark.open=false;
-                this.getDetails(this.customerId)
-            },
-            handleAddRemark(){
-                this.addRemark.open=true;
-                var that=this;
-                setTimeout(() => {
-                    that.$refs.remark.reset(this.item);
-                }, 500);
-            },
-            closeTag(){
-                this.addTag.open=false;
-                this.getDetails(this.customerId)
-            },
-            handleAddTag(){
-                this.addTag.open=true;
-                var that=this;
+            })
+            getCustomerDetails1(data).then(response => {
+                console.log("this.item", response.customer)
+                this.item = response.customer
+                this.isReceive = response.isReceive
+                if (this.item.extJson != null) {
+                    var extList = JSON.parse(this.item.extJson)
+                    that.exts.forEach(item => {
+                        extList.forEach(element => {
+                            if (item.extId == element.extId) {
+                                item.value = element.value
+                            }
+                        })
+                    })
+                }
+                this.activeName = 'visit'
                 setTimeout(() => {
-                    that.$refs.tag.reset(this.item);
-                }, 500);
+                    that.$refs.visit.getData(customerId)
+                }, 500)
+            })
+        },
+        initDuplicate(isDuplicate, dCustomerId) {
+            this.showDuplicate = isDuplicate
+            this.dCustomerId = dCustomerId
+        },
+        handleDuplicate() {
+            this.duplicate.open = true
+            var that = this
+            setTimeout(() => {
+                that.$refs.duplicateCustomer.getDetails(that.dCustomerId)
+            }, 200)
+        },
+        closeDuplicate() {
+            this.duplicate.open = false
+            this.getDetails(this.customerId)
+        },
+        handleTagChange() {
+            console.log('AI 标签发生变化')
+        },
+        // 打开历史沟通弹窗(数据复用 item.historicalCommunication,无需额外请求)
+        handleViewHistory() {
+            this.historyDialog.open = true
+        }
+    }
+}
+</script>
 
-            },
-            handleClick(tab, event) {
-                if(tab.name=="contacts"){
-                    this.$refs.contacts.getData(this.item.customerId);
-                }
-                if(tab.name == "visit"){
-                    this.$refs.visit.getData(this.item.customerId,this.isReceive);
-                }
-                if(tab.name=="logs"){
-                    this.$refs.logs.getData(this.item.customerId);
-                }
-                if(tab.name=="voiceLogs"){
-                    this.$refs.voiceLogs.getData(this.item.customerId);
-                }
-                if(tab.name=="storeOrder"){
-                    this.$refs.storeOrder.getData(this.item.customerId);
-                }
-                if(tab.name=="smsLogs"){
-                    this.$refs.smsLogs.getData(this.item.customerId);
+<style lang="scss" scoped>
+.customer-detail-container {
+    background-color: #f5f7fa;
+    min-height: 100%;
+    padding: 20px;
+}
+
+.customer-header-card {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    border-radius: 12px;
+    padding: 24px;
+    display: flex;
+    align-items: center;
+    margin-bottom: 20px;
+    box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+
+    .customer-avatar {
+        margin-right: 20px;
+    }
+
+    .customer-info {
+        flex: 1;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+    }
+
+    .customer-main-info {
+        .customer-name-title {
+            color: #fff;
+            font-size: 24px;
+            margin: 0 0 12px 0;
+            display: flex;
+            align-items: center;
+        }
+
+        .customer-meta {
+            display: flex;
+            gap: 24px;
+
+            .meta-item {
+                color: rgba(255, 255, 255, 0.9);
+                font-size: 14px;
+                display: flex;
+                align-items: center;
+                gap: 6px;
+
+                i {
+                    font-size: 16px;
+                    flex-shrink: 0;
                 }
-                if(tab.name=="hisOrder"){
-                    this.$refs.hisOrder.getData(this.item.customerId);
+            }
+
+            .meta-item-phone {
+                flex: 1;
+                min-width: 0;
+                max-width: 420px;
+
+                .mobile-value {
+                    word-break: break-all;
+                    line-height: 1.4;
                 }
-            },
-            getDetails(customerId) {
-                var data={customerId:customerId}
-                this.customerId=customerId;
-                var that=this;
-                this.exts=[];
-                listCustomerExt(data).then(response => {
-                    this.customerExts = response.data;
-                    this.customerExts.forEach(element => {
-                        var data={extId:element.extId,name:element.name,value:""};
-                        this.exts.push(data)
-                    });
-                });
-                getCustomerDetails1(data).then(response => {
-                    this.item = response.customer;
-                    this.isReceive=response.isReceive;
-                    if(this.item.extJson!=null){
-                        var extList=JSON.parse(this.item.extJson);
-                        that.exts.forEach(item => {
-                            extList.forEach(element => {
-                                if(item.extId==element.extId){
-                                    item.value=element.value
-                                }
-                            });
-
-                        });
-                    }
-                    this.activeName="visit"
-                    setTimeout(() => {
-                        that.$refs.visit.getData(customerId);
-                    }, 500);
-                });
-            },
-            initDuplicate(isDuplicate,dCustomerId){
-
-                this.showDuplicate=isDuplicate;
-                this.dCustomerId=dCustomerId;
-            },
-            handleDuplicate(){
-                this.duplicate.open=true;
-                var that=this;
 
-                setTimeout(() => {
-                    that.$refs.duplicateCustomer.getDetails(that.dCustomerId);
-                }, 200);
-            },
-            closeDuplicate(){
-                this.duplicate.open=false;
-                this.getDetails(this.customerId)
-            },
+                .view-phone-btn {
+                    flex-shrink: 0;
+                    margin-left: 4px;
+                    padding: 2px 10px;
+                    border-radius: 12px;
+                    border: 1px solid rgba(255, 255, 255, 0.55);
+                    background: rgba(255, 255, 255, 0.12);
+                    color: #fff;
+
+                    &:hover,
+                    &:focus {
+                        color: #667eea;
+                        background: #fff;
+                        border-color: #fff;
+                    }
+                }
+            }
         }
-    };
-</script>
-<style lang="scss" scoped>
-.contents{
-    height: 100%;
-    background-color: #fff;
-    padding: 0px 20px;
+    }
 
+    .customer-actions {
+        display: flex;
+        gap: 12px;
+    }
 }
-.customer-title{
-    margin-bottom: 15px;
+
+.customer-tags-section {
+    background: #fff;
+    border-radius: 8px;
+    padding: 16px 20px;
+    margin-bottom: 20px;
     display: flex;
     align-items: center;
-    justify-content: space-between;
-    .customer-name{
-        font-size: 28px;
+
+    .tags-label {
+        font-weight: 500;
+        color: #606266;
+        margin-right: 12px;
     }
 }
-</style>
-<style>
-  .el-descriptions-item__label.is-bordered-label{
-    font-weight: normal;
-  }
 
+.info-sections {
+    margin-bottom: 20px;
+}
+
+.info-section {
+    background: #fff;
+    border-radius: 8px;
+    margin-bottom: 16px;
+    overflow: hidden;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+
+    .section-header {
+        padding: 16px 20px;
+        border-bottom: 1px solid #ebeef5;
+        background: #fafbfc;
+
+        .section-title {
+            font-size: 15px;
+            font-weight: 600;
+            color: #303133;
+            display: flex;
+            align-items: center;
+            gap: 8px;
+
+            i {
+                color: #409eff;
+            }
+        }
+    }
+
+    .section-content {
+        padding: 20px;
+    }
+}
+
+.info-item {
+    margin-bottom: 16px;
+
+    .info-label {
+        display: block;
+        font-size: 12px;
+        color: #909399;
+        margin-bottom: 6px;
+    }
+
+    .info-value {
+        font-size: 14px;
+        color: #303133;
+
+        &.highlight {
+            color: #f56c6c;
+            font-weight: 600;
+            font-size: 16px;
+        }
+
+        &.text-ellipsis {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+    }
+}
+
+.remark-content {
+    background: #fafbfc;
+    padding: 16px;
+    border-radius: 6px;
+    color: #606266;
+    line-height: 1.6;
+}
+
+.content-area {
+    .customer-tabs {
+        background: #fff;
+        border-radius: 8px;
+        padding: 16px;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+    }
+
+    .ai-tag-wrapper {
+        position: sticky;
+        top: 20px;
+    }
+}
+
+.history-communication-content {
+    max-height: 500px;
+    overflow-y: auto;
+    padding: 4px 8px;
+
+    .history-text {
+        margin: 0;
+        padding: 16px;
+        background: #fafbfc;
+        border-radius: 6px;
+        color: #303133;
+        font-size: 14px;
+        line-height: 1.8;
+        white-space: pre-wrap;
+        word-break: break-word;
+        font-family: inherit;
+    }
+}
 </style>

+ 773 - 0
src/views/crm/components/customerDetailsOld.vue

@@ -0,0 +1,773 @@
+<template>
+    <div class="customer-detail-container" v-if="item!=null">
+        <!-- 顶部客户信息卡片 -->
+        <div class="customer-header-card">
+            <div class="customer-avatar">
+                <el-avatar :size="64" icon="el-icon-user-solid"></el-avatar>
+            </div>
+            <div class="customer-info">
+                <div class="customer-main-info">
+                    <h2 class="customer-name-title">
+                        {{ showDuplicate ? item.customerName + '[从]' : item.customerName }}
+                        <el-tag v-if="item.status" :type="getStatusType(item.status)" size="medium"
+                                style="margin-left: 12px;"
+                        >
+                            {{ getStatusText(item.status) }}
+                        </el-tag>
+                    </h2>
+                    <div class="customer-meta">
+                        <span class="meta-item">
+                            <i class="el-icon-phone"></i>
+                            {{ item.mobile || '未填写' }}
+                            <el-button type="text" v-if="isReceive" size="mini"
+                                       @click="callNumber(item.customerId,null,null,null)" icon="el-icon-phone-outline"
+                            >拨打</el-button>
+                            <el-button type="text" v-if="isReceive" size="mini" @click="handleSms(item.mobile)"
+                                       icon="el-icon-message"
+                            >短信</el-button>
+                        </span>
+                        <span class="meta-item">
+                            <i class="el-icon-location"></i>
+                            {{ item.address || '未填写' }}
+                        </span>
+                        <span class="meta-item">
+                            <i class="el-icon-time"></i>
+                            创建于 {{ item.createTime }}
+                        </span>
+                    </div>
+                </div>
+                <div class="customer-actions">
+                    <el-button type="primary" size="small" icon="el-icon-edit" v-hasPermi="['crm:customer:edit']"
+                               @click="handleEdit()"
+                    >编辑客户
+                    </el-button>
+                    <el-button type="warning" size="small" icon="el-icon-price-tag" v-hasPermi="['crm:customer:addTag']"
+                               @click="handleAddTag()"
+                    >打标签
+                    </el-button>
+                    <el-button type="info" size="small" icon="el-icon-document" v-hasPermi="['crm:customer:addRemark']"
+                               @click="handleAddRemark()"
+                    >备注
+                    </el-button>
+                    <el-button size="small" v-if="showDuplicate" v-hasPermi="['crm:customer:lookDuplicate']"
+                               @click="getDetails"
+                    >
+                        主客户
+                    </el-button>
+                </div>
+            </div>
+        </div>
+
+        <!-- 标签展示区 -->
+        <div class="customer-tags-section" v-if="item && item.tags">
+            <span class="tags-label">客户标签:</span>
+            <el-tag v-for="tag in item.tags.split(',')" :key="tag" size="small" effect="plain"
+                    style="margin-right: 8px;"
+            >
+                {{ tag }}
+            </el-tag>
+        </div>
+
+        <!-- 信息分组展示 -->
+        <div class="info-sections">
+            <!-- 基本信息 -->
+            <div class="info-section">
+                <div class="section-header">
+                    <span class="section-title"><i class="el-icon-user"></i> 基本信息</span>
+                </div>
+                <div class="section-content">
+                    <el-row :gutter="24">
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">客户编号</span>
+                                <span class="info-value">{{ item.customerCode || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">客户名称</span>
+                                <span class="info-value">{{ item.customerName || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">性别</span>
+                                <span class="info-value">
+                                    <el-tag v-for="dict in sexOptions" :key="dict.dictValue"
+                                            v-if="item.sex==dict.dictValue" size="mini"
+                                    >{{ dict.dictLabel }}</el-tag>
+                                    <span v-if="!item.sex">-</span>
+                                </span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">微信号</span>
+                                <span class="info-value">{{ item.weixin || '-' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                    <el-row :gutter="24">
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">客户来源</span>
+                                <span class="info-value">
+                                    <el-tag v-for="dict in sourceOptions" :key="dict.dictValue"
+                                            v-if="item.source==dict.dictValue" size="mini" type="info"
+                                    >{{ dict.dictLabel }}</el-tag>
+                                    <span v-if="!item.source">-</span>
+                                </span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">客户类型</span>
+                                <span class="info-value">
+                                    <el-tag v-for="dict in typeOptions" :key="dict.dictValue"
+                                            v-if="item.customerType==dict.dictValue" size="mini" type="warning"
+                                    >{{ dict.dictLabel }}</el-tag>
+                                    <span v-if="!item.customerType">-</span>
+                                </span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">最后一次跟进</span>
+                                <span class="info-value">{{ item.visitTime || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">入公海时间</span>
+                                <span class="info-value">{{ item.poolTime || '-' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                </div>
+            </div>
+
+            <!-- 购买信息 -->
+            <div class="info-section">
+                <div class="section-header">
+                    <span class="section-title"><i class="el-icon-shopping-cart-2"></i> 购买信息</span>
+                </div>
+                <div class="section-content">
+                    <el-row :gutter="24">
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">购买渠道</span>
+                                <span class="info-value">{{ item.platformName || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">购买商品</span>
+                                <span class="info-value">{{ item.goodsName || '-' }} {{
+                                        item.goodsSpecification || ''
+                                    }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">购买店铺</span>
+                                <span class="info-value">{{ item.shopName || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">消费金额</span>
+                                <span class="info-value highlight">¥{{ item.payMoney || '0' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                    <el-row :gutter="24">
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">购买次数</span>
+                                <span class="info-value">{{ item.buyCount || '0' }} 次</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">来源渠道编码</span>
+                                <span class="info-value">{{ item.sourceCode || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">推荐编码</span>
+                                <span class="info-value">{{ item.pushCode || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">推荐时间</span>
+                                <span class="info-value">{{ item.pushTime || '-' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                </div>
+            </div>
+
+            <!-- 进线信息 -->
+            <div class="info-section">
+                <div class="section-header">
+                    <span class="section-title"><i class="el-icon-connection"></i> 进线信息</span>
+                </div>
+                <div class="section-content">
+                    <el-row :gutter="24">
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">进线日期</span>
+                                <span class="info-value">{{ item.registerDate || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">进线方式</span>
+                                <span class="info-value">{{ item.registerType || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">进线填写时间</span>
+                                <span class="info-value">{{ item.registerSubmitTime || '-' }}</span>
+                            </div>
+                        </el-col>
+                        <el-col :span="6">
+                            <div class="info-item">
+                                <span class="info-label">进线链接</span>
+                                <span class="info-value text-ellipsis" :title="item.registerLinkUrl">{{
+                                        item.registerLinkUrl || '-'
+                                    }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                    <el-row :gutter="24">
+                        <el-col :span="24">
+                            <div class="info-item">
+                                <span class="info-label">进线客户详情</span>
+                                <span class="info-value">{{ item.registerDesc || '-' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                </div>
+            </div>
+
+            <!-- 扩展信息 -->
+            <div class="info-section" v-if="exts && exts.length > 0">
+                <div class="section-header">
+                    <span class="section-title"><i class="el-icon-tickets"></i> 扩展信息</span>
+                </div>
+                <div class="section-content">
+                    <el-row :gutter="24">
+                        <el-col :span="6" v-for="ext in exts" :key="ext.extId">
+                            <div class="info-item">
+                                <span class="info-label">{{ ext.name }}</span>
+                                <span class="info-value">{{ ext.value || '-' }}</span>
+                            </div>
+                        </el-col>
+                    </el-row>
+                </div>
+            </div>
+
+            <!-- 备注信息 -->
+            <div class="info-section" v-if="item.remark">
+                <div class="section-header">
+                    <span class="section-title"><i class="el-icon-document"></i> 备注信息</span>
+                </div>
+                <div class="section-content">
+                    <div class="remark-content">{{ item.remark }}</div>
+                </div>
+            </div>
+
+            <!-- AI标签 -->
+            <div class="info-section">
+                <div class="ai-tag-wrapper" v-if="item && item.customerId">
+                    <ai-tag-panel ref="aiTagPanel" :customer-id="item.customerId" @tag-change="handleTagChange"
+                    ></ai-tag-panel>
+                </div>
+            </div>
+        </div>
+
+        <!-- Tab 和 AI 标签区域 -->
+        <div class="content-area">
+            <el-tabs v-model="activeName" type="card" @tab-click="handleClick" class="customer-tabs">
+                <el-tab-pane label="跟进记录" name="visit">
+                    <customer-visit-list ref="visit"></customer-visit-list>
+                </el-tab-pane>
+                <el-tab-pane label="联系人" name="contacts">
+                    <customer-contacts ref="contacts"></customer-contacts>
+                </el-tab-pane>
+                <el-tab-pane label="订单记录" name="storeOrder">
+                    <customer-store-order-list ref="storeOrder"></customer-store-order-list>
+                </el-tab-pane>
+                <el-tab-pane label="通话记录" name="voiceLogs">
+                    <customer-voice-logs-list ref="voiceLogs"></customer-voice-logs-list>
+                </el-tab-pane>
+                <el-tab-pane label="短信记录" name="smsLogs">
+                    <customer-sms-logs-list ref="smsLogs"></customer-sms-logs-list>
+                </el-tab-pane>
+                <el-tab-pane label="客户日志" name="logs">
+                    <customer-logs-list ref="logs"></customer-logs-list>
+                </el-tab-pane>
+                <el-tab-pane label="历史订单" name="hisOrder">
+                    <customer-his-order-list ref="hisOrder"></customer-his-order-list>
+                </el-tab-pane>
+                <el-tab-pane label="AI 通话" name="aiVoiceLogs">
+                    <ai-call-voice-log ref="aiVoiceRef"></ai-call-voice-log>
+                </el-tab-pane>
+                <el-tab-pane label="AI 加微" name="aiAddWxLogs">
+                    <ai-add-wx-log ref="aiAddWxRef"></ai-add-wx-log>
+                </el-tab-pane>
+                <el-tab-pane label="AI 短信" name="aiSendMsgLogs">
+                    <ai-send-msg-log ref="aiSendMsgRef"></ai-send-msg-log>
+                </el-tab-pane>
+            </el-tabs>
+        </div>
+
+        <!-- 弹窗 -->
+        <el-dialog :title="addTag.title" :visible.sync="addTag.open" width="600px" append-to-body :show-close="true" class="customer-dialog">
+            <add-tag ref="tag" @close="closeTag()"></add-tag>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="addTag.open = false">取 消</el-button>
+                <el-button type="primary" @click="$refs.tag.submitForm()">确 定</el-button>
+            </span>
+        </el-dialog>
+        <el-dialog :title="addRemark.title" :visible.sync="addRemark.open" width="600px" append-to-body :show-close="true" class="customer-dialog">
+            <add-remark ref="remark" @close="closeRemark()"></add-remark>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="addRemark.open = false">取 消</el-button>
+                <el-button type="primary" @click="$refs.remark.submitForm()">确 定</el-button>
+            </span>
+        </el-dialog>
+        <el-dialog :title="addSms.title" :visible.sync="addSms.open" width="800px" append-to-body class="customer-dialog">
+            <add-sms ref="sms" @close="closeSms()"></add-sms>
+        </el-dialog>
+        <el-dialog :title="customer.title" :visible.sync="customer.open" width="1000px" append-to-body :show-close="true" class="customer-dialog">
+            <add-or-edit-customer ref="customer" @close="closeCustomer()"></add-or-edit-customer>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="customer.open = false">取 消</el-button>
+                <el-button type="primary" @click="$refs.customer.submitForm()">确 定</el-button>
+            </span>
+        </el-dialog>
+        <el-drawer size="75%" :modal="false" :title="duplicate.title" :visible.sync="duplicate.open">
+            <duplicate-customer ref="duplicateCustomer"/>
+        </el-drawer>
+    </div>
+</template>
+
+<script>
+import { listCustomerExt } from '@/api/crm/customerExt'
+import customerVisitList from '../components/customerVisitList.vue'
+import customerLogsList from '../components/customerLogsList.vue'
+import customerSmsLogsList from '../components/customerSmsLogsList.vue'
+import customerVoiceLogsList from '../components/customerVoiceLogsList.vue'
+import customerStoreOrderList from '../components/customerStoreOrderList.vue'
+import duplicateCustomer from '../components/duplicateCustomer.vue'
+import customerContacts from './customerContacts.vue'
+import customerHisOrderList from '../components/customerHisOrderList.vue'
+import aiCallVoiceLog from './aiCallVoiceLog'
+import aiAddWxLog from './aiAddWxLog'
+import aiSendMsgLog from './aiSendMsgLog'
+import AiTagPanel from './AiTagPanel.vue'
+import { getCustomerDetails1, updateCustomer, getCustomer1 } from '@/api/crm/customer'
+import addTag from './addTag.vue'
+import addRemark from './addRemark.vue'
+import addSms from './addSms.vue'
+import addOrEditCustomer from '../components/addOrEditCustomer.vue'
+
+export default {
+    name: 'customer',
+    components: {
+        customerHisOrderList,
+        addOrEditCustomer,
+        addSms,
+        addTag,
+        addRemark,
+        customerContacts,
+        customerVisitList,
+        customerLogsList,
+        customerVoiceLogsList,
+        customerStoreOrderList,
+        customerSmsLogsList,
+        duplicateCustomer,
+        aiCallVoiceLog,
+        aiAddWxLog,
+        aiSendMsgLog,
+        AiTagPanel
+    },
+    data() {
+        return {
+            calleesId: null,
+            roboticId: null,
+            customer: { open: false, title: '修改客户' },
+            isReceive: false,
+            tagId: null,
+            tagsOptions: [],
+            addSms: { open: false, title: '发短信' },
+            addTag: { open: false, title: '打标签' },
+            addRemark: { open: false, title: '客户备注' },
+            duplicate: { open: false, title: '客户详情' },
+            customerId: null,
+            title: '',
+            open: false,
+            cityIds: [],
+            citys: [],
+            tags: [],
+            inputVisible: false,
+            inputValue: '',
+            receiveOptions: [],
+            statusOptions: [],
+            typeOptions: [],
+            sourceOptions: [],
+            sexOptions: [],
+            customerExts: [],
+            activeName: '',
+            item: null,
+            showDuplicate: false,
+            dCustomerId: null
+        }
+    },
+    created() {
+        this.getDicts('crm_customer_source').then((response) => {
+            this.sourceOptions = response.data
+        })
+        this.getDicts('sys_sex').then((response) => {
+            this.sexOptions = response.data
+        })
+        this.getDicts('crm_customer_tag').then((response) => {
+            this.tagsOptions = response.data
+        })
+        this.getDicts('crm_customer_status').then((response) => {
+            this.statusOptions = response.data
+        })
+        this.getDicts('crm_customer_type').then((response) => {
+            this.typeOptions = response.data
+        })
+        this.getDicts('crm_customer_is_receive').then((response) => {
+            this.receiveOptions = response.data
+        })
+    },
+    methods: {
+        getStatusType(status) {
+            const statusMap = { '1': 'success', '2': 'warning', '3': 'danger', '4': 'info' }
+            return statusMap[status] || 'info'
+        },
+        getStatusText(status) {
+            const item = this.statusOptions.find(d => d.dictValue === status)
+            return item ? item.dictLabel : status
+        },
+        handleMobile() {
+            const customerId = this.item.customerId
+            getCustomer1(customerId).then(response => {
+                this.item.mobile = response.mobile
+            })
+        },
+        handleEdit() {
+            this.customer.open = true
+            var that = this
+            setTimeout(() => {
+                that.$refs.customer.handleUpdate(that.customerId)
+            }, 200)
+        },
+        closeCustomer() {
+            this.customer.open = false
+            this.getDetails(this.customerId)
+        },
+        tagsChange(e) {
+            var item = this.tagsOptions.find(val => val.dictValue === e)
+            this.tags.push(item.dictLabel)
+            this.form.tags = this.tags.toString()
+        },
+        closeSms() {
+            this.addSms.open = false
+            this.getDetails(this.customerId)
+        },
+        handleSms(mobile) {
+            this.addSms.open = true
+            var that = this
+            setTimeout(() => {
+                that.$refs.sms.reset(this.item.customerId, mobile, 1)
+            }, 500)
+        },
+        closeRemark() {
+            this.addRemark.open = false
+            this.getDetails(this.customerId)
+        },
+        handleAddRemark() {
+            this.addRemark.open = true
+            var that = this
+            setTimeout(() => {
+                that.$refs.remark.reset(this.item)
+            }, 500)
+        },
+        closeTag() {
+            this.addTag.open = false
+            this.getDetails(this.customerId)
+        },
+        handleAddTag() {
+            this.addTag.open = true
+            var that = this
+            setTimeout(() => {
+                that.$refs.tag.reset(this.item)
+            }, 500)
+        },
+        handleClick(tab, event) {
+            if (tab.name == 'contacts') {
+                this.$refs.contacts.getData(this.item.customerId)
+            }
+            if (tab.name == 'visit') {
+                this.$refs.visit.getData(this.item.customerId, this.isReceive)
+            }
+            if (tab.name == 'logs') {
+                this.$refs.logs.getData(this.item.customerId)
+            }
+            if (tab.name == 'voiceLogs') {
+                this.$refs.voiceLogs.getData(this.item.customerId)
+            }
+            if (tab.name == 'storeOrder') {
+                this.$refs.storeOrder.getData(this.item.customerId)
+            }
+            if (tab.name == 'smsLogs') {
+                this.$refs.smsLogs.getData(this.item.customerId)
+            }
+            if (tab.name == 'hisOrder') {
+                this.$refs.hisOrder.getData(this.item.customerId)
+            }
+            if (tab.name == 'aiVoiceLogs') {
+                this.$refs.aiVoiceRef.getData(this.item.customerId, this.calleesId)
+            } else if (tab.name == 'aiAddWxLogs') {
+                this.$refs.aiAddWxRef.getData(this.item.customerId, this.roboticId)
+            } else if (tab.name == 'aiSendMsgLogs') {
+                this.$refs.aiSendMsgRef.getData(this.item.customerId, this.calleesId, this.roboticId)
+            }
+        },
+        getDetails(customerId, calleesId, roboticId) {
+            if (!!calleesId) {
+                this.calleesId = calleesId
+            }
+            if (!!roboticId) {
+                this.roboticId = roboticId
+            }
+            var data = { customerId: customerId }
+            this.customerId = customerId
+            var that = this
+            this.exts = []
+            listCustomerExt(data).then(response => {
+                this.customerExts = response.data
+                this.customerExts.forEach(element => {
+                    var data = { extId: element.extId, name: element.name, value: '' }
+                    this.exts.push(data)
+                })
+            })
+            getCustomerDetails1(data).then(response => {
+                this.item = response.customer
+                this.isReceive = response.isReceive
+                if (this.item.extJson != null) {
+                    var extList = JSON.parse(this.item.extJson)
+                    that.exts.forEach(item => {
+                        extList.forEach(element => {
+                            if (item.extId == element.extId) {
+                                item.value = element.value
+                            }
+                        })
+                    })
+                }
+                this.activeName = 'visit'
+                setTimeout(() => {
+                    that.$refs.visit.getData(customerId)
+                }, 500)
+            })
+        },
+        initDuplicate(isDuplicate, dCustomerId) {
+            this.showDuplicate = isDuplicate
+            this.dCustomerId = dCustomerId
+        },
+        handleDuplicate() {
+            this.duplicate.open = true
+            var that = this
+            setTimeout(() => {
+                that.$refs.duplicateCustomer.getDetails(that.dCustomerId)
+            }, 200)
+        },
+        closeDuplicate() {
+            this.duplicate.open = false
+            this.getDetails(this.customerId)
+        },
+        handleTagChange() {
+            console.log('AI 标签发生变化')
+        }
+    }
+}
+</script>
+
+<style lang="scss" scoped>
+.customer-detail-container {
+    background-color: #f5f7fa;
+    min-height: 100%;
+    padding: 20px;
+}
+
+.customer-header-card {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    border-radius: 12px;
+    padding: 24px;
+    display: flex;
+    align-items: center;
+    margin-bottom: 20px;
+    box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
+
+    .customer-avatar {
+        margin-right: 20px;
+    }
+
+    .customer-info {
+        flex: 1;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+    }
+
+    .customer-main-info {
+        .customer-name-title {
+            color: #fff;
+            font-size: 24px;
+            margin: 0 0 12px 0;
+            display: flex;
+            align-items: center;
+        }
+
+        .customer-meta {
+            display: flex;
+            gap: 24px;
+
+            .meta-item {
+                color: rgba(255, 255, 255, 0.9);
+                font-size: 14px;
+                display: flex;
+                align-items: center;
+                gap: 6px;
+
+                i {
+                    font-size: 16px;
+                }
+
+                .el-button {
+                    color: rgba(255, 255, 255, 0.9);
+                    padding: 0 8px;
+
+                    &:hover {
+                        color: #fff;
+                    }
+                }
+            }
+        }
+    }
+
+    .customer-actions {
+        display: flex;
+        gap: 12px;
+    }
+}
+
+.customer-tags-section {
+    background: #fff;
+    border-radius: 8px;
+    padding: 16px 20px;
+    margin-bottom: 20px;
+    display: flex;
+    align-items: center;
+
+    .tags-label {
+        font-weight: 500;
+        color: #606266;
+        margin-right: 12px;
+    }
+}
+
+.info-sections {
+    margin-bottom: 20px;
+}
+
+.info-section {
+    background: #fff;
+    border-radius: 8px;
+    margin-bottom: 16px;
+    overflow: hidden;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+
+    .section-header {
+        padding: 16px 20px;
+        border-bottom: 1px solid #ebeef5;
+        background: #fafbfc;
+
+        .section-title {
+            font-size: 15px;
+            font-weight: 600;
+            color: #303133;
+            display: flex;
+            align-items: center;
+            gap: 8px;
+
+            i {
+                color: #409eff;
+            }
+        }
+    }
+
+    .section-content {
+        padding: 20px;
+    }
+}
+
+.info-item {
+    margin-bottom: 16px;
+
+    .info-label {
+        display: block;
+        font-size: 12px;
+        color: #909399;
+        margin-bottom: 6px;
+    }
+
+    .info-value {
+        font-size: 14px;
+        color: #303133;
+
+        &.highlight {
+            color: #f56c6c;
+            font-weight: 600;
+            font-size: 16px;
+        }
+
+        &.text-ellipsis {
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+    }
+}
+
+.remark-content {
+    background: #fafbfc;
+    padding: 16px;
+    border-radius: 6px;
+    color: #606266;
+    line-height: 1.6;
+}
+
+.content-area {
+    .customer-tabs {
+        background: #fff;
+        border-radius: 8px;
+        padding: 16px;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+    }
+
+    .ai-tag-wrapper {
+        position: sticky;
+        top: 20px;
+    }
+}
+</style>

+ 2159 - 0
src/views/crm/customer/customerDetailOld.vue

@@ -0,0 +1,2159 @@
+<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">
+                        <div class="profile-item profile-item-main">
+                            <span class="label"><i class="fas fa-user"></i> 客户姓名:</span>
+                            <span class="value highlight">{{ customerData.customerName }}</span>
+                        </div>
+                        <template v-for="(value, key) in customerPortraitData">
+                            <div
+                                v-if="key !== '需求'"
+                                :key="key"
+                                class="profile-item"
+                            >
+                                <span class="label">
+                                    <i class="fas fa-info-circle"></i> {{ key }}:
+                                </span>
+                                <span class="value">{{ value }}</span>
+                            </div>
+                        </template>
+                        <!-- 需求单独显示,占满整行 -->
+                        <div
+                            v-if="customerPortraitData['需求']"
+                            key="需求"
+                            class="profile-item profile-item-full"
+                        >
+                            <span class="label">
+                                <i class="fas fa-bullseye"></i> 需求:
+                            </span>
+                            <span class="value long-text">{{ customerPortraitData['需求'] }}</span>
+                        </div>
+                    </div>
+                </div>
+                <!-- AI 标签 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3>
+                            <i class="fas fa-tags"></i> AI 标签
+                        </h3>
+                    </div>
+                    <div class="tags-container">
+                        <div v-if="allAiTags.length > 0" class="tags-list">
+                            <div
+                                v-for="(item, index) in visibleTags"
+                                :key="item.id"
+                                class="tag-item"
+                                :class="{ 'tag-highlight': index < 3 }"
+                            >
+                                <span class="tag-key">{{ item.propertyName }}</span>
+                                <span class="tag-separator">:</span>
+                                <span class="tag-value">{{ item.propertyValue }}</span>
+                            </div>
+                        </div>
+                        <div v-else class="empty-tags">
+                            <i class="fas fa-inbox"></i>
+                            <span>暂无 AI 标签</span>
+                        </div>
+
+                        <!-- 加载更多按钮 -->
+                        <div v-if="allAiTags.length > tagsPageSize" class="tags-actions">
+                            <button
+                                v-if="!isExpanded"
+                                @click="loadMoreTags"
+                                class="btn-expand-tags"
+                                type="button"
+                            >
+                                <i class="fas fa-chevron-down"></i> 展开全部 ({{ allAiTags.length - tagsPageSize }})
+                            </button>
+
+                            <!-- 收起按钮 -->
+                            <button
+                                v-else
+                                @click="collapseTags"
+                                class="btn-collapse-tags"
+                                type="button"
+                            >
+                                <i class="fas fa-chevron-up"></i> 收起标签
+                            </button>
+                        </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">
+                        {{ getCommunicationAbstract() }}
+                    </div>
+                </div>
+                <!-- AI 沟通总结 -->
+                <div class="card">
+                    <div class="card-header">
+                        <h3><i class="fas fa-robot"></i> AI 沟通总结</h3>
+                    </div>
+                    <div class="summary-text compact">
+                        {{ getCommunicationSummary() }}
+                    </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 risk-card" :class="getRiskLevelClass()">
+                    <div class="card-header">
+                        <h3><i class="fas fa-chart-line"></i> 流失风险等级</h3>
+                        <span class="risk-badge" :class="getRiskLevelBadgeClass()">{{ getRiskLevelLabel() }}</span>
+                    </div>
+                    <div class="risk-analysis">
+                        <p class="risk-text">{{ getRiskLevelAnalysis() }}</p>
+                        <div class="risk-tip" v-if="getRiskLevelTip()">
+                            <i class="fas fa-exclamation-triangle"></i> {{ getRiskLevelTip() }}
+                        </div>
+                    </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 v-for="(point, idx) in customerFocusPoints" :key="idx" class="focus-item">
+                                <i class="fas fa-dot-circle"></i> {{ point }}
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="intention-section">
+                        <div class="intention-header">
+                            <span class="intention-label"><i class="fas fa-heart"></i> 客户意向度</span>
+                            <el-tooltip placement="top" effect="light">
+                                <i class="el-icon-info intention-info-icon"></i>
+                                <div slot="content" class="intention-tooltip">
+                                    <div><strong>A 级</strong> - 最高意向度</div>
+                                    <div><strong>B 级</strong> - 高意向度</div>
+                                    <div><strong>C 级</strong> - 中等意向度</div>
+                                    <div><strong>D 级</strong> - 较低意向度</div>
+                                    <div><strong>E 级</strong> - 低意向度</div>
+                                    <div><strong>F 级</strong> - 最低意向度</div>
+                                </div>
+                            </el-tooltip>
+                        </div>
+                        <div class="intention-watermark"
+                             v-if="getIntentionDegree()"
+                             :class="getIntentionColorClass(getIntentionDegree())">
+                            {{ getIntentionDegree() }}
+                        </div>
+                        <div class="no-intention-tip" v-else>
+                            暂无评级
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+
+import {listByCustomerId} from "../../../api/crm/customerProperty";
+import {listAnalyze} from "../../../api/crm/customerAnalyze";
+
+export default {
+    props: {
+        // 抽屉模式下由父页面传入;路由模式下可不传
+        customerId: {
+            type: [String, Number],
+            default: null
+        },
+        customerRow: {
+            type: Object,
+            default: null
+        }
+    },
+    data() {
+        return {
+            customerUserId: null, // 客户 ID,从路由参数获取
+            customerData: null, // 从列表页传递过来的完整客户数据
+            aiTags: [],// 需要显示的 AI 标签
+            allAiTags: [], // 存储所有 AI 标签
+            tagsPageSize: 5,//默认展开标签的数量
+            isExpanded: false, // 是否已展开显示全部标签
+            // 聊天记录分页相关
+            communicationRecords: [],
+            communicationRecordsTotal: 0,
+            communicationRecordsPageNum: 1,
+            communicationRecordsPageSize: 4,
+            // 聊天弹窗相关
+            chatDialogVisible: false, // 聊天弹窗是否显示
+            currentChatRecord: null, // 当前查看的聊天记录
+        }
+    },
+    computed: {
+        // 根据是否展开控制显示的标签数量
+        visibleTags() {
+            if (this.isExpanded) {
+                return this.allAiTags;
+            } else {
+                // 未展开时只显示前 3 条
+                return this.allAiTags.slice(0, this.tagsPageSize);
+            }
+        },
+        // 客户画像数据(从最新的沟通记录中获取)
+        customerPortraitData() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return {};
+            }
+            // 获取最新的沟通记录
+            const latestRecord = this.communicationRecords[0];
+            if (latestRecord && latestRecord.customerPortraitJson) {
+                try {
+                    // 如果是字符串,解析为 JSON 对象
+                    if (typeof latestRecord.customerPortraitJson === 'string') {
+                        return JSON.parse(latestRecord.customerPortraitJson);
+                    }
+                    // 如果已经是对象,直接返回
+                    return latestRecord.customerPortraitJson;
+                } catch (error) {
+                    console.error('解析客户画像 JSON 失败:', error);
+                    return {};
+                }
+            }
+            return {};
+        },
+        // 客户关注点数据(从最新的沟通记录中获取)
+        customerFocusPoints() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return ['暂无分析数据'];
+            }
+            const latestRecord = this.communicationRecords[0];
+            if (latestRecord && latestRecord.customerFocusJson) {
+                return this.normalizeFocusPoints(latestRecord.customerFocusJson);
+            }
+            return ['暂无分析数据'];
+        }
+    },
+    created() {
+        this.initFromParentOrRoute();
+    },
+    watch: {
+        customerId: {
+            immediate: false,
+            handler() {
+                // 抽屉重复打开/切换客户时刷新
+                this.initFromParentOrRoute();
+            }
+        }
+    },
+    methods: {
+        normalizeFocusPoints(value) {
+            if (value === null || value === undefined) return [];
+            if (Array.isArray(value)) {
+                return value.map(v => String(v)).filter(Boolean);
+            }
+
+            // 字符串:可能是 JSON 数组字符串,也可能是普通字符串
+            if (typeof value === 'string') {
+                const raw = value.trim();
+                if (!raw) return [];
+
+                // JSON 数组 / JSON 字符串尝试解析
+                if (
+                    (raw.startsWith('[') && raw.endsWith(']')) ||
+                    (raw.startsWith('"') && raw.endsWith('"')) ||
+                    (raw.startsWith("'") && raw.endsWith("'"))
+                ) {
+                    try {
+                        const parsed = JSON.parse(raw);
+                        if (Array.isArray(parsed)) {
+                            return parsed.map(v => String(v)).map(s => s.trim()).filter(Boolean);
+                        }
+                        if (typeof parsed === 'string') {
+                            return this.normalizeFocusPoints(parsed);
+                        }
+                    } catch (e) {
+                        // ignore,走后面的兜底清洗
+                    }
+                }
+
+                // 兜底:去掉中括号/引号后按分隔符拆分
+                let cleaned = raw;
+                if (cleaned.startsWith('[') && cleaned.endsWith(']')) {
+                    cleaned = cleaned.slice(1, -1);
+                }
+                cleaned = cleaned.replace(/["']/g, '');
+
+                const parts = cleaned
+                    .split(/[,,、;;\n]/g)
+                    .map(s => s.trim())
+                    .filter(Boolean);
+
+                return parts.length ? parts : [cleaned.trim()].filter(Boolean);
+            }
+
+            // 其它类型兜底
+            return [String(value)].filter(Boolean);
+        },
+        initFromParentOrRoute() {
+            // 优先用父组件传参(抽屉模式)
+            const idFromParent = this.customerId;
+            if (idFromParent !== null && idFromParent !== undefined && idFromParent !== '') {
+                this.customerUserId = idFromParent;
+                this.customerData = this.customerRow || null;
+            } else {
+                // 路由模式兜底
+                this.customerUserId = this.$route.params.customerId || this.$route.query.customerUserId;
+                if (this.$route.query.customerData) {
+                    try {
+                        this.customerData = JSON.parse(this.$route.query.customerData);
+                    } catch (error) {
+                        console.error('解析客户数据失败:', error);
+                    }
+                }
+            }
+
+            // 重置分页(切换客户时)
+            this.communicationRecordsPageNum = 1;
+            // 获取客户标签
+            this.loadCustomerTags();
+            // 加载客户分析信息
+            this.getCustomerInfoList();
+        },
+        loadCustomerTags() {
+            listByCustomerId(this.customerUserId).then((response) => {
+                if (response.code === 200) {
+                    this.allAiTags = response.data || [];
+                    // 强制 Vue 更新视图
+                    this.$forceUpdate();
+                } else {
+                    console.error('获取 AI 标签失败:', response);
+                }
+            }).catch(error => {
+                console.error('获取 AI 标签异常:', error);
+            });
+        },
+        getCustomerInfoList() {
+            const params = {
+                pageNum: this.communicationRecordsPageNum,
+                pageSize: this.communicationRecordsPageSize,
+                customerId: this.customerUserId
+            };
+            listAnalyze(params).then((response) => {
+                if (response.code === 200) {
+                    this.communicationRecords = response.rows || [];
+                    this.communicationRecordsTotal = response.total || 0;
+                } else {
+                    console.error('获取客户信息失败:', response);
+                }
+            }).catch(error => {
+                console.error('获取客户信息异常:', error);
+            });
+        },
+        // 加载更多标签 - 显示全部
+        loadMoreTags() {
+            this.isExpanded = true;
+        },
+        // 收起标签 - 只显示前 3 条
+        collapseTags() {
+            this.isExpanded = false;
+        },
+        // 查看聊天内容
+        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();
+        },
+        // 获取沟通摘要
+        getCommunicationAbstract() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '暂无沟通记录';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.communicationAbstract || '暂无沟通摘要';
+        },
+        // 获取 AI 沟通总结
+        getCommunicationSummary() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '暂无沟通记录';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.communicationSummary || '暂无 AI 沟通总结';
+        },
+        // 获取最后更新时间
+        getUpdateTime() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '-';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.createTime || '-';
+        },
+        // 获取流失风险等级数值
+        getAttritionLevel() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return 0;
+            }
+            const latestRecord = this.communicationRecords[0];
+            return parseInt(latestRecord.attritionLevel) || 0;
+        },
+        // 获取流失风险等级标签
+        getRiskLevelLabel() {
+            const level = this.getAttritionLevel();
+            const labels = ['未知', '无风险', '低风险', '中风险', '高风险'];
+            return labels[level] || '未知';
+        },
+        // 获取流失风险等级徽章样式类
+        getRiskLevelBadgeClass() {
+            const level = this.getAttritionLevel();
+            const badgeClasses = [
+                'badge-unknown',
+                'badge-none',
+                'badge-low',
+                'badge-medium',
+                'badge-high'
+            ];
+            return badgeClasses[level] || 'badge-unknown';
+        },
+        // 获取客户意向度
+        getIntentionDegree() {
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.intentionDegree || '';
+        },
+        // 根据意向度等级获取颜色样式类
+        getIntentionColorClass(grade) {
+            if (!grade) return '';
+            const gradeUpper = grade.toUpperCase();
+            const colorMap = {
+                'A': 'intention-grade-a',
+                'B': 'intention-grade-b',
+                'C': 'intention-grade-c',
+                'D': 'intention-grade-d',
+                'E': 'intention-grade-e',
+                'F': 'intention-grade-f'
+            };
+            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;
+        },
+        // 获取流失风险等级样式类
+        getRiskLevelClass() {
+            const level = this.getAttritionLevel();
+            const classes = ['risk-unknown', 'risk-none', 'risk-low', 'risk-medium', 'risk-high'];
+            return classes[level] || 'risk-unknown';
+        },
+        // 获取流失风险分析
+        getRiskLevelAnalysis() {
+            const level = this.getAttritionLevel();
+            if (!this.communicationRecords || this.communicationRecords.length === 0) {
+                return '暂无分析数据';
+            }
+            const latestRecord = this.communicationRecords[0];
+            return latestRecord.attritionLevelPrompt || "暂无分析数据";
+        },
+        // 获取流失风险提示
+        getRiskLevelTip() {
+            const level = this.getAttritionLevel();
+            const tips = [
+                '暂无分析',
+                '客户稳定,可以放心。',
+                '建议定期回访,了解客户最新需求。',
+                '建议安排专项跟进,深入了解客户痛点和需求。',
+                '建议立即联系客户,了解问题原因并提供解决方案。'
+            ];
+            return tips[level] || '';
+        },
+    }
+}</script>
+
+<style scoped>
+* {
+    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);
+    }
+}
+
+.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;
+}
+
+/* A 级 - 金色/绿色,最高级别 */
+.intention-grade-a {
+    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-a::after {
+    content: 'A';
+    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;
+}
+
+/* B 级 - 蓝色 */
+.intention-grade-b {
+    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-b::after {
+    content: 'B';
+    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;
+}
+
+/* C 级 - 紫色 */
+.intention-grade-c {
+    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-c::after {
+    content: 'C';
+    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;
+}
+
+/* D 级 - 橙色 */
+.intention-grade-d {
+    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-d::after {
+    content: 'D';
+    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;
+}
+
+/* E 级 - 粉红色 */
+.intention-grade-e {
+    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-e::after {
+    content: 'E';
+    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;
+}
+
+/* F 级 - 红色,最低级别 */
+.intention-grade-f {
+    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-f::after {
+    content: 'F';
+    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>

File diff suppressed because it is too large
+ 797 - 664
src/views/crm/customer/line.vue


+ 851 - 0
src/views/crm/customer/lineOlde.vue

@@ -0,0 +1,851 @@
+<template>
+    <div class="app-container">
+        <!--用户数据-->
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+            <el-form-item label="客户编码" prop="customerCode">
+                <el-input
+                    v-model="queryParams.customerCode"
+                    placeholder="请输入客户编码"
+                    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="手机" prop="mobile">
+                <el-input
+                    v-model="queryParams.mobile"
+                    placeholder="请输入手机"
+                    clearable
+                    size="small"
+                    @keyup.enter.native="handleQuery"
+                />
+            </el-form-item>
+            <el-form-item label="加密手机" prop="encryptedMobile">
+                <el-input
+                    v-model="queryParams.encryptedMobile"
+                    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="item in statusOptions"
+                        :key="item.dictValue"
+                        :label="item.dictLabel"
+                        :value="item.dictValue"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item   label="客户类型" prop="customerType">
+                <el-select filterable v-model="queryParams.customerType" placeholder="请选择客户类型" clearable size="small">
+                    <el-option
+                        v-for="item in typeOptions"
+                        :key="item.dictValue"
+                        :label="item.dictLabel"
+                        :value="item.dictValue"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item label="客户标签" prop="tags">
+                <el-select multiple  filterable v-model="tagIds" placeholder="请选择客户标签" clearable size="small">
+                    <el-option
+                        v-for="item in tagsOptions"
+                        :key="item.dictLabel"
+                        :label="item.dictLabel"
+                        :value="item.dictLabel"
+                    />
+                </el-select>
+            </el-form-item>
+
+            <el-form-item label="从客户" prop="isDuplicate">
+                <el-select   filterable v-model="queryParams.isDuplicate" placeholder="请选择从客户" clearable size="small">
+                    <el-option
+                        v-for="item in isDuplicateOptions"
+                        :key="item.dictLabel"
+                        :label="item.dictLabel"
+                        :value="item.dictValue"
+                    />
+                </el-select>
+            </el-form-item>
+
+
+            <el-form-item label="创建时间" prop="createTime">
+                <el-date-picker
+                    style="width:205.4px"
+                    clearable size="small"
+                    v-model="dateRange"
+                    type="daterange"
+                    value-format="yyyy-MM-dd"
+                    start-placeholder="开始日期"
+                    end-placeholder="结束日期">
+                </el-date-picker>
+
+            </el-form-item>
+            <el-form-item  label="客户来源" prop="source">
+                <el-select filterable  v-model="queryParams.source" placeholder="请选择客户来源" clearable size="small">
+                    <el-option
+                        v-for="item in sourceOptions"
+                        :key="item.dictValue"
+                        :label="item.dictLabel"
+                        :value="item.dictValue"
+                    />
+                </el-select>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+                <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-row :gutter="10" class="mb8">
+            <el-col :span="1.5">
+                <el-button
+                    type="primary"
+                    size="mini"
+                    icon="el-icon-plus"
+                    @click="handleAdd"
+                    v-hasPermi="['crm:customer:add']"
+                >新增客户</el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    type="success"
+                    icon="el-icon-edit"
+                    size="mini"
+                    :disabled="multiple"
+                    @click="handleAssign"
+                    v-hasPermi="['crm:customer:assignToUser']"
+                >客户分配</el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    type="success"
+                    size="mini"
+                    @click="handleAssignList"
+                    v-hasPermi="['crm:customerAssign:list']"
+                >分配历史记录</el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    type="success"
+                    size="mini"
+                    :disabled="multiple"
+                    @click="handleEditScource"
+                    v-hasPermi="['crm:customer:editScource']"
+                >修改客户来源</el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    type="warning"
+                    icon="el-icon-upload2"
+                    size="mini"
+                    @click="handleImport"
+                    v-hasPermi="['crm:customer:importLine']"
+                >导入线索客户</el-button>
+            </el-col>
+
+            <el-col :span="1.5">
+                <el-button
+                    type="warning"
+                    icon="el-icon-download"
+                    size="mini"
+                    @click="handleExport"
+                    v-hasPermi="['crm:customer:export']"
+                >导出</el-button>
+            </el-col>
+            <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+
+        <el-table  height="500" border v-loading="loading" :data="customerList" @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" align="center" />
+            <el-table-column label="客户编码" align="center" prop="customerCode" />
+            <el-table-column  label="客户名称"   align="center" prop="customerName" :show-overflow-tooltip="true">
+                <template slot-scope="scope">
+                    <el-link @click="handleShow(scope.row)" :underline="false" type="primary" >{{  scope.row.isDuplicate==1?getCustomerName(scope.row.customerName)+"[从]":getCustomerName(scope.row.customerName)   }}</el-link>
+                </template>
+            </el-table-column>
+            <el-table-column  label="手机" width="150px"  align="center" prop="mobile"   >
+                <template slot-scope="scope">
+                    <span>{{scope.row.mobile}}</span>
+                    <el-button
+                        v-if="scope.row.customerId"
+                        type="text"
+                        icon="el-icon-view"
+                        size="mini"
+                        v-hasPermi="['crm:customer:queryPhone']"
+                        @click="handleQueryPhone(scope.row)"
+                        style="margin-left: 5px;"
+                    >查看</el-button>
+                </template>
+            </el-table-column>
+            <el-table-column  label="客户来源" align="center" prop="source">
+                <template slot-scope="scope">
+                    <el-tag prop="status" v-for="(item, index) in sourceOptions"    v-if="scope.row.source==item.dictValue">{{item.dictLabel}}</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column  label="跟进阶段"  align="center" prop="visitStatus" >
+                <template slot-scope="scope">
+                    <el-tag prop="visitStatus" v-for="(item, index) in visitStatusOptions"    v-if="scope.row.visitStatus==item.dictValue">{{item.dictLabel}}</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column  label="客户类型"  align="center" prop="customerType" >
+                <template slot-scope="scope">
+                    <el-tag prop="status" v-for="(item, index) in typeOptions"    v-if="scope.row.customerType==item.dictValue">{{item.dictLabel}}</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column label="标签" align="center" prop="tags" />
+            <el-table-column label="备注" align="center" prop="remark" width="120"  />
+            <el-table-column label="进线客户详情" align="center" :show-overflow-tooltip="true" prop="registerDesc" />
+            <el-table-column label="领取时间" align="center" prop="startTime" />
+            <el-table-column label="进线客户提交日期" align="center" prop="registerSubmitTime" width="130" />
+            <el-table-column  label="认领人"  align="center" prop="companyUserNickName" :show-overflow-tooltip="true">
+            </el-table-column>
+            <el-table-column label="创建时间" align="center" prop="createTime" width="100">
+            </el-table-column>
+
+            <el-table-column label="最后一次跟进时间" align="center" prop="receiveTime" width="100">
+            </el-table-column>
+            <el-table-column label="入公海时间" align="center" prop="poolTime" width="100">
+            </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="assign.title" :visible.sync="assign.open" width="800px" @close="closeAssignPop" append-to-body>
+            <assign-user  ref="assignUser" @close="closeAssign"   />
+        </el-dialog>
+        <el-drawer
+            size="75%"
+            :title="show.title" :visible.sync="show.open">
+            <customer-details  ref="customerDetails" />
+        </el-drawer>
+        <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
+            <el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :on-error="handleFileError" :auto-upload="false" drag>
+                <i class="el-icon-upload"></i>
+                <div class="el-upload__text">
+                    将文件拖到此处,或
+                    <em>点击上传</em>
+                </div>
+                <div class="el-upload__tip" slot="tip">
+                    <el-link type="info" style="font-size:12px" @click="importTemplate">下载模板</el-link>
+                </div>
+                <div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入“xls”或“xlsx”格式文件!</div>
+            </el-upload>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" :loading="upload.isUploading" :disabled="upload.isUploading" @click="submitFileForm">{{ upload.isUploading ? '导入中...' : '确 定' }}</el-button>
+                <el-button :disabled="upload.isUploading" @click="upload.open = false">取 消</el-button>
+            </div>
+        </el-dialog>
+
+        <el-dialog :title="source.title" :visible.sync="source.open" width="1000px" append-to-body>
+            <edit-source ref="editSource" @close="closeSource()"></edit-source>
+        </el-dialog>
+        <el-dialog :title="customer.title" :visible.sync="customer.open" width="1000px" append-to-body>
+            <add-or-edit-customer ref="customer" @close="closeCustomer()"></add-or-edit-customer>
+            <span slot="footer" class="dialog-footer">
+        <el-button @click="customer.open = false">取 消</el-button>
+        <el-button type="primary" @click="$refs.customer.submitForm()">确 定</el-button>
+       </span>
+        </el-dialog>
+        <el-dialog title="导入结果" :close-on-press-escape="false" :close-on-click-modal="false" :visible.sync="importMsgOpen" width="500px" append-to-body>
+            <div class="import-msg">
+                <div class="import-summary">
+                    <i :class="importResult.hasFailure ? 'el-icon-warning' : 'el-icon-success'"
+                       :style="{color: importResult.hasFailure ? '#E6A23C' : '#67C23A', fontSize: '24px', marginRight: '8px'}"></i>
+                    <span>{{ importResult.summary }}</span>
+                </div>
+                <div v-if="importResult.hasFailure && importResult.failureDetail" class="import-detail">
+                    <div class="detail-title">失败详情:</div>
+                    <div class="detail-content" v-html="importResult.failureDetail"></div>
+                </div>
+            </div>
+        </el-dialog>
+        <el-dialog :title="assignList.title" :visible.sync="assignList.open" width="1000px" append-to-body>
+            <customer-assign-list  ref="assignList" @close="closeAssignList"></customer-assign-list>
+        </el-dialog>
+
+        <el-dialog :title="xy.title" :visible.sync="xy.open" width="1000px" @close="xy.open=false" append-to-body>
+            <div class="xy-content">
+                <div class="xy-img">
+                    <img src="@/assets/image/xy_01.jpg"/>
+                    <img src="@/assets/image/xy_02.jpg"/>
+                </div>
+                <div  class="footer">
+                    <el-button  @click="submitExport" :disabled="exportDisable">{{exportBtnText}}</el-button>
+                </div>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import store from "@/store";
+import { importLineTemplate,getLineCustomerList ,assignToUser,getCustomerDetails,exportCustomer,queryPhone } from "@/api/crm/customer";
+import customerDetails from '../components/customerDetails.vue';
+import { treeselect } from "@/api/company/companyDept";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import { getToken } from "@/utils/auth";
+import editSource from '../components/editSource.vue';
+import addOrEditCustomer from '../components/addOrEditCustomer.vue';
+import customerAssignList from '../components/customerAssignList.vue';
+import assignUser from '../components/assignUser.vue';
+export default {
+    name: "Customer",
+    components: { assignUser,customerAssignList,addOrEditCustomer,customerDetails,Treeselect,editSource },
+    data() {
+        return {
+            companyUser:store.getters.user,
+            exportLoading:null,
+            exportInteval:null,
+            exportTime:60,
+            exportBtnText:"导出",
+            exportDisable:true,
+            xy:{
+                title:"免责协议",
+                open:false,
+            },
+            visitStatusOptions:[],
+            isDuplicateOptions:[ {"dictLabel":"不限","dictValue":""},{"dictLabel":"从客户","dictValue":1},{"dictLabel":"非从客户","dictValue":2}],
+            tagIds:[],
+            tagsOptions:[],
+            assignList:{
+                title:"分配历史记录",
+                open:false,
+            },
+            importMsgOpen:false,
+            importMsg:"",
+            importResult: {
+                summary: '',
+                hasFailure: false,
+                failureDetail: ''
+            },
+            customer:{
+                open:false,
+                title:"新增客户"
+            },
+            source:{
+                open:false,
+                title:"修改客户来源"
+            },
+            dateRange:[],
+            users:[],
+            // 用户导入参数
+            upload: {
+                // 是否显示弹出层(用户导入)
+                open: false,
+                // 弹出层标题(用户导入)
+                title: "",
+                // 是否禁用上传
+                isUploading: false,
+                // 设置上传的请求头部
+                headers: { Authorization: "Bearer " + getToken() },
+                // 上传的地址
+                url: process.env.VUE_APP_BASE_API + "/crm/customer/importLineData",
+            },
+            assign:{
+                title:"分配客户",
+                open:false,
+            },
+            // 部门树选项
+            deptOptions: undefined,
+            cityIds:[],
+            citys:[],
+            tags:[],
+            inputVisible: false,
+            inputValue: '',
+            statusOptions:[],
+            typeOptions:[],
+            sourceOptions:[],
+            sexOptions:[],
+            show:{
+                title:"客户详情",
+                open:false,
+            },
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 客户表格数据
+            customerList: [],
+            // 弹出层标题
+            title: "",
+            // 是否显示弹出层
+            open: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                customerCode: null,
+                customerName: null,
+                mobile: null,
+                encryptedMobile: null,
+                sex: null,
+                weixin: null,
+                userId: null,
+                createUserId: null,
+                receiveUserId: null,
+                customerUserId: null,
+                address: null,
+                location: null,
+                detailAddress: null,
+                lng: null,
+                lat: null,
+                status: null,
+                isReceive: null,
+                deptId: null,
+                isDel: null,
+                customerType: null,
+                receiveTime: null,
+                poolTime: null,
+                companyId: null,
+                isLine: 1,
+                source: null,
+                tags: null,
+                isDuplicate:null
+            },
+            // 表单参数
+            form: {
+                province:null,
+                city:null,
+                district:null,
+            },
+            // 表单校验
+            rules: {
+                customerName: [
+                    { required: true, message: "客户名称不能为空", trigger: "blur" }
+                ],
+                mobile: [
+                    { required: true, message: "手机号不能为空", trigger: "blur" }
+                ],
+                sex: [
+                    { required: true, message: "性别不能为空", trigger: "blur" }
+                ],
+                source: [
+                    { required: true, message: "客户来源不能为空", trigger: "blur" }
+                ],
+            }
+        };
+    },
+
+    created() {
+        this.getDicts("crm_customer_source").then((response) => {
+            this.sourceOptions = response.data;
+        });
+        this.getDicts("sys_sex").then((response) => {
+            this.sexOptions = response.data;
+        });
+        this.getDicts("crm_customer_source").then((response) => {
+            this.sourceOptions = response.data;
+        });
+        this.getDicts("sys_sex").then((response) => {
+            this.sexOptions = response.data;
+        });
+        this.getDicts("crm_customer_status").then((response) => {
+            this.statusOptions = response.data;
+        });
+        this.getDicts("crm_customer_type").then((response) => {
+            this.typeOptions = response.data;
+        });
+        this.getDicts("crm_customer_tag").then((response) => {
+            this.tagsOptions = response.data;
+        });
+        this.getDicts("crm_customer_user_status").then((response) => {
+            this.visitStatusOptions = response.data;
+        });
+
+        this.getList();
+    },
+    methods: {
+        handleQueryPhone(row) {
+            queryPhone(row.customerId).then(response => {
+                this.$alert(response.mobile, '手机号', {
+                    confirmButtonText: '确定',
+                    callback: action => {}
+                });
+            });
+        },
+        handleShow(row){
+            this.show.open=true;
+            var that=this;
+            setTimeout(() => {
+                console.log("qxj dCustomerId ",row.dcustomerId);
+                that.$refs.customerDetails.initDuplicate(row.isDuplicate,row.dcustomerId);
+                that.$refs.customerDetails.getDetails(row.customerId);
+            }, 200);
+        },
+        handleAssignList(){
+            this.assignList.open=true;
+            var that=this;
+            setTimeout(() => {
+                that.$refs.assignList.getData();
+            }, 200);
+        },
+        closeAssignList(){
+            this.assignList.open=false;
+            this.getList();
+        },
+        handleAdd() {
+            this.customer.open = true;
+            var that=this;
+            setTimeout(() => {
+                that.$refs.customer.handleAdd(1);
+            }, 200);
+        },
+        closeCustomer(){
+            this.customer.open=false;
+            this.getList();
+        },
+        handleEditScource(){
+            this.source.open=true;
+            var that=this;
+            setTimeout(() => {
+                that.$refs.editSource.handleEdit(that.ids);
+            }, 200);
+
+        },
+        closeSource(){
+            this.source.open=false;
+            this.getList();
+        },
+
+        /** 查询部门下拉树结构 */
+        getTreeselect() {
+            treeselect().then((response) => {
+                this.deptOptions = response.data;
+            });
+        },
+        handleCityChange(value) {
+            console.log(value);
+            var nodes=this.$refs.citySelect.getCheckedNodes();
+            this.form.address=nodes[0].pathLabels[0]+"-"+nodes[0].pathLabels[1]+"-"+nodes[0].pathLabels[2];
+            this.form.cityIds=value.toString();
+        },
+
+        handleAssign(){
+            var that=this;
+            var ids=this.ids;
+            that.assign.open=true;
+            setTimeout(() => {
+                that.$refs.assignUser.init(ids,4);
+            }, 200);
+        },
+        closeAssign(){
+            this.assign.open=false;
+            this.getList();
+        },
+        closeAssignPop(){
+            this.$refs.assignUser.closeAction();
+        },
+        /** 查询客户列表 */
+        getList() {
+            this.loading = true;
+            if(this.tagIds.length>0){
+                this.queryParams.tags=this.tagIds.toString();
+            }
+            else{
+                this.queryParams.tags=null
+            }
+            getLineCustomerList(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
+                this.customerList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                customerId: null,
+                customerCode: null,
+                customerName: null,
+                mobile: null,
+                sex: null,
+                weixin: null,
+                remark: null,
+                userId: null,
+                createUserId: null,
+                receiveUserId: null,
+                customerUserId: null,
+                address: null,
+                location: null,
+                detailAddress: null,
+                lng: null,
+                lat: null,
+                createTime: null,
+                updateTime: null,
+                status: 0,
+                isReceive: null,
+                deptId: null,
+                isDel: null,
+                customerType: null,
+                receiveTime: null,
+                poolTime: null,
+                companyId: null,
+                isLine: null,
+                source: null,
+                tags: null
+            };
+            this.tags=[];
+            this.cityIds=[];
+            this.resetForm("form");
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            if (this.queryParams.mobile && this.queryParams.encryptedMobile) {
+                this.$message.warning('手机和加密手机不能同时填写,将以加密手机为准进行查询');
+                this.queryParams.mobile = null;
+            }
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm("queryForm");
+            this.tagIds = [];
+            this.dateRange = null;
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map(item => item.customerId)
+            this.single = selection.length!==1
+            this.multiple = !selection.length
+            console.log(this.ids)
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const customerIds = row.customerId || this.ids;
+            this.$confirm('是否确认删除客户编号为"' + customerIds + '"的数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(function() {
+                return delLineCustomer(customerIds);
+            }).then(() => {
+                this.getList();
+                this.msgSuccess("删除成功");
+            }).catch(function() {});
+        },
+        /** 导出按钮操作 */
+        handleExport() {
+            this.exportTime=10;
+            this.xy.open=true;
+            var that=this;
+            this.exportDisable=true;
+            that.exportInteval= setInterval(function(){
+                that.exportBtnText="同意隐私协议("+that.exportTime+")"
+                that.exportTime--;
+                if(that.exportTime<1){
+                    that.exportBtnText="同意隐私协议"
+                    clearInterval(that.exportInteval)
+                    that.exportInteval=null;
+                    that.exportDisable=false;
+                }
+            },1000);
+        },
+        submitExport(){
+            var that=this;
+            if(this.tagIds.length>0){
+                this.queryParams.tags=this.tagIds.toString();
+            }
+            else{
+                this.queryParams.tags=null
+            }
+            this.$confirm('是否确认导出所有客户数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(function() {
+                that.exportLoading = that.$loading({
+                    lock: true,
+                    text: '处理中...',
+                    spinner: 'el-icon-loading',
+                    background: 'rgba(0, 0, 0, 0.7)'
+                });
+                return exportCustomer(that.addDateRange(that.queryParams, that.dateRange));
+            }).then(response => {
+                that.exportLoading.close();
+                that.download(response.msg);
+            }).catch(function() {});
+        },
+
+
+        /** 导入按钮操作 */
+        handleImport() {
+            this.upload.title = "线索客户导入";
+            this.upload.open = true;
+
+        },
+        /** 下载模板操作 */
+        importTemplate() {
+            importLineTemplate().then((response) => {
+                this.download(response.msg);
+            });
+        },
+        // 文件上传中处理
+        handleFileUploadProgress(event, file, fileList) {
+            this.upload.isUploading = true;
+        },
+        // 文件上传失败处理
+        handleFileError(err, file, fileList) {
+            this.upload.isUploading = false;
+            this.$message.error('导入失败,请重试');
+        },
+        // 文件上传成功处理
+        handleFileSuccess(response, file, fileList) {
+            this.upload.open = false;
+            this.upload.isUploading = false;
+            this.$refs.upload.clearFiles();
+            this.importMsgOpen=true;
+            this.importMsg=response.msg;
+            this.parseImportResult(response.msg);
+            this.getList();
+        },
+        // 解析导入结果消息
+        parseImportResult(msg) {
+            const hasFailure = msg.includes('导入失败');
+            const hasSuccess = msg.includes('导入成功');
+
+            let summary = '';
+            let failureDetail = '';
+
+            if (hasFailure && hasSuccess) {
+                const failMatch = msg.match(/共\s*(\d+)\s*条数据格式不正确/);
+                const successMatch = msg.match(/共\s*(\d+)\s*条,数据如下/);
+                const failNum = failMatch ? failMatch[1] : '?';
+                const successNum = successMatch ? successMatch[1] : '?';
+                summary = `导入完成:成功 ${successNum} 条,失败 ${failNum} 条`;
+
+                const failDetailMatch = msg.match(/错误如下[::](.*?)恭喜您/);
+                if (failDetailMatch) {
+                    failureDetail = failDetailMatch[1];
+                }
+            } else if (hasFailure) {
+                const failMatch = msg.match(/共\s*(\d+)\s*条数据格式不正确/);
+                const failNum = failMatch ? failMatch[1] : '?';
+                summary = `导入失败:${failNum} 条数据格式不正确`;
+
+                const failDetailMatch = msg.match(/错误如下[::](.*)/s);
+                if (failDetailMatch) {
+                    failureDetail = failDetailMatch[1];
+                }
+            } else {
+                const successMatch = msg.match(/共\s*(\d+)\s*条/);
+                const successNum = successMatch ? successMatch[1] : '?';
+                summary = `导入成功!共 ${successNum} 条数据已全部导入`;
+            }
+
+            this.importResult = {
+                summary,
+                hasFailure,
+                failureDetail
+            };
+        },
+        // 提交上传文件
+        submitFileForm() {
+            this.$refs.upload.submit();
+        },
+
+        getCustomerName(customerName){
+            if(customerName==null || customerName==""){
+                return "";
+            }
+            return customerName;
+        }
+    }
+};
+</script>
+<style>
+.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;
+}
+.el-dialog__wrapper{
+    z-index: 100000;
+}
+.xy-content{
+
+}
+.xy-img{
+    border: 1px solid #dedede;
+    padding: 15px;
+    margin: 15px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+}
+.footer{
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+}
+.import-summary {
+    display: flex;
+    align-items: center;
+    font-size: 16px;
+    font-weight: bold;
+    margin-bottom: 16px;
+}
+.import-detail {
+    border-top: 1px solid #EBEEF5;
+    padding-top: 12px;
+}
+.detail-title {
+    font-size: 14px;
+    color: #606266;
+    margin-bottom: 8px;
+}
+.detail-content {
+    max-height: 300px;
+    overflow-y: auto;
+    font-size: 13px;
+    color: #909399;
+    line-height: 1.8;
+    padding: 8px;
+    background: #f5f7fa;
+    border-radius: 4px;
+}
+</style>

+ 17 - 13
src/views/store/externalOrder/index.vue

@@ -247,6 +247,7 @@
       <el-table-column label="乡镇" align="center" prop="township" width="90" column-key="township" :filters="getFilters('township')" :filtered-value="columnFilters.township" />
       <el-table-column label="病名" align="center" prop="diseaseName" width="100" show-overflow-tooltip column-key="diseaseName" :filters="getFilters('diseaseName')" :filtered-value="columnFilters.diseaseName" />
       <el-table-column label="商品名称" align="center" prop="productNames" width="200" show-overflow-tooltip column-key="productNames" :filters="getFilters('productNames')" :filtered-value="columnFilters.productNames" />
+      <el-table-column label="商品数量" align="center" prop="productNum" width="100" />
       <el-table-column label="商品总额" align="center" prop="totalPrice" width="100" column-key="totalPrice" :filters="getFilters('totalPrice', null, true)" :filtered-value="columnFilters.totalPrice" />
       <el-table-column label="应收金额" align="center" prop="receivablePrice" width="100" column-key="receivablePrice" :filters="getFilters('receivablePrice', null, true)" :filtered-value="columnFilters.receivablePrice" />
       <el-table-column label="定金(实收)" align="center" prop="payPrice" width="100" column-key="payPrice" :filters="getFilters('payPrice', null, true)" :filtered-value="columnFilters.payPrice" />
@@ -256,29 +257,29 @@
           <dict-tag :options="payTypeOptions" :value="scope.row.payType" />
         </template>
       </el-table-column>
-      <el-table-column label="定金回款时间" align="center" prop="depositReturnTime" width="160" column-key="depositReturnTime" :filters="getDepositReturnTimeFilters()" :filtered-value="columnFilters.depositReturnTime">
+      <el-table-column label="定金回款时间" align="center" prop="depositReturnTime" width="160" column-key="depositReturnTime" :filter-multiple="false" :filters="getDepositReturnTimeFilters()" :filtered-value="columnFilters.depositReturnTime">
         <template slot-scope="scope">
           <span>{{ scope.row.status == 4 ? scope.row.updateTime : '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="创建时间" align="center" prop="externalCreateTime" width="160" column-key="externalCreateTime" :filters="getFilters('externalCreateTime')" :filtered-value="columnFilters.externalCreateTime" />
+      <el-table-column label="创建时间" align="center" prop="externalCreateTime" width="160" column-key="externalCreateTime" :filter-multiple="false" :filters="getFilters('externalCreateTime')" :filtered-value="columnFilters.externalCreateTime" />
       <el-table-column label="快递单号" align="center" prop="deliverySn" width="150" show-overflow-tooltip column-key="deliverySn" :filters="getFilters('deliverySn')" :filtered-value="columnFilters.deliverySn" />
       <el-table-column label="快递公司" align="center" prop="deliveryCode" width="100" column-key="deliveryCode" :filters="getFilters('deliveryCode')" :filtered-value="columnFilters.deliveryCode" />
       <el-table-column label="快递类型" align="center" prop="deliveryName" width="100" column-key="deliveryName" :filters="getFilters('deliveryName')" :filtered-value="columnFilters.deliveryName" />
-      <el-table-column label="发货日期" align="center" prop="deliveryTime" width="160" column-key="deliveryTime" :filters="getFilters('deliveryTime')" :filtered-value="columnFilters.deliveryTime" />
+      <el-table-column label="发货日期" align="center" prop="deliveryTime" width="160" column-key="deliveryTime" :filter-multiple="false" :filters="getFilters('deliveryTime')" :filtered-value="columnFilters.deliveryTime" />
       <el-table-column label="签收状态" align="center" prop="deliveryStatus" width="100" column-key="deliveryStatus" :filters="deliveryStatusFilters" :filtered-value="columnFilters.deliveryStatus">
         <template slot-scope="scope">
           <dict-tag v-if="scope.row.deliveryStatus != null" :options="deliveryStatusOptions" :value="scope.row.deliveryStatus" />
           <span v-else>-</span>
         </template>
       </el-table-column>
-      <el-table-column label="跟单时间" align="center" prop="followTime" width="160" column-key="followTime" :filters="getFilters('followTime')" :filtered-value="columnFilters.followTime" />
+      <el-table-column label="跟单时间" align="center" prop="followTime" width="160" column-key="followTime" :filter-multiple="false" :filters="getFilters('followTime')" :filtered-value="columnFilters.followTime" />
       <el-table-column label="退款金额" align="center" prop="refundPrice" width="100" column-key="refundPrice" :filters="getFilters('refundPrice', null, true)" :filtered-value="columnFilters.refundPrice" />
-      <el-table-column label="退款日期" align="center" prop="refundTime" width="160" column-key="refundTime" :filters="getFilters('refundTime')" :filtered-value="columnFilters.refundTime" />
-      <el-table-column label="拒收回退时间" align="center" prop="rejectReturnTime" width="160" column-key="rejectReturnTime" :filters="getFilters('rejectReturnTime')" :filtered-value="columnFilters.rejectReturnTime" />
+      <el-table-column label="退款日期" align="center" prop="refundTime" width="160" column-key="refundTime" :filter-multiple="false" :filters="getFilters('refundTime')" :filtered-value="columnFilters.refundTime" />
+      <el-table-column label="拒收回退时间" align="center" prop="rejectReturnTime" width="160" column-key="rejectReturnTime" :filter-multiple="false" :filters="getFilters('rejectReturnTime')" :filtered-value="columnFilters.rejectReturnTime" />
       <el-table-column label="备注" align="center" prop="remark" width="150" show-overflow-tooltip column-key="remark" :filters="getFilters('remark')" :filtered-value="columnFilters.remark" />
       <el-table-column label="审核人" align="center" prop="auditor" width="90" column-key="auditor" :filters="getFilters('auditor')" :filtered-value="columnFilters.auditor" />
-      <el-table-column label="审核时间" align="center" prop="auditTime" width="160" column-key="auditTime" :filters="getFilters('auditTime')" :filtered-value="columnFilters.auditTime" />
+      <el-table-column label="审核时间" align="center" prop="auditTime" width="160" column-key="auditTime" :filter-multiple="false" :filters="getFilters('auditTime')" :filtered-value="columnFilters.auditTime" />
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="200">
         <template slot-scope="scope">
           <el-button size="mini" type="text" v-hasPermi="['store:externalOrder:query']" @click="handleView(scope.row)">查看</el-button>
@@ -1007,9 +1008,9 @@ export default {
       });
       return Array.from(seen.keys()).map(v => ({ text: String(v), value: v }));
     },
-    /** 列头筛选变化:单选语义,取最后选中值映射到后端查询参数并重新请求 */
+    /** 列头筛选变化:多选语义,保留所有选中值并映射到后端查询参数 */
     handleFilterChange(filters) {
-      // 时间字段:选中值当天范围映射
+      // 时间字段:选中值当天范围映射(时间字段保持单选语义)
       const timeRangeMap = {
         externalCreateTime: ['sTime', 'eTime'],
         deliveryTime: ['deliveryStartTime', 'deliveryEndTime'],
@@ -1025,10 +1026,10 @@ export default {
       };
       Object.keys(filters).forEach(key => {
         const vals = filters[key] || [];
-        // 单选:只保留最后选中的一项
-        const last = vals.length > 0 ? vals[vals.length - 1] : null;
-        this.$set(this.columnFilters, key, last != null ? [last] : []);
         if (timeRangeMap[key]) {
+          // 时间字段保持单选语义,取最后选中值映射当天范围
+          const last = vals.length > 0 ? vals[vals.length - 1] : null;
+          this.$set(this.columnFilters, key, last != null ? [last] : []);
           const [startField, endField] = timeRangeMap[key];
           if (last != null && String(last).length >= 10) {
             const day = String(last).substring(0, 10);
@@ -1039,8 +1040,11 @@ export default {
             this.queryParams[endField] = null;
           }
         } else {
+          // 多选:保留所有选中值
+          this.$set(this.columnFilters, key, vals);
           const fieldName = fieldMap[key] || key;
-          this.queryParams[fieldName] = last;
+          // 多选:用逗号拼接成字符串传给后端
+          this.queryParams[fieldName] = vals.length > 0 ? vals.join(',') : null;
         }
       });
       // 延迟到下一个 tick 再查询,等筛选面板关闭后再更新表格数据,避免 insertBefore DOM 错误

+ 11 - 7
src/views/store/financeOrder/index.vue

@@ -152,12 +152,12 @@
     <el-table ref="orderTable" :key="tableKey" v-loading="loading" border :data="orderList" row-key="orderId" style="width:100%" show-summary :summary-method="getSummaries" @filter-change="handleFilterChange" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="50" :reserve-selection="true" fixed="left" />
       <el-table-column label="订单编号" align="center" prop="orderCode" width="170" fixed="left" column-key="orderCode" :filters="getFilters('orderCode')" :filtered-value="columnFilters.orderCode" />
-      <el-table-column label="订单日期" align="center" prop="externalCreateTime" width="160" column-key="externalCreateTime" :filters="getFilters('externalCreateTime')" :filtered-value="columnFilters.externalCreateTime">
+      <el-table-column label="订单日期" align="center" prop="externalCreateTime" width="160" :filter-multiple="false" column-key="externalCreateTime" :filters="getFilters('externalCreateTime')" :filtered-value="columnFilters.externalCreateTime">
         <template slot-scope="scope">
           <span>{{ parseTime(scope.row.externalCreateTime) }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="回款日期" align="center" prop="returnTime" width="160" column-key="returnTime" :filters="getFilters('returnTime')" :filtered-value="columnFilters.returnTime">
+      <el-table-column label="回款日期" align="center" prop="returnTime" width="160" :filter-multiple="false" column-key="returnTime" :filters="getFilters('returnTime')" :filtered-value="columnFilters.returnTime">
         <template slot-scope="scope">
           <span>{{ scope.row.returnTime ? parseTime(scope.row.returnTime) : '-' }}</span>
         </template>
@@ -176,6 +176,7 @@
       <el-table-column label="回款金额" align="center" prop="returnPrice" width="100" column-key="returnPrice" :filters="getFilters('returnPrice', null, true)" :filtered-value="columnFilters.returnPrice" />
       <el-table-column label="收件人电话" align="center" prop="receiverPhone" width="130" show-overflow-tooltip column-key="receiverPhone" :filters="getFilters('receiverPhone')" :filtered-value="columnFilters.receiverPhone" />
       <el-table-column label="订购产品" align="center" prop="productNames" width="180" show-overflow-tooltip column-key="productNames" :filters="getFilters('productNames')" :filtered-value="columnFilters.productNames" />
+      <el-table-column label="产品数量" align="center" prop="productNum" width="90" />
       <el-table-column label="付款方式" align="center" prop="payType" width="90" column-key="payType" :filters="payTypeFilters" :filtered-value="columnFilters.payType">
         <template slot-scope="scope">
           <dict-tag :options="payTypeOptions" :value="scope.row.payType" />
@@ -189,7 +190,7 @@
         </template>
       </el-table-column>
       <el-table-column label="备注" align="center" prop="remark" width="150" show-overflow-tooltip column-key="remark" :filters="getFilters('remark')" :filtered-value="columnFilters.remark" />
-      <el-table-column label="发货日期" align="center" prop="deliveryTime" width="160" column-key="deliveryTime" :filters="getFilters('deliveryTime')" :filtered-value="columnFilters.deliveryTime">
+      <el-table-column label="发货日期" align="center" prop="deliveryTime" width="160" :filter-multiple="false" column-key="deliveryTime" :filters="getFilters('deliveryTime')" :filtered-value="columnFilters.deliveryTime">
         <template slot-scope="scope">
           <span>{{ scope.row.deliveryTime ? parseTime(scope.row.deliveryTime) : '-' }}</span>
         </template>
@@ -469,7 +470,7 @@ export default {
         value: v
       }));
     },
-    /** 列头筛选变化:单选语义,取最后选中值映射到后端查询参数并重新请求 */
+    /** 列头筛选变化:非时间字段多选(逗号拼接),时间字段单选(取最后选中值映射当天范围) */
     handleFilterChange(filters) {
       const timeRangeMap = {
         externalCreateTime: ['sTime', 'eTime'],
@@ -481,9 +482,10 @@ export default {
       };
       Object.keys(filters).forEach(key => {
         const vals = filters[key] || [];
-        const last = vals.length > 0 ? vals[vals.length - 1] : null;
-        this.$set(this.columnFilters, key, last != null ? [last] : []);
         if (timeRangeMap[key]) {
+          // 时间字段:单选语义,取最后选中值映射当天范围
+          const last = vals.length > 0 ? vals[vals.length - 1] : null;
+          this.$set(this.columnFilters, key, last != null ? [last] : []);
           const [startField, endField] = timeRangeMap[key];
           if (last != null && String(last).length >= 10) {
             const day = String(last).substring(0, 10);
@@ -494,8 +496,10 @@ export default {
             this.queryParams[endField] = null;
           }
         } else {
+          // 非时间字段:多选,逗号拼接成字符串传给后端
+          this.$set(this.columnFilters, key, vals);
           const fieldName = fieldMap[key] || key;
-          this.queryParams[fieldName] = last;
+          this.queryParams[fieldName] = vals.length > 0 ? vals.join(',') : null;
         }
       });
       this.handleQuery();

+ 42 - 16
src/views/store/rejectNoReturn/index.vue

@@ -75,13 +75,13 @@
       <el-table-column label="运单号" align="center" prop="deliverySn" min-width="140" show-overflow-tooltip column-key="deliverySn" :filters="getFilters('deliverySn')" :filtered-value="columnFilters.deliverySn" />
       <el-table-column label="会员姓名" align="center" prop="userName" min-width="80" column-key="userName" :filters="getFilters('userName')" :filtered-value="columnFilters.userName" />
       <el-table-column label="地址" align="center" prop="userAddress" min-width="180" show-overflow-tooltip column-key="userAddress" :filters="getFilters('userAddress')" :filtered-value="columnFilters.userAddress" />
-      <el-table-column label="发货日期" align="center" prop="deliveryTime" min-width="150" column-key="deliveryTime" :filters="getFilters('deliveryTime')" :filtered-value="columnFilters.deliveryTime">
+      <el-table-column label="发货日期" align="center" prop="deliveryTime" min-width="150" :filter-multiple="false" column-key="deliveryTime" :filters="getFilters('deliveryTime')" :filtered-value="columnFilters.deliveryTime">
         <template slot-scope="scope">
           <span>{{ scope.row.deliveryTime ? parseTime(scope.row.deliveryTime) : '-' }}</span>
         </template>
       </el-table-column>
       <el-table-column label="金额" align="center" prop="totalPrice" min-width="100" column-key="totalPrice" :filters="getFilters('totalPrice', null, true)" :filtered-value="columnFilters.totalPrice" />
-      <el-table-column label="跟单时间" align="center" prop="followTime" min-width="150" column-key="followTime" :filters="getFilters('followTime')" :filtered-value="columnFilters.followTime">
+      <el-table-column label="跟单时间" align="center" prop="followTime" min-width="150" :filter-multiple="false" column-key="followTime" :filters="getFilters('followTime')" :filtered-value="columnFilters.followTime">
         <template slot-scope="scope">
           <span>{{ scope.row.followTime ? parseTime(scope.row.followTime) : '-' }}</span>
         </template>
@@ -141,6 +141,7 @@ export default {
   methods: {
     handleOrderTypeChange() {
       this.resetColumnFilters()
+      this.clearColumnFilterParams()
       this.handleQuery()
     },
     getList() {
@@ -199,30 +200,37 @@ export default {
         value: v
       }))
     },
-    /** 列头筛选变化:单选语义,取最后选中值映射到后端查询参数并重新请求 */
+    /** 列头筛选变化:外部订单多选(逗号拼接),店铺/谷币订单单选,时间字段始终单选 */
     handleFilterChange(filters) {
       // 时间字段:选中值当天范围映射
       const timeRangeMap = {
         deliveryTime: ['deliveryStartTime', 'deliveryEndTime'],
         followTime: ['followTimeStart', 'followTimeEnd']
       }
+      const isExternal = this.orderType === 'external'
       Object.keys(filters).forEach(key => {
         const vals = filters[key] || []
-        // 单选:只保留最后选中的一项
-        const last = vals.length > 0 ? vals[vals.length - 1] : null
-        this.$set(this.columnFilters, key, last != null ? [last] : [])
-        if (timeRangeMap[key]) {
-          const [startField, endField] = timeRangeMap[key]
-          if (last != null && String(last).length >= 10) {
-            const day = String(last).substring(0, 10)
-            this.queryParams[startField] = day + ' 00:00:00'
-            this.queryParams[endField] = day + ' 23:59:59'
+        if (isExternal && !timeRangeMap[key]) {
+          // 外部订单非时间字段:多选,逗号拼接成字符串传给后端
+          this.$set(this.columnFilters, key, vals)
+          this.queryParams[key] = vals.length > 0 ? vals.join(',') : null
+        } else {
+          // 时间字段 或 非外部订单:单选,取最后选中值
+          const last = vals.length > 0 ? vals[vals.length - 1] : null
+          this.$set(this.columnFilters, key, last != null ? [last] : [])
+          if (timeRangeMap[key]) {
+            const [startField, endField] = timeRangeMap[key]
+            if (last != null && String(last).length >= 10) {
+              const day = String(last).substring(0, 10)
+              this.queryParams[startField] = day + ' 00:00:00'
+              this.queryParams[endField] = day + ' 23:59:59'
+            } else {
+              this.queryParams[startField] = null
+              this.queryParams[endField] = null
+            }
           } else {
-            this.queryParams[startField] = null
-            this.queryParams[endField] = null
+            this.queryParams[key] = last
           }
-        } else {
-          this.queryParams[key] = last
         }
       })
       this.handleQuery()
@@ -233,6 +241,24 @@ export default {
         this.columnFilters[key] = []
       })
     },
+    /** 清空列头筛选对应的查询参数(切换订单类型时调用,避免多选字符串残留导致类型转换异常) */
+    clearColumnFilterParams() {
+      const timeRangeMap = {
+        deliveryTime: ['deliveryStartTime', 'deliveryEndTime'],
+        followTime: ['followTimeStart', 'followTimeEnd']
+      }
+      Object.keys(this.columnFilters).forEach(key => {
+        if (timeRangeMap[key]) {
+          const [startField, endField] = timeRangeMap[key]
+          this.queryParams[startField] = null
+          this.queryParams[endField] = null
+        } else {
+          this.queryParams[key] = null
+        }
+      })
+      this.deliveryTime = null
+      this.followTime = null
+    },
     /** 合计行:使用后端返回的整个数据集汇总(非当前页) */
     getSummaries({ columns }) {
       const sums = []

+ 52 - 19
src/views/store/rejectReport/index.vue

@@ -107,24 +107,24 @@
       <el-table-column label="订单编号" align="center" prop="orderCode" min-width="160" column-key="orderCode" :filters="getFilters(orderList, 'orderCode')" :filtered-value="columnFilters.orderCode" />
       <el-table-column label="快递单号" align="center" prop="deliverySn" min-width="140" show-overflow-tooltip column-key="deliverySn" :filters="getFilters(orderList, 'deliverySn')" :filtered-value="columnFilters.deliverySn" />
       <el-table-column label="会员姓名" align="center" prop="userName" min-width="80" column-key="userName" :filters="getFilters(orderList, 'userName')" :filtered-value="columnFilters.userName" />
-      <el-table-column label="订单日期" align="center" prop="externalCreateTime" min-width="150" column-key="externalCreateTime" :filters="getFilters(orderList, 'externalCreateTime')" :filtered-value="columnFilters.externalCreateTime">
+      <el-table-column label="订单日期" align="center" prop="externalCreateTime" min-width="150" :filter-multiple="false" column-key="externalCreateTime" :filters="getFilters(orderList, 'externalCreateTime')" :filtered-value="columnFilters.externalCreateTime">
         <template slot-scope="scope">
           <span>{{ scope.row.externalCreateTime ? parseTime(scope.row.externalCreateTime) : '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="发货日期" align="center" prop="deliveryTime" min-width="150" column-key="deliveryTime" :filters="getFilters(orderList, 'deliveryTime')" :filtered-value="columnFilters.deliveryTime">
+      <el-table-column label="发货日期" align="center" prop="deliveryTime" min-width="150" :filter-multiple="false" column-key="deliveryTime" :filters="getFilters(orderList, 'deliveryTime')" :filtered-value="columnFilters.deliveryTime">
         <template slot-scope="scope">
           <span>{{ scope.row.deliveryTime ? parseTime(scope.row.deliveryTime) : '-' }}</span>
         </template>
       </el-table-column>
       <el-table-column label="金额" align="center" prop="totalPrice" min-width="90" column-key="totalPrice" :filters="getFilters(orderList, 'totalPrice', true)" :filtered-value="columnFilters.totalPrice" />
       <el-table-column label="快递公司" align="center" prop="deliveryName" min-width="100" column-key="deliveryName" :filters="getFilters(orderList, 'deliveryName')" :filtered-value="columnFilters.deliveryName" />
-      <el-table-column label="跟单时间" align="center" prop="followTime" min-width="150" column-key="followTime" :filters="getFilters(orderList, 'followTime')" :filtered-value="columnFilters.followTime">
+      <el-table-column label="跟单时间" align="center" prop="followTime" min-width="150" :filter-multiple="false" column-key="followTime" :filters="getFilters(orderList, 'followTime')" :filtered-value="columnFilters.followTime">
         <template slot-scope="scope">
           <span>{{ scope.row.followTime ? parseTime(scope.row.followTime) : '-' }}</span>
         </template>
       </el-table-column>
-      <el-table-column label="拒收退回日期" align="center" prop="rejectReturnTime" min-width="150" column-key="rejectReturnTime" :filters="getFilters(orderList, 'rejectReturnTime')" :filtered-value="columnFilters.rejectReturnTime">
+      <el-table-column label="拒收退回日期" align="center" prop="rejectReturnTime" min-width="150" :filter-multiple="false" column-key="rejectReturnTime" :filters="getFilters(orderList, 'rejectReturnTime')" :filtered-value="columnFilters.rejectReturnTime">
         <template slot-scope="scope">
           <span>{{ scope.row.rejectReturnTime ? parseTime(scope.row.rejectReturnTime) : '-' }}</span>
         </template>
@@ -281,7 +281,7 @@ export default {
       }
       return keys.map(v => ({ text: String(v), value: v }))
     },
-    /** 列头筛选变化:单选语义,取最后选中值映射到后端查询参数并重新请求 */
+    /** 列头筛选变化:外部订单多选(逗号拼接),店铺/谷币订单单选,时间字段始终单选 */
     handleFilterChange(filters) {
       const isOrderTab = this.activeTab === 'order'
       const queryObj = isOrderTab ? this.orderQuery : this.productQuery
@@ -290,24 +290,33 @@ export default {
         deliveryTime: ['deliveryStartTime', 'deliveryEndTime'],
         rejectReturnTime: ['rejectReturnTimeStart', 'rejectReturnTimeEnd']
       } : {}
-      const fieldMap = { productNames: 'productName' }
+      const fieldMap = { productNames: 'productName', rejectUser: 'returnUser' }
+      const isExternal = this.orderType === 'external'
       Object.keys(filters).forEach(key => {
         const vals = filters[key] || []
-        const last = vals.length > 0 ? vals[vals.length - 1] : null
-        this.$set(this.columnFilters, key, last != null ? [last] : [])
-        if (timeRangeMap[key]) {
-          const [startField, endField] = timeRangeMap[key]
-          if (last != null && String(last).length >= 10) {
-            const day = String(last).substring(0, 10)
-            queryObj[startField] = day + ' 00:00:00'
-            queryObj[endField] = day + ' 23:59:59'
+        if (isExternal && !timeRangeMap[key]) {
+          // 外部订单非时间字段:多选,逗号拼接成字符串传给后端
+          this.$set(this.columnFilters, key, vals)
+          const fieldName = fieldMap[key] || key
+          queryObj[fieldName] = vals.length > 0 ? vals.join(',') : null
+        } else {
+          // 时间字段 或 非外部订单:单选,取最后选中值
+          const last = vals.length > 0 ? vals[vals.length - 1] : null
+          this.$set(this.columnFilters, key, last != null ? [last] : [])
+          if (timeRangeMap[key]) {
+            const [startField, endField] = timeRangeMap[key]
+            if (last != null && String(last).length >= 10) {
+              const day = String(last).substring(0, 10)
+              queryObj[startField] = day + ' 00:00:00'
+              queryObj[endField] = day + ' 23:59:59'
+            } else {
+              queryObj[startField] = null
+              queryObj[endField] = null
+            }
           } else {
-            queryObj[startField] = null
-            queryObj[endField] = null
+            const fieldName = fieldMap[key] || key
+            queryObj[fieldName] = last
           }
-        } else {
-          const fieldName = fieldMap[key] || key
-          queryObj[fieldName] = last
         }
       })
       if (isOrderTab) {
@@ -318,6 +327,7 @@ export default {
     },
     handleOrderTypeChange() {
       this.resetColumnFilters()
+      this.clearColumnFilterParams()
       if (this.activeTab === 'order') {
         this.handleOrderQuery()
       } else {
@@ -330,6 +340,29 @@ export default {
         this.columnFilters[key] = []
       })
     },
+    /** 清空列头筛选对应的查询参数(切换订单类型时调用,避免多选字符串残留导致类型转换异常) */
+    clearColumnFilterParams() {
+      const orderTimeRangeMap = {
+        externalCreateTime: ['sTime', 'eTime'],
+        deliveryTime: ['deliveryStartTime', 'deliveryEndTime'],
+        rejectReturnTime: ['rejectReturnTimeStart', 'rejectReturnTimeEnd']
+      }
+      const fieldMap = { productNames: 'productName', rejectUser: 'returnUser' }
+      Object.keys(this.columnFilters).forEach(key => {
+        const fieldName = fieldMap[key] || key
+        if (orderTimeRangeMap[key]) {
+          const [startField, endField] = orderTimeRangeMap[key]
+          this.orderQuery[startField] = null
+          this.orderQuery[endField] = null
+        } else {
+          this.orderQuery[fieldName] = null
+          this.productQuery[fieldName] = null
+        }
+      })
+      this.orderDeliveryTime = null
+      this.orderCreateTime = null
+      this.orderRejectReturnTime = null
+    },
     /** 合计行:使用后端返回的整个数据集汇总(非当前页) */
     getSummaries({ columns }) {
       const sums = []

+ 51 - 17
src/views/store/returnReport/index.vue

@@ -113,7 +113,7 @@
       <el-table-column label="快递单号" align="center" prop="deliverySn" min-width="140" show-overflow-tooltip column-key="deliverySn" :filters="getFilters(orderList, 'deliverySn')" :filtered-value="columnFilters.deliverySn" />
       <el-table-column label="姓名" align="center" prop="userName" min-width="80" column-key="userName" :filters="getFilters(orderList, 'userName')" :filtered-value="columnFilters.userName" />
       <el-table-column label="地址" align="center" prop="userAddress" min-width="180" show-overflow-tooltip column-key="userAddress" :filters="getFilters(orderList, 'userAddress')" :filtered-value="columnFilters.userAddress" />
-      <el-table-column label="订单日期" align="center" prop="externalCreateTime" min-width="150" column-key="externalCreateTime" :filters="getFilters(orderList, 'externalCreateTime')" :filtered-value="columnFilters.externalCreateTime">
+      <el-table-column label="订单日期" align="center" prop="externalCreateTime" min-width="150" :filter-multiple="false" column-key="externalCreateTime" :filters="getFilters(orderList, 'externalCreateTime')" :filtered-value="columnFilters.externalCreateTime">
         <template slot-scope="scope">
           <span>{{ scope.row.externalCreateTime ? parseTime(scope.row.externalCreateTime) : '-' }}</span>
         </template>
@@ -122,7 +122,7 @@
       <el-table-column label="金额" align="center" prop="totalPrice" min-width="90" column-key="totalPrice" :filters="getFilters(orderList, 'totalPrice', true)" :filtered-value="columnFilters.totalPrice" />
       <el-table-column label="快递公司" align="center" prop="deliveryName" min-width="100" column-key="deliveryName" :filters="getFilters(orderList, 'deliveryName')" :filtered-value="columnFilters.deliveryName" />
       <el-table-column label="回款人" align="center" prop="returnUser" min-width="90" column-key="returnUser" :filters="getFilters(orderList, 'returnUser')" :filtered-value="columnFilters.returnUser" />
-      <el-table-column label="回款日期" align="center" prop="returnTime" min-width="150" column-key="returnTime" :filters="getFilters(orderList, 'returnTime')" :filtered-value="columnFilters.returnTime">
+      <el-table-column label="回款日期" align="center" prop="returnTime" min-width="150" :filter-multiple="false" column-key="returnTime" :filters="getFilters(orderList, 'returnTime')" :filtered-value="columnFilters.returnTime">
         <template slot-scope="scope">
           <span>{{ scope.row.returnTime ? parseTime(scope.row.returnTime) : '-' }}</span>
         </template>
@@ -132,7 +132,7 @@
       <el-table-column label="销售" align="center" prop="companyUserName" min-width="90" column-key="companyUserName" :filters="getFilters(orderList, 'companyUserName')" :filtered-value="columnFilters.companyUserName" />
       <el-table-column label="部门" align="center" prop="deptName" min-width="110" show-overflow-tooltip column-key="deptName" :filters="getFilters(orderList, 'deptName')" :filtered-value="columnFilters.deptName" />
       <el-table-column label="订购产品" align="center" prop="productNames" min-width="180" show-overflow-tooltip column-key="productNames" :filters="getFilters(orderList, 'productNames')" :filtered-value="columnFilters.productNames" />
-      <el-table-column label="发货日期" align="center" prop="deliveryTime" min-width="150" column-key="deliveryTime" :filters="getFilters(orderList, 'deliveryTime')" :filtered-value="columnFilters.deliveryTime">
+      <el-table-column label="发货日期" align="center" prop="deliveryTime" min-width="150" :filter-multiple="false" column-key="deliveryTime" :filters="getFilters(orderList, 'deliveryTime')" :filtered-value="columnFilters.deliveryTime">
         <template slot-scope="scope">
           <span>{{ scope.row.deliveryTime ? parseTime(scope.row.deliveryTime) : '-' }}</span>
         </template>
@@ -311,7 +311,7 @@ export default {
       }
       return keys.map(v => ({ text: String(v), value: v }))
     },
-    /** 列头筛选变化:单选语义,取最后选中值映射到后端查询参数并重新请求 */
+    /** 列头筛选变化:外部订单多选(逗号拼接),店铺订单单选,时间字段始终单选 */
     handleFilterChange(filters) {
       const isOrderTab = this.activeTab === 'order'
       const queryObj = isOrderTab ? this.orderQuery : this.productQuery
@@ -321,23 +321,32 @@ export default {
         deliveryTime: ['deliveryStartTime', 'deliveryEndTime']
       } : {}
       const fieldMap = { productNames: 'productName' }
+      const isExternal = this.orderType === 'external'
       Object.keys(filters).forEach(key => {
         const vals = filters[key] || []
-        const last = vals.length > 0 ? vals[vals.length - 1] : null
-        this.$set(this.columnFilters, key, last != null ? [last] : [])
-        if (timeRangeMap[key]) {
-          const [startField, endField] = timeRangeMap[key]
-          if (last != null && String(last).length >= 10) {
-            const day = String(last).substring(0, 10)
-            queryObj[startField] = day + ' 00:00:00'
-            queryObj[endField] = day + ' 23:59:59'
+        if (isExternal && !timeRangeMap[key]) {
+          // 外部订单非时间字段:多选,逗号拼接成字符串传给后端
+          this.$set(this.columnFilters, key, vals)
+          const fieldName = fieldMap[key] || key
+          queryObj[fieldName] = vals.length > 0 ? vals.join(',') : null
+        } else {
+          // 时间字段 或 非外部订单:单选,取最后选中值
+          const last = vals.length > 0 ? vals[vals.length - 1] : null
+          this.$set(this.columnFilters, key, last != null ? [last] : [])
+          if (timeRangeMap[key]) {
+            const [startField, endField] = timeRangeMap[key]
+            if (last != null && String(last).length >= 10) {
+              const day = String(last).substring(0, 10)
+              queryObj[startField] = day + ' 00:00:00'
+              queryObj[endField] = day + ' 23:59:59'
+            } else {
+              queryObj[startField] = null
+              queryObj[endField] = null
+            }
           } else {
-            queryObj[startField] = null
-            queryObj[endField] = null
+            const fieldName = fieldMap[key] || key
+            queryObj[fieldName] = last
           }
-        } else {
-          const fieldName = fieldMap[key] || key
-          queryObj[fieldName] = last
         }
       })
       if (isOrderTab) {
@@ -354,12 +363,37 @@ export default {
     },
     handleOrderTypeChange() {
       this.resetColumnFilters()
+      this.clearColumnFilterParams()
       if (this.activeTab === 'order') {
         this.handleOrderQuery()
       } else {
         this.handleProductQuery()
       }
     },
+    /** 清空列头筛选对应的查询参数(切换订单类型时调用,避免多选字符串残留导致类型转换异常) */
+    clearColumnFilterParams() {
+      const orderTimeRangeMap = {
+        externalCreateTime: ['sTime', 'eTime'],
+        returnTime: ['returnTimeStart', 'returnTimeEnd'],
+        deliveryTime: ['deliveryStartTime', 'deliveryEndTime']
+      }
+      const fieldMap = { productNames: 'productName' }
+      Object.keys(this.columnFilters).forEach(key => {
+        const fieldName = fieldMap[key] || key
+        if (orderTimeRangeMap[key]) {
+          const [startField, endField] = orderTimeRangeMap[key]
+          this.orderQuery[startField] = null
+          this.orderQuery[endField] = null
+        } else {
+          this.orderQuery[fieldName] = null
+          this.productQuery[fieldName] = null
+        }
+      })
+      this.orderCreateTime = null
+      this.orderReturnTime = null
+      this.orderDeliveryTime = null
+      this.productReturnTime = null
+    },
     /** 合计行:使用后端返回的整个数据集汇总(非当前页) */
     getSummaries({ columns }) {
       const sums = []

+ 19 - 5
src/views/store/signDetailReport/index.vue

@@ -154,6 +154,7 @@ export default {
       // 切换订单类型时清空内部订单类型筛选,并重置列头筛选
       this.queryParams.orderType = null
       this.resetColumnFilters()
+      this.clearColumnFilterParams()
       this.handleQuery()
     },
     getList() {
@@ -219,17 +220,30 @@ export default {
         value: v
       }))
     },
-    /** 列头筛选变化:单选语义,取最后选中值映射到后端查询参数并重新请求 */
+    /** 列头筛选变化:外部订单多选(逗号拼接),店铺/谷币订单单选 */
     handleFilterChange(filters) {
+      const isExternal = this.orderType === 'external'
       Object.keys(filters).forEach(key => {
         const vals = filters[key] || []
-        // 单选:只保留最后选中的一项
-        const last = vals.length > 0 ? vals[vals.length - 1] : null
-        this.$set(this.columnFilters, key, last != null ? [last] : [])
-        this.queryParams[key] = last
+        if (isExternal) {
+          // 外部订单:多选,逗号拼接成字符串传给后端
+          this.$set(this.columnFilters, key, vals)
+          this.queryParams[key] = vals.length > 0 ? vals.join(',') : null
+        } else {
+          // 店铺/谷币订单:单选,取最后选中值
+          const last = vals.length > 0 ? vals[vals.length - 1] : null
+          this.$set(this.columnFilters, key, last != null ? [last] : [])
+          this.queryParams[key] = last
+        }
       })
       this.handleQuery()
     },
+    /** 清空列头筛选对应的查询参数(切换订单类型时调用,避免多选字符串残留导致类型转换异常) */
+    clearColumnFilterParams() {
+      Object.keys(this.columnFilters).forEach(key => {
+        this.queryParams[key] = null
+      })
+    },
     /** 重置所有列头筛选选中状态 */
     resetColumnFilters() {
       Object.keys(this.columnFilters).forEach(key => {

+ 42 - 15
src/views/store/signReport/index.vue

@@ -83,7 +83,7 @@
       <el-table-column label="运单号" align="center" prop="deliverySn" min-width="140" show-overflow-tooltip column-key="deliverySn" :filters="getFilters('deliverySn')" :filtered-value="columnFilters.deliverySn" />
       <el-table-column label="会员姓名" align="center" prop="userName" min-width="80" column-key="userName" :filters="getFilters('userName')" :filtered-value="columnFilters.userName" />
       <el-table-column label="地址" align="center" prop="userAddress" min-width="180" show-overflow-tooltip column-key="userAddress" :filters="getFilters('userAddress')" :filtered-value="columnFilters.userAddress" />
-      <el-table-column label="发货日期" align="center" prop="deliveryTime" min-width="150" column-key="deliveryTime" :filters="getFilters('deliveryTime')" :filtered-value="columnFilters.deliveryTime">
+      <el-table-column label="发货日期" align="center" prop="deliveryTime" min-width="150" :filter-multiple="false" column-key="deliveryTime" :filters="getFilters('deliveryTime')" :filtered-value="columnFilters.deliveryTime">
         <template slot-scope="scope">
           <span>{{ scope.row.deliveryTime ? parseTime(scope.row.deliveryTime) : '-' }}</span>
         </template>
@@ -91,7 +91,7 @@
       <el-table-column label="定金" align="center" prop="payPrice" min-width="90" column-key="payPrice" :filters="getFilters('payPrice', null, true)" :filtered-value="columnFilters.payPrice" />
       <el-table-column label="代收金额" align="center" prop="collectionPrice" min-width="90" column-key="collectionPrice" :filters="getFilters('collectionPrice', null, true)" :filtered-value="columnFilters.collectionPrice" />
       <el-table-column label="订单总金额" align="center" prop="totalPrice" min-width="100" column-key="totalPrice" :filters="getFilters('totalPrice', null, true)" :filtered-value="columnFilters.totalPrice" />
-      <el-table-column label="跟单时间" align="center" prop="followTime" min-width="150" column-key="followTime" :filters="getFilters('followTime')" :filtered-value="columnFilters.followTime">
+      <el-table-column label="跟单时间" align="center" prop="followTime" min-width="150" :filter-multiple="false" column-key="followTime" :filters="getFilters('followTime')" :filtered-value="columnFilters.followTime">
         <template slot-scope="scope">
           <span>{{ scope.row.followTime ? parseTime(scope.row.followTime) : '-' }}</span>
         </template>
@@ -162,6 +162,7 @@ export default {
   methods: {
     handleOrderTypeChange() {
       this.resetColumnFilters()
+      this.clearColumnFilterParams()
       this.handleQuery()
     },
     getTreeselect() {
@@ -203,28 +204,36 @@ export default {
         value: v
       }));
     },
-    /** 列头筛选变化:单选语义,取最后选中值映射到后端查询参数并重新请求 */
+    /** 列头筛选变化:外部订单多选(逗号拼接),店铺订单单选,时间字段始终单选 */
     handleFilterChange(filters) {
       const timeRangeMap = {
         deliveryTime: ['deliveryStartTime', 'deliveryEndTime'],
         followTime: ['followTimeStart', 'followTimeEnd']
       };
+      const isExternal = this.orderType === 'external'
       Object.keys(filters).forEach(key => {
         const vals = filters[key] || [];
-        const last = vals.length > 0 ? vals[vals.length - 1] : null;
-        this.$set(this.columnFilters, key, last != null ? [last] : []);
-        if (timeRangeMap[key]) {
-          const [startField, endField] = timeRangeMap[key];
-          if (last != null && String(last).length >= 10) {
-            const day = String(last).substring(0, 10);
-            this.queryParams[startField] = day + ' 00:00:00';
-            this.queryParams[endField] = day + ' 23:59:59';
+        if (isExternal && !timeRangeMap[key]) {
+          // 外部订单非时间字段:多选,逗号拼接成字符串传给后端
+          this.$set(this.columnFilters, key, vals);
+          this.queryParams[key] = vals.length > 0 ? vals.join(',') : null;
+        } else {
+          // 时间字段 或 非外部订单:单选,取最后选中值
+          const last = vals.length > 0 ? vals[vals.length - 1] : null;
+          this.$set(this.columnFilters, key, last != null ? [last] : []);
+          if (timeRangeMap[key]) {
+            const [startField, endField] = timeRangeMap[key];
+            if (last != null && String(last).length >= 10) {
+              const day = String(last).substring(0, 10);
+              this.queryParams[startField] = day + ' 00:00:00';
+              this.queryParams[endField] = day + ' 23:59:59';
+            } else {
+              this.queryParams[startField] = null;
+              this.queryParams[endField] = null;
+            }
           } else {
-            this.queryParams[startField] = null;
-            this.queryParams[endField] = null;
+            this.queryParams[key] = last;
           }
-        } else {
-          this.queryParams[key] = last;
         }
       });
       this.handleQuery();
@@ -235,6 +244,24 @@ export default {
         this.columnFilters[key] = [];
       });
     },
+    /** 清空列头筛选对应的查询参数(切换订单类型时调用,避免多选字符串残留导致类型转换异常) */
+    clearColumnFilterParams() {
+      const timeRangeMap = {
+        deliveryTime: ['deliveryStartTime', 'deliveryEndTime'],
+        followTime: ['followTimeStart', 'followTimeEnd']
+      };
+      Object.keys(this.columnFilters).forEach(key => {
+        if (timeRangeMap[key]) {
+          const [startField, endField] = timeRangeMap[key];
+          this.queryParams[startField] = null;
+          this.queryParams[endField] = null;
+        } else {
+          this.queryParams[key] = null;
+        }
+      });
+      this.deliveryTime = null;
+      this.followTime = null;
+    },
     /** 合计行:使用后端返回的整个数据集汇总(非当前页) */
     getSummaries({ columns }) {
       const sums = [];

+ 856 - 0
src/views/taskStatistics/callLog/callLogDetail.vue

@@ -0,0 +1,856 @@
+<template>
+  <div class="app-container">
+    <!-- 统计概览 -->
+    <el-row :gutter="24" class="baseInfo">
+      <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+        <el-card shadow="hover" class="stat-card">
+          <div slot="header" class="card-header">
+            <span>记录总数</span>
+          </div>
+          <div class="content" v-loading="loading">
+            <span class="card-number total">
+              <count-to v-if="!loading" :start-val="0" :end-val="summary.totalCount || 0" :duration="2000" />
+            </span>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+        <el-card shadow="hover" class="stat-card">
+          <div slot="header" class="card-header">
+            <span>接通量</span>
+          </div>
+          <div class="content" v-loading="loading">
+            <span class="card-number success">
+              <count-to v-if="!loading" :start-val="0" :end-val="summary.connectedCount || 0" :duration="2000" />
+            </span>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+        <el-card shadow="hover" class="stat-card">
+          <div slot="header" class="card-header">
+            <span>计费分钟合计</span>
+          </div>
+          <div class="content" v-loading="loading">
+            <span class="card-number billing">
+              <count-to v-if="!loading" :start-val="0" :end-val="summary.totalBillingMinute || 0" :duration="2000" />
+            </span>
+            <span v-if="!loading" class="billing-unit">分钟</span>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+        <el-card shadow="hover" class="stat-card">
+          <div slot="header" class="card-header">
+            <span>接通率</span>
+          </div>
+          <div class="content" v-loading="loading">
+            <span class="card-number rate">
+              <count-to v-if="!loading" :start-val="0" :end-val="summary.connectRate || 0" :duration="2000" />%
+            </span>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 筛选区域 -->
+    <el-form
+      ref="queryForm"
+      :model="queryParams"
+      :inline="true"
+      size="small"
+      label-width="100px"
+      class="detail-query-form"
+    >
+      <el-form-item label="任务" prop="roboticId">
+        <el-select
+          v-model="queryParams.roboticId"
+          filterable
+          remote
+          reserve-keyword
+          clearable
+          placeholder="请选择任务"
+          style="width: 280px"
+          :remote-method="searchRoboticTask"
+          :loading="roboticLoading"
+          @visible-change="handleRoboticDropdownVisible"
+          @change="handleTaskChange"
+        >
+          <el-option
+            v-for="item in roboticList"
+            :key="item.id"
+            :label="formatRoboticLabel(item)"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="手机号" prop="phone">
+        <el-input
+          v-model="queryParams.phone"
+          placeholder="请输入手机号"
+          clearable
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
+          <el-option label="执行中" :value="1" />
+          <el-option label="成功" :value="2" />
+          <el-option label="失败" :value="3" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否接通" prop="isConnected">
+        <el-select v-model="queryParams.isConnected" placeholder="请选择" clearable>
+          <el-option label="是" :value="1" />
+          <el-option label="否" :value="0" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="客户类型" prop="intention">
+        <el-select v-model="queryParams.intention" placeholder="请选择" clearable>
+          <el-option
+            v-for="dict in intentionOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="运行时间" prop="runDateRange">
+        <el-date-picker
+          v-model="runDateRange"
+          type="daterange"
+          value-format="yyyy-MM-dd"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          style="width: 240px"
+        />
+      </el-form-item>
+      <el-form-item label="通话时长(秒)">
+        <div class="call-time-range">
+          <el-input
+            v-model="queryParams.minCallTime"
+            placeholder="最小时长"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+          <span class="range-separator">至</span>
+          <el-input
+            v-model="queryParams.maxCallTime"
+            placeholder="最大时长"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+        </div>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+        <el-button
+          v-hasPermi="['company:callphoneDetail:list']"
+          type="primary"
+          icon="el-icon-refresh"
+          :loading="syncLoading"
+          @click="handleSyncAiCallData"
+        >同步AI外呼数据</el-button>
+        <el-button
+          v-hasPermi="['company:callphonelog:exportPhone']"
+          type="warning"
+          icon="el-icon-download"
+          :loading="exportLoading"
+          @click="handleExport"
+        >导出</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 表格 -->
+    <div v-hasPermi="['company:callphoneDetail:list']">
+      <el-table
+        border
+        v-loading="loading"
+        :data="list"
+        height="520"
+      >
+        <el-table-column label="ID" align="center" prop="logId" width="80" fixed="left" />
+        <el-table-column label="任务名称" align="center" prop="roboticName" min-width="150" show-overflow-tooltip />
+        <el-table-column label="运行时间" align="center" prop="runTime" width="160">
+          <template slot-scope="scope">
+            <span>{{ formatDateTime(scope.row.runTime) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" prop="status" width="80">
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.status === 1" type="warning" size="mini">执行中</el-tag>
+            <el-tag v-else-if="scope.row.status === 2" type="success" size="mini">成功</el-tag>
+            <el-tag v-else-if="scope.row.status === 3" type="danger" size="mini">失败</el-tag>
+            <span v-else>--</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="客户号码" align="center" prop="callerNum" width="155">
+          <template slot-scope="scope">
+            <span>{{ formatCallerNum(scope.row) }}</span>
+            <el-button
+              v-if="scope.row.callerNum || scope.row.customerId || scope.row.callerId"
+              v-hasPermi="['crm:customer:queryPhone']"
+              type="text"
+              size="mini"
+              icon="el-icon-view"
+              @click="handleQueryPhone(scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column label="客户类型" align="center" prop="intention" width="90">
+          <template slot-scope="scope">
+            <template v-for="dict in intentionOptions">
+              <el-tag
+                v-if="normalizeIntention(scope.row.intention) == dict.dictValue"
+                :key="dict.dictValue"
+                size="mini"
+              >{{ dict.dictLabel }}</el-tag>
+            </template>
+            <span v-if="!matchIntentionDict(scope.row.intention)">--</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="通话时长(秒)" align="center" prop="callTime" width="105">
+          <template slot-scope="scope">
+            <span>{{ scope.row.callTime != null ? scope.row.callTime : '--' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="计费分钟" align="center" prop="billingMinute" width="95">
+          <template slot-scope="scope">
+            <span v-if="scope.row.billingMinute != null">{{ scope.row.billingMinute }}分钟</span>
+            <span v-else>--</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="销售名称" align="center" prop="companyUserName" width="100" show-overflow-tooltip />
+        <el-table-column
+          v-if="checkPermi(['crm:customer:showCallInfo'])"
+          label="录音"
+          align="center"
+          prop="recordPath"
+          min-width="260"
+        >
+          <template slot-scope="scope">
+            <audio v-if="scope.row.recordPath" controls :src="handleRecordPath(scope.row.recordPath)" />
+            <span v-else>--</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="90" fixed="right">
+          <template slot-scope="scope">
+            <el-button
+              v-if="scope.row.contentList && checkPermi(['crm:customer:showCallInfo'])"
+              size="mini"
+              type="text"
+              @click="handleViewChat(scope.row)"
+            >查看对话</el-button>
+            <span v-else>--</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        :page.sync="queryParams.pageNum"
+        :limit.sync="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </div>
+
+    <!-- 对话记录弹窗 -->
+    <el-dialog
+      title="通话详情"
+      :visible.sync="chatDialogVisible"
+      width="720px"
+      append-to-body
+      custom-class="chat-dialog"
+      @close="handleChatClose"
+    >
+      <div v-if="chatList.length > 0" class="chat-summary">
+        <div class="chat-summary-item">
+          <span class="chat-summary-label">总条数:</span>
+          <span class="chat-summary-value">{{ chatList.length }}</span>
+        </div>
+        <div class="chat-summary-item">
+          <span class="chat-summary-label">机器人:</span>
+          <span class="chat-summary-value blue">{{ chatStats.agentCount }}</span>
+        </div>
+        <div class="chat-summary-item">
+          <span class="chat-summary-label">用户:</span>
+          <span class="chat-summary-value green">{{ chatStats.userCount }}</span>
+        </div>
+        <div class="chat-summary-item">
+          <span class="chat-summary-label">敏感词命中:</span>
+          <span class="chat-summary-value red">{{ chatStats.sensitiveCount }}</span>
+        </div>
+      </div>
+      <div v-if="chatList.length > 0" class="chat-container">
+        <div
+          v-for="(item, index) in chatList"
+          :key="index"
+          class="chat-item"
+          :class="item.role === 'agent' ? 'chat-item-right' : 'chat-item-left'"
+        >
+          <div class="chat-avatar">
+            <div
+              class="chat-avatar-circle"
+              :class="item.role === 'agent' ? 'avatar-agent' : 'avatar-user'"
+            >
+              <i :class="item.role === 'agent' ? 'el-icon-headset' : 'el-icon-user-solid'" />
+            </div>
+          </div>
+          <div class="chat-content-wrap">
+            <div class="chat-role-line">
+              <span class="chat-role">{{ item.role === 'agent' ? '机器人' : '用户' }}</span>
+              <span class="chat-index">#{{ index + 1 }}</span>
+            </div>
+            <div class="chat-bubble" v-html="item.content" />
+          </div>
+        </div>
+      </div>
+      <el-empty v-else description="暂无通话内容" />
+      <div slot="footer" class="dialog-footer chat-dialog-footer">
+        <span class="chat-footer-tip">标红内容为命中的敏感词</span>
+        <el-button @click="chatDialogVisible = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { parseTime } from '@/utils/common'
+import { checkPermi } from '@/utils/permission'
+import {
+  listCallphoneDetail,
+  getCallphoneDetailSummary,
+  exportDetailPhone,
+  queryCallphoneLogPhone,
+  syncAiCallData
+} from '@/api/company/callphone'
+import { listRoboticSelectOptions, getRobotic } from '@/api/company/companyVoiceRobotic'
+import { queryPhone } from '@/api/crm/customer'
+import CountTo from 'vue-count-to'
+
+export default {
+  name: 'CallLogDetail',
+  components: { CountTo },
+  data() {
+    return {
+      loading: false,
+      exportLoading: false,
+      syncLoading: false,
+      total: 0,
+      list: [],
+      roboticList: [],
+      roboticLoading: false,
+      roboticQueryParams: {
+        pageNum: 1,
+        pageSize: 20,
+        name: null
+      },
+      runDateRange: [],
+      intentionOptions: [],
+      chatDialogVisible: false,
+      chatList: [],
+      taskInfo: {
+        roboticId: null,
+        roboticName: ''
+      },
+      summary: {
+        totalCount: 0,
+        connectedCount: 0,
+        connectRate: 0,
+        totalBillingMinute: 0
+      },
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        roboticId: null,
+        phone: null,
+        status: null,
+        isConnected: null,
+        intention: null,
+        minCallTime: null,
+        maxCallTime: null,
+        beginRunTime: null,
+        endRunTime: null
+      }
+    }
+  },
+  computed: {
+    chatStats() {
+      const stats = { agentCount: 0, userCount: 0, sensitiveCount: 0 }
+      this.chatList.forEach(item => {
+        if (item.role === 'agent') stats.agentCount++
+        else stats.userCount++
+        const matches = (item.content || '').match(/<span[^>]*class="sensitive-word"[^>]*>/g)
+        if (matches) stats.sensitiveCount += matches.length
+      })
+      return stats
+    }
+  },
+  created() {
+    this.initFromRoute()
+    this.loadIntentionDict()
+    this.ensureSelectedRoboticInOptions().finally(() => {
+      this.getList()
+    })
+  },
+  methods: {
+    checkPermi,
+    formatRoboticLabel(item) {
+      if (!item) return ''
+      return item.name ? `${item.name}(${item.id})` : String(item.id)
+    },
+    normalizeIntention(intention) {
+      if (intention === null || intention === undefined || intention === '') {
+        return '0'
+      }
+      return String(intention)
+    },
+    matchIntentionDict(intention) {
+      if (!Array.isArray(this.intentionOptions) || this.intentionOptions.length === 0) {
+        return false
+      }
+      const value = this.normalizeIntention(intention)
+      return this.intentionOptions.some(dict => String(dict.dictValue) === value)
+    },
+    initFromRoute() {
+      const query = this.$route.query || {}
+      this.taskInfo.roboticId = query.roboticId ? Number(query.roboticId) : null
+      this.taskInfo.roboticName = query.roboticName || ''
+      this.queryParams.roboticId = this.taskInfo.roboticId
+    },
+    ensureSelectedRoboticInOptions() {
+      const roboticId = this.queryParams.roboticId
+      if (!roboticId) {
+        return Promise.resolve()
+      }
+      if (this.roboticList.some(item => item.id === roboticId)) {
+        return Promise.resolve()
+      }
+      if (this.taskInfo.roboticName) {
+        this.roboticList = [{ id: roboticId, name: this.taskInfo.roboticName }]
+        return Promise.resolve()
+      }
+      return getRobotic(roboticId).then(res => {
+        const data = res.data
+        if (data && data.id) {
+          this.roboticList = [{ id: data.id, name: data.name }]
+          this.taskInfo.roboticName = data.name
+        }
+      }).catch(() => {})
+    },
+    loadRoboticOptions(reset = false) {
+      if (reset) {
+        this.roboticQueryParams.pageNum = 1
+      }
+      this.roboticLoading = true
+      return listRoboticSelectOptions(this.roboticQueryParams).then(res => {
+        const rows = res.rows || []
+        if (reset || this.roboticQueryParams.pageNum === 1) {
+          const selectedId = this.queryParams.roboticId
+          const selected = selectedId
+            ? this.roboticList.find(item => item.id === selectedId) || rows.find(item => item.id === selectedId)
+            : null
+          this.roboticList = selected && !rows.some(item => item.id === selected.id)
+            ? [selected, ...rows]
+            : rows
+        } else {
+          const existIds = new Set(this.roboticList.map(item => item.id))
+          rows.forEach(item => {
+            if (!existIds.has(item.id)) {
+              this.roboticList.push(item)
+            }
+          })
+        }
+      }).catch(() => {
+        if (reset || this.roboticQueryParams.pageNum === 1) {
+          this.roboticList = []
+        }
+      }).finally(() => {
+        this.roboticLoading = false
+      })
+    },
+    searchRoboticTask(query) {
+      this.roboticQueryParams.name = query || null
+      this.loadRoboticOptions(true)
+    },
+    handleRoboticDropdownVisible(visible) {
+      if (visible && this.roboticList.length === 0) {
+        this.roboticQueryParams.name = null
+        this.loadRoboticOptions(true)
+      }
+    },
+    handleTaskChange(roboticId) {
+      const task = this.roboticList.find(item => item.id === roboticId)
+      this.taskInfo.roboticId = roboticId || null
+      this.taskInfo.roboticName = task ? task.name : ''
+      this.handleQuery()
+    },
+    loadIntentionDict() {
+      this.getDicts('customer_intention_level').then(response => {
+        if (response.data && response.data.length) {
+          this.intentionOptions = response.data
+        }
+      }).catch(() => {})
+    },
+    desensitizePhone(phone) {
+      if (!phone) return '--'
+      if (String(phone).includes('****')) return phone
+      if (phone.length < 7) return phone
+      return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4)
+    },
+    formatCallerNum(row) {
+      if (row.callerNum) {
+        return this.desensitizePhone(row.callerNum)
+      }
+      if (row.customerId || row.callerId) {
+        return '******'
+      }
+      return '--'
+    },
+    formatDateTime(time) {
+      if (!time) return '--'
+      if (typeof time === 'string') return time.replace('T', ' ').substring(0, 16)
+      return parseTime(time, '{y}-{m}-{d} {h}:{i}') || '--'
+    },
+    handleRecordPath(url) {
+      if (!url) return ''
+      if (url.startsWith('http')) {
+        return process.env.VUE_APP_BASE_API + '/common/proxy/recording?url=' + encodeURIComponent(url)
+      }
+      const fullUrl = 'http://129.28.164.235:8899/recordings/files?filename=' + url
+      return process.env.VUE_APP_BASE_API + '/common/proxy/recording?url=' + encodeURIComponent(fullUrl)
+    },
+    handleQueryPhone(row) {
+      const customerId = row.customerId
+      if (customerId) {
+        queryPhone(customerId).then(response => {
+          this.$alert(response.mobile, '手机号', { confirmButtonText: '确定' })
+        })
+        return
+      }
+      if (row.logId) {
+        queryCallphoneLogPhone(row.logId).then(response => {
+          this.$alert(response.mobile, '手机号', { confirmButtonText: '确定' })
+        })
+        return
+      }
+      this.$message.warning('无法查看手机号')
+    },
+    handleExport() {
+      this.$confirm('是否确认导出当前筛选条件下的外呼记录(含解密手机号)?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.exportLoading = true
+        return exportDetailPhone(this.getExportParams())
+      }).then(response => {
+        this.download(response.msg)
+      }).finally(() => {
+        this.exportLoading = false
+      }).catch(() => {})
+    },
+    handleSyncAiCallData() {
+      this.syncLoading = true
+      syncAiCallData().then(res => {
+        this.$message.success(res.msg || '正在同步中')
+      }).finally(() => {
+        this.syncLoading = false
+      })
+    },
+    parseCallTime(val) {
+      if (val === null || val === undefined || val === '') return null
+      const num = Number(val)
+      return isNaN(num) || num < 0 ? null : num
+    },
+    buildQueryParams() {
+      const params = { ...this.queryParams }
+      params.minCallTime = this.parseCallTime(params.minCallTime)
+      params.maxCallTime = this.parseCallTime(params.maxCallTime)
+      if (this.runDateRange && this.runDateRange.length === 2) {
+        params.beginRunTime = this.runDateRange[0]
+        params.endRunTime = this.runDateRange[1]
+      } else {
+        params.beginRunTime = null
+        params.endRunTime = null
+      }
+      return params
+    },
+    getExportParams() {
+      const params = this.buildQueryParams()
+      delete params.pageNum
+      delete params.pageSize
+      return params
+    },
+    getSummary() {
+      return getCallphoneDetailSummary(this.buildQueryParams()).then(res => {
+        const data = res.data || {}
+        this.summary = {
+          totalCount: data.totalCount || 0,
+          connectedCount: data.connectedCount || 0,
+          connectRate: data.connectRate != null ? data.connectRate : 0,
+          totalBillingMinute: data.totalBillingMinute || 0
+        }
+      }).catch(() => {
+        this.summary = { totalCount: 0, connectedCount: 0, connectRate: 0, totalBillingMinute: 0 }
+      })
+    },
+    getList() {
+      this.loading = true
+      const params = this.buildQueryParams()
+      listCallphoneDetail(params).then(listRes => {
+        this.list = listRes.rows || []
+        this.total = listRes.total || 0
+        if (this.list.length === 0 && this.total > 0 && this.queryParams.pageNum > 1) {
+          this.queryParams.pageNum = 1
+          this.getList()
+          return
+        }
+      }).catch(() => {
+        this.list = []
+        this.total = 0
+      }).finally(() => {
+        this.loading = false
+      })
+      this.getSummary()
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.runDateRange = []
+      this.resetForm('queryForm')
+      this.queryParams.minCallTime = null
+      this.queryParams.maxCallTime = null
+      this.queryParams.beginRunTime = null
+      this.queryParams.endRunTime = null
+      const query = this.$route.query || {}
+      this.queryParams.roboticId = query.roboticId ? Number(query.roboticId) : null
+      this.taskInfo.roboticId = this.queryParams.roboticId
+      this.taskInfo.roboticName = query.roboticName || ''
+      this.ensureSelectedRoboticInOptions()
+      this.handleQuery()
+    },
+    isSystemChatRole(role) {
+      return String(role || '').toLowerCase() === 'system'
+    },
+    normalizeChatRole(role) {
+      const r = String(role || '').toLowerCase()
+      if (['agent', 'robot', 'ai', 'bot', 'assistant'].includes(r)) return 'agent'
+      return 'user'
+    },
+    handleViewChat(row) {
+      let list = []
+      try {
+        const raw = row.contentList
+        list = typeof raw === 'string' ? JSON.parse(raw) : raw
+      } catch (e) {
+        list = []
+      }
+      this.chatList = (Array.isArray(list) ? list : [])
+        .filter(item => item && !this.isSystemChatRole(item.role) && String(item.content || '').trim())
+        .map(item => ({
+          ...item,
+          role: this.normalizeChatRole(item.role || item.speaker || item.from)
+        }))
+      this.chatDialogVisible = true
+    },
+    handleChatClose() {
+      this.chatList = []
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.baseInfo {
+  margin-bottom: 20px;
+}
+
+.stat-card {
+  border-radius: 10px;
+  transition: all 0.3s;
+}
+
+.stat-card:hover {
+  transform: translateY(-3px);
+}
+
+.card-header {
+  font-size: 14px;
+  color: #666;
+}
+
+.content {
+  height: 60px;
+  display: flex;
+  align-items: center;
+}
+
+.card-number {
+  font-size: 28px;
+  font-weight: bold;
+
+  &.total { color: #409eff; }
+  &.success { color: #67c23a; }
+  &.rate { color: #e6a23c; }
+  &.billing { color: #9b59b6; }
+}
+
+.billing-unit {
+  margin-left: 4px;
+  font-size: 14px;
+  color: #909399;
+  font-weight: normal;
+}
+
+.detail-query-form {
+  margin-bottom: 16px;
+  padding: 16px 16px 4px;
+  background: #fff;
+  border-radius: 4px;
+}
+
+.call-time-range {
+  display: inline-flex;
+  align-items: center;
+}
+
+.call-time-range .el-input {
+  width: 100px;
+}
+
+.range-separator {
+  margin: 0 6px;
+  color: #909399;
+}
+</style>
+
+<style lang="scss">
+.chat-dialog {
+  margin-top: 5vh !important;
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+
+  .el-dialog__body {
+    padding: 0;
+    background-color: #f5f7fa;
+    max-height: calc(90vh - 120px);
+    overflow-y: auto;
+  }
+
+  .chat-summary {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 20px;
+    padding: 12px 20px;
+    background: #fff;
+    border-bottom: 1px solid #ebeef5;
+    position: sticky;
+    top: 0;
+    z-index: 2;
+
+    .chat-summary-label { color: #909399; font-size: 13px; }
+    .chat-summary-value {
+      font-weight: 600;
+      &.blue { color: #409eff; }
+      &.green { color: #67c23a; }
+      &.red { color: #f56c6c; }
+    }
+  }
+
+  .chat-container {
+    padding: 16px 20px;
+  }
+
+  .chat-item {
+    display: flex;
+    margin-bottom: 14px;
+    align-items: flex-start;
+  }
+
+  .chat-item-left {
+    .chat-avatar { margin-right: 10px; }
+    .chat-bubble {
+      background: #fff;
+      border: 1px solid #ebeef5;
+      border-radius: 2px 8px 8px 8px;
+    }
+    .chat-role { color: #67c23a; }
+  }
+
+  .chat-item-right {
+    flex-direction: row-reverse;
+    .chat-avatar { margin-left: 10px; }
+    .chat-content-wrap { align-items: flex-end; }
+    .chat-role-line { flex-direction: row-reverse; }
+    .chat-bubble {
+      background: #ecf5ff;
+      border: 1px solid #d9ecff;
+      border-radius: 8px 2px 8px 8px;
+    }
+    .chat-role { color: #409eff; }
+  }
+
+  .chat-avatar-circle {
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+    font-size: 18px;
+
+    &.avatar-agent { background: #409eff; }
+    &.avatar-user { background: #67c23a; }
+  }
+
+  .chat-content-wrap {
+    display: flex;
+    flex-direction: column;
+    max-width: 70%;
+  }
+
+  .chat-role-line {
+    display: flex;
+    gap: 8px;
+    margin-bottom: 4px;
+    font-size: 12px;
+  }
+
+  .chat-index { color: #c0c4cc; }
+
+  .chat-bubble {
+    padding: 8px 12px;
+    font-size: 14px;
+    line-height: 1.6;
+    word-break: break-word;
+
+    .sensitive-word {
+      color: #f56c6c !important;
+      font-weight: 600;
+      background: rgba(245, 108, 108, 0.12);
+      padding: 0 3px;
+      border-radius: 2px;
+    }
+  }
+
+  .chat-dialog-footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    width: 100%;
+
+    .chat-footer-tip {
+      font-size: 12px;
+      color: #909399;
+    }
+  }
+}
+</style>

+ 697 - 28
src/views/taskStatistics/callLog/index.vue

@@ -45,7 +45,7 @@
             <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
                 <el-card shadow="hover" class="stat-card">
                     <div slot="header" class="card-header">
-                        <span>今日发送</span>
+                        <span>今日外呼</span>
                     </div>
 
                     <div class="content" v-loading="loadingCount">
@@ -81,6 +81,85 @@
                 </el-card>
             </el-col>
 
+            <!-- 总接通数 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>总接通数</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number total">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.connectionCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
+
+            <!-- 总未接通数 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>总未接通数</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number total">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.unansweredCallsCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
+
+            <!-- 今日接通 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>今日接通</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number today">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.todayConnectionCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
+
+            <!-- 今日未接通 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>今日未接通</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number today">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.todayUnansweredCallsCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
         </el-row>
 
         <el-form v-show="showSearch" ref="queryForm" :model="queryParams" :inline="true" label-width="96px">
@@ -128,10 +207,10 @@
             <!--      <el-table-column label="记录 ID" align="center" prop="logId" width="90" />-->
             <el-table-column label="任务 ID" align="center" prop="roboticId" width="150"/>
             <el-table-column label="任务名称" align="center" prop="roboticName" width="250"/>
-            <el-table-column label="发送数量" align="center" prop="totalRecordCount" width="150"/>
-            <el-table-column label="发送成功数量" align="center" prop="successCount" width="150"/>
-            <el-table-column label="发送失败数量" align="center" prop="failCount" width="150"/>
-            <el-table-column label="发送中数量" align="center" prop="runningCount" width="150"/>
+            <el-table-column label="数量" align="center" prop="totalRecordCount" width="150"/>
+            <el-table-column label="外呼成功数量" align="center" prop="successCount" width="150"/>
+            <el-table-column label="外呼失败数量" align="center" prop="failCount" width="150"/>
+            <el-table-column label="外呼中数量" align="center" prop="runningCount" width="150"/>
             <!--      <el-table-column label="销售名称" align="center" prop="companyUserName" width="180" />-->
             <!--      <el-table-column label="花费金额" align="center" prop="cost" />-->
             <!--      <el-table-column label="手机号" align="center" prop="phone" />-->
@@ -180,7 +259,7 @@
                 ref="detailQueryForm"
                 :inline="true"
                 size="small"
-                label-width="70px"
+                label-width="100px"
                 class="detail-query-form"
             >
                 <!--                <el-form-item label="调用时间">-->
@@ -204,6 +283,52 @@
                     />
                 </el-form-item>
 
+                <el-form-item label="是否接通">
+                    <el-select
+                        v-model="detailQueryParams.isConnected"
+                        placeholder="请选择"
+                        clearable
+                        @change="handleDetailQuery"
+                    >
+                        <el-option label="是" :value="1" />
+                        <el-option label="否" :value="0" />
+                    </el-select>
+                </el-form-item>
+
+                <el-form-item label="客户类型">
+                    <el-select
+                        v-model="detailQueryParams.intention"
+                        placeholder="请选择"
+                        clearable
+                        @change="handleDetailQuery"
+                    >
+                        <el-option
+                            v-for="dict in intentionOptions"
+                            :key="dict.dictValue"
+                            :label="dict.dictLabel"
+                            :value="dict.dictValue"
+                        />
+                    </el-select>
+                </el-form-item>
+
+                <el-form-item label="通话时长(秒)">
+                    <div class="call-time-range">
+                        <el-input
+                            v-model="detailQueryParams.minCallTime"
+                            placeholder="最小时长"
+                            clearable
+                            @keyup.enter.native="handleDetailQuery"
+                        />
+                        <span class="range-separator">至</span>
+                        <el-input
+                            v-model="detailQueryParams.maxCallTime"
+                            placeholder="最大时长"
+                            clearable
+                            @keyup.enter.native="handleDetailQuery"
+                        />
+                    </div>
+                </el-form-item>
+
                 <el-form-item>
                     <el-button type="primary" icon="el-icon-search" @click="handleDetailQuery">
                         查询
@@ -211,6 +336,15 @@
                     <el-button icon="el-icon-refresh" @click="resetDetailQuery">
                         重置
                     </el-button>
+                    <el-button
+                        v-hasPermi="['company:callphonelog:exportPhone']"
+                        type="warning"
+                        icon="el-icon-download"
+                        :loading="detailExportLoading"
+                        @click="handleDetailExport"
+                    >
+                        导出
+                    </el-button>
                 </el-form-item>
             </el-form>
 
@@ -220,34 +354,78 @@
                 border
                 style="width: 100%"
             >
-                <el-table-column label="日志ID" align="center" prop="logId" width="90" />
-                <el-table-column label="任务名称" align="center" prop="roboticName" min-width="160" show-overflow-tooltip />
-                <el-table-column label="记录调用时间" align="center" prop="runTime" width="160" />
-                <el-table-column label="执行状态" align="center" prop="status" width="100">
+                <el-table-column label="日志ID" align="center" prop="logId" width="70" />
+                <el-table-column label="任务名称" align="center" prop="roboticName" min-width="80" show-overflow-tooltip />
+                <el-table-column label="运行时间" align="center" prop="runTime" width="160" />
+                <el-table-column label="外呼状态" align="center" prop="status" width="80">
                     <template slot-scope="scope">
                         <el-tag v-if="scope.row.status === 1" type="warning">执行中</el-tag>
-                        <el-tag v-else-if="scope.row.status === 2" type="success">执行成功</el-tag>
-                        <el-tag v-else-if="scope.row.status === 3" type="danger">执行失败</el-tag>
+                        <el-tag v-else-if="scope.row.status === 2" type="success">成功</el-tag>
+                        <el-tag v-else-if="scope.row.status === 3" type="danger">失败</el-tag>
                         <span v-else>--</span>
                     </template>
                 </el-table-column>
-                <el-table-column label="客户号码" align="center" prop="callerNum" width="130" />
-                <el-table-column label="话术号码" align="center" prop="calleeNum" width="130" />
-                <el-table-column label="客户类型" align="center" prop="intention" width="120" />
-                <el-table-column label="通话时长(秒)" align="center" prop="callTime" width="120" />
-                <el-table-column label="花费金额" align="center" prop="cost" width="100" />
-                <el-table-column label="公司名称" align="center" prop="companyName" min-width="140" show-overflow-tooltip />
-                <el-table-column label="销售名称" align="center" prop="companyUserName" min-width="120" show-overflow-tooltip />
-                <el-table-column label="录音地址" align="center" min-width="120">
+                <el-table-column label="是否接通" align="center" prop="isConnected" width="80">
                     <template slot-scope="scope">
-                        <el-link
-                            v-if="scope.row.recordPath"
-                            :href="scope.row.recordPath"
-                            type="primary"
-                            target="_blank"
+                        <el-tag v-if="scope.row.callTime > 0" type="success">是</el-tag>
+                        <el-tag v-else type="danger">否</el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column label="客户号码" align="center" prop="callerNum" width="180">
+                    <template slot-scope="scope">
+                        <span>{{ scope.row.callerNum || '--' }}</span>
+                        <el-button
+                            v-if="scope.row.callerNum || scope.row.customerId"
+                            v-hasPermi="['crm:customer:queryPhone']"
+                            type="text"
+                            size="mini"
+                            icon="el-icon-view"
+                            @click="handleQueryPhone(scope.row)"
+                        >查看</el-button>
+                    </template>
+                </el-table-column>
+                <!-- <el-table-column label="话术号码" align="center" prop="calleeNum" width="120" /> -->
+                <el-table-column label="客户类型" align="center" prop="intention" width="80">
+                    <template slot-scope="scope">
+                        <el-tag v-if="scope.row.intention" v-for="dict in intentionOptions" :key="dict.dictValue" v-show="scope.row.intention == dict.dictValue" size="mini">{{ dict.dictLabel }}</el-tag>
+                        <span v-else>--</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="时长(秒)" align="center" prop="callTime" width="80" />
+                <!-- <el-table-column label="金额" align="center" prop="cost" width="70" /> -->
+                <el-table-column label="公司名称" align="center" prop="companyName" min-width="100" show-overflow-tooltip />
+                <el-table-column label="销售名称" align="center" prop="companyUserName" min-width="80" show-overflow-tooltip />
+                <el-table-column label="录音" align="center" show-overflow-tooltip width="350">
+                    <template slot-scope="scope">
+                        <audio v-if="scope.row.recordPath" controls :src="handleRecordPath(scope.row.recordPath)"></audio>
+                        <span v-else>--</span>
+                    </template>
+                </el-table-column>
+              <el-table-column label="敏感词检测" align="center" prop="isWarning" width="100">
+                <template slot-scope="scope">
+                  <el-tag
+                    v-if="scope.row.isWarning === 1"
+                    type="danger"
+                    class="warning-tag-clickable"
+                    @click="handleViewChat(scope.row)"
+                  >
+                    <el-tooltip content="点击查看聊天记录" placement="top">
+                      <span><i class="el-icon-warning" />异常</span>
+                    </el-tooltip>
+                  </el-tag>
+                  <el-tag v-else type="success">正常</el-tag>
+                </template>
+              </el-table-column>
+                <el-table-column label="对话内容" align="center" width="90">
+                    <template slot-scope="scope">
+                        <el-button
+                            v-if="scope.row.contentList"
+                            size="mini"
+                            type="text"
+                            @click="handleViewChat(scope.row)"
                         >
-                            查看录音
-                        </el-link>
+                            查看对话
+                        </el-button>
                         <span v-else>--</span>
                     </template>
                 </el-table-column>
@@ -267,6 +445,66 @@
             </div>
         </el-dialog>
 
+        <!-- 聊天记录对话弹窗 -->
+        <el-dialog
+            title="通话详情"
+            :visible.sync="chatDialogVisible"
+            width="720px"
+            append-to-body
+            custom-class="chat-dialog"
+            @close="handleChatClose"
+        >
+            <!-- 统计条 -->
+            <div class="chat-summary" v-if="chatList.length > 0">
+                <div class="chat-summary-item">
+                    <span class="chat-summary-label">总条数:</span>
+                    <span class="chat-summary-value">{{ chatList.length }}</span>
+                </div>
+                <div class="chat-summary-item">
+                    <span class="chat-summary-label">机器人:</span>
+                    <span class="chat-summary-value blue">{{ chatStats.agentCount }}</span>
+                </div>
+                <div class="chat-summary-item">
+                    <span class="chat-summary-label">用户:</span>
+                    <span class="chat-summary-value green">{{ chatStats.userCount }}</span>
+                </div>
+                <div class="chat-summary-item">
+                    <span class="chat-summary-label">敏感词命中(条数):</span>
+                    <span class="chat-summary-value red">{{ chatStats.sensitiveCount }}</span>
+                </div>
+            </div>
+
+            <div class="chat-container" v-if="chatList.length > 0">
+                <div
+                    v-for="(item, index) in chatList"
+                    :key="index"
+                    class="chat-item"
+                    :class="item.role === 'agent' ? 'chat-item-right' : 'chat-item-left'"
+                >
+                    <div class="chat-avatar">
+                        <div
+                            class="chat-avatar-circle"
+                            :class="item.role === 'agent' ? 'avatar-agent' : 'avatar-user'"
+                        >
+                            <i :class="item.role === 'agent' ? 'el-icon-headset' : 'el-icon-user-solid'" />
+                        </div>
+                    </div>
+                    <div class="chat-content-wrap">
+                        <div class="chat-role-line">
+                            <span class="chat-role">{{ item.role === 'agent' ? '机器人' : '用户' }}</span>
+                            <span class="chat-index">#{{ index + 1 }}</span>
+                        </div>
+                        <div class="chat-bubble" v-html="item.content"></div>
+                    </div>
+                </div>
+            </div>
+            <el-empty v-else description="暂无通话内容" />
+            <div slot="footer" class="dialog-footer">
+                <span class="chat-footer-tip">标红内容为命中的敏感词</span>
+                <el-button @click="chatDialogVisible = false">关 闭</el-button>
+            </div>
+        </el-dialog>
+
         <pagination
             v-show="total > 0"
             :total="total"
@@ -278,7 +516,8 @@
 </template>
 
 <script>
-import {listCallphone, listCallPhoneByRoboticId, delSendmsg, getCallPhoneLogCount, exportCallphone, groupList} from '@/api/company/callphone'
+import {listCallphone, listCallPhoneByRoboticId, delSendmsg, getCallPhoneLogCount, exportCallphone, exportDetailPhone, queryCallphoneLogPhone, groupList} from '@/api/company/callphone'
+import { queryPhone } from '@/api/crm/customer'
 import {listAll as roboticListAll} from '@/api/company/companyVoiceRobotic'
 import {listAll as accountListAll} from '@/api/company/companyAccount'
 import CountTo from 'vue-count-to'
@@ -299,10 +538,14 @@ export default {
             successRecordCount: 0,
             detailDialogVisible: false,
             detailLoading: false,
+            detailExportLoading: false,
             detailList: [],
             detailTitle: "查看详情",
             detailTimeRange: [],
             detailTotal: 0,
+            intentionOptions: [],
+            chatDialogVisible: false,
+            chatList: [],
             queryParams: {
                 pageNum: 1,
                 pageSize: 10,
@@ -317,6 +560,10 @@ export default {
                 pageSize: 10,
                 roboticId: null,
                 phone: null,
+                isConnected: null,
+                intention: null,
+                minCallTime: null,
+                maxCallTime: null,
                 beginRunTime: null,
                 endRunTime: null
             },
@@ -327,6 +574,23 @@ export default {
         this.initOptions()
         this.handleQuery()
         this.getCount()
+        this.getDicts('customer_intention_level').then(response => {
+            this.intentionOptions = response.data
+        })
+    },
+    computed: {
+        chatStats() {
+            const stats = { agentCount: 0, userCount: 0, sensitiveCount: 0 }
+            const list = this.chatList || []
+            list.forEach(item => {
+                if (item.role === 'agent') stats.agentCount++
+                else stats.userCount++
+                const content = item.content || ''
+                const matches = content.match(/<span[^>]*class="sensitive-word"[^>]*>/g)
+                if (matches) stats.sensitiveCount += matches.length
+            })
+            return stats
+        }
     },
     methods: {
         initOptions() {
@@ -425,6 +689,10 @@ export default {
             this.detailQueryParams.callerId = row.callerId;
             this.detailQueryParams.roboticId = row.roboticId;
             this.detailQueryParams.phone = null;
+            this.detailQueryParams.isConnected = null;
+            this.detailQueryParams.intention = null;
+            this.detailQueryParams.minCallTime = null;
+            this.detailQueryParams.maxCallTime = null;
             this.detailQueryParams.beginRunTime = null;
             this.detailQueryParams.endRunTime = null;
             this.detailTimeRange = [];
@@ -436,6 +704,7 @@ export default {
         getDetailList() {
             this.detailLoading = true;
             listCallPhoneByRoboticId(this.detailQueryParams).then(response => {
+
                 this.detailList = response.rows || [];
                 this.detailTotal = response.total || 0;
                 this.detailLoading = false;
@@ -454,6 +723,9 @@ export default {
                 this.detailQueryParams.endRunTime = null;
             }
 
+            this.detailQueryParams.minCallTime = this.parseCallTime(this.detailQueryParams.minCallTime);
+            this.detailQueryParams.maxCallTime = this.parseCallTime(this.detailQueryParams.maxCallTime);
+
             this.getDetailList();
         },
         resetDetailQuery() {
@@ -461,10 +733,57 @@ export default {
             this.detailQueryParams.pageNum = 1;
             this.detailQueryParams.pageSize = 10;
             this.detailQueryParams.phone = null;
+            this.detailQueryParams.isConnected = null;
+            this.detailQueryParams.intention = null;
+            this.detailQueryParams.minCallTime = null;
+            this.detailQueryParams.maxCallTime = null;
             this.detailQueryParams.beginRunTime = null;
             this.detailQueryParams.endRunTime = null;
             this.getDetailList();
         },
+        getDetailExportParams() {
+            const params = JSON.parse(JSON.stringify(this.detailQueryParams));
+            delete params.pageNum;
+            delete params.pageSize;
+            params.minCallTime = this.parseCallTime(params.minCallTime);
+            params.maxCallTime = this.parseCallTime(params.maxCallTime);
+            return params;
+        },
+        /** 查看解密手机号(与 CRM 客户详情一致) */
+        handleQueryPhone(row) {
+            const customerId = row.customerId;
+            if (customerId) {
+                queryPhone(customerId).then(response => {
+                    this.$alert(response.mobile, '手机号', {
+                        confirmButtonText: '确定'
+                    });
+                });
+                return;
+            }
+            if (row.logId) {
+                queryCallphoneLogPhone(row.logId).then(response => {
+                    this.$alert(response.mobile, '手机号', {
+                        confirmButtonText: '确定'
+                    });
+                });
+                return;
+            }
+            this.$message.warning('无法查看手机号');
+        },
+        handleDetailExport() {
+            this.$confirm('是否确认导出当前筛选条件下的外呼记录(含解密手机号)?', '警告', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                this.detailExportLoading = true;
+                return exportDetailPhone(this.getDetailExportParams());
+            }).then((response) => {
+                this.download(response.msg);
+            }).finally(() => {
+                this.detailExportLoading = false;
+            }).catch(() => {});
+        },
         /** 获取统计数据 */
         getCount() {
             this.loadingCount = true;
@@ -485,10 +804,93 @@ export default {
                 callerId: null,
                 roboticId: null,
                 phone: null,
+                isConnected: null,
+                intention: null,
+                minCallTime: null,
+                maxCallTime: null,
                 beginRunTime: null,
                 endRunTime: null
             };
         },
+        handleRecordPath(url) {
+            if (!url) return '';
+            let fullUrl = '';
+            if (url.startsWith('http')) {
+                fullUrl = url;
+            } else {
+                fullUrl = 'http://129.28.164.235:8899/recordings/files?filename=' + url;
+            }
+            return process.env.VUE_APP_BASE_API + '/common/proxy/recording?url=' + encodeURIComponent(fullUrl);
+        },
+        parseCallTime(val) {
+            if (val === null || val === undefined || val === '') return null;
+            var num = Number(val);
+            if (isNaN(num) || num < 0) return null;
+            return num;
+        },
+        handleShowContent(row) {
+            // 兑现为统一到聊天详情弹窗(保留名称以防外部引用)
+            this.handleViewChat(row);
+        },
+        parseContentList(content) {
+            if (!content) return [];
+            try {
+                const parsed = typeof content === 'string' ? JSON.parse(content) : content;
+                if (!Array.isArray(parsed)) return [];
+                return parsed.filter(item => {
+                    if (!item) return false;
+                    if (this.isSystemChatRole(item.role)) return false;
+                    const text = item.content || '';
+                    if (!String(text).trim()) return false;
+                    return true;
+                }).map(item => ({
+                    ...item,
+                    role: this.normalizeChatRole(item.role || item.speaker || item.from)
+                }));
+            } catch (e) {
+                console.error('解析 contentList 失败', e);
+                return [];
+            }
+        },
+        isSystemChatRole(role) {
+            return String(role || '').toLowerCase() === 'system';
+        },
+        normalizeChatRole(role) {
+            const r = String(role || '').toLowerCase();
+            if (r === 'agent' || r === 'robot' || r === 'ai' || r === 'bot' || r === 'assistant') {
+                return 'agent';
+            }
+            if (r === 'user' || r === 'customer' || r === 'client' || r === 'human') {
+                return 'user';
+            }
+            return 'user';
+        },
+        /** 查看聊天记录 */
+        handleViewChat(row) {
+            const raw = row.contentList;
+            let list = [];
+            if (raw) {
+                try {
+                    list = typeof raw === 'string' ? JSON.parse(raw) : raw;
+                } catch (e) {
+                    list = [];
+                }
+            }
+            const filtered = (Array.isArray(list) ? list : []).filter(item => {
+                if (!item) return false;
+                if (this.isSystemChatRole(item.role)) return false;
+                const text = item.content || '';
+                return !!String(text).trim();
+            }).map(item => ({
+                ...item,
+                role: this.normalizeChatRole(item.role || item.speaker || item.from)
+            }));
+            this.chatList = filtered;
+            this.chatDialogVisible = true;
+        },
+        handleChatClose() {
+            this.chatList = [];
+        },
     }
 }
 </script>
@@ -528,6 +930,21 @@ export default {
     box-shadow: 0 0 8px rgba(64, 158, 255, 0.3);
     background-color: #ffffff;
 }
+
+.call-time-range {
+    display: inline-flex;
+    align-items: center;
+}
+
+.call-time-range .el-input {
+    width: 100px;
+}
+
+.range-separator {
+    margin: 0 6px;
+    color: #909399;
+}
+
 /* 卡片整体 */
 .stat-card {
     border-radius: 10px;
@@ -573,4 +990,256 @@ export default {
 .card-number.today-success {
     color: #F56C6C; /* 红色 */
 }
+
+.content-dialog-wrapper {
+    padding: 0 10px;
+}
+
+.content-empty {
+    text-align: center;
+    color: #909399;
+    padding: 40px 0;
+}
+
+.chat-container {
+    max-height: 600px;
+    overflow-y: auto;
+    padding: 16px;
+    background: #f5f7fa;
+    border-radius: 8px;
+}
+
+.chat-item {
+    display: flex;
+    margin-bottom: 14px;
+}
+
+.chat-left {
+    justify-content: flex-start;
+}
+
+.chat-right {
+    justify-content: flex-end;
+}
+
+.chat-bubble-wrapper {
+    max-width: 75%;
+}
+
+.chat-role {
+    font-size: 12px;
+    color: #909399;
+    margin-bottom: 4px;
+}
+
+.chat-left .chat-role {
+    text-align: left;
+}
+
+.chat-right .chat-role {
+    text-align: right;
+}
+
+.chat-bubble {
+    padding: 10px 14px;
+    border-radius: 12px;
+    font-size: 14px;
+    line-height: 1.6;
+    word-break: break-word;
+    white-space: pre-wrap;
+}
+
+.chat-left .chat-bubble {
+    background: #fff;
+    border: 1px solid #ebeef5;
+    color: #303133;
+}
+
+.chat-right .chat-bubble {
+    background: #409eff;
+    color: #fff;
+}
+
+/* 警告标签可点击 */
+.warning-tag-clickable {
+    cursor: pointer;
+    transition: all 0.2s;
+}
+
+.warning-tag-clickable:hover {
+    opacity: 0.85;
+    transform: scale(1.05);
+}
+</style>
+
+<style lang="scss">
+/* 聊天弹窗(全局样式,以便 v-html 渲染的敏感词样式生效) */
+.chat-dialog {
+    /* 锁定弹窗不超出视口,避免外层 wrapper 产生滚动条 */
+    margin-top: 5vh !important;
+    margin-bottom: 5vh !important;
+    max-height: 90vh;
+    display: flex;
+    flex-direction: column;
+
+    .el-dialog__header {
+        flex-shrink: 0;
+    }
+
+    .el-dialog__body {
+        padding: 0;
+        background-color: #f5f7fa;
+        max-height: calc(90vh - 54px - 60px);
+        overflow-y: auto;
+        flex: 1;
+    }
+
+    .el-dialog__footer {
+        flex-shrink: 0;
+
+        .dialog-footer {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+        }
+
+        .chat-footer-tip {
+            font-size: 12px;
+            color: #909399;
+        }
+    }
+
+    /* 统计条 */
+    .chat-summary {
+        display: flex;
+        align-items: center;
+        flex-wrap: wrap;
+        gap: 24px;
+        padding: 12px 20px;
+        background-color: #fff;
+        border-bottom: 1px solid #ebeef5;
+        position: sticky;
+        top: 0;
+        z-index: 2;
+
+        .chat-summary-item {
+            font-size: 13px;
+            color: #606266;
+        }
+        .chat-summary-label {
+            color: #909399;
+        }
+        .chat-summary-value {
+            font-weight: 600;
+            color: #303133;
+
+            &.blue { color: #409EFF; }
+            &.green { color: #67C23A; }
+            &.red { color: #f56c6c; }
+        }
+    }
+
+    .chat-container {
+        display: flex;
+        flex-direction: column;
+        padding: 16px 20px 8px;
+    }
+
+    .chat-item {
+        display: flex;
+        margin-bottom: 14px;
+        align-items: flex-start;
+    }
+
+    /* 用户在左 */
+    .chat-item-left {
+        justify-content: flex-start;
+
+        .chat-avatar { margin-right: 10px; }
+        .chat-content-wrap { align-items: flex-start; }
+        .chat-role-line { justify-content: flex-start; }
+        .chat-bubble {
+            background-color: #ffffff;
+            color: #303133;
+            border: 1px solid #ebeef5;
+            border-radius: 2px 8px 8px 8px;
+        }
+        .chat-role { color: #67C23A; }
+    }
+
+    /* 销售在右 */
+    .chat-item-right {
+        flex-direction: row-reverse;
+        justify-content: flex-start;
+
+        .chat-avatar { margin-left: 10px; }
+        .chat-content-wrap { align-items: flex-end; }
+        .chat-role-line {
+            justify-content: flex-end;
+            flex-direction: row-reverse;
+        }
+        .chat-bubble {
+            background-color: #ecf5ff;
+            color: #303133;
+            border: 1px solid #d9ecff;
+            border-radius: 8px 2px 8px 8px;
+        }
+        .chat-role { color: #409EFF; }
+    }
+
+    /* 头像 */
+    .chat-avatar-circle {
+        width: 36px;
+        height: 36px;
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: #fff;
+        font-size: 18px;
+        flex-shrink: 0;
+
+        &.avatar-agent { background-color: #409EFF; }
+        &.avatar-user { background-color: #67C23A; }
+    }
+
+    .chat-content-wrap {
+        display: flex;
+        flex-direction: column;
+        max-width: 70%;
+    }
+
+    .chat-role-line {
+        display: flex;
+        align-items: center;
+        margin-bottom: 4px;
+        gap: 8px;
+    }
+
+    .chat-role {
+        font-size: 12px;
+        font-weight: 500;
+    }
+
+    .chat-index {
+        font-size: 12px;
+        color: #c0c4cc;
+    }
+
+    .chat-bubble {
+        padding: 8px 12px;
+        font-size: 14px;
+        line-height: 1.6;
+        word-break: break-word;
+        max-width: 100%;
+
+        .sensitive-word {
+            color: #f56c6c !important;
+            font-weight: 600;
+            background-color: rgba(245, 108, 108, 0.12);
+            padding: 0 3px;
+            border-radius: 2px;
+        }
+    }
+}
 </style>

+ 2 - 2
src/views/taskStatistics/wxClient/index.vue

@@ -123,8 +123,8 @@
     <el-table v-loading="loading" :data="companyClientList">
       <el-table-column label="记录 ID" align="center" prop="logId" width="90" />
       <el-table-column label="任务 ID" align="center" prop="roboticId" width="90" />
-      <el-table-column label="企微账号" align="center" prop="wxAccountName" width="180" />
-      <el-table-column label="微信昵称" align="center" prop="wxClientName" />
+      <el-table-column label="企微员工昵称" align="center" prop="wxAccountName" width="180" />
+      <el-table-column label="客户昵称" align="center" prop="wxClientName" />
       <el-table-column label="手机号" align="center" prop="phone" />
 <!--      <el-table-column label="话术" align="center" prop="dialogName" />-->
       <el-table-column label="加微类型" align="center" width="100">

+ 188 - 0
src/views/wx/sopUserLogsWx/detail.vue

@@ -0,0 +1,188 @@
+<template>
+  <div class="app-container">
+    <!-- SOP信息卡片 -->
+    <div style="margin-bottom: 10px">
+      <el-card>
+        <span class="custom-style" style="display: block; margin-bottom: 10px">SOP编号:{{ queryParams.sopId }}</span>
+        <span class="custom-style" style="display: block;">营期编号:{{ queryParams.sopUserId }}</span>
+      </el-card>
+    </div>
+
+    <!-- 搜索表单 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="联系人ID" prop="wxContactId">
+        <el-input
+          v-model="queryParams.wxContactId"
+          placeholder="请输入联系人ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <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="grade">
+        <el-select v-model="queryParams.grade" placeholder="请选择客户评级" clearable size="small">
+          <el-option label="无" :value="0" />
+          <el-option label="A" :value="1" />
+          <el-option label="B" :value="2" />
+          <el-option label="C" :value="3" />
+          <el-option label="D" :value="4" />
+          <el-option label="E" :value="5" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option label="正常" :value="0" />
+          <el-option label="禁用" :value="1" />
+        </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>
+
+    <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+
+    <!-- 客户列表表格 -->
+    <el-table border v-loading="loading" :data="customerList">
+      <el-table-column type="index" label="序号" width="60" align="center" />
+      <el-table-column label="用户ID" align="center" prop="customerId" width="180" />
+      <el-table-column label="总完课天数" align="center" prop="finishCout" width="110" />
+      <el-table-column label="最近完课时间" align="center" prop="finishTime" width="160">
+        <template slot-scope="scope">
+          <span v-if="scope.row.finishTime">{{ parseTime(scope.row.finishTime) }}</span>
+          <span v-else style="color: #999;">-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="连续完课天数" align="center" prop="finishCourseDays" width="110" />
+      <el-table-column label="客户标签" align="center" prop="tagNames" min-width="150" show-overflow-tooltip />
+      <el-table-column label="客户评级" align="center" prop="grade" width="90">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.grade === 0" type="info">无</el-tag>
+          <el-tag v-else-if="scope.row.grade === 1" type="success">A</el-tag>
+          <el-tag v-else-if="scope.row.grade === 2" type="primary">B</el-tag>
+          <el-tag v-else-if="scope.row.grade === 3" type="warning">C</el-tag>
+          <el-tag v-else-if="scope.row.grade === 4" type="danger">D</el-tag>
+          <el-tag v-else-if="scope.row.grade === 5" type="danger">E</el-tag>
+          <el-tag v-else type="info">-</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="7天未看课" align="center" prop="isDaysNotStudy" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.isDaysNotStudy === 0" type="success">否</el-tag>
+          <el-tag v-else-if="scope.row.isDaysNotStudy === 1" type="warning">是</el-tag>
+          <el-tag v-else type="info">-</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status" width="80">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 0" type="success">正常</el-tag>
+          <el-tag v-else-if="scope.row.status === 1" type="warning">禁用</el-tag>
+          <el-tag v-else type="info">未知</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+
+    </el-table>
+
+    <!-- 分页组件 -->
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import { getSopUserLogsDetail } from "@/api/wx/sopUserLogsWx";
+
+export default {
+  name: "SopUserLogsDetailWx",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 客户列表数据
+      customerList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        sopId: null,
+        sopUserId: null,
+        wxContactId: null,
+        fsUserId: null,
+        grade: null,
+        status: null
+      }
+    };
+  },
+  created() {
+    // 从路由参数获取sopUserId和sopId
+    const sopUserId = this.$route.params.sopUserId || this.$route.query.sopUserId;
+    const sopId = this.$route.params.sopId || this.$route.query.sopId;
+
+    if (sopUserId) {
+      this.queryParams.sopUserId = sopUserId;
+    }
+    if (sopId) {
+      this.queryParams.sopId = sopId;
+    }
+
+    // 加载数据
+    this.getList();
+  },
+  methods: {
+    /** 查询客户列表 */
+    getList() {
+      this.loading = true;
+      getSopUserLogsDetail(this.queryParams.sopUserId, this.queryParams).then(response => {
+        console.log('个微SOP营期详情接口返回数据:', response);
+        this.customerList = response.rows || [];
+        this.total = response.total || 0;
+        this.loading = false;
+      }).catch(error => {
+        console.error('查询个微SOP营期详情失败:', error);
+        this.loading = false;
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    }
+  }
+};
+</script>
+
+<style scoped>
+.custom-style {
+  font-size: 14px;
+  color: #606266;
+}
+</style>

+ 351 - 0
src/views/wx/sopUserLogsWx/sopUserLogsScheduleWx.vue

@@ -0,0 +1,351 @@
+<template>
+  <div class="app-container">
+    <!-- SOP信息卡片 -->
+    <div style="margin-bottom: 10px">
+      <el-card>
+        <span class="custom-style" style="display: block; margin-bottom: 10px">SOP规则名称:{{ sopName }}</span>
+        <span class="custom-style" style="display: block; margin-bottom: 10px">SOP规则编号:{{ 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="执行账号" prop="accountId">
+        <el-input
+          v-model="queryParams.accountId"
+          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-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option label="正常" :value="0" />
+          <el-option label="暂停" :value="1" />
+        </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="danger"
+            icon="el-icon-delete"
+            size="mini"
+            :disabled="multiple"
+            @click="handleDelete"
+            v-hasPermi="['wx:sopUserLogsWx:remove']"
+          >批量删除营期</el-button>
+        </el-tooltip>
+      </el-col>
+      <el-col :span="1.5">
+        <el-tooltip class="item" effect="dark" content="修改选择的营期时间" placement="top">
+          <el-button
+            type="primary"
+            icon="el-icon-edit"
+            size="mini"
+            :disabled="multiple"
+            @click="handleUpdateTime"
+          >批量修改营期时间</el-button>
+        </el-tooltip>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 提示信息 -->
+    <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="sopUserLogsWxList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="营期编号" align="center" prop="id" width="100" />
+      <el-table-column label="SOP任务ID" align="center" prop="sopId" width="120" />
+      <el-table-column label="执行账号ID" align="center" prop="accountId" width="150" />
+      <el-table-column label="执行账号名称" align="center" prop="accountName" width="150">
+        <template slot-scope="scope">
+          <span v-if="scope.row.accountName">{{ scope.row.accountName }}</span>
+          <span v-else style="color: #999;">未匹配</span>
+        </template>
+      </el-table-column>
+      <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" width="100" />
+      <el-table-column label="状态" align="center" prop="status" width="120">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.status === 0" type="success">正常</el-tag>
+          <el-tag v-else-if="scope.row.status === 1" type="warning">暂停</el-tag>
+          <el-tag v-else type="info">未知</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-view"
+            @click="handleDetail(scope.row)"
+          >详情</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['wx:sopUserLogsWx:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页组件 -->
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 批量修改营期时间对话框 -->
+    <el-dialog title="批量修改营期时间" :visible.sync="updateTimeOpen" width="500px" append-to-body>
+      <el-form ref="updateTimeForm" :model="updateTimeForm" :rules="updateTimeRules" label-width="120px">
+        <el-form-item label="选择营期时间" prop="newStartTime">
+          <el-date-picker
+            clearable
+            size="small"
+            v-model="updateTimeForm.newStartTime"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择新的营期时间"
+            style="width: 100%;"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitUpdateTime">确 定</el-button>
+        <el-button @click="updateTimeOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listSopUserLogsWx,
+  getSopUserLogsWx,
+  delSopUserLogsWx,
+  exportSopUserLogsWx,
+  updateLogDate
+} from "@/api/wx/sopUserLogsWx";
+
+export default {
+  name: "SopUserLogsScheduleWx",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 营期表格数据
+      sopUserLogsWxList: [],
+      // SOP名称
+      sopName: '',
+      // 模板ID
+      tempId: '',
+      // 批量修改营期时间对话框
+      updateTimeOpen: false,
+      // 批量修改营期时间表单
+      updateTimeForm: {
+        ids: [],
+        newStartTime: null
+      },
+      // 批量修改营期时间表单校验
+      updateTimeRules: {
+        newStartTime: [
+          { required: true, message: "请选择新的营期时间", trigger: "change" }
+        ]
+      },
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        sopId: null,
+        accountId: null,
+        startTime: null,
+        status: null
+      }
+    };
+  },
+  created() {
+    // 从路由参数获取SOP ID
+    const sopId = this.$route.params.id;
+    if (sopId) {
+      this.queryParams.sopId = sopId;
+      this.sopName = this.$route.query.name || '';
+      this.tempId = this.$route.query.tempId || '';
+    }
+
+    // 加载数据
+    this.getList();
+  },
+  methods: {
+    /** 查询营期列表 */
+    getList() {
+      this.loading = true;
+      listSopUserLogsWx(this.queryParams).then(response => {
+        console.log('个微SOP营期接口返回数据:', response);
+        this.sopUserLogsWxList = response.rows || [];
+        this.total = response.total || 0;
+        this.loading = false;
+      }).catch(error => {
+        console.error('查询个微SOP营期失败:', error);
+        this.loading = false;
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 多选框选中数据 */
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    /** 打开批量修改营期时间对话框 */
+    handleUpdateTime() {
+      this.updateTimeForm = {
+        ids: this.ids,
+        newStartTime: null
+      };
+      this.updateTimeOpen = true;
+      this.$nextTick(() => {
+        this.$refs["updateTimeForm"].clearValidate();
+      });
+    },
+    /** 提交批量修改营期时间 */
+    submitUpdateTime() {
+      this.$refs["updateTimeForm"].validate(valid => {
+        if (valid) {
+          const loading = this.$loading({
+            lock: true,
+            text: '正在修改中,请稍候...',
+            spinner: 'el-icon-loading',
+            background: 'rgba(0, 0, 0, 0.7)'
+          });
+
+          updateLogDate({
+            ids: this.updateTimeForm.ids.join(','),
+            newStartTime: this.updateTimeForm.newStartTime
+          }).then(response => {
+            console.log('批量修改个微SOP营期时间返回数据:', response);
+            this.msgSuccess("修改成功");
+            this.updateTimeOpen = false;
+            this.getList();
+          }).catch(error => {
+            console.error('批量修改个微SOP营期时间失败:', error);
+            this.msgError("修改失败");
+          }).finally(() => {
+            loading.close();
+          });
+        }
+      });
+    },
+    /** 跳转到营期详情页面 */
+    handleDetail(row) {
+      this.$router.push({
+        path: '/wxSop/sopUserLogsWx/detail',
+        query: {
+          sopUserId: row.id,
+          sopId: row.sopId
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids.join(',');
+      this.$confirm('删除营期后将不再给该营期发送消息,删除后不可恢复。是否确认删除营期编号为"' + ids + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return delSopUserLogsWx(ids);
+      }).then(response => {
+        console.log('删除个微SOP营期返回数据:', response);
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(error => {
+        if (error !== 'cancel') {
+          console.error('删除个微SOP营期失败:', error);
+        }
+      });
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      this.$confirm('是否确认导出所有个微SOP营期数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportSopUserLogsWx(this.queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {
+        this.exportLoading = false;
+      });
+    }
+  }
+};
+</script>
+
+<style scoped>
+.custom-style {
+  font-size: 14px;
+  color: #606266;
+}
+</style>

+ 916 - 0
src/views/wx/wxSop/index.vue

@@ -0,0 +1,916 @@
+<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="filterType">
+        <el-select
+          v-model="queryParams.filterType"
+          placeholder="请选择筛选方式"
+          clearable
+          size="small"
+        >
+          <el-option
+            v-for="item in filterTypeOptions"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="item.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="模板" prop="tempId">
+        <el-select
+          v-model="queryParams.tempId"
+          placeholder="请选择模板"
+          clearable
+          filterable
+          size="small"
+          style="width: 200px;"
+        >
+          <el-option
+            v-for="item in tempList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="固定营期" prop="isFixed">
+        <el-select
+          v-model="queryParams.isFixed"
+          placeholder="请选择固定营期"
+          clearable
+          size="small"
+        >
+          <el-option
+            v-for="item in isFixedOptions"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="item.dictValue"
+          />
+        </el-select>
+      </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>
+        <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="['wx:wxSop:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-video-play"
+          size="mini"
+          :disabled="multiple"
+          @click="handleBatchExecute"
+          v-hasPermi="['qw:sop:batchExecuteWx']"
+        >批量执行个微SOP</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-s-promotion"
+          size="mini"
+          :disabled="multiple"
+          @click="handleSendMsg"
+        >SOP营期一键群发</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="wxSopList" @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="status" width="120">
+        <template slot-scope="scope">
+          <el-tag type="info" v-if="scope.row.status === 0">停止</el-tag>
+          <el-tag type="success" v-else-if="scope.row.status === 1">启用</el-tag>
+          <el-tag type="warning" v-else-if="scope.row.status === 2">执行中</el-tag>
+          <el-tag v-else>未知</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="名称" align="center" prop="name" />
+      <el-table-column label="筛选方式" align="center" prop="filterType">
+        <template slot-scope="scope">
+          <dict-tag :options="filterTypeOptions" :value="scope.row.filterType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="模板" align="center" prop="tempId">
+        <template slot-scope="scope">
+          <span
+            v-for="item in tempList"
+            :key="item.id"
+            v-if="String(item.id) === String(scope.row.tempId)"
+          >
+            {{ item.name }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否固定营期" align="center" prop="isFixed">
+        <template slot-scope="scope">
+          <dict-tag :options="isFixedOptions" :value="scope.row.isFixed" />
+        </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="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="remark" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="280">
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.status === 2 || scope.row.status === 0"
+            size="mini"
+            type="text"
+            icon="el-icon-tickets"
+            @click="handleViewSchedule(scope.row)"
+            v-hasPermi="['wx:wxSop:list']"
+          >营期</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-view"
+            @click="handleViewDetail(scope.row)"
+            v-hasPermi="['wx:wxSop:list']"
+          >执行详情</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['wx:wxSop:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['wx:wxSop:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改个微SOP对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="70%" append-to-body class="wx-sop-dialog">
+      <div class="wx-sop-dialog-content">
+        <el-form ref="form" :model="form" :rules="rules" label-width="130px" class="wx-sop-form">
+          <!-- 基本信息 -->
+          <div class="form-section">
+            <div class="section-title">
+              <i class="el-icon-document"></i>
+              <span>基本信息</span>
+            </div>
+            <el-form-item label="规则名称" prop="name">
+              <el-input v-model="form.name" placeholder="请输入规则名称" clearable maxlength="50" show-word-limit />
+            </el-form-item>
+            <el-form-item label="模板" prop="tempId">
+              <el-select
+                v-model="form.tempId"
+                placeholder="请选择模板"
+                filterable
+                clearable
+                style="width: 100%;"
+                v-loading="tempListLoading"
+                @focus="loadTempList"
+              >
+                <el-option
+                  v-for="item in tempList"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+              <Tip title="选择想要发送的模板规则" />
+            </el-form-item>
+          </div>
+
+          <!-- 筛选规则 -->
+          <div class="form-section">
+            <div class="section-title">
+              <i class="el-icon-s-operation"></i>
+              <span>筛选规则</span>
+            </div>
+            <el-form-item label="筛选方式" prop="filterType">
+              <el-radio-group v-model="form.filterType">
+                <el-radio :label="'0'">按标签</el-radio>
+                <el-radio :label="'1'">按群聊</el-radio>
+              </el-radio-group>
+              <Tip :title="'标签:按标签筛选客户进入SOP;群聊:按群聊筛选进入SOP'" />
+            </el-form-item>
+            <template v-if="form.filterType === '0'">
+              <el-form-item label="选择的标签" prop="selectTags">
+                <el-select
+                  v-model="selectTagsList"
+                  multiple
+                  filterable
+                  clearable
+                  placeholder="请选择进入SOP的标签"
+                  style="width: 100%;"
+                  @change="onSelectTagsChange"
+                >
+                  <el-option
+                    v-for="item in tagsOptions"
+                    :key="item.dictValue"
+                    :label="item.dictLabel"
+                    :value="item.dictValue"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-form-item label="排除的标签" prop="excludeTags">
+                <el-select
+                  v-model="excludeTagsList"
+                  multiple
+                  filterable
+                  clearable
+                  placeholder="请选择不想进入SOP的标签"
+                  style="width: 100%;"
+                  @change="onExcludeTagsChange"
+                >
+                  <el-option
+                    v-for="item in tagsOptions"
+                    :key="item.dictValue"
+                    :label="item.dictLabel"
+                    :value="item.dictValue"
+                  />
+                </el-select>
+              </el-form-item>
+            </template>
+            <el-form-item label="执行账号" prop="accountIds">
+              <el-button
+                type="primary"
+                plain
+                icon="el-icon-user"
+                size="small"
+                @click="openQwUserSelect"
+              >
+                选择账号
+                <el-tag v-if="selectedQwUsers && selectedQwUsers.length" type="success" size="mini" style="margin-left: 6px;">
+                  {{ selectedQwUsers.length }}
+                </el-tag>
+              </el-button>
+              <div v-if="selectedQwUsers && selectedQwUsers.length" class="selected-tags">
+                <el-tag
+                  v-for="item in selectedQwUsers"
+                  :key="item.id"
+                  size="mini"
+                  closable
+                  @close="removeQwUser(item)"
+                >
+                  {{ item.wxNickName || item.wxNo || item.companyUserName }}
+                </el-tag>
+              </div>
+              <Tip title="从企微账号列表中选择用于执行个微SOP的账号" />
+            </el-form-item>
+          </div>
+
+          <!-- 时间与营期 -->
+          <div class="form-section">
+            <div class="section-title">
+              <i class="el-icon-time"></i>
+              <span>时间与营期</span>
+            </div>
+            <el-row :gutter="24">
+              <el-col :span="12">
+                <el-form-item label="开始时间" prop="startTime">
+                  <el-date-picker
+                    clearable
+                    size="small"
+                    v-model="form.startTime"
+                    type="date"
+                    value-format="yyyy-MM-dd"
+                    placeholder="选择开始时间"
+                    style="width: 100%;"
+                  />
+                  <Tip title="SOP开始发送时间" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="过期时间" prop="expiryTime">
+                  <div class="expiry-time-wrap">
+                    <el-input-number
+                      v-model="form.expiryTime"
+                      :min="1"
+                      :max="100"
+                      controls-position="right"
+                      style="flex: 1;"
+                    />
+                    <span class="unit-text">小时</span>
+                  </div>
+                  <Tip title="待发送消息超过此时间未发送将自动作废" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-form-item label="固定营期" prop="isFixed">
+              <el-radio-group v-model="form.isFixed">
+                <el-radio :label="'0'">否</el-radio>
+                <el-radio :label="'1'">是</el-radio>
+              </el-radio-group>
+              <Tip title="固定营期:不管什么时候进入SOP的客户,营期时间都为任务开始时间" />
+            </el-form-item>
+          </div>
+
+          <!-- 其他 -->
+          <div class="form-section">
+            <div class="section-title">
+              <i class="el-icon-edit-outline"></i>
+              <span>其他</span>
+            </div>
+            <el-form-item label="备注" prop="remark">
+              <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注(选填)" maxlength="200" show-word-limit />
+            </el-form-item>
+          </div>
+        </el-form>
+      </div>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="cancel">取 消</el-button>
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+      </div>
+
+      <qw-user-select @success="selectQwUserFun" ref="qwUserSelect" />
+    </el-dialog>
+
+    <!-- 一键群发弹窗 -->
+    <el-dialog title="一键批量群发" :visible.sync="sendMsgOpen" width="600px" append-to-body>
+      <el-alert
+        title="此功能用于给 选中的 SOP营期 内【所有的】客户 发送 消息"
+        type="error"
+        :closable="false"
+        show-icon
+        style="margin-bottom: 20px;"
+      />
+
+      <el-form label-width="80px">
+        <el-form-item label="规则">
+          <div style="border: 1px solid #DCDFE6; border-radius: 4px; padding: 15px;">
+            <div style="margin-bottom: 10px;">
+              <span style="margin-right: 10px;">内容类别</span>
+              <el-radio :value="'1'" :label="'1'" disabled>文本</el-radio>
+            </div>
+            <el-form-item label="内容" label-width="60px" style="margin-bottom: 0;">
+              <el-input
+                v-model="sendMsgForm.content"
+                type="textarea"
+                :rows="4"
+                placeholder="内容"
+                maxlength="500"
+                show-word-limit
+              />
+              <div style="margin-top: 8px;">
+                <el-link type="primary" :underline="false" @click="addPlaceholder('#销售称呼#')">添加#销售称呼#</el-link>
+                <span style="margin: 0 10px;"></span>
+                <el-link type="primary" :underline="false" @click="addPlaceholder('#客户称呼#')">添加#客户称呼#</el-link>
+              </div>
+            </el-form-item>
+          </div>
+        </el-form-item>
+
+        <el-form-item label="发送时间">
+          <el-time-picker
+            v-model="sendMsgForm.sendTime"
+            format="HH:mm"
+            value-format="HH:mm"
+            placeholder="时间"
+            size="small"
+            style="width: 120px;"
+          />
+          <span style="margin-left: 10px; color: #909399; font-size: 13px;">不填时,默认为系统当前时间(立即发送)</span>
+        </el-form-item>
+      </el-form>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitSendMsg">确 定</el-button>
+        <el-button @click="sendMsgOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 执行详情抽屉 -->
+    <el-drawer
+      :title="'执行详情 - ' + (detailRow ? detailRow.name : '')"
+      :visible.sync="detailDrawerVisible"
+      direction="rtl"
+      size="75%"
+      destroy-on-close
+    >
+      <sop-logs-list
+        v-if="detailDrawerVisible && detailRow"
+        :row-detail-from="detailRow"
+      />
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { listWxSop, getWxSop, delWxSop, addWxSop, updateWxSop, exportWxSop, updateWxStatus, sendWxSopMsg } from "@/api/wx/wxSop";
+import { listSopTemp } from "@/api/qw/sopTemp";
+import Tip from "@/components/Tip";
+import QwUserSelect from "@/views/components/QwUserSelect.vue";
+import SopLogsList from "./sopLogsList.vue";
+
+export default {
+  name: "WxSop",
+  components: { Tip, QwUserSelect, SopLogsList },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 模板下拉数据
+      tempList: [],
+      tempListLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 个微SOP表格数据
+      wxSopList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 选择的企微账号
+      selectedQwUsers: [],
+      // 标签库(与线索客户打标签共用 crm_customer_tag)
+      tagsOptions: [],
+      selectTagsList: [],
+      excludeTagsList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        filterType: null,
+        selectTags: null,
+        excludeTags: null,
+        tempId: null,
+        isFixed: null,
+        expiryTime: null,
+        startTime: null,
+      },
+      // 本地字典(筛选方式、是否固定营期)
+      filterTypeOptions: [
+        { dictValue: "0", dictLabel: "按标签筛选" },
+        { dictValue: "1", dictLabel: "按群聊筛选" },
+      ],
+      isFixedOptions: [
+        { dictValue: "0", dictLabel: "否" },
+        { dictValue: "1", dictLabel: "是" },
+      ],
+      // 执行详情抽屉
+      detailDrawerVisible: false,
+      detailRow: null,
+      // 一键群发
+      sendMsgOpen: false,
+      sendMsgForm: {
+        content: '',
+        sendTime: null
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        name: [{ required: true, message: "请输入规则名称", trigger: "blur" }],
+        tempId: [{ required: true, message: "请选择模板", trigger: "change" }]
+      }
+    };
+  },
+  created() {
+    this.getList();
+    this.loadTempList();
+    this.getDicts("crm_customer_tag").then((response) => {
+      this.tagsOptions = response.data || [];
+    });
+  },
+  methods: {
+    loadTempList() {
+      this.tempListLoading = true;
+      listSopTemp()
+        .then(response => {
+          this.tempList = response.rows || [];
+        })
+        .finally(() => {
+          this.tempListLoading = false;
+        });
+    },
+    onSelectTagsChange(val) {
+      this.form.selectTags = Array.isArray(val) ? val.join(",") : "";
+    },
+    onExcludeTagsChange(val) {
+      this.form.excludeTags = Array.isArray(val) ? val.join(",") : "";
+    },
+    syncTagsFromForm() {
+      this.selectTagsList = this.form.selectTags ? String(this.form.selectTags).split(",").filter(Boolean) : [];
+      this.excludeTagsList = this.form.excludeTags ? String(this.form.excludeTags).split(",").filter(Boolean) : [];
+    },
+    /** 查询个微SOP列表 */
+    getList() {
+      this.loading = true;
+      listWxSop(this.queryParams).then(response => {
+        this.wxSopList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        filterType: "0",
+        selectTags: null,
+        excludeTags: null,
+        tempId: null,
+        tempName: null,
+        isFixed: "0",
+        accountIds: null,
+        expiryTime: 24,
+        startTime: null,
+        createTime: null,
+        createBy: null,
+        updateTime: null,
+        updateBy: null,
+        remark: null
+      };
+      this.selectTagsList = [];
+      this.excludeTagsList = [];
+      this.resetForm("form");
+    },
+    // 打开企微账号选择
+    openQwUserSelect() {
+      this.$nextTick(() => {
+        this.$refs.qwUserSelect && this.$refs.qwUserSelect.setRows(this.selectedQwUsers);
+      });
+    },
+    // 选择企微账号回调(数据源与外呼任务分配账号一致)
+    selectQwUserFun(e) {
+      this.selectedQwUsers = e.rows || [];
+      const ids = e.ids || [];
+      this.form.accountIds = ids.join(",");
+      this.$forceUpdate();
+    },
+    removeQwUser(item) {
+      this.selectedQwUsers = this.selectedQwUsers.filter((u) => u.id !== item.id);
+      this.form.accountIds = this.selectedQwUsers.map((u) => u.id).join(",");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加个微SOP";
+      this.loadTempList();
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getWxSop(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改个微SOP";
+        // 兼容后端返回 number,确保 radio 正常回显
+        this.form.filterType =
+          this.form.filterType === null || typeof this.form.filterType === "undefined"
+            ? "0"
+            : String(this.form.filterType);
+        this.form.isFixed =
+          this.form.isFixed === null || typeof this.form.isFixed === "undefined"
+            ? "0"
+            : String(this.form.isFixed);
+        this.syncTagsFromForm();
+        // 回显执行账号
+        this.selectedQwUsers = response.data.selectedQwUsers || [];
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          const payload = this.buildSubmitPayload();
+          if (this.form.id != null) {
+            updateWxSop(payload).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addWxSop(payload).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 构建提交数据,确保与后端 WxSop 字段一致 */
+    buildSubmitPayload() {
+      const f = this.form;
+      return {
+        id: f.id,
+        name: f.name,
+        filterType: f.filterType != null ? parseInt(f.filterType, 10) : 0,
+        selectTags: f.selectTags || null,
+        excludeTags: f.excludeTags || null,
+        tempId: f.tempId != null ? String(f.tempId) : null,
+        isFixed: f.isFixed != null ? parseInt(f.isFixed, 10) : 0,
+        expiryTime: f.expiryTime != null ? parseInt(f.expiryTime, 10) : 24,
+        startTime: f.startTime || null,
+        remark: f.remark || null,
+        accountIds: f.accountIds || null
+      };
+    },
+    /** 查看营期按钮操作 */
+    handleViewSchedule(row) {
+      this.$router.push({
+        path: '/wxSop/sopUserLogsWx/sopUserLogsScheduleWx/' + row.id,
+        query: {
+          name: row.name,
+          tempId: row.tempId
+        }
+      });
+    },
+    /** 查看执行详情按钮操作 */
+    handleViewDetail(row) {
+      this.detailRow = row;
+      this.detailDrawerVisible = true;
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除个微SOP编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delWxSop(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 批量执行个微SOP */
+    handleBatchExecute() {
+      if (this.ids.length === 0) {
+        this.$message.warning("请至少选择一条记录");
+        return;
+      }
+
+      // 过滤出启用状态的SOP
+      const enabledSops = this.wxSopList.filter(item =>
+        this.ids.includes(item.id) && item.status === 1
+      );
+
+      if (enabledSops.length === 0) {
+        this.$message.warning("所选SOP中没有启用状态的记录,只能执行启用状态的SOP");
+        return;
+      }
+
+      const enabledIds = enabledSops.map(item => item.id);
+      const notEnabledCount = this.ids.length - enabledIds.length;
+
+      let confirmMsg = `是否确认批量执行所选的${enabledIds.length}条个微SOP?`;
+      if (notEnabledCount > 0) {
+        confirmMsg += `\n注意:已自动过滤${notEnabledCount}条非启用状态的记录`;
+      }
+
+      this.$confirm(confirmMsg, "批量执行确认", {
+        confirmButtonText: "确定执行",
+        cancelButtonText: "取消",
+        type: "warning",
+        distinguishCancelAndClose: true
+      }).then(() => {
+        this.loading = true;
+        return updateWxStatus(enabledIds.join(","));
+      }).then(response => {
+        this.loading = false;
+
+        console.log('批量执行个微SOP接口返回数据:', response);
+
+        // 解析后端返回的统计信息
+        if (response && response.msg) {
+          const successCount = response.data?.successCount || enabledIds.length;
+          const failCount = response.data?.failCount || 0;
+
+          let msg = `批量执行完成!`;
+          msg += `\n成功:${successCount}条`;
+          if (failCount > 0) {
+            msg += `\n失败:${failCount}条`;
+          }
+
+          this.msgSuccess(msg);
+        } else {
+          this.msgSuccess(`已成功执行${enabledIds.length}条个微SOP`);
+        }
+
+        // 刷新列表
+        this.getList();
+      }).catch(error => {
+        this.loading = false;
+        if (error !== 'cancel' && error !== 'close') {
+          console.error('批量执行个微SOP失败:', error);
+          this.msgError("批量执行失败,请稍后重试");
+        }
+      });
+    },
+    /** 一键群发按钮操作 */
+    handleSendMsg() {
+      if (this.ids.length === 0) {
+        this.$message.warning("请至少选择一条SOP记录");
+        return;
+      }
+      this.sendMsgForm.content = '';
+      this.sendMsgForm.sendTime = null;
+      this.sendMsgOpen = true;
+    },
+    /** 添加占位符 */
+    addPlaceholder(text) {
+      this.sendMsgForm.content += text;
+    },
+    /** 提交一键群发 */
+    submitSendMsg() {
+      if (!this.sendMsgForm.content || !this.sendMsgForm.content.trim()) {
+        this.$message.warning("请输入发送内容");
+        return;
+      }
+
+      this.$confirm('确认对选中的SOP营期内所有客户发送消息吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        const param = {
+          sopIds: this.ids,
+          setting: JSON.stringify([{ contentType: '1', value: this.sendMsgForm.content }]),
+          sendTime: this.sendMsgForm.sendTime || ''
+        };
+        sendWxSopMsg(param).then(response => {
+          this.msgSuccess("一键群发成功");
+          this.sendMsgOpen = false;
+        });
+      }).catch(() => {});
+    },
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有个微SOP数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportWxSop(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.wx-sop-dialog-content {
+  padding: 8px 16px 24px;
+  max-height: 70vh;
+  overflow-y: auto;
+}
+
+.wx-sop-form {
+  .form-section {
+    background: #fafbfc;
+    border-radius: 10px;
+    padding: 24px 28px;
+    margin-bottom: 24px;
+    border: 1px solid #ebeef5;
+  }
+
+  .form-section:last-of-type {
+    margin-bottom: 0;
+  }
+
+  .section-title {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    font-size: 15px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 20px;
+    padding-bottom: 14px;
+    border-bottom: 1px solid #e4e7ed;
+  }
+
+  .section-title i {
+    font-size: 18px;
+    color: #409eff;
+  }
+
+  ::v-deep .el-form-item {
+    margin-bottom: 22px;
+  }
+
+  ::v-deep .el-form-item:last-child {
+    margin-bottom: 0;
+  }
+
+  .expiry-time-wrap {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    width: 100%;
+  }
+
+  .unit-text {
+    color: #909399;
+    font-size: 13px;
+    flex-shrink: 0;
+  }
+
+  .selected-tags {
+    margin-top: 12px;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+
+  .selected-tags .el-tag {
+    margin: 0;
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 16px;
+  padding: 16px 0 8px;
+}
+</style>

+ 404 - 0
src/views/wx/wxSop/sopLogsList.vue

@@ -0,0 +1,404 @@
+<template>
+    <div class="app-container">
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="120px">
+            <el-form-item label="个微账号昵称" prop="accountName">
+                <el-input
+                    v-model="queryParams.accountName"
+                    placeholder="请输入个微账号昵称"
+                    clearable
+                    size="small"
+                    @keyup.enter.native="handleQuery"
+                />
+            </el-form-item>
+            <el-form-item label="客户昵称" prop="wxContactName">
+                <el-input
+                    v-model="queryParams.wxContactName"
+                    placeholder="请输入客户昵称"
+                    clearable
+                    size="small"
+                    @keyup.enter.native="handleQuery"
+                />
+            </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 sendStatusOptions"
+                        :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="type">
+                <el-select v-model="queryParams.type" placeholder="请选择消息类型" clearable size="small">
+                    <el-option label="个人" :value="0"/>
+                    <el-option label="群" :value="1"/>
+                </el-select>
+            </el-form-item>
+            <el-form-item label="预计发送时间" prop="scheduleTime">
+                <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>
+                <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="['wx:wxSopLogs:export']"
+                >导出
+                </el-button>
+            </el-col>
+            <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+
+        <el-table border v-loading="loading" :data="wxSopLogsList">
+            <el-table-column label="编号" align="center" prop="id" width="80"/>
+            <el-table-column label="个微账号昵称" align="center" prop="accountName"/>
+            <el-table-column label="客户昵称" align="center" prop="wxContactName"/>
+            <el-table-column label="客户标签" align="center" prop="tagNames" show-overflow-tooltip>
+                <template slot-scope="scope">
+                    <span v-if="scope.row.tagNames">{{ scope.row.tagNames }}</span>
+                    <span v-else style="color: #909399;">-</span>
+                </template>
+            </el-table-column>
+            <el-table-column label="发送状态" align="center" prop="sendStatus" width="100">
+                <template slot-scope="scope">
+                    <el-tag type="info" v-if="scope.row.sendStatus === 0">待发送</el-tag>
+                    <el-tag type="success" v-else-if="scope.row.sendStatus === 1">发送成功</el-tag>
+                    <el-tag type="danger" v-else-if="scope.row.sendStatus === 2">发送失败</el-tag>
+                    <el-tag type="warning" v-else-if="scope.row.sendStatus === 3">消息作废</el-tag>
+                    <el-tag v-else>未知</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column label="发送类型" align="center" prop="sendType" width="100">
+                <template slot-scope="scope">
+                    <dict-tag :options="sysQwSopType" :value="scope.row.sendType"/>
+                </template>
+            </el-table-column>
+            <el-table-column label="生成类型" align="center" prop="generateType" width="100">
+                <template slot-scope="scope">
+                    <el-tag type="success" v-if="scope.row.generateType === 0">自动</el-tag>
+                    <el-tag type="primary" v-else-if="scope.row.generateType === 1">手动</el-tag>
+                    <el-tag v-else>未知</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column label="生成时间" align="center" prop="createTime" width="165"/>
+            <el-table-column label="预计发送时间" align="center" prop="sendTime" width="165">
+                <template slot-scope="scope">
+                    <span v-if="scope.row.sendTime">{{ scope.row.sendTime }}</span>
+                    <span v-else style="color: #909399;">-</span>
+                </template>
+            </el-table-column>
+            <el-table-column label="实际发送时间" align="center" prop="realSendTime" width="165">
+                <template slot-scope="scope">
+                    <span v-if="scope.row.realSendTime">{{ scope.row.realSendTime }}</span>
+                    <span v-else style="color: #909399;">-</span>
+                </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" width="100">
+                <template slot-scope="scope">
+                    <el-button
+                        v-if="scope.row.contentJson"
+                        type="text"
+                        size="mini"
+                        @click="showContentDialog(scope.row.contentJson)"
+                    >查看详情
+                    </el-button>
+                    <span v-else style="color: #909399;">-</span>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <!-- 消息详情弹窗 -->
+        <el-dialog title="发送消息详情" :visible.sync="contentDialogVisible" width="800px" append-to-body>
+            <div v-if="contentDialogData && contentDialogData.length > 0" class="content-detail">
+                <div v-for="(item, index) in contentDialogData" :key="index" class="content-item">
+                    <div class="content-type-tag">
+                        <el-tag size="small" :type="getContentTypeTag(item.contentType).type">
+                            {{ getContentTypeTag(item.contentType).label }}
+                        </el-tag>
+                        <el-tag
+                            v-if="item.sendStatus !== undefined && item.sendStatus !== null"
+                            size="small"
+                            :type="item.sendStatus === 1 ? 'success' : item.sendStatus === 2 ? 'danger' : 'warning'"
+                            style="margin-left: 8px;"
+                        >
+                            {{ item.sendStatus === 0 ? '待发送' : item.sendStatus === 1 ? '发送成功' : '发送失败' }}
+                        </el-tag>
+                    </div>
+                    <div v-if="item.sendStatus === 2 && item.sendRemarks" style="color: #F56C6C; font-size: 13px; margin-bottom: 8px;">
+                        <i class="el-icon-warning-outline"></i> {{ item.sendRemarks }}
+                    </div>
+                    <div class="content-body">
+                        <!-- 文本内容 -->
+                        <div v-if="item.contentType === '1' || item.contentType === 1" class="text-content">
+                            {{ item.value }}
+                        </div>
+                        <!-- 图片 -->
+                        <div v-else-if="item.contentType === '2' || item.contentType === 2">
+                            <el-image :src="item.imgUrl || item.value" style="max-width: 200px; max-height: 200px;"
+                                      fit="contain"
+                            />
+                        </div>
+                        <!-- 其他类型显示JSON -->
+                        <div v-else class="text-content">
+                            <pre style="white-space: pre-wrap; word-break: break-all; margin: 0;"
+                            >{{ JSON.stringify(item, null, 2) }}</pre>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div v-else style="text-align: center; color: #909399; padding: 20px;">暂无消息内容</div>
+        </el-dialog>
+
+        <pagination
+            v-show="total>0"
+            :total="total"
+            :page.sync="queryParams.pageNum"
+            :limit.sync="queryParams.pageSize"
+            @pagination="getList"
+        />
+    </div>
+</template>
+
+<script>
+import { listWxSopLogsCVO, exportWxSopLogsCVO } from '@/api/wx/wxSopLogs'
+
+export default {
+    name: 'WxSopLogsList',
+    props: {
+        rowDetailFrom: {
+            type: Object,
+            default: () => ({})
+        }
+    },
+    watch: {
+        rowDetailFrom: {
+            handler(newVal) {
+                // 当formData变化时重新查询
+                this.getList(newVal)
+            },
+            deep: true
+        }
+    },
+    data() {
+        return {
+            // 时间选择
+            scheduleTime: null,
+            // 遮罩层
+            loading: true,
+            // 导出遮罩层
+            exportLoading: false,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 个微SOP执行详情表格数据
+            wxSopLogsList: [],
+            // 发送状态字典
+            sendStatusOptions: [
+                { dictValue: '0', dictLabel: '待发送', listClass: 'default' },
+                { dictValue: '1', dictLabel: '发送成功', listClass: 'success' },
+                { dictValue: '2', dictLabel: '发送失败', listClass: 'danger' },
+                { dictValue: '3', dictLabel: '消息作废', listClass: 'warning' }
+            ],
+            // 企微SOP发送类型
+            sysQwSopType: [],
+            // 消息详情弹窗
+            contentDialogVisible: false,
+            contentDialogData: [],
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                sopId: null,
+                accountName: null,
+                wxContactName: null,
+                sendStatus: null,
+                sendType: null,
+                type: null,
+                scheduleStartTime: null,
+                scheduleEndTime: null
+            }
+        }
+    },
+    created() {
+        this.getList(this.rowDetailFrom)
+
+        // 发送消息类型
+        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.loading = true
+
+            listWxSopLogsCVO(this.queryParams).then(response => {
+                console.log('个微SOP执行详情接口返回数据:', response)
+                this.wxSopLogsList = response.rows
+                this.total = response.total
+                this.loading = false
+            }).catch(error => {
+                console.error('查询个微SOP执行详情失败:', error)
+                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
+
+            const options = {
+                timeZone: 'Asia/Shanghai',
+                year: 'numeric',
+                month: '2-digit',
+                day: '2-digit',
+                hour: '2-digit',
+                minute: '2-digit',
+                second: '2-digit',
+                hour12: false
+            }
+
+            const formattedDate = new Intl.DateTimeFormat('zh-CN', options).format(new Date(date))
+            const [datePart, timePart] = formattedDate.replace(',', '').split(' ')
+            return `${datePart} ${timePart}`
+        },
+
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1
+            this.getList(this.rowDetailFrom)
+        },
+
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm')
+            this.queryParams.scheduleStartTime = null
+            this.queryParams.scheduleEndTime = null
+            this.scheduleTime = null
+            this.handleQuery()
+        },
+
+        /** 显示消息详情弹窗 */
+        showContentDialog(contentJson) {
+            if (!contentJson) return
+            try {
+                const sanitizedJson = contentJson.replace(/[\u0000-\u001F\u007F]/g, '')
+                const parsedData = JSON.parse(sanitizedJson)
+                if (Array.isArray(parsedData)) {
+                    this.contentDialogData = parsedData
+                } else if (parsedData.setting) {
+                    this.contentDialogData = parsedData.setting
+                } else {
+                    this.contentDialogData = [parsedData]
+                }
+                this.contentDialogVisible = true
+            } catch (e) {
+                this.$message.error('消息内容解析失败')
+                console.error('解析contentJson失败:', e)
+            }
+        },
+        /** 获取内容类型标签 */
+        getContentTypeTag(contentType) {
+            const typeMap = {
+                '1': { label: '文本', type: '' },
+                '2': { label: '图片', type: 'success' },
+                '3': { label: '链接', type: 'warning' },
+                '5': { label: '文件', type: 'info' },
+                '6': { label: '视频', type: 'danger' }
+            }
+            return typeMap[String(contentType)] || { label: '其他(' + contentType + ')', type: 'info' }
+        },
+        /** 导出按钮操作 */
+        handleExport() {
+            const queryParams = this.queryParams
+            this.$confirm('是否确认导出所有个微SOP执行详情数据项?', '警告', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                this.exportLoading = true
+                return exportWxSopLogsCVO(queryParams)
+            }).then(response => {
+                this.download(response.msg)
+                this.exportLoading = false
+            }).catch(() => {
+                this.exportLoading = false
+            })
+        }
+    }
+}
+</script>
+
+<style scoped>
+.content-detail {
+    max-height: 400px;
+    overflow-y: auto;
+}
+
+.content-item {
+    padding: 12px 16px;
+    margin-bottom: 12px;
+    background: #f8f9fa;
+    border-radius: 8px;
+    border: 1px solid #ebeef5;
+}
+
+.content-type-tag {
+    margin-bottom: 8px;
+}
+
+.content-body {
+    font-size: 14px;
+    line-height: 1.6;
+    color: #303133;
+}
+
+.text-content {
+    white-space: pre-wrap;
+    word-break: break-all;
+}
+</style>

+ 481 - 0
src/views/wx/wxSopLogs/index.vue

@@ -0,0 +1,481 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="消息类型0个人1群" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择消息类型0个人1群" clearable size="small">
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="任务ID" prop="sopId">
+        <el-input
+          v-model="queryParams.sopId"
+          placeholder="请输入任务ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="营期ID" prop="sopUserId">
+        <el-input
+          v-model="queryParams.sopUserId"
+          placeholder="请输入营期ID"
+          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">
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="生成类型(0自动1手动)" prop="generateType">
+        <el-select v-model="queryParams.generateType" placeholder="请选择生成类型(0自动1手动)" clearable size="small">
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="发送账号ID" prop="accountId">
+        <el-input
+          v-model="queryParams.accountId"
+          placeholder="请输入发送账号ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="发送对象ID" prop="wxContactId">
+        <el-input
+          v-model="queryParams.wxContactId"
+          placeholder="请输入发送对象ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="发送对象名称" prop="wxContactName">
+        <el-input
+          v-model="queryParams.wxContactName"
+          placeholder="请输入发送对象名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="发送群聊ID" prop="wxRoomId">
+        <el-input
+          v-model="queryParams.wxRoomId"
+          placeholder="请输入发送群聊ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="发送群聊名称" prop="wxRoomName">
+        <el-input
+          v-model="queryParams.wxRoomName"
+          placeholder="请输入发送群聊名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <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="发送状态0待发送1发送成功2发送失败3消息作废" prop="sendStatus">
+        <el-select v-model="queryParams.sendStatus" placeholder="请选择发送状态0待发送1发送成功2发送失败3消息作废" clearable size="small">
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="发送备注" prop="sendRemark">
+        <el-input
+          v-model="queryParams.sendRemark"
+          placeholder="请输入发送备注"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="发送排序" prop="sendSort">
+        <el-input
+          v-model="queryParams.sendSort"
+          placeholder="请输入发送排序"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="消息过期时间" prop="expirationTime">
+        <el-date-picker clearable size="small"
+          v-model="queryParams.expirationTime"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="选择消息过期时间">
+        </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="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['wx:wxSopLogs:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="single"
+          @click="handleUpdate"
+          v-hasPermi="['wx:wxSopLogs:edit']"
+        >修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['wx:wxSopLogs:remove']"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['wx:wxSopLogs:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="wxSopLogsList" @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="消息类型0个人1群" align="center" prop="type" />
+      <el-table-column label="任务ID" align="center" prop="sopId" />
+      <el-table-column label="营期ID" align="center" prop="sopUserId" />
+      <el-table-column label="发送类型" align="center" prop="sendType" />
+      <el-table-column label="生成类型(0自动1手动)" align="center" prop="generateType" />
+      <el-table-column label="发送账号ID" align="center" prop="accountId" />
+      <el-table-column label="发送对象ID" align="center" prop="wxContactId" />
+      <el-table-column label="发送对象名称" align="center" prop="wxContactName" />
+      <el-table-column label="发送群聊ID" align="center" prop="wxRoomId" />
+      <el-table-column label="发送群聊名称" align="center" prop="wxRoomName" />
+      <el-table-column label="小程序ID" align="center" prop="fsUserId" />
+      <el-table-column label="发送状态0待发送1发送成功2发送失败3消息作废" align="center" prop="sendStatus" />
+      <el-table-column label="发送备注" align="center" prop="sendRemark" />
+      <el-table-column label="发送排序" align="center" prop="sendSort" />
+      <el-table-column label="消息过期时间" align="center" prop="expirationTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.expirationTime, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" />
+      <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="['wx:wxSopLogs:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['wx:wxSopLogs:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改个微发送记录对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="消息类型0个人1群" prop="type">
+          <el-select v-model="form.type" placeholder="请选择消息类型0个人1群">
+            <el-option label="请选择字典生成" value="" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="任务ID" prop="sopId">
+          <el-input v-model="form.sopId" placeholder="请输入任务ID" />
+        </el-form-item>
+        <el-form-item label="营期ID" prop="sopUserId">
+          <el-input v-model="form.sopUserId" placeholder="请输入营期ID" />
+        </el-form-item>
+        <el-form-item label="发送类型" prop="sendType">
+          <el-select v-model="form.sendType" placeholder="请选择发送类型">
+            <el-option label="请选择字典生成" value="" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="生成类型(0自动1手动)" prop="generateType">
+          <el-select v-model="form.generateType" placeholder="请选择生成类型(0自动1手动)">
+            <el-option label="请选择字典生成" value="" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="发送账号ID" prop="accountId">
+          <el-input v-model="form.accountId" placeholder="请输入发送账号ID" />
+        </el-form-item>
+        <el-form-item label="发送对象ID" prop="wxContactId">
+          <el-input v-model="form.wxContactId" placeholder="请输入发送对象ID" />
+        </el-form-item>
+        <el-form-item label="发送对象名称" prop="wxContactName">
+          <el-input v-model="form.wxContactName" placeholder="请输入发送对象名称" />
+        </el-form-item>
+        <el-form-item label="发送群聊ID" prop="wxRoomId">
+          <el-input v-model="form.wxRoomId" placeholder="请输入发送群聊ID" />
+        </el-form-item>
+        <el-form-item label="发送群聊名称" prop="wxRoomName">
+          <el-input v-model="form.wxRoomName" placeholder="请输入发送群聊名称" />
+        </el-form-item>
+        <el-form-item label="小程序ID" prop="fsUserId">
+          <el-input v-model="form.fsUserId" placeholder="请输入小程序ID" />
+        </el-form-item>
+        <el-form-item label="发送状态0待发送1发送成功2发送失败3消息作废">
+          <el-radio-group v-model="form.sendStatus">
+            <el-radio label="1">请选择字典生成</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="发送备注" prop="sendRemark">
+          <el-input v-model="form.sendRemark" placeholder="请输入发送备注" />
+        </el-form-item>
+        <el-form-item label="发送排序" prop="sendSort">
+          <el-input v-model="form.sendSort" placeholder="请输入发送排序" />
+        </el-form-item>
+        <el-form-item label="消息过期时间" prop="expirationTime">
+          <el-date-picker clearable size="small"
+            v-model="form.expirationTime"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择消息过期时间">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" 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 { listWxSopLogs, getWxSopLogs, delWxSopLogs, addWxSopLogs, updateWxSopLogs, exportWxSopLogs } from "@/api/wx/wxSopLogs";
+
+export default {
+  name: "WxSopLogs",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 个微发送记录表格数据
+      wxSopLogsList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        type: null,
+        sopId: null,
+        sopUserId: null,
+        sendType: null,
+        generateType: null,
+        accountId: null,
+        wxContactId: null,
+        wxContactName: null,
+        wxRoomId: null,
+        wxRoomName: null,
+        fsUserId: null,
+        sendStatus: null,
+        sendRemark: null,
+        sendSort: null,
+        expirationTime: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询个微发送记录列表 */
+    getList() {
+      this.loading = true;
+      listWxSopLogs(this.queryParams).then(response => {
+        this.wxSopLogsList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        type: null,
+        sopId: null,
+        sopUserId: null,
+        sendType: null,
+        generateType: null,
+        accountId: null,
+        wxContactId: null,
+        wxContactName: null,
+        wxRoomId: null,
+        wxRoomName: null,
+        fsUserId: null,
+        sendStatus: 0,
+        sendRemark: null,
+        sendSort: null,
+        expirationTime: null,
+        createTime: null,
+        createBy: null,
+        updateTime: null,
+        updateBy: null,
+        remark: null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加个微发送记录";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getWxSopLogs(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改个微发送记录";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateWxSopLogs(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addWxSopLogs(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除个微发送记录编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delWxSopLogs(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有个微发送记录数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportWxSopLogs(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>

+ 350 - 0
src/views/wx/wxSopUser/index.vue

@@ -0,0 +1,350 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="类型(0个人1群聊)" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择类型(0个人1群聊)" clearable size="small">
+          <el-option label="请选择字典生成" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="任务ID" prop="sopId">
+        <el-input
+          v-model="queryParams.sopId"
+          placeholder="请输入任务ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="个微账号ID" prop="accountId">
+        <el-input
+          v-model="queryParams.accountId"
+          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="群聊ID" prop="chatId">
+        <el-input
+          v-model="queryParams.chatId"
+          placeholder="请输入群聊ID"
+          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 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">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['wx:wxSopUser:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="single"
+          @click="handleUpdate"
+          v-hasPermi="['wx:wxSopUser:edit']"
+        >修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['wx:wxSopUser:remove']"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['wx:wxSopUser:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="wxSopUserList" @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="类型(0个人1群聊)" align="center" prop="type" />
+      <el-table-column label="任务ID" align="center" prop="sopId" />
+      <el-table-column label="个微账号ID" align="center" prop="accountId" />
+      <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="群聊ID" align="center" prop="chatId" />
+      <el-table-column label="状态" align="center" prop="status" />
+      <el-table-column label="备注" align="center" prop="remark" />
+      <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="['wx:wxSopUser:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['wx:wxSopUser:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改个微营期对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="类型(0个人1群聊)" prop="type">
+          <el-select v-model="form.type" placeholder="请选择类型(0个人1群聊)">
+            <el-option label="请选择字典生成" value="" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="任务ID" prop="sopId">
+          <el-input v-model="form.sopId" placeholder="请输入任务ID" />
+        </el-form-item>
+        <el-form-item label="个微账号ID" prop="accountId">
+          <el-input v-model="form.accountId" placeholder="请输入个微账号ID" />
+        </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="群聊ID" prop="chatId">
+          <el-input v-model="form.chatId" placeholder="请输入群聊ID" />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio label="1">请选择字典生成</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" 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 { listWxSopUser, getWxSopUser, delWxSopUser, addWxSopUser, updateWxSopUser, exportWxSopUser } from "@/api/wx/wxSopUser";
+
+export default {
+  name: "WxSopUser",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 个微营期表格数据
+      wxSopUserList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        type: null,
+        sopId: null,
+        accountId: null,
+        startTime: null,
+        chatId: null,
+        status: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询个微营期列表 */
+    getList() {
+      this.loading = true;
+      listWxSopUser(this.queryParams).then(response => {
+        this.wxSopUserList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        type: null,
+        sopId: null,
+        accountId: null,
+        startTime: null,
+        chatId: null,
+        status: 0,
+        createTime: null,
+        createBy: null,
+        updateTime: null,
+        updateBy: null,
+        remark: null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加个微营期";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getWxSopUser(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改个微营期";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateWxSopUser(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addWxSopUser(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除个微营期编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delWxSopUser(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有个微营期数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportWxSopUser(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>

+ 412 - 0
src/views/wx/wxSopUserInfo/index.vue

@@ -0,0 +1,412 @@
+<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="sopId">
+        <el-input
+          v-model="queryParams.sopId"
+          placeholder="请输入任务ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="营期ID" prop="sopUserId">
+        <el-input
+          v-model="queryParams.sopUserId"
+          placeholder="请输入营期ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="联系人ID" prop="wxContactId">
+        <el-input
+          v-model="queryParams.wxContactId"
+          placeholder="请输入联系人ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <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="是否7天都没有看课 0否 1是" prop="isDaysNotStudy">
+        <el-input
+          v-model="queryParams.isDaysNotStudy"
+          placeholder="请输入是否7天都没有看课 0否 1是"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="总完课天数" prop="finishCout">
+        <el-input
+          v-model="queryParams.finishCout"
+          placeholder="请输入总完课天数"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="最近完课时间" prop="finishTime">
+        <el-date-picker clearable size="small"
+          v-model="queryParams.finishTime"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="选择最近完课时间">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="连续完课天数" prop="finishCourseDays">
+        <el-input
+          v-model="queryParams.finishCourseDays"
+          placeholder="请输入连续完课天数"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="客户评级的等级" prop="grade">
+        <el-input
+          v-model="queryParams.grade"
+          placeholder="请输入客户评级的等级"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="禁用状态 0 正常 1禁用" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择禁用状态 0 正常 1禁用" clearable size="small">
+          <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">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['wx:wxSopUserInfo:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-edit"
+          size="mini"
+          :disabled="single"
+          @click="handleUpdate"
+          v-hasPermi="['wx:wxSopUserInfo:edit']"
+        >修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['wx:wxSopUserInfo:remove']"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['wx:wxSopUserInfo:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="wxSopUserInfoList" @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="任务ID" align="center" prop="sopId" />
+      <el-table-column label="营期ID" align="center" prop="sopUserId" />
+      <el-table-column label="联系人ID" align="center" prop="wxContactId" />
+      <el-table-column label="小程序ID" align="center" prop="fsUserId" />
+      <el-table-column label="是否7天都没有看课 0否 1是" align="center" prop="isDaysNotStudy" />
+      <el-table-column label="总完课天数" align="center" prop="finishCout" />
+      <el-table-column label="最近完课时间" align="center" prop="finishTime" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.finishTime, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="连续完课天数" align="center" prop="finishCourseDays" />
+      <el-table-column label="客户评级的等级" align="center" prop="grade" />
+      <el-table-column label="禁用状态 0 正常 1禁用" align="center" prop="status" />
+      <el-table-column label="备注" align="center" prop="remark" />
+      <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="['wx:wxSopUserInfo:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['wx:wxSopUserInfo:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改个微营期详情对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="任务ID" prop="sopId">
+          <el-input v-model="form.sopId" placeholder="请输入任务ID" />
+        </el-form-item>
+        <el-form-item label="营期ID" prop="sopUserId">
+          <el-input v-model="form.sopUserId" placeholder="请输入营期ID" />
+        </el-form-item>
+        <el-form-item label="联系人ID" prop="wxContactId">
+          <el-input v-model="form.wxContactId" placeholder="请输入联系人ID" />
+        </el-form-item>
+        <el-form-item label="小程序ID" prop="fsUserId">
+          <el-input v-model="form.fsUserId" placeholder="请输入小程序ID" />
+        </el-form-item>
+        <el-form-item label="是否7天都没有看课 0否 1是" prop="isDaysNotStudy">
+          <el-input v-model="form.isDaysNotStudy" placeholder="请输入是否7天都没有看课 0否 1是" />
+        </el-form-item>
+        <el-form-item label="总完课天数" prop="finishCout">
+          <el-input v-model="form.finishCout" placeholder="请输入总完课天数" />
+        </el-form-item>
+        <el-form-item label="最近完课时间" prop="finishTime">
+          <el-date-picker clearable size="small"
+            v-model="form.finishTime"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择最近完课时间">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="连续完课天数" prop="finishCourseDays">
+          <el-input v-model="form.finishCourseDays" placeholder="请输入连续完课天数" />
+        </el-form-item>
+        <el-form-item label="客户评级的等级" prop="grade">
+          <el-input v-model="form.grade" placeholder="请输入客户评级的等级" />
+        </el-form-item>
+        <el-form-item label="禁用状态 0 正常 1禁用">
+          <el-radio-group v-model="form.status">
+            <el-radio label="1">请选择字典生成</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" 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 { listWxSopUserInfo, getWxSopUserInfo, delWxSopUserInfo, addWxSopUserInfo, updateWxSopUserInfo, exportWxSopUserInfo } from "@/api/wx/wxSopUserInfo";
+
+export default {
+  name: "WxSopUserInfo",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 个微营期详情表格数据
+      wxSopUserInfoList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        sopId: null,
+        sopUserId: null,
+        wxContactId: null,
+        fsUserId: null,
+        isDaysNotStudy: null,
+        finishCout: null,
+        finishTime: null,
+        finishCourseDays: null,
+        grade: null,
+        status: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询个微营期详情列表 */
+    getList() {
+      this.loading = true;
+      listWxSopUserInfo(this.queryParams).then(response => {
+        this.wxSopUserInfoList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        sopId: null,
+        sopUserId: null,
+        wxContactId: null,
+        fsUserId: null,
+        isDaysNotStudy: null,
+        finishCout: null,
+        finishTime: null,
+        finishCourseDays: null,
+        grade: null,
+        status: 0,
+        createTime: null,
+        createBy: null,
+        updateTime: null,
+        updateBy: null,
+        remark: null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加个微营期详情";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getWxSopUserInfo(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改个微营期详情";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateWxSopUserInfo(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addWxSopUserInfo(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除个微营期详情编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delWxSopUserInfo(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有个微营期详情数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportWxSopUserInfo(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>

Some files were not shown because too many files changed in this diff