Kaynağa Gözat

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_companyUI

caoliqin 4 gün önce
ebeveyn
işleme
076b1664be
49 değiştirilmiş dosya ile 10869 ekleme ve 1646 silme
  1. 1 1
      .env.prod-mengniu
  2. 27 0
      .env.prod-sxtb
  3. 1 0
      package.json
  4. 8 0
      src/api/company/companyVoiceRobotic.js
  5. 8 0
      src/api/company/companyWorkflow.js
  6. 142 0
      src/api/company/inboundCallManage.js
  7. 52 54
      src/api/live/liveData.js
  8. 45 0
      src/api/live/trainingCamp.js
  9. 9 0
      src/api/watch/common.js
  10. 121 5
      src/api/watch/deviceInfo.js
  11. 240 0
      src/api/xsy/xsy.js
  12. BIN
      src/assets/logo/tbyg.png
  13. 1 1
      src/components/DeviceInfo/AtrialFibrillation.vue
  14. 5 0
      src/components/DeviceInfo/BloodSugar.vue
  15. 8 3
      src/components/DeviceInfo/Bloodoxygen.vue
  16. 5 0
      src/components/DeviceInfo/Heartrate.vue
  17. 9 4
      src/components/DeviceInfo/Pressure.vue
  18. 291 0
      src/components/DeviceInfo/PressureValue.vue
  19. 6 1
      src/components/DeviceInfo/Pulse.vue
  20. 1942 0
      src/components/DeviceInfo/Report.vue
  21. 13 183
      src/components/DeviceInfo/SettingDialog/SetInfoDialog.vue
  22. 22 27
      src/components/DeviceInfo/Sleep.vue
  23. 6 1
      src/components/DeviceInfo/Sports.vue
  24. 9 5
      src/components/DeviceInfo/Temperature.vue
  25. 5 0
      src/components/DeviceInfo/Uricacid.vue
  26. 5 0
      src/components/DeviceInfo/Urineketones.vue
  27. 2 2
      src/views/company/aiModel/account/info.vue
  28. 672 0
      src/views/company/aiModel/inboundCallManage/inboundCallRecord.vue
  29. 709 0
      src/views/company/aiModel/inboundCallManage/index.vue
  30. 8 2
      src/views/company/companyConfig/index.vue
  31. 129 1
      src/views/company/companyUser/index.vue
  32. 186 12
      src/views/company/companyVoiceRobotic/index.vue
  33. 2308 0
      src/views/company/companyVoiceRobotic/myIndex.vue
  34. 67 10
      src/views/company/companyWorkflow/design.vue
  35. 2 0
      src/views/company/companyWorkflow/index.vue
  36. 657 0
      src/views/company/companyWorkflow/myIndex.vue
  37. 46 3
      src/views/company/wxAccount/index.vue
  38. 54 3
      src/views/components/QwUserSelect.vue
  39. 230 119
      src/views/live/liveConfig/answer.vue
  40. 1 1
      src/views/live/liveConfig/index.vue
  41. 1203 0
      src/views/live/liveStatistics/LiveStatisticsPanel.vue
  42. 3 1166
      src/views/live/liveStatistics/index.vue
  43. 302 0
      src/views/live/trainingCamp/PeriodWorkspace.vue
  44. 809 0
      src/views/live/trainingCamp/index.vue
  45. 2 2
      src/views/qw/friendWelcome/indexNew.vue
  46. 35 13
      src/views/watch/deviceInfo/details.vue
  47. 160 13
      src/views/watch/deviceInfo/index.vue
  48. 46 14
      src/views/watch/deviceInfo/my.vue
  49. 257 0
      src/views/xsy/index.vue

+ 1 - 1
.env.prod-mengniu

@@ -1,5 +1,5 @@
 # 页面标题
-VUE_APP_TITLE =蒙牛SCRM销售端
+VUE_APP_TITLE = 销售管理系统
 # 公司名称
 VUE_APP_COMPANY_NAME =云联融智互联网医院有限公司
 # ICP备案号

+ 27 - 0
.env.prod-sxtb

@@ -0,0 +1,27 @@
+# 页面标题
+VUE_APP_TITLE =挑宝易购SCRM客服系统
+# 公司名称
+VUE_APP_COMPANY_NAME =西安挑宝益康医药连锁有限公司
+# ICP备案号
+VUE_APP_ICP_RECORD =陕ICP备17006076号-26
+# ICP网站访问地址
+VUE_APP_ICP_URL =https://beian.miit.gov.cn
+# 网站LOG
+VUE_APP_LOG_URL =@/assets/logo/tbyg.png
+
+# 生产环境配置
+ENV = 'production'
+
+# FS管理系统/开发环境
+VUE_APP_BASE_API = '/prod-api'
+
+#默认 1、会员 2、企微
+VUE_APP_COURSE_DEFAULT = 2
+
+#项目所属
+VUE_APP_PROJECT_FROM=sxtb
+
+# 路由懒加载
+VUE_CLI_BABEL_TRANSPILE_MODULES = true
+
+VUE_APP_LIVE_WS_URL = wss://websocket.xianhetaihuij.cn/ws

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
     "build:prod-hcl": "vue-cli-service build --mode prod-hcl",
     "build:prod-test": "vue-cli-service build --mode prod-test",
     "build:prod-sxjz": "vue-cli-service build --mode prod-sxjz",
+    "build:prod-sxtb": "vue-cli-service build --mode prod-sxtb",
     "build:prod-jnmy": "vue-cli-service build --mode prod-jnmy",
     "build:prod-knt": "vue-cli-service build --mode prod-knt",
     "build:prod-hnyth": "vue-cli-service build --mode prod-hnyth",

+ 8 - 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({

+ 8 - 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({

+ 142 - 0
src/api/company/inboundCallManage.js

@@ -0,0 +1,142 @@
+import request from '@/utils/request'
+
+/**
+ * 查询呼入大模型配置列表
+ * @param {Object} query 查询参数
+ */
+export function listInboundLlm(query) {
+  return request({
+    url: '/company/inboundCallManage/list',
+    method: 'get',
+    params: query
+  })
+}
+
+/**
+ * 新增呼入大模型配置
+ * @param {Object} data 配置数据
+ */
+export function addInboundLlm(data) {
+  return request({
+    url: '/company/inboundCallManage',
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 修改呼入大模型配置
+ * @param {Object} data 配置数据
+ */
+export function updateInboundLlm(data) {
+  return request({
+    url: '/company/inboundCallManage',
+    method: 'put',
+    data: data
+  })
+}
+
+/**
+ * 删除呼入大模型配置
+ * @param {String} ids 配置ID,多个用逗号分隔
+ */
+export function delInboundLlm(ids) {
+  return request({
+    url: '/company/inboundCallManage/' + ids,
+    method: 'delete'
+  })
+}
+
+/**
+ * 获取大模型账户下拉列表
+ */
+export function listLlmAccount() {
+  return request({
+    url: '/company/inboundCallManage/llmAccountList',
+    method: 'get'
+  })
+}
+
+/**
+ * 校验被叫号码是否唯一
+ */
+export function checkCallee(id, callee) {
+  return request({
+    url: '/company/inboundCallManage/checkCallee',
+    method: 'get',
+    params: { id, callee }
+  })
+}
+
+/**
+ * 获取ASR提供商列表
+ */
+export function listAsrProvider() {
+  return request({
+    url: '/company/inboundCallManage/asrProviderList',
+    method: 'get'
+  })
+}
+
+/**
+ * 获取TTS音色来源列表
+ */
+export function listVoiceSource() {
+  return request({
+    url: '/company/inboundCallManage/voiceSourceList',
+    method: 'get'
+  })
+}
+
+/**
+ * 根据音色来源获取音色列表
+ */
+export function listVoiceBySource(voiceSource) {
+  return request({
+    url: '/company/inboundCallManage/voiceList',
+    method: 'get',
+    params: { voiceSource }
+  })
+}
+
+/**
+ * 获取业务组列表
+ */
+export function listBizGroup() {
+  return request({
+    url: '/company/inboundCallManage/bizGroupList',
+    method: 'get'
+  })
+}
+
+/**
+ * 获取出局网关列表
+ */
+export function listGateway() {
+  return request({
+    url: '/company/inboundCallManage/gatewayList',
+    method: 'get'
+  })
+}
+
+/**
+ * 获取IVR列表
+ */
+export function listIvr() {
+  return request({
+    url: '/company/inboundCallManage/ivrList',
+    method: 'get'
+  })
+}
+
+/**
+ * 查询呼入通话记录列表
+ * @param {Object} query 查询参数
+ */
+export function listInboundCdr(query) {
+  return request({
+    url: '/company/inboundCallManage/inboundCdrList',
+    method: 'get',
+    params: query
+  })
+}

+ 52 - 54
src/api/live/liveData.js

@@ -122,58 +122,56 @@ export function exportLiveDataCompany(data) {
   })
 }
 
+// 直播数据统计-数据概览(多直播间聚合指标)
+export function getLiveStatisticsOverview(liveIds) {
+  return request({
+    url: '/liveData/liveData/getLiveStatisticsOverview',
+    method: 'post',
+    data: liveIds || []
+  })
+}
+
+// 直播趋势-进入人数折线图
+export function getLiveEntryTrend(liveIds) {
+  return request({
+    url: '/liveData/liveData/getLiveEntryTrend',
+    method: 'post',
+    data: liveIds || []
+  })
+}
+
+// 直播间学员列表(分页)
+export function listLiveRoomStudents(data) {
+  return request({
+    url: '/liveData/liveData/listLiveRoomStudents',
+    method: 'post',
+    data: data || {}
+  })
+}
 
-//
-// // 直播数据统计-数据概览(12项指标)
-// export function getLiveStatisticsOverview(liveIds) {
-//     return request({
-//         url: '/liveData/liveData/getLiveStatisticsOverview',
-//         method: 'post',
-//         data: liveIds || []
-//     })
-// }
-//
-// // 直播趋势-进入人数折线图(基于 live_user_first_entry 与直播开始时间的相对时间)
-// export function getLiveEntryTrend(liveIds) {
-//     return request({
-//         url: '/liveData/liveData/getLiveEntryTrend',
-//         method: 'post',
-//         data: liveIds || []
-//     })
-// }
-//
-// // 直播间学员列表(分页)
-// export function listLiveRoomStudents(data) {
-//     return request({
-//         url: '/liveData/liveData/listLiveRoomStudents',
-//         method: 'post',
-//         data: data || {}
-//     })
-// }
-//
-// // 商品对比统计(商品名称、下单未支付人数、成交人数、成交金额)
-// export function listProductCompareStats(data) {
-//     return request({
-//         url: '/liveData/liveData/listProductCompareStats',
-//         method: 'post',
-//         data: data || {}
-//     })
-// }
-//
-// // 邀课对比-分享人选项列表(基于 live_user_first_entry 中存在的销售)
-// export function listInviteSalesOptions(liveIds) {
-//     return request({
-//         url: '/liveData/liveData/listInviteSalesOptions',
-//         method: 'post',
-//         data: liveIds || []
-//     })
-// }
-//
-// // 邀课对比统计(归属公司、销售名称、邀请人数、已支付订单数、订单总金额)
-// export function listInviteCompareStats(data) {
-//     return request({
-//         url: '/liveData/liveData/listInviteCompareStats',
-//         method: 'post',
-//         data: data || {}
-//     })
-// }
+// 商品对比统计
+export function listProductCompareStats(data) {
+  return request({
+    url: '/liveData/liveData/listProductCompareStats',
+    method: 'post',
+    data: data || {}
+  })
+}
+
+// 邀课对比-分享人选项列表
+export function listInviteSalesOptions(liveIds) {
+  return request({
+    url: '/liveData/liveData/listInviteSalesOptions',
+    method: 'post',
+    data: liveIds || []
+  })
+}
+
+// 邀课对比统计
+export function listInviteCompareStats(data) {
+  return request({
+    url: '/liveData/liveData/listInviteCompareStats',
+    method: 'post',
+    data: data || {}
+  })
+}

+ 45 - 0
src/api/live/trainingCamp.js

@@ -0,0 +1,45 @@
+import request from '@/utils/request'
+
+const base = '/live/trainingCamp'
+
+// 训练营
+export function listTrainingCamp(query) {
+  return request({ url: base + '/camp/list', method: 'get', params: query })
+}
+export function getTrainingCamp(campId) {
+  return request({ url: base + '/camp/' + campId, method: 'get' })
+}
+export function addTrainingCamp(data) {
+  return request({ url: base + '/camp', method: 'post', data })
+}
+export function updateTrainingCamp(data) {
+  return request({ url: base + '/camp', method: 'put', data })
+}
+export function delTrainingCamp(campIds) {
+  return request({ url: base + '/camp/' + campIds, method: 'delete' })
+}
+
+// 营期
+export function listTrainingPeriod(query) {
+  return request({ url: base + '/period/list', method: 'get', params: query })
+}
+export function getTrainingPeriod(periodId) {
+  return request({ url: base + '/period/' + periodId, method: 'get' })
+}
+export function addTrainingPeriod(data) {
+  return request({ url: base + '/period', method: 'post', data })
+}
+export function updateTrainingPeriod(data) {
+  return request({ url: base + '/period', method: 'put', data })
+}
+export function delTrainingPeriod(periodIds) {
+  return request({ url: base + '/period/' + periodIds, method: 'delete' })
+}
+
+// 营期下直播间
+export function listTrainingLive(query) {
+  return request({ url: base + '/live/list', method: 'get', params: query })
+}
+export function addTrainingLive(data) {
+  return request({ url: base + '/live', method: 'post', data })
+}

+ 9 - 0
src/api/watch/common.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+import { status } from 'nprogress'
+
+const baseURL = process.env.VUE_APP_BASE_API
+// watch端 通用下载方法
+export function watchDownload(fileName) {
+    window.location.href = "/watch-api/watchCommon/download?fileName=" + encodeURI(fileName) + "&delete=" + true;
+}
+

+ 121 - 5
src/api/watch/deviceInfo.js

@@ -9,7 +9,6 @@ export function listDeviceInfo(query) {
     params: query
   })
 }
-
 // 查询设备信息列表
 export function myDeviceInfo(query) {
   return request({
@@ -18,7 +17,6 @@ export function myDeviceInfo(query) {
     params: query
   })
 }
-
 // 查询设备信息详细
 export function getDeviceInfo(deviceId) {
   return request({
@@ -90,11 +88,11 @@ export function exportMy(query) {
 }
 
   // 查询用户信息
-export function getUser(deviceId) {
+export function getUser(data) {
   return request({
     url: '/watch-api/device/set/up/getUserInfo',
     method: 'get',
-    params: { deviceId } 
+    params:data
   })
 }
 
@@ -123,6 +121,14 @@ export function queryByDate(date,deviceId) {
   })
 }
 
+export function queryHrList(data) {
+  return request({
+    url: `/watch-api/watch/heart/rate/list`,
+    method: 'get',
+    params: data
+  })
+}
+
 //exportHeartDate导出
 export function exportHeartDate(date,deviceId) {
   return request({
@@ -168,6 +174,15 @@ export function queryGlucoseDate(date,deviceId) {
   })
 }
 
+// 根据时间获取血糖数据
+export function queryGlucoseList(data) {
+  return request({
+    url: `/watch-api/watch/blood/glucose/list`,
+    method: 'get',
+    params: data
+  })
+}
+
 // 根据时间获取房颤数据
 export function queryAfDate(date,deviceId) {
   return request({
@@ -186,6 +201,15 @@ export function queryPressure(date,deviceId) {
   })
 }
 
+// 根据时间获取血压数据
+export function queryPressureList(data) {
+  return request({
+    url: `/watch-api/watch/blood/pressure/list`,
+    method: 'get',
+    params:data
+  })
+}
+
 // 根据时间获取温度数据
 export function queryTemperature(date,deviceId) {
   return request({
@@ -213,6 +237,15 @@ export function querySpo2(date,deviceId) {
   })
 }
 
+// 根据时间获取血氧数据
+export function querySpo2List(data) {
+  return request({
+    url: `/watch-api/watch/spo2/data/list`,
+    method: 'get',
+    params: data
+  })
+}
+
 // 根据时间获取睡眠数据
 export function querySleep(date,deviceId) {
   return request({
@@ -222,6 +255,15 @@ export function querySleep(date,deviceId) {
   })
 }
 
+// 根据时间获取睡眠数据
+export function querySleepList(data) {
+  return request({
+    url: `/watch-api/watch/sleep/data/list`,
+    method: 'get',
+    params: data
+  })
+}
+
 // 获取预警数据
 export function queryAlarm(status) {
   return request({
@@ -288,6 +330,15 @@ export function queryUaData(data) {
   })
 }
 
+//获取尿酸数据
+export function queryUaList(data) {
+  return request({
+      url: '/watch-api/watch/third/ua/list',
+      method: 'get',
+      params:data
+  })
+}
+
 //获最新健康数据
 export function queryLastHealthData(data) {
   return request({
@@ -350,4 +401,69 @@ export function queryTemperaturePageByDate(data) {
       method: 'get',
       params:data
   })
-}
+}
+
+//查询某个时间段的温度数据 分页
+export function queryBOPageByDate(data) {
+  return request({
+      url: '/watch-api/watch/continuous/spo2/data/queryPageByDateAndDeviceId',
+      method: 'get',
+      params:data
+  })
+}
+
+//物联网 分页
+export function queryIotList(data) {
+  return request({
+      url: '/watch-api/watch/deviceInfo/query',
+      method: 'get',
+      params:data
+  })
+}
+
+//查询某个时间段的压力数据 分页
+export function queryFatiguePageByDate(data) {
+  return request({
+      url: '/watch-api/watch/fatigue/page',
+      method: 'get',
+      params:data
+  })
+}
+
+// 根据时间获取压力数据
+export function queryFatigue(date,deviceId) {
+  return request({
+    url: `/watch-api/watch/fatigue/queryByDateAndDeviceId`,
+    method: 'get',
+    params: { date,deviceId } 
+  })
+}
+
+
+//健康周报-步数,卡路里,睡眠等信息
+export function getUserHealthInfoByDeviceId(data) {
+  return request({
+      url: '/watch-api/watch/sport/data/getUserHealthInfoByDeviceId',
+      method: 'get',
+      params:data
+  })
+}
+
+//健康周报-步数,卡路里,睡眠等信息
+export function queryHealthReport(data) {
+  return request({
+      url: '/watch-api/device/queryHealthReport',
+      method: 'get',
+      params:data
+  })
+}
+
+//舌诊
+export function queryTongueList(data) {
+  return request({
+      url: '/watch-api/device/getHealthTongueList',
+      method: 'get',
+      params:data
+  })
+}
+

+ 240 - 0
src/api/xsy/xsy.js

@@ -0,0 +1,240 @@
+import request from '@/utils/request'
+
+
+
+/**
+ * 查询销售易账号列表
+ */
+export function listXsyAccount(query) {
+  return request({
+    url: '/xsy/account/company/list',
+    method: 'get',
+    params: query
+  })
+}
+
+/**
+ * 查询销售易账号详情
+ */
+export function getXsyAccount(id) {
+  return request({
+    url: '/xsy/account/get/' + id,
+    method: 'get'
+  })
+}
+
+/**
+ * 新增销售易账号
+ */
+export function addXsyAccount(data) {
+  return request({
+    url: '/xsy/account/add',
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 修改销售易账号
+ */
+export function updateXsyAccount(data) {
+  return request({
+    url: '/xsy/account/update',
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 删除销售易账号
+ */
+export function delXsyAccount(id) {
+  return request({
+    url: '/xsy/account/delete/' + id,
+    method: 'post'
+  })
+}
+
+/**
+ * 启用/禁用销售易账号
+ */
+export function updateXsyAccountStatus(id, status) {
+  return request({
+    url: '/xsy/account/status',
+    method: 'post',
+    params: {
+      id: id,
+      status: status
+    }
+  })
+}
+
+/**
+ * 获取授权地址
+ */
+export function getXsyAuthUrl(accountId) {
+  return request({
+    url: '/xsy/account/authUrl/' + accountId,
+    method: 'get'
+  })
+}
+
+
+
+/**
+ * 绑定销售易账号
+ */
+export function bindXsyAccount(data) {
+  return request({
+    url: '/xsy/bind/bind',
+    method: 'post',
+    data: data
+  })
+}
+
+export function unbindXsyAccount(data) {
+  return request({
+    url: '/xsy/bind/unbind',
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 查询绑定关系
+ */
+export function getXsyBind(companyUserId) {
+  return request({
+    url: '/xsy/bind/get',
+    method: 'get',
+    params: {
+      companyUserId: companyUserId
+    }
+  })
+}
+
+
+
+/**
+ * 获取授权URL(新接口)
+ */
+export function getXsyOauthUrl(accountId) {
+  return request({
+    url: '/xiaoShouYi/New/auth/url/' + accountId,
+    method: 'get'
+  })
+}
+
+
+
+/**
+ * 生成素材追踪链接
+ */
+export function generateXsyLink(companyUserId, data) {
+  return request({
+    url: '/xiaoShouYi/New/generateLink',
+    method: 'post',
+    params: {
+      companyUserId: companyUserId
+    },
+    data: data
+  })
+}
+
+/**
+ * 发送确认
+ */
+export function sendXsyConfirm(companyUserId, forwardId, forwardType) {
+  return request({
+    url: '/xiaoShouYi/New/sendConfirm',
+    method: 'post',
+    params: {
+      companyUserId: companyUserId,
+      forwardId: forwardId,
+      forwardType: forwardType
+    }
+  })
+}
+
+/**
+ * 查询素材列表
+ */
+export function queryXsyMaterials(companyUserId, data) {
+  return request({
+    url: '/xiaoShouYi/New/materials/query',
+    method: 'post',
+    params: {
+      companyUserId: companyUserId
+    },
+    data: data
+  })
+}
+
+/**
+ * 上传素材文件
+ */
+export function uploadXsyFile(companyUserId, file, isVideo) {
+  const formData = new FormData()
+  formData.append('file', file)
+  formData.append('isVideo', isVideo == null ? false : isVideo)
+
+  return request({
+    url: '/xiaoShouYi/New/uploadFile?companyUserId=' + companyUserId,
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
+}
+
+/**
+ * 创建素材
+ */
+export function createXsyMaterial(companyUserId, data) {
+  return request({
+    url: '/xiaoShouYi/New/createMaterial',
+    method: 'post',
+    params: {
+      companyUserId: companyUserId
+    },
+    data: data
+  })
+}
+
+/**
+ * 上传并创建素材
+ */
+export function createXsyMaterialWithUpload(params) {
+  const formData = new FormData()
+  formData.append('companyUserId', params.companyUserId)
+  formData.append('file', params.file)
+  formData.append('isVideo', params.isVideo == null ? false : params.isVideo)
+  formData.append('corpName', params.corpName)
+  formData.append('materialType', params.materialType)
+  formData.append('categoryName', params.categoryName)
+  formData.append('title', params.title)
+
+  return request({
+    url: '/xiaoShouYi/New/createMaterialWithUpload',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  })
+}
+
+/**
+ * 完整流程:生成链接并发送确认
+ */
+export function fullProcessXsy(companyUserId, data) {
+  return request({
+    url: '/xiaoShouYi/New/fullProcess',
+    method: 'post',
+    params: {
+      companyUserId: companyUserId
+    },
+    data: data
+  })
+}

BIN
src/assets/logo/tbyg.png


+ 1 - 1
src/components/DeviceInfo/AtrialFibrillation.vue

@@ -21,7 +21,7 @@
     </div>
     <div class="afib-diagnosis">
       <div class="afib-diagnosis-title">诊断结论:</div>
-      <div class="afib-diagnosis-note">{{conclusion||'没有房颤数据'}}</div>
+      <div class="afib-diagnosis-note">{{conclusion||'今日暂无数据,请选择其他日期'}}</div>
       <div class="afib-diagnosis-tips">
         注:非医疗诊断,供参考,请以医院确诊结论为准。
       </div>

+ 5 - 0
src/components/DeviceInfo/BloodSugar.vue

@@ -17,6 +17,11 @@
             </el-table-column>
             <el-table-column prop="bloodGlucose" align="center" label="测量结果">
             </el-table-column>
+            <template #empty>
+                <div>
+                    今日暂无数据,请选择其他日期
+                </div>
+            </template>
         </el-table>
 
     </div>

+ 8 - 3
src/components/DeviceInfo/Bloodoxygen.vue

@@ -22,6 +22,11 @@
             <el-table-column type="index" label="序号" align="center"/>
             <el-table-column label="时间" align="center" prop="time" />
             <el-table-column label="血氧" align="center" prop="avgBoxy" />
+            <template #empty>
+                <div>
+                    今日暂无数据,请选择其他日期
+                </div>
+            </template>
             </el-table>
             <pagination v-show="(total>0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
         </div>
@@ -87,9 +92,9 @@ export default {
                 xAxis: {
                     type: "time",
                     splitNumber: 6,
-                    axisLine: {
-                        show: false,
-                    },
+                    // axisLine: {
+                    //     show: false,
+                    // },
                     axisTick: {
                         show: false,
                     },

+ 5 - 0
src/components/DeviceInfo/Heartrate.vue

@@ -65,6 +65,11 @@
         <el-table-column label="最小心率" align="center" prop="minBpm" />
         <el-table-column label="状态" align="center" prop="status" :formatter="statusFormatter"/>
         <el-table-column label="时间" align="center" prop="createTime" />
+        <template #empty>
+            <div>
+                今日暂无数据,请选择其他日期
+            </div>
+        </template>
       </el-table>
       
     </div>

+ 9 - 4
src/components/DeviceInfo/Pressure.vue

@@ -24,8 +24,13 @@
                 <el-table-column label="sbp(mmgh)" align="center" prop="sbp" />
                 <el-table-column label="dbp(mmgh)" align="center" prop="dbp" />
                 <el-table-column label="hr(次/分)" align="center" prop="hr" />
+                <template #empty>
+                    <div>
+                        今日暂无数据,请选择其他日期
+                    </div>
+                </template>
             </el-table>
-            <pagination v-show="(dataListTotal > 0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
+            <pagination v-show="(dataListTotal>0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
         </div>
     </div>
 </template>
@@ -66,9 +71,9 @@ export default {
                     boundaryGap: false,
                     type: "time",
                     splitNumber: 6,
-                    axisLine: {
-                        show: false,
-                    },
+                    // axisLine: {
+                    //     show: false,
+                    // },
                     axisTick: {
                         show: false,
                     },

+ 291 - 0
src/components/DeviceInfo/PressureValue.vue

@@ -0,0 +1,291 @@
+<template>
+    <div>
+        <div class="box-header">
+            <div class="box-title boldtext">
+                压力 <span style="margin-left: 14px">{{ currentDate }}</span>
+            </div>
+            <div>
+                <!-- <el-button @click="exportData" size="small" style="margin-right: 20px"
+            >导出</el-button
+          > -->
+                <el-date-picker size="small" v-model="currentDatePicker" type="date" placeholder="选择日期"
+                    value-format="yyyy-MM-dd" @change="datePickerchange">
+                </el-date-picker>
+            </div>
+        </div>
+        <div v-show="!detailsType">
+            <div style="min-width: 841px; height: 320px; width: 100%;">
+                <div ref="heartrateChart" style="width: 100%; height: 100%;min-width: 783px;"></div>
+            </div>
+        </div>
+        <!-- 列表展示 -->
+        <div v-show="detailsType">
+            <el-table :data="dataList" style="width: 100%">
+                <el-table-column prop="createTime" label="时间" />
+                <el-table-column prop="pressure" label="压力值" />
+                <template #empty>
+                    <div>
+                        今日暂无数据,请选择其他日期
+                    </div>
+                </template>
+            </el-table>
+            <pagination v-show="(total > 0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
+        </div>
+        
+    </div>
+</template>
+
+<script>
+import * as echarts from "echarts";
+import { queryFatiguePageByDate, queryFatigue} from "@/api/watch/deviceInfo";
+export default {
+    props: {
+        detailsType: { 
+            type: Boolean,
+            default: false
+        },
+        deviceId: {
+            type: String || Number,
+            default: "",
+        },
+    },
+    data() {
+        return {
+            total:0,
+            dataList:[],
+            queryParams:{
+                pageNum: 1,
+                pageSize: 10,
+                deviceId:"",
+                beginTime:"",
+                endTime:""
+            },
+            currentDate: this.parseTime(new Date(), "{y}-{m}-{d}"),
+            currentDatePicker: "",
+            exportLoading: false,
+            chart: null,
+            chartOptions: {
+                tooltip: {
+                    trigger: 'axis'
+                },
+                legend: {},
+                xAxis: {
+                    boundaryGap: false,
+                    type: "time",
+                    splitNumber: 6,
+                    axisTick: {
+                        show: false,
+                    },
+                    splitLine: {
+                        show: false,
+                    },
+                    axisLabel: {
+                        color: "#999999",
+                        margin: 20,
+                        // fontSize: 14,
+                        fontWeight: "bold",
+                        formatter: (value) => {
+                            return this.parseTime(value, "{h}:{i}");
+                        },
+                    }
+                },
+                yAxis: {
+                    type: 'value',
+                    max: 100, // 设置y轴最大值为100
+                    // splitLine: {
+                    //     show: false // 去掉y轴的横线
+                    // }
+                },
+                series: [
+                    {
+                        name: '压力值',
+                        type: 'line',
+                        data: [],
+                        smooth: true
+                    }
+                    
+                ]
+            }
+
+        }
+    },
+    methods: {
+        getDataPage(){
+            this.loading = true;
+            this.queryParams.deviceId = this.deviceId;
+            this.queryParams.beginTime = this.currentDate + " 00:00:00";
+            this.queryParams.endTime = this.currentDate + " 23:59:59";
+            queryFatiguePageByDate(this.queryParams)
+                .then((response) => {
+                if (response.data) {
+                    this.dataList = response.data.rows;
+                    this.total = response.data.total;
+                    this.loading = false;
+                } else {
+                    console.warn("分页数据返回为空或格式不正确:", response);
+                }
+                })
+                .catch((error) => {
+                console.error("获取分页数据失败:", error);
+                });
+        },
+        datePickerchange() {
+            this.currentDate = this.currentDatePicker || this.parseTime(new Date(), "{y}-{m}-{d}");
+            if(this.detailsType){
+                this.getDataPage();
+            } else{
+                this.getDetailsData();
+            }
+        },
+        initChart() {
+            const chartElement = this.$refs.heartrateChart;
+            const sportChartElement = this.$refs.sportHeartrate;
+            if (chartElement) {
+                this.chart = echarts.init(chartElement);
+                this.seChartOptions()
+            }
+            if (sportChartElement) {
+                this.sportChart = echarts.init(sportChartElement);
+                this.sportChart.setOption(this.sportChartOption);
+            }
+        },
+        seChartOptions() {
+            this.chartOptions.xAxis.min = this.currentDate + ' 00:00:00'
+            this.chartOptions.xAxis.max = this.currentDate + ' 23:59:59'
+            this.chart.setOption(this.chartOptions);
+        },
+        // 获取体温折线图
+        getDetailsData() {
+            queryFatigue(this.currentDate, this.deviceId)
+                .then((response) => {
+                    if (response && response.data) {
+                        // console.log("------------------" + JSON.stringify(response.data, null, 2))
+                        const data = response.data.map((item) => (
+                            {
+                                value: [
+                                    new Date(item.createTime).getTime(),
+                                    item.pressure,
+                                ],
+                            }));
+                     
+
+                        // 将数据分别赋值给不同的 series
+                        this.chartOptions.series[0].data = data;
+                        // 更新图表
+                        if (this.detailsType == false) {
+                            this.initChart();
+                        }
+                    } else {
+                        console.warn("数据返回为空或格式不正确:", response);
+                    }
+                })
+                .catch((error) => {
+                    console.error("获取数据失败:", error);
+                });
+        },
+       
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+@mixin u-flex($flexD, $alignI, $justifyC) {
+    display: flex;
+    flex-direction: $flexD;
+    align-items: $alignI;
+    justify-content: $justifyC;
+}
+
+.fz15 {
+    font-size: 15px !important;
+}
+
+.boldtext {
+    font-size: 16px;
+    color: #292929;
+    line-height: 30px;
+    font-weight: 700;
+}
+
+.box-header {
+    padding: 0 15px;
+    @include u-flex(row, center, space-between);
+    height: 50px;
+}
+
+.box-title {
+    color: #292929;
+    font-size: 16px;
+    font-family: PingFang SC-Medium;
+    padding-left: 10px;
+    border-left: 4px solid #2284ff;
+    line-height: 13px;
+}
+
+.heartrate-data {
+    @include u-flex(row, center, space-around);
+    margin-top: 30px;
+
+    &-item {
+        height: 60px;
+        width: 196px;
+        border-radius: 4px;
+        box-shadow: 0 0 10px rgba(1, 24, 54, 0.1);
+        overflow: hidden;
+        @include u-flex(row, center, flex-start);
+
+        img {
+            height: 41px;
+            width: 41px;
+        }
+    }
+
+    &-imgbox {
+        flex-shrink: 0;
+        height: 60px;
+        width: 60px;
+        @include u-flex(row, center, center);
+    }
+
+    &-right {
+        flex: 1;
+        text-align: center;
+        font-size: 13px;
+        color: #606165;
+        font-family: PingFang SC-Medium;
+        background: #fff;
+        height: 60px;
+    }
+
+    &-num {
+        font-size: 22px;
+        color: #2c2c3b;
+        line-height: 38px;
+    }
+}
+
+.heartrate-sport {
+    @include u-flex(row, flex-start, flex-start);
+    margin-top: 20px;
+
+    &-l {
+        width: 490px;
+        flex-shrink: 0;
+    }
+
+    &-r {
+        flex: 1;
+        max-width: 720px;
+    }
+
+    &-lechart {
+        width: 490px;
+        height: 300px;
+    }
+
+    &-rechart {
+        width: 100%;
+        @include u-flex(row, center, center);
+    }
+}
+</style>

+ 6 - 1
src/components/DeviceInfo/Pulse.vue

@@ -70,8 +70,13 @@
           <el-table-column type="index" label="序号" align="center"/>
           <el-table-column label="时间" align="center" prop="time" />
           <el-table-column label="脉搏" align="center" prop="pulseRate" />
+          <template #empty>
+              <div>
+                  今日暂无数据,请选择其他日期
+              </div>
+          </template>
         </el-table>
-        <pagination v-show="(dataListTotal > 0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
+        <pagination v-show="(dataListTotal>0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
       </div>
     </div>
   </template>

+ 1942 - 0
src/components/DeviceInfo/Report.vue

@@ -0,0 +1,1942 @@
+<template>
+    <div class="weekly-report no-scrollbar">
+      <div class="report-header">
+        <div class="report-title">
+          <img class="title-icon" src="@/assets/images/watchApi/title-icon.png" />
+          <span>周报</span>
+        </div>
+        <div class="date-selector">
+          <el-date-picker
+            :picker-options="{ firstDayOfWeek: 1 }"
+            v-model="selectedDate"
+            type="week"
+            format="yyyy 第 WW 周"
+            placeholder="选择周"
+            @change="handleDateChange"
+          ></el-date-picker>
+        </div>
+      </div>
+  
+      <el-card class="report-card">
+        <div class="progress-section">
+          <div class="progress-container">
+            <el-progress type="circle" :percentage="healthIndex" :width="120" color="#FF9F43"></el-progress>
+            <div class="progress-info">
+              <div class="progress-label">健康指数</div>
+            </div>
+            <div class="health-status">{{ healthStatus }}</div>
+            <div class="health-description">{{ healthDesc }}</div>
+          </div>
+          
+          <div class="health-metrics-container">
+            <div class="health-metrics-row">
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ height }}<span class="metric-unit">cm</span></div>
+                  <div class="metric-label">身高</div>
+                </div>
+                <div class="metric-icon orange">
+                  <i class="el-icon-sort"></i>
+                </div>
+              </div>
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ weight }}<span class="metric-unit">kg</span></div>
+                <div class="metric-label">体重</div>
+                </div>
+                <div class="metric-icon green">
+                  <i class="el-icon-top"></i>
+                </div>
+              </div>
+            </div>
+            
+            <div class="health-metrics-row">
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ bmiStatus }}</div>
+                <div class="metric-label">BMI</div>
+                </div>
+                <div class="metric-icon blue">
+                  <i class="el-icon-plus"></i>
+                </div>
+              </div>
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ caloriesData.value }}<span class="metric-unit">{{ caloriesData.unit }}</span></div>
+                  <div class="metric-label">{{ caloriesData.label }}</div>
+                </div>
+                <div class="metric-icon red">
+                  <i class="el-icon-hot-water"></i>
+                </div>
+              </div>
+            </div>
+            
+            <div class="health-metrics-row">
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ stepsData.value }}<span class="metric-unit">{{ stepsData.unit }}</span></div>
+                  <div class="metric-label">{{ stepsData.label }}</div>
+                </div>
+                <div class="metric-icon orange">
+                  <i class="el-icon-s-promotion"></i>
+                </div>
+              </div>
+              <div class="metric-item">
+                <div class="metric-data">
+                  <div class="metric-value">{{ sleepCardData.status }}</div>
+                <div class="metric-label">{{ sleepCardData.label }}</div>
+                </div>
+                <div class="metric-icon purple">
+                  <i class="el-icon-moon"></i>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+  
+        <!-- 健康数据 (Health Data) Section -->
+        <div class="health-data-section">
+          <div class="section-title">健康数据</div>
+          
+          <!-- 血糖趋势 (Blood Sugar Trend) -->
+          <div class="trend-chart">
+            <div class="trend-title">血糖趋势</div>
+            <div class="trend-subtitle">单位: mmol/L</div>
+            <div class="line-chart-container" ref="bloodSugarChart"></div>
+            <div class="trend-advice">
+            </div>
+          </div>
+          
+          <!-- 尿酸趋势 (Uric Acid Trend) -->
+          <div class="trend-chart">
+            <div class="trend-title">尿酸趋势</div>
+            <div class="trend-subtitle">单位: μmol/L</div>
+            <div class="bar-chart-container" ref="uricAcidChart"></div>
+            <div class="legend">
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #4CD964;"></span>
+                <span>150~416μmol/L</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #FFCC00;"></span>
+                <span>&lt;150μmol/L</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #FF6B6B;"></span>
+                <span>&gt;416μmol/L</span>
+              </div>
+            </div>
+          </div>
+
+           <div class="trend-chart">
+            <div class="trend-title">血压趋势</div>
+            <div class="trend-subtitle">单位: mmgh</div>
+            <div class="line-chart-container" ref="bloodPressureChart"></div>
+          </div>
+
+           <div class="trend-chart">
+            <div class="trend-title">心率趋势</div>
+            <div class="trend-subtitle">单位: 次/分</div>
+            <div class="line-chart-container" ref="heartRateChart"></div>
+          </div>
+
+           <div class="trend-chart">
+            <div class="trend-title">血氧趋势</div>
+            <div class="trend-subtitle"></div>
+            <div class="line-chart-container" ref="bloodOxygenChart"></div>
+            <div class="legend">
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #4CD964;"></span>
+                <span>&lt;90%</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #FFCC00;"></span>
+                <span>&lt;95%</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #FF6B6B;"></span>
+                <span>其他</span>
+              </div>
+            </div>
+          </div>
+
+           <div class="trend-chart">
+            <div class="trend-title">睡眠趋势</div>
+            <div class="trend-subtitle"></div>
+            <div class="line-chart-container" ref="sleepChart"></div>
+            <div class="legend">
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #8C37E6;"></span>
+                <span>深睡</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #D138CF;"></span>
+                <span>浅睡</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #F88082;"></span>
+                <span>快速眼动</span>
+              </div>
+              <div class="legend-item">
+                <span class="legend-color" style="background-color: #FDBD27;"></span>
+                <span>清醒</span>
+              </div>
+            </div>
+          </div>
+          
+         
+        </div>
+  
+        
+        <div class="health-archive-section">
+          <div class="section-title">健康档案</div>
+          <div class="archive-items">
+            <!-- 健康预测 -->
+            <div class="archive-item">
+              <div class="archive-icon warning">
+                <i class="el-icon-warning-outline"></i>
+              </div>
+              <div class="archive-content">
+                <div class="archive-title">健康预测</div>
+                <div class="archive-text" v-for="(item, idx) in healthArchive" :key="'prediction' + idx">
+                  {{ item.prediction }}
+                </div>
+              </div>
+            </div>
+
+            <!-- 解析 -->
+            <div class="archive-item">
+              <div class="archive-icon analysis">
+                <i class="el-icon-data-analysis"></i>
+              </div>
+              <div class="archive-content">
+                <div class="archive-title">解析</div>
+                <div class="archive-text" v-for="(item, idx) in healthArchive" :key="'analysis' + idx">
+                  {{ item.analysis }}
+                </div>
+              </div>
+            </div>
+
+            <!-- 治疗建议 -->
+            <div class="archive-item">
+              <div class="archive-icon suggestion">
+                <i class="el-icon-first-aid-kit"></i>
+              </div>
+              <div class="archive-content">
+                <div class="archive-title">治疗建议</div>
+                <div class="archive-text" v-for="(item, idx) in healthArchive" :key="'suggestion' + idx">
+                  {{ item.suggestion }}
+                </div>
+              </div>
+            </div>
+
+            <!-- 注意事项 -->
+            <div class="archive-item">
+              <div class="archive-icon attention">
+                <i class="el-icon-info"></i>
+              </div>
+              <div class="archive-content">
+                <div class="archive-title">注意事项</div>
+                <div class="archive-text" v-for="(item, idx) in healthArchive" :key="'precautions' + idx">
+                  {{ item.precautions }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        
+        <!-- AI舌诊 (AI Tongue Diagnosis) Section -->
+        <div class="ai-diagnosis-section">
+          <div class="section-title">AI舌诊</div>
+          <div class="date-selector-tabs">
+            <div 
+              v-for="date in weekDates" 
+              :key="date"
+              :class="['date-tab', selectedDiagnosisDate === date ? 'active' : '']"
+              @click="selectedDiagnosisDate = date"
+            >
+              {{ date }}
+            </div>
+          </div>
+          
+          <div class="diagnosis-content" v-if="tongueData">
+            <div class="diagnosis-time">检测时间 {{ tongueData.createTime || '未知时间' }}</div>
+            
+            <div class="diagnosis-result">
+              <div class="diagnosis-label">您属于</div>
+              <div class="diagnosis-type">{{ tongueData.typeName || '未知体质' }}</div>
+            </div>
+            
+            <div class="diagnosis-section">
+              <div class="diagnosis-section-title">舌苔特征</div>
+              <div class="diagnosis-feature">
+                <div class="feature-dot"></div>
+                <div class="feature-title">{{ tongueData.taiseName || '未知' }}</div>
+                <div class="feature-description">{{ tongueData.taiseDesc || '未知' }}</div>
+              </div>
+              <div class="diagnosis-feature">
+                <div class="feature-dot"></div>
+                <div class="feature-title">{{ tongueData.shemianName || '未知' }}</div>
+                <div class="feature-description">{{ tongueData.shemianDesc || '未知' }}</div>
+              </div>
+            </div>
+            
+            <div class="diagnosis-section">
+              <!-- <div class="diagnosis-section-title">体质解析</div> -->
+              <div 
+                class="diagnosis-analysis" 
+                v-for="(item, index) in tongueData.typeJsonObj" 
+                :key="index"
+              >
+                <div class="diagnosis-section-title">{{ item.name }}</div>
+                <div class="analysis-content">{{ item.value }}</div>
+              </div>
+            </div>
+          </div>
+          <div class="diagnosis-content" v-else>
+            <div class="no-data">暂无舌诊数据</div>
+          </div>
+        </div>
+      </el-card>
+    </div>
+  </template>
+  
+  <script>
+  import * as echarts from 'echarts';
+  import {getUserHealthInfoByDeviceId,queryHealthReport,queryGlucoseList,queryUaList,queryPressureList,queryHrList,querySpo2List,querySleepList,queryTongueList} from "@/api/watch/deviceInfo";
+  export default {
+    props: {
+        // detailsType: { 
+        //     type: Boolean,
+        //     default: false
+        // },
+        userInfo: {
+          type: Object,
+          default: () => ({})
+        },
+        deviceId: {
+            type: String || Number,
+            default: "",
+        },
+    },
+    name: 'WeeklyReport',
+    data() {
+      return {
+        sleepDefaultInfo: {
+          4: { name: "浅睡", color: "#D138CF", yValue: 7 },
+          3: { name: "深睡", color: "#8C37E6", yValue: 3 },
+          6: { name: "清醒", color: "#FDBD27", yValue: 9 },
+          7: { name: "快速眼动", color: "#F88082", yValue: 12 },
+        },
+        startTime: null,
+        endTime:  null,
+        // 基础数据
+        height: '--',
+        weight: '--',
+        bmi: 0,
+        bmiStatus: '--',
+        healthIndex: 0,
+        healthStatus: '--',
+        healthDesc:"",
+        // 健康指标数据
+        caloriesData: {
+          value: '--',
+          unit: 'kcal',
+          label: '卡路里'
+        },
+        stepsData: {
+          value: '--',
+          unit: '步',
+          label: '步数'
+        },
+        sleepCardData: {
+          status: '--',
+          label: '睡眠'
+        },
+        selectedDate: new Date(),
+        // 健康档案
+        healthArchive: [],
+        selectedDiagnosisDate: '12/27',
+        bloodSugarChart: null,
+        uricAcidChart: null,
+        bloodPressureChart:null,
+        heartRateChart:null,
+        bloodOxygenChart:null,
+        sleepChart:null,
+        bloodSugarData: {
+          values: []
+        },
+       
+        uricAcidData: {
+          values: [
+          ]
+        },
+        bloodPressureData:{},
+        heartRateData:{},
+        bloodOxygenData:{values: []},
+        sleepData:[],
+        tongueData:null
+      };
+    },
+    mounted() {
+      this.$nextTick(() => {
+        this.initBloodSugarChart();
+        this.initBloodPressureChart();
+        this.initUricAcidChart();
+        this.initHeartRateChart();
+        this.initBloodOxygenChart();
+        this.initSleepChart();
+        this.setInitialDiagnosisDate();
+      });
+    },
+    computed: {
+      weekDates() {
+        const dates = [];
+        const startOfWeek = new Date(this.selectedDate);
+        startOfWeek.setDate(this.selectedDate.getDate() - (this.selectedDate.getDay() || 7) + 1); // Set to Monday
+        for (let i = 0; i < 7; i++) {
+          const date = new Date(startOfWeek);
+          date.setDate(startOfWeek.getDate() + i);
+          dates.push(this.formatDate(date));
+        }
+        return dates;
+      }
+    },
+    watch: {
+      userInfo: {
+        handler(newVal) {
+          if (newVal) {
+            this.updateUserInfo();
+          }
+        },
+        immediate: true,
+        deep: true
+      },
+      selectedDate() {
+        this.setInitialDiagnosisDate();
+      },
+      selectedDiagnosisDate(newVal) {
+        if (newVal) {
+          this.getTongueDate();
+        }
+      }
+    },
+    methods: {
+      formatDate(date) {
+        return `${date.getMonth() + 1}/${date.getDate()}`;
+      },
+      setInitialDiagnosisDate() {
+        this.selectedDiagnosisDate = this.weekDates[0] || null;
+      },
+      getWeekDates(date) {
+        const startOfWeek = new Date(date);
+        const endOfWeek = new Date(date);
+        startOfWeek.setDate(date.getDate() - (date.getDay() || 7) + 1); // 如果是周日,getDay() 返回 0,需要加 7
+         // 将日期设置为周一,时间为00:00:00
+        // startOfWeek.setDate(date.getDate() - date.getDay());
+        startOfWeek.setHours(0, 0, 0, 0);
+
+        // 将日期设置为周日,时间为23:59:59
+        endOfWeek.setDate(date.getDate() - (date.getDay() || 7) + 7); // 如果是周日,getDay() 返回 0,需要加 7
+        // endOfWeek.setDate(date.getDate() - date.getDay() + 6);
+        endOfWeek.setHours(23, 59, 59, 999);
+       
+        this.startTime= startOfWeek,
+        this.endTime=  endOfWeek
+      },
+      // 更新基本信息
+      updateUserInfo() {
+        if (this.userInfo) {
+          // 设置身高体重
+          this.height = this.userInfo.height || '--';
+          this.weight = this.userInfo.weight || '--';
+          
+          // 计算BMI
+          this.calculateBMI();}
+      },
+      // 计算BMI
+      calculateBMI() {
+        if (this.height && this.weight && this.height !== '--' && this.weight !== '--') {
+          // BMI = 体重(kg) / 身高(m)²
+          const heightInMeters = parseFloat(this.height) / 100;
+          const weightInKg = parseFloat(this.weight);
+          
+          if (!isNaN(heightInMeters) && !isNaN(weightInKg) && heightInMeters > 0) {
+            this.bmi = (weightInKg / (heightInMeters * heightInMeters)).toFixed(1);
+            
+            // 设置BMI状态
+            if (this.bmi < 18.5) {
+              this.bmiStatus = '偏瘦';
+            } else if (this.bmi >= 18.5 && this.bmi < 24) {
+              this.bmiStatus = '正常';
+            } else if (this.bmi >= 24 && this.bmi < 28) {
+              this.bmiStatus = '超重';
+            } else {
+              this.bmiStatus = '肥胖';
+            }
+          }
+        }
+      },
+      async getSportData() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime),
+            endTime: this.parseTime(this.endTime),
+          };
+          const response = await getUserHealthInfoByDeviceId(params);
+          if(response.data){
+            this.caloriesData.value = response.data.calorie;
+            this.stepsData.value = response.data.step;
+            this.sleepData.status = response.data.sleepStatus;
+          }
+        }catch(error){
+          console.error("获取步数,卡路里,睡眠等信息数据失败:", error);
+        }
+      },
+
+      async getHealthArchive() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime),
+            endTime: this.parseTime(this.endTime),
+          };
+          const response = await queryHealthReport(params);
+          if(response.data){
+            this.healthArchive = response.data.list;
+            this.healthIndex = response.data.score;
+            this.healthStatus = response.data.title;
+            this.healthDesc = response.data.desc;
+
+          }
+        }catch(error){
+          console.error("获取步数,卡路里,睡眠等信息数据失败:", error);
+        }
+      },
+      async getGlucoseDate() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await queryGlucoseList(params);
+          if(response.data && response.data.length > 0){
+            const values = response.data.map(item => {
+              const timestamp = new Date(item.createTime).getTime();
+              return [timestamp, item.bloodGlucose, item.createTime];
+            });
+            this.bloodSugarData = {
+              values: values
+            };
+        
+          } else {
+            this.bloodSugarData = {
+              values: []
+            };
+          }
+        }catch(error){
+          console.error("获取血糖数据失败:", error);
+          this.bloodSugarData = {
+            values: []
+          };
+        }
+      },
+
+      async getUaDate() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await queryUaList(params);
+          if(response.data && response.data.length > 0){
+            const values = response.data.map(item => {
+              const timestamp = new Date(item.createTime).getTime();
+              const val = item.val/10;
+              let color;
+              if (val < 150) {
+                  color = '#FFCC00';
+              } else if (val < 416) {
+                color = '#4CD964';
+              } else {
+                color = '#FF6B6B';
+              }
+              return [timestamp, val, item.createTime,color];
+            });
+            this.uricAcidData.values = values;
+        
+          } else {
+            this.uricAcidChart.values = [];
+          }
+        }catch(error){
+          console.error("获取尿酸数据失败:", error);
+          this.uricAcidChart.values = [];
+        }
+      },
+      async getSleepDate() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await querySleepList(params);
+          if(response.data.sleepSection && response.data.sleepSection.length > 0){
+            this.sleepData = response.data.sleepSection;
+          }
+        }catch(error){
+          console.error("获取睡眠数据失败:", error);
+        }
+      },
+      async querySpo2Data() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await querySpo2List(params);
+          if(response.data.list && response.data.list.length > 0){
+            const values = response.data.list.map(item => {
+              const timestamp = new Date(item.createTime).getTime();
+              const val = item.avgBoxy;
+              let color;
+              if (val < 150) {
+                  color = '#FFCC00';
+              } else if (val < 416) {
+                color = '#4CD964';
+              } else {
+                color = '#FF6B6B';
+              }
+              return [timestamp, val, item.createTime,color];
+            });
+            this.bloodOxygenData.values = values;
+        
+          } else {
+            this.bloodOxygenData.values = [];
+          }
+        }catch(error){
+          console.error("获取尿酸数据失败:", error);
+          this.bloodOxygenData.values = [];
+        }
+      },
+
+      async getHrDate() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await queryHrList(params);
+          if(response.data.list && response.data.list.length > 0){
+            const values = response.data.list.map(item => {
+              const timestamp = new Date(item.createTime).getTime();
+              return [timestamp, (item.maxBpm + item.minBpm)/2, item.createTime];
+            });
+            this.heartRateData = {
+              values: values
+            };
+           
+          } else {
+            this.heartRateData = {
+              values: []
+            };
+          }
+        }catch(error){
+          console.error("获取心率数据失败:", error);
+          this.bloodSugarData = {
+            values: []
+          };
+        }
+      },
+
+      
+      async queryPressureData() {
+        try{
+          const params = {
+            deviceId: this.deviceId,
+            startTime: this.parseTime(this.startTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+            endTime: this.parseTime(this.endTime,'{y}/{m}/{d} {h}:{i}:{s}'),
+          };
+          const response = await queryPressureList(params);
+          if(response.data && response.data.length > 0){
+            
+
+            const sbp = response.data.map((item) => ({
+              value: [
+                new Date(item.createTime).getTime(),
+                item.sbp,
+              ]
+            }));
+            const dbp = response.data.map((item) => ({
+              value: [
+                  new Date(item.createTime).getTime(),
+                  item.dbp,
+              ]
+            }));
+            const hr = response.data.map((item) => ({
+              value: [
+                  new Date(item.createTime).getTime(),
+                  item.hr,
+              ]
+            }));
+            const createTime = response.data.map((item) => ({
+              value: [
+                  item.createTime
+              ]
+            }));
+            this.bloodPressureData = {
+              sbp : sbp,
+              dbp : dbp,
+              hr : hr,
+              createTime:createTime,
+            };
+          } else {
+            this.bloodPressureData = {
+              sbp : [],
+              dbp : [],
+              hr :  [],
+              createTime:[]
+            };
+          }
+        }catch(error){
+          console.error("获取血压数据失败:", error);
+          this.bloodPressureData = {
+            sbp : [],
+            dbp : [],
+            hr :  [],
+            createTime:[]
+          };
+        }
+      },
+
+      formatCreateDay(dateStr) {
+        const [month, day] = dateStr.split('/').map(Number);
+        const year = this.selectedDate.getFullYear();
+        const date = new Date(year, month - 1, day);
+        return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
+      },
+
+      async getTongueDate(){
+        if(this.userInfo && this.userInfo.userId){
+          const params = {
+            userId: this.userInfo.userId,
+            createDay: this.formatCreateDay(this.selectedDiagnosisDate),
+          };
+          
+          const response = await queryTongueList(params);
+          if(response && response.rows.length > 0){
+            this.tongueData = response.rows[0]
+            this.tongueData.typeJsonObj = JSON.parse(this.tongueData.typeJson || '[]')
+            console.log(this.tongueData)
+          } else {
+            this.tongueData = null;
+          }
+        }
+      },
+      // 获取周报详情数据
+      async getDetailsData() {
+        try {
+          this.getWeekDates(this.selectedDate);
+          // 获取健康周报-步数,卡路里,睡眠等信息
+          await this.getSportData();
+          await this.getHealthArchive();
+          await this.getGlucoseDate();
+          await this.getUaDate();
+          await this.queryPressureData();
+          await this.getHrDate();
+          await this.querySpo2Data();
+          await this.getSleepDate();
+          await this.getTongueDate();
+          this.updateCharts();
+        } catch (error) {
+          console.error("获取周报数据失败:", error);
+        }
+      },
+
+        
+      handleDateChange(date) {
+        this.selectedDate = date || this.parseTime(new Date(), "{y}-{m}-{d}");
+        this.getDetailsData();
+      },
+     
+      initBloodSugarChart() {
+        const chartDom = this.$refs.bloodSugarChart;
+        this.bloodSugarChart = echarts.init(chartDom);
+        const option = {
+          tooltip: {
+            trigger: 'item',
+            formatter: function(params) {
+              if (!params.data || params.data.length < 3) {
+                return '无数据';
+              }
+              const value = params.data[1];
+              const createTime = params.data[2];
+              return `时间: ${createTime}<br/>血糖: ${value} mmol/L`;
+            }
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: 'time',
+            boundaryGap: false,
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime() + 1,
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              // fontSize: 14,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          yAxis: {
+            type: 'value',
+            min: 0,
+            max: 12,
+            interval: 2,
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          series: [
+            {
+              name: '血糖',
+              data: this.bloodSugarData.values,
+              type: 'line',
+              smooth: true,
+              symbol: 'circle',
+              symbolSize: 7,
+              itemStyle: {
+                color: '#FF9F43'
+              },
+              lineStyle: {
+                color: '#FF9F43'
+              },
+              areaStyle: {
+                color: {
+                  type: 'linear',
+                  x: 0,
+                  y: 0,
+                  x2: 0,
+                  y2: 1,
+                  colorStops: [
+                    {
+                      offset: 0,
+                      color: 'rgba(255, 159, 67, 0.3)'
+                    },
+                    {
+                      offset: 1,
+                      color: 'rgba(255, 159, 67, 0.1)'
+                    }
+                  ]
+                }
+              }
+            }
+          ]
+        };
+        
+        this.bloodSugarChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          this.bloodSugarChart.resize();
+        });
+      },
+      initBloodPressureChart() {
+        const chartDom = this.$refs.bloodPressureChart;
+        this.bloodPressureChart = echarts.init(chartDom);
+        const option = {
+          tooltip: {
+            trigger: 'axis',
+            formatter: function(params) {
+              if (!params) {
+                return '无数据';
+              }
+              const sbp = params[0].data.value;
+              const dbp = params[1].data.value;
+              const hr = params[2].data.value;
+              const createTime = new Date(sbp[0]).toLocaleString();
+              return `时间: ${createTime}<br/>sbp: ${sbp[1]} mmgh<br/>bdp: ${dbp[1]} mmgh<br/>hr: ${hr[1]} 次/分`;
+            }
+          },
+          legend: {},
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: "time",
+            boundaryGap: false,
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime(),
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+                show: false,
+            },
+            splitLine: {
+                show: false,
+            },
+          },
+          yAxis: {
+              type: 'value',
+              min: 0,
+              max: 180,
+              interval: 30,
+              axisLabel: {
+                  formatter: '{value}'
+              },
+              axisTick: {
+                show: false,
+              },
+              splitLine: {
+                show: false,
+              },
+          },
+          
+          series: [
+              {
+                  name: 'sbp(mmgh)',
+                  type: "line",
+                  symbol: "none",
+                  data: this.bloodPressureData.sbp
+              },
+              {
+                  name: 'dbp(mmgh)',
+                  type: "line",
+                  symbol: "none",
+                  data: this.bloodPressureData.dbp
+              },
+              {
+                  name: 'hr(次/分)',
+                  type: "line",
+                  symbol: "none",
+                  data: this.bloodPressureData.hr
+              }
+          ]
+          
+        };
+        
+        this.bloodPressureChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          this.bloodPressureChart.resize();
+        });
+      },
+      initUricAcidChart() {
+        const chartDom = this.$refs.uricAcidChart;
+        this.uricAcidChart = echarts.init(chartDom);
+
+        const data = this.uricAcidData.values.map((item, index) => {
+          return {
+            value: [item[0],item[1]],
+            itemStyle: {
+              color: item[3]
+            },
+            time:item[2]
+          };
+        });
+        
+        const option = {
+          tooltip: {
+            trigger: 'item',
+            formatter: function(params) {
+              if (!params.data || params.data.length < 4) {
+                return '无数据';
+              }
+              const value = params.data.value[1];
+              const createTime = params.data.time;
+              return `时间: ${createTime}<br/>尿酸: ${value} μmol/L`;
+            }
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: 'time',
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime() + 1,
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          yAxis: {
+            type: 'value',
+            min: 0,
+            max: 600,
+            interval: 100,
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          series: [
+            {
+              name: '尿酸',
+              type: 'bar',
+              data: data,
+              // barWidth: '40%'
+              barWidth: 10, // 设置柱子的宽度为 20 像素
+            }
+          ]
+        };
+        
+        this.uricAcidChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          if (this.uricAcidChart) {
+            this.uricAcidChart.resize();
+          }
+        });
+      },
+      initHeartRateChart(){
+        const chartDom = this.$refs.heartRateChart;
+        this.heartRateChart = echarts.init(chartDom);
+        const option = {
+          tooltip: {
+            trigger: 'item',
+            formatter: function(params) {
+              if (!params.data || params.data.length < 3) {
+                return '无数据';
+              }
+              const value = params.data[1];
+              const createTime = params.data[2];
+              return `时间: ${createTime}<br/>心率: ${value} 次/分`;
+            }
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: 'time',
+            boundaryGap: false,
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime() + 1,
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              // fontSize: 14,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          yAxis: {
+            type: 'value',
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          series: [
+            {
+              name: '心率',
+              data: this.heartRateData.values,
+              type: 'line',
+              smooth: true,
+              symbol: 'circle',
+              symbolSize: 7,
+              itemStyle: {
+                color: '#FF9F43'
+              },
+              lineStyle: {
+                color: '#FF9F43'
+              },
+              areaStyle: {
+                color: {
+                  type: 'linear',
+                  x: 0,
+                  y: 0,
+                  x2: 0,
+                  y2: 1,
+                  colorStops: [
+                    {
+                      offset: 0,
+                      color: 'rgba(255, 159, 67, 0.3)'
+                    },
+                    {
+                      offset: 1,
+                      color: 'rgba(255, 159, 67, 0.1)'
+                    }
+                  ]
+                }
+              }
+            }
+          ]
+        };
+        
+        this.heartRateChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          this.heartRateChart.resize();
+        });
+      },
+      initBloodOxygenChart(){
+        const chartDom = this.$refs.bloodOxygenChart;
+        this.bloodOxygenChart = echarts.init(chartDom);
+
+        const data = this.bloodOxygenData.values.map((item, index) => {
+          return {
+            value: [item[0],item[1]],
+            itemStyle: {
+              color: item[3]
+            },
+            time:item[2]
+          };
+        });
+        
+        const option = {
+          tooltip: {
+            trigger: 'item',
+            formatter: function(params) {
+              if (!params.data || params.data.length < 4) {
+                return '无数据';
+              }
+              const value = params.data.value[1];
+              const createTime = params.data.time;
+              return `时间: ${createTime}<br/>血氧: ${value} %`;
+            }
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: 'time',
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime() + 1,
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          yAxis: {
+            type: 'value',
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          series: [
+            {
+              name: '血氧',
+              type: 'bar',
+              data: data,
+              // barWidth: '40%'
+              barWidth: 10, // 设置柱子的宽度为 20 像素
+            }
+          ]
+        };
+        
+        this.bloodOxygenChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          if (this.bloodOxygenChart) {
+            this.bloodOxygenChart.resize();
+          }
+        });
+      },
+      initSleepChart(){
+        const chartDom = this.$refs.sleepChart;
+        this.sleepChart = echarts.init(chartDom);
+        const option = {
+          tooltip: {
+            show: true,
+            formatter: (params) => {
+              const start = params.value[3]
+              const end = params.value[4]
+              return `${start}到${end}<br/>${params.name}`;
+            },
+          },
+          grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            top: '3%',
+            containLabel: true
+          },
+          xAxis: {
+            type: 'time',
+            boundaryGap: false,
+            splitNumber: 7,
+            min:new Date(this.startTime).getTime(),
+            max:new Date(this.endTime).getTime() + 1,
+            axisLabel: {
+              color: "#999999",
+              margin: 20,
+              // fontSize: 14,
+              fontWeight: "bold",
+              formatter: (value) => {
+                return this.parseTime(value, "{m}/{d}");
+              },
+            },
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          yAxis: {
+            max: 14,
+            show: false,
+            axisTick: {
+              show: false,
+            },
+            splitLine: {
+              show: false,
+            },
+          },
+          series: [
+          {
+            type: "custom",
+            renderItem: (params, api) => {
+              let yValue = api.value(2);
+              let start = api.coord([api.value(0), yValue]);
+              let size = api.size([api.value(1) - api.value(0), yValue]);
+              let style = api.style();
+              return {
+                type: "rect",
+                shape: {
+                  x: start[0],
+                  y: start[1],
+                  width: size[0],
+                  height: size[1],
+                },
+                style: style,
+              };
+            },
+            data: [],
+          },
+          ]
+        };
+        
+        this.sleepChart.setOption(option);
+        
+        // 响应式调整
+        window.addEventListener('resize', () => {
+          this.sleepChart.resize();
+        });
+      },
+      updateCharts() {
+        if (this.bloodSugarChart) {
+          // 更新血糖图表数据
+          this.bloodSugarChart.setOption({
+            series: [
+              {
+                data: this.bloodSugarData.values
+              }
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime() + 1,
+            },
+          });
+        }
+
+        if (this.bloodPressureChart) {
+          this.bloodPressureChart.setOption({
+            series: [
+              {
+                name: 'sbp(mmgh)',
+                type: "line",
+                symbol: "none",
+                data: this.bloodPressureData.sbp
+              },
+              {
+                name: 'dbp(mmgh)',
+                type: "line",
+                symbol: "none",
+                data: this.bloodPressureData.dbp
+              },
+              {
+                name: 'hr(次/分)',
+                type: "line",
+                symbol: "none",
+                data: this.bloodPressureData.hr
+              },
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime()  + 1,
+            },
+          });
+        }
+        
+        if (this.uricAcidChart) {
+          // 更新尿酸图表数据
+          const data = this.uricAcidData.values.map((item, index) => {
+            return {
+              value: [item[0],item[1]],
+              itemStyle: {
+                color: item[3]
+              },
+              time:item[2]
+            };
+          });
+          
+          this.uricAcidChart.setOption({
+            series: [
+              {
+                // type: 'bar',
+                data: data
+              }
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime() + 1,
+            },
+          });
+        }
+        if (this.heartRateChart) {
+          this.heartRateChart.setOption({
+            series: [
+              {
+                data: this.heartRateData.values
+              }
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime() + 1,
+            },
+          });
+        }
+        if (this.bloodOxygenChart) {
+          const data = this.bloodOxygenData.values.map((item, index) => {
+            return {
+              value: [item[0],item[1]],
+              itemStyle: {
+                color: item[3]
+              },
+              time:item[2]
+            };
+          });
+          
+          this.bloodOxygenChart.setOption({
+            series: [
+              {
+                // type: 'bar',
+                data: data
+              }
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime() + 1,
+            },
+          });
+        }
+       
+        if (this.sleepChart) {
+          const chartData = this.sleepData.map((item, index) => {
+            return {
+              name: this.sleepDefaultInfo[item.type] && this.sleepDefaultInfo[item.type].name,
+              value: [
+                new Date(item.start).getTime(),
+                new Date(item.end).getTime(),
+                this.sleepDefaultInfo[item.type] && this.sleepDefaultInfo[item.type].yValue,
+                item.start,
+                item.end
+              ],
+              itemStyle: {
+                color: this.sleepDefaultInfo[item.type] && this.sleepDefaultInfo[item.type].color,
+              },
+            };
+          });
+          this.sleepChart.setOption({
+            series: [
+              {
+                data: chartData
+              }
+            ],
+            xAxis: {
+              min:new Date(this.startTime).getTime(),
+              max:new Date(this.endTime).getTime() + 1,
+            },
+          });
+        }
+      }
+    },
+    beforeDestroy() {
+      // 清除事件监听
+      window.removeEventListener('resize', this.resizeHandler);
+      
+      // 销毁图表实例
+      if (this.bloodSugarChart) {
+        this.bloodSugarChart.dispose();
+        this.bloodSugarChart = null;
+      }
+      if (this.bloodPressureChart) {
+        this.bloodPressureChart.dispose();
+        this.bloodPressureChart = null;
+      }
+      if (this.uricAcidChart) {
+        this.uricAcidChart.dispose();
+        this.uricAcidChart = null;
+      }
+      if (this.heartRateChart) {
+        this.heartRateChart.dispose();
+        this.heartRateChart = null;
+      }
+      if (this.bloodOxygenChart) {
+        this.bloodOxygenChart.dispose();
+        this.bloodOxygenChart = null;
+      }
+      if (this.sleepChart) {
+        this.sleepChart.dispose();
+        this.sleepChart = null;
+      }
+    }
+  };
+  </script>
+  
+  <style scoped lang="scss">
+  
+  /* 确保内容不会因为隐藏滚动条而被截断 */
+  .weekly-report {
+    padding: 20px;
+    background-color: #f5f7fa;
+    min-height: 100%;
+    overflow-y: auto;
+    overflow-x: hidden;
+  }
+  
+  .report-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+  }
+  
+  .report-title {
+    display: flex;
+    align-items: center;
+    font-size: 18px;
+    font-weight: bold;
+    color: #2284ff;
+    
+    .title-icon {
+      width: 13px;
+      height: 20px;
+      margin-right: 10px;
+    }
+  }
+  
+  .report-card {
+    margin-bottom: 20px;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+    overflow: hidden; /* 确保内容不会溢出卡片 */
+  }
+  
+  .progress-section {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 20px 0;
+    
+    @media (min-width: 768px) {
+      flex-direction: row;
+      justify-content: space-around;
+      align-items: flex-start;
+    }
+  }
+  
+  .progress-container {
+    position: relative;
+    margin-bottom: 20px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    
+    @media (min-width: 768px) {
+      margin-bottom: 0;
+    }
+    
+    .progress-info {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      text-align: center;
+      
+      .progress-value {
+        font-size: 24px;
+        font-weight: bold;
+        color: #FF9F43;
+        
+        .progress-unit {
+          font-size: 14px;
+        }
+      }
+      
+      .progress-label {
+        font-size: 12px;
+        color: #606266;
+      }
+    }
+    
+    .health-status {
+      margin-top: 10px;
+      font-size: 16px;
+      font-weight: bold;
+      color: #FF9F43;
+    }
+    .health-description {
+    margin-top: 8px;
+    font-size: 12px;
+    color: #606266;
+    text-align: center;
+    max-width: 240px;
+    line-height: 1.5;
+    padding: 0 10px;
+  }
+}
+  
+  .health-metrics-container {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    
+    @media (min-width: 768px) {
+      width: 60%;
+    }
+  }
+  
+  .health-metrics-row {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 10px;
+  }
+  
+  .metric-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background-color: #fff;
+    border-radius: 8px;
+    padding: 10px 15px;
+    width: 48%;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+    
+    .metric-data {
+      .metric-value {
+        font-size: 16px;
+        font-weight: bold;
+        
+        .metric-unit {
+          font-size: 12px;
+          font-weight: normal;
+        }
+      }
+      
+      .metric-label {
+        font-size: 12px;
+        color: #606266;
+      }
+    }
+    
+    .metric-icon {
+      width: 32px;
+      height: 32px;
+      border-radius: 50%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      
+      i {
+        font-size: 16px;
+        color: white;
+      }
+      
+      &.green {
+        background-color: #4CD964;
+      }
+      
+      &.blue {
+        background-color: #2284FF;
+      }
+      
+      &.orange {
+        background-color: #FF9F43;
+      }
+      
+      &.red {
+        background-color: #FF6B6B;
+      }
+      
+      &.purple {
+        background-color: #8A70D6;
+      }
+    }
+  }
+  
+  .section-title {
+    font-size: 16px;
+    font-weight: bold;
+    margin: 20px 0 15px;
+    color: #303133;
+    padding-left: 10px;
+  }
+  
+  /* 健康数据部分 */
+  .health-data-section {
+    padding: 0 10px;
+  }
+  
+  .trend-chart {
+    background-color: #fff;
+    border-radius: 8px;
+    padding: 15px;
+    margin-bottom: 20px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  }
+  
+  .trend-title {
+    font-size: 14px;
+    font-weight: bold;
+    color: #303133;
+  }
+  
+  .trend-subtitle {
+    font-size: 12px;
+    color: #909399;
+    margin-bottom: 10px;
+  }
+  
+  .line-chart-container,
+  .bar-chart-container {
+    height: 250px;
+    width: 100%;
+  }
+  
+  .trend-advice {
+    font-size: 12px;
+    color: #606266;
+    background-color: #f5f7fa;
+    padding: 10px;
+    border-radius: 4px;
+    margin-top: 10px;
+  }
+  
+  .legend {
+    display: flex;
+    flex-wrap: wrap;
+    margin-top: 10px;
+  }
+  
+  .legend-item {
+    display: flex;
+    align-items: center;
+    margin-right: 15px;
+    font-size: 12px;
+    color: #606266;
+  }
+  
+  .legend-color {
+    display: inline-block;
+    width: 12px;
+    height: 12px;
+    margin-right: 5px;
+    border-radius: 2px;
+  }
+  
+  .health-tabs {
+    margin-top: 20px;
+  }
+  
+  /* 健康档案部分 */
+  .health-archive-section {
+    padding: 0 10px;
+  }
+  
+  .archive-items {
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+  }
+  
+  .archive-item {
+    display: flex;
+    background-color: #fff;
+    border-radius: 8px;
+    padding: 15px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  }
+  
+  .archive-icon {
+    width: 36px;
+    height: 36px;
+    border-radius: 50%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-right: 15px;
+    flex-shrink: 0;
+    
+    i {
+      font-size: 18px;
+      color: white;
+    }
+    
+    &.warning {
+      background-color: #FF6B6B;
+    }
+    
+    &.analysis {
+      background-color: #4CD964;
+    }
+    
+    &.suggestion {
+      background-color: #2284FF;
+    }
+    
+    &.attention {
+      background-color: #FF9F43;
+    }
+  }
+  
+  .archive-content {
+    flex: 1;
+  }
+  
+  .archive-title {
+    font-size: 14px;
+    font-weight: bold;
+    color: #303133;
+    margin-bottom: 5px;
+  }
+  
+  .archive-text {
+    font-size: 12px;
+    color: #606266;
+    line-height: 1.5;
+  }
+  
+  /* AI舌诊部分 */
+  .ai-diagnosis-section {
+    padding: 0 10px 20px;
+  }
+  
+  .date-selector-tabs {
+    display: flex;
+    overflow-x: auto;
+    margin-bottom: 15px;
+  }
+  
+  .date-tab {
+    padding: 8px 15px;
+    background-color: #f5f7fa;
+    border-radius: 4px;
+    margin-right: 8px;
+    font-size: 14px;
+    cursor: pointer;
+    white-space: nowrap;
+    
+    &.active {
+      background-color: #FF9F43;
+      color: white;
+    }
+  }
+  
+  .diagnosis-content {
+    background-color: #fff;
+    border-radius: 8px;
+    padding: 15px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  }
+  
+  .diagnosis-time {
+    text-align: center;
+    font-size: 14px;
+    color: #909399;
+    margin-bottom: 15px;
+  }
+  
+  .diagnosis-result {
+    text-align: center;
+    margin-bottom: 20px;
+  }
+  
+  .diagnosis-label {
+    font-size: 14px;
+    color: #606266;
+  }
+  
+  .diagnosis-type {
+    font-size: 20px;
+    font-weight: bold;
+    color: #FF6B6B;
+    margin-top: 5px;
+  }
+  
+  .diagnosis-section {
+    margin-bottom: 20px;
+  }
+  
+  .diagnosis-section-title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #FF9F43;
+    margin-bottom: 10px;
+    position: relative;
+    padding-left: 10px;
+    
+    &:before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 4px;
+      height: 16px;
+      background-color: #FF9F43;
+      border-radius: 2px;
+    }
+  }
+  
+  .diagnosis-feature {
+    margin-bottom: 10px;
+    display: flex;
+    flex-wrap: wrap;
+  }
+  
+  .feature-dot {
+    width: 8px;
+    height: 8px;
+    background-color: #FF9F43;
+    border-radius: 50%;
+    margin-right: 8px;
+    margin-top: 6px;
+  }
+  
+  .feature-title {
+    font-size: 14px;
+    font-weight: bold;
+    color: #303133;
+    margin-right: 10px;
+  }
+  
+  .feature-description {
+    font-size: 12px;
+    color: #606266;
+    flex: 1;
+  }
+  
+  .diagnosis-analysis {
+    margin-bottom: 15px;
+  }
+  
+  .analysis-title {
+    font-size: 14px;
+    font-weight: bold;
+    color: #303133;
+    margin-bottom: 5px;
+  }
+  
+  .analysis-content {
+    font-size: 12px;
+    color: #606266;
+    line-height: 1.5;
+  }
+  
+  .diagnosis-characteristics {
+    margin-bottom: 10px;
+  }
+  
+  .characteristics-title {
+    font-size: 13px;
+    font-weight: bold;
+    color: #303133;
+    margin-bottom: 3px;
+  }
+  
+  .characteristics-content {
+    font-size: 12px;
+    color: #606266;
+    line-height: 1.5;
+  }
+  </style>

+ 13 - 183
src/components/DeviceInfo/SettingDialog/SetInfoDialog.vue

@@ -2,7 +2,6 @@
   <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body :close-on-click-modal="false">
     <el-form ref="form" :model="form" :rules="rules" label-width="80px">
       <el-row>
-        
         <el-col :span="24"
           v-if="type != 'goal' && type != 'languageSet' && type != 'messageSend' && type != 'fallcheckSensitivity' && type != 'measureIntervalHr' && type != 'measureIntervalOther' && type != 'phonebookSync'">
           <el-form-item label="开关设置" prop="flag">
@@ -10,51 +9,6 @@
             </el-switch>
           </el-form-item>
         </el-col>
-        <el-col :span="24" v-if="type == 'medication' && form.flag">
-          <el-form-item label="开始提醒时间" prop="medicationStart" label-width="110px">
-            <el-time-picker
-              value-format="HH:mm:ss"
-              v-model="form.medicationStart"
-              placeholder="请选择开始时间"
-              :picker-options="{
-                selectableRange: '00:00:00 - 23:59:59'
-              }"
-            ></el-time-picker>
-          </el-form-item>
-          <el-form-item label="结束提醒时间" prop="medicationEnd" label-width="110px">
-            <el-time-picker
-              value-format="HH:mm:ss"
-              v-model="form.medicationEnd"
-              placeholder="请选择结束时间"
-              :picker-options="{
-                selectableRange: '00:00:00 - 23:59:59'
-              }"
-            ></el-time-picker>
-          </el-form-item>
-        </el-col>
-        <el-col :span="24" v-if="type == 'study' && form.flag">
-          <el-form-item label="开始提醒时间" prop="studyStart" label-width="110px">
-            <el-time-picker
-              value-format="HH:mm:ss"
-              v-model="form.studyStart"
-              placeholder="请选择开始时间"
-              :picker-options="{
-                selectableRange: '00:00:00 - 23:59:59'
-              }"
-            ></el-time-picker>
-          </el-form-item>
-          <el-form-item label="结束提醒时间" prop="studyEnd" label-width="110px">
-            <el-time-picker
-              value-format="HH:mm:ss"
-              v-model="form.studyEnd"
-              placeholder="请选择结束时间"
-              :picker-options="{
-                selectableRange: '00:00:00 - 23:59:59'
-              }"
-            ></el-time-picker>
-          </el-form-item>
-        </el-col>
-
         <el-col :span="24" v-if="type == 'autolocate' || type == 'datafreq'">
           <el-form-item label="设备省电设置" prop="mode" label-width="105px">
             <el-radio-group v-model="form.mode">
@@ -303,8 +257,6 @@ import {
   fallcheck,
   autolocate,
   datafreq,
-  medication,
-  study,
   lcdgesture,
   hralarm,
   spo2alarm,
@@ -316,8 +268,7 @@ import {
   messageSend,
   fallcheckSensitivity,
   measureIntervalHr,
-  measureIntervalOther,
-  getSet
+  measureIntervalOther
 } from "@/api/watch/deviceInfoSet.js";
 
 export default {
@@ -329,50 +280,6 @@ export default {
         callback();
       }
     };
-    const checkStudyTimes = (rule, value, callback, type) => {
-      if (!value) {
-        return callback(new Error('时间不能为空'));
-      }
-
-      const start = this.form.studyStart;
-      const end = this.form.studyEnd;
-
-      if (type === 'start') {
-        if (end && start && new Date(start) >= new Date(end)) {
-          callback(new Error('开始时间必须小于结束时间'));
-        } else {
-          callback();
-        }
-      } else if (type === 'end') {
-        if (start && new Date(start) > new Date(end)) {
-          callback(new Error('结束时间必须大于开始时间'));
-        } else {
-          callback();
-        }
-      }
-    };
-    const checkMedicationTimes = (rule, value, callback, type) => {
-      if (!value) {
-        return callback(new Error('时间不能为空'));
-      }
-
-      const start = this.form.medicationStart;
-      const end = this.form.medicationEnd;
-
-      if (type === 'start') {
-        if (end && start && new Date(start) >= new Date(end)) {
-          callback(new Error('开始时间必须小于结束时间'));
-        } else {
-          callback();
-        }
-      } else if (type === 'end') {
-        if (start && new Date(start) > new Date(end)) {
-          callback(new Error('结束时间必须大于开始时间'));
-        } else {
-          callback();
-        }
-      }
-    };
     //  检查翻腕亮屏开始时间
     const checkStart = (rule, value, callback) => {
       if (value && this.form.end) {
@@ -568,12 +475,6 @@ export default {
       open: false,
       // 表单参数
       form: {},
-      //
-      param:{
-        flag:null,
-        start:null,
-        end:null
-      },
       // 表单校验
       rules: {
         time: [
@@ -587,22 +488,6 @@ export default {
           { required: true, message: "结束时间不能为空", trigger: ["blur", "change"] },
           { validator: checkEnd, trigger: ["blur", "change"] }
         ],
-        medicationStart: [
-          { required: true, message: "开始时间不能为空", trigger: "blur" },
-          { validator: (rule, value, callback) => checkMedicationTimes(rule, value, callback, 'start'), trigger: "blur" }
-        ],
-        medicationEnd: [
-          { required: true, message: "结束时间不能为空", trigger: "blur" },
-          { validator: (rule, value, callback) => checkMedicationTimes(rule, value, callback, 'end'), trigger: "blur" }
-        ],
-        studyStart: [
-          { required: true, message: "看课提醒开始时间不能为空", trigger: "blur" },
-          { validator: (rule, value, callback) => checkStudyTimes(rule, value, callback, 'start'), trigger: "blur" }
-        ],
-        studyEnd: [
-          { required: true, message: "看课提醒结束时间不能为空", trigger: "blur" },
-          { validator: (rule, value, callback) => checkStudyTimes(rule, value, callback, 'end'), trigger: "blur" }
-        ],
         high: [
           { required: true, message: "正常心率最高值不能为空", trigger: ["blur", "change"] },
           { validator: checkHigh, trigger: ["blur", "change"] }
@@ -732,14 +617,11 @@ export default {
         description: undefined, // 发送设备消息 描述
         fallThreshold: 14000, // 跌倒检测的灵敏度,默认14000
         measureIntervalHr: undefined, // 心率数据测量间隔设置 分钟
-        medicationStart: undefined, // 用药提醒的开始时间
-        medicationEnd: undefined, // 用药提醒的结束时间
-        studyStart: undefined, // 看课提醒的开始时间
-        studyEnd: undefined, // 看课提醒的结束时间
       };
       this.resetForm("form");
       this.btnLoading = false
     },
+    /** 修改按钮操作 */
     handleSet(deviceid, deviceNumber, item) {
       this.reset();
       this.form.deviceid = deviceid
@@ -748,35 +630,10 @@ export default {
       this.title = item.label;
       this.type = item.eventName
     },
-    handleTimeSet(item) {
-      console.log('设置用药提醒发送时间');
-      //查询对应设置
-      this.reset();
-      this.type = item.eventName;
-      this.open = true;
-      getSet({type:this.type}).then(response => {
-        this.param = response.data;
-        console.log("==========================",JSON.stringify(this.param,null,2));
-        this.form.flag = this.param.flag;
-        console.log("==========================开关:"+ this.form.flag);
-        this.title = item.label;
-        if(this.type == 'medication'){
-          this.form.medicationStart = this.param.start;
-          this.form.medicationEnd = this.param.end;
-        } else if(this.type == 'study'){
-          this.form.studyStart = this.param.start;
-          this.form.studyEnd = this.param.end;
-          console.log("==========================开关:"+ JSON.stringify(this.form,null,2));
-        }
-      });
-      
-    },
     /** 提交按钮 */
     submitForm() {
       this.$refs["form"].validate((valid) => {
-        if(this.type=='medication' || this.type=='study'){
-          this.confirmFun()
-        } else if (valid) {
+        if (valid) {
           if (this.form.deviceNumber != undefined) {
             this.confirmFun()
           } else {
@@ -791,31 +648,16 @@ export default {
         fallcheck: `是否确认${flagTxt}设备信息编号为${this.form.deviceNumber}的跌倒检测?`,
         autolocate: `是否确认${flagTxt}设备信息编号为${this.form.deviceNumber}的自动定位?`,
       }
-      if(this.type=='medication' || this.type=='study'){
-        this.$confirm(`是否确认对所有设备进行以上操作?`, "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(() => {
-          this.handleFun()
-        }).then(() => {
-          this.open = false
-          // this.$emit("refreshList")
-        }).catch(() => { });
-        
-      } else {
-        this.$confirm(typeTxt[this.type] || `是否确认对设备id为${this.form.deviceNumber}进行以上操作?`, "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(() => {
-          this.handleFun()
-        }).then(() => {
-          this.open = false
-          // this.$emit("refreshList")
-        }).catch(() => { });
-        }
-      
+      this.$confirm(typeTxt[this.type] || `是否确认对设备id为${this.form.deviceNumber}进行以上操作?`, "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.handleFun()
+      }).then(() => {
+        this.open = false
+        // this.$emit("refreshList")
+      }).catch(() => { });
     },
     handleFun() {
       const options = {
@@ -838,18 +680,6 @@ export default {
           time: this.form.time,
           mode: this.form.mode
         }),
-        // 用药提醒设置
-        medication: () => medication({
-          flag: this.form.flag,
-          start: this.form.flag ? this.form.medicationStart : undefined,
-          end: this.form.flag ? this.form.medicationEnd : undefined
-        }),
-        // 看课提醒设置
-        study: () => study({
-          flag: this.form.flag,
-          start: this.form.flag ? this.form.studyStart : undefined,
-          end: this.form.flag ? this.form.studyEnd : undefined
-        }),
         // 翻腕亮屏设置
         lcdgesture: () => lcdgesture({
           deviceNumber: this.form.deviceNumber,

+ 22 - 27
src/components/DeviceInfo/Sleep.vue

@@ -117,8 +117,13 @@
             <span v-if="scope.row.type === 7">快速眼动</span>
           </template>
         </el-table-column>
+        <template #empty>
+            <div>
+                今日暂无数据,请选择其他日期
+            </div>
+        </template>
       </el-table>
-      <pagination v-show="(total > 0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
+      <pagination v-show="(total>0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
     </div> 
   </div>
 </template>
@@ -157,10 +162,10 @@ export default {
       startTime: "2024-08-04 20:58:00",
       endTime: "2024-08-05 00:27:00",
       sleepDefaultInfo: {
-        4: { name: "浅睡", color: "#93c0fa", yValue: 7 },
-        3: { name: "深睡", color: "#427ff7", yValue: 3 },
-        6: { name: "清醒", color: "#fadeaf", yValue: 9 },
-        7: { name: "快速眼动", color: "#9899f5", yValue: 12 },
+        1: { name: "浅睡", color: "#93c0fa", yValue: 7 },
+        2: { name: "深睡", color: "#427ff7", yValue: 3 },
+        3: { name: "清醒", color: "#fadeaf", yValue: 9 },
+        4: { name: "快速眼动", color: "#9899f5", yValue: 12 },
       },
       sleepInfo: {
         score: '',
@@ -390,58 +395,48 @@ export default {
 
             //统计
             this.sleepInfo.score = response.data.score==null?0:response.data.score;
-            
             //浅睡
             if (response.data.lightSleep != null) {
-              // this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
+              this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
               this.sleepInfo.light_sleep = response.data.lightSleep;
 
             } else{
-              // this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
+              this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
               this.sleepInfo.light_sleep = 0;
             }
-            this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
-
             //深睡
             if (response.data.deepSleep != null) {
-              // this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
+              this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
               this.sleepInfo.deep_sleep = response.data.deepSleep;
 
             } else{
-              // this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
+              this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
               this.sleepInfo.deep_sleep = 0;
             }
-            this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
             //清醒
             if (response.data.weakSleep != null) {
-              // this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
+              this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
               this.sleepInfo.weak_sleep = response.data.weakSleep;
 
             } else{
-              // this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
+              this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
               this.sleepInfo.weak_sleep = 0;
             }
-            this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
             //快速眼动
             if (response.data.eyemoveSleep != null) {
-              // this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
+              this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
               this.sleepInfo.eyemove_sleep = response.data.eyemoveSleep;
 
             } else{
-              // this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
+              this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
               this.sleepInfo.eyemove_sleep = 0;
             }
-            this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
-            // if (this.sportChart) {
-            //   this.sportChart.setOption(this.sportChartOption);
-            // }
+            if (this.sportChart) {
+              this.sportChart.setOption(this.sportChartOption);
+            }
 
           } else {
-            this.defaultChart();
-            console.warn("睡眠数据返回为空", response);
-          }
-          if (this.sportChart) {
-              this.sportChart.setOption(this.sportChartOption);
+            console.warn("睡眠数据返回为空或格式不正确:", response);
           }
         })
         .catch((error) => {

+ 6 - 1
src/components/DeviceInfo/Sports.vue

@@ -29,8 +29,13 @@
       <el-table-column prop="sportTime" label="运动时长(min)" />
       <el-table-column prop="startTime" label="开始时间" />
       <el-table-column prop="type" label="运动类型" />
+      <template #empty>
+          <div>
+              今日暂无数据,请选择其他日期
+          </div>
+      </template>
     </el-table>
-    <pagination v-show="(total > 0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
+    <pagination v-show="(total>0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
     </div>
   </div>
 </template>

+ 9 - 5
src/components/DeviceInfo/Temperature.vue

@@ -24,9 +24,13 @@
                 <el-table-column prop="createTime" label="时间" />
                 <el-table-column prop="estTemp" label="体温(℃)" />
                 <el-table-column prop="shellTemp" label="体表温度(℃)" />
-                
+                <template #empty>
+                    <div>
+                        今日暂无数据,请选择其他日期
+                    </div>
+                </template>
             </el-table>
-            <pagination v-show="(total > 0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
+            <pagination v-show="(total>0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
         </div>
         
     </div>
@@ -70,9 +74,9 @@ export default {
                     boundaryGap: false,
                     type: "time",
                     splitNumber: 6,
-                    axisLine: {
-                        show: false,
-                    },
+                    // axisLine: {
+                    //     show: false,
+                    // },
                     axisTick: {
                         show: false,
                     },

+ 5 - 0
src/components/DeviceInfo/Uricacid.vue

@@ -17,6 +17,11 @@
             </el-table-column>
             <el-table-column prop="val" label="测量结果">
             </el-table-column>
+            <template #empty>
+                <div>
+                    今日暂无数据,请选择其他日期
+                </div>
+            </template>
         </el-table>
 
     </div>

+ 5 - 0
src/components/DeviceInfo/Urineketones.vue

@@ -16,6 +16,11 @@
             </el-table-column>
             <el-table-column prop="val" align="center" label="测量结果">
             </el-table-column>
+            <template #empty>
+                <div>
+                    今日暂无数据,请选择其他日期
+                </div>
+            </template>
         </el-table>
 
     </div>

+ 2 - 2
src/views/company/aiModel/account/info.vue

@@ -226,7 +226,7 @@ export default {
                 id: undefined,
                 name: '',
                 providerClassName: '',
-                concurrentNum: '',
+                concurrentNum: null,
                 interruptFlag: 0,
                 interruptKeywords: '',
                 interruptIgnoreKeywords: '呃 哦 哦哦 嗯 嗯嗯 嗯好的 好的 对 对对 是的 明白 啊 这样啊 是这样啊这样的 您好 你好',
@@ -344,7 +344,7 @@ export default {
             this.form.id = data.id
             this.form.name = data.name || ''
             this.form.providerClassName = data.providerClassName || ''
-            this.form.concurrentNum = data.concurrentNum || ''
+            this.form.concurrentNum = data.concurrentNum
             this.form.interruptFlag = data.interruptFlag || 0
             this.form.interruptKeywords = data.interruptKeywords || ''
             this.form.interruptIgnoreKeywords = data.interruptIgnoreKeywords || ''

+ 672 - 0
src/views/company/aiModel/inboundCallManage/inboundCallRecord.vue

@@ -0,0 +1,672 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索表单 -->
+    <el-form
+      v-show="showSearch"
+      ref="queryForm"
+      :inline="true"
+      :model="queryParams"
+      label-width="100px"
+    >
+      <el-form-item label="通话UUID" prop="uuid">
+        <el-input
+          v-model="queryParams.uuid"
+          clearable
+          placeholder="请输入通话UUID"
+          size="small"
+          style="width: 200px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="主叫号码" prop="caller">
+        <el-input
+          v-model="queryParams.caller"
+          clearable
+          placeholder="请输入主叫号码"
+          size="small"
+          style="width: 200px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="接听分机号" prop="extnum">
+        <el-input
+          v-model="queryParams.extnum"
+          clearable
+          placeholder="请输入接听分机号"
+          size="small"
+          style="width: 200px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="通话时长(秒)">
+        <el-input-number
+          v-model="queryParams.params.timeLenStart"
+          :min="0"
+          size="small"
+          style="width: 120px"
+          placeholder="最小值"
+          controls-position="right"
+        />
+        <span class="range-separator">-</span>
+        <el-input-number
+          v-model="queryParams.params.timeLenEnd"
+          :min="0"
+          size="small"
+          style="width: 120px"
+          placeholder="最大值"
+          controls-position="right"
+        />
+      </el-form-item>
+      <el-form-item label="呼入时间">
+        <el-date-picker
+          v-model="inboundTimeRange"
+          type="datetimerange"
+          size="small"
+          style="width: 340px"
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="yyyy-MM-dd HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item label="接听时间">
+        <el-date-picker
+          v-model="answeredTimeRange"
+          type="datetimerange"
+          size="small"
+          style="width: 340px"
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="yyyy-MM-dd HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item label="挂机时间">
+        <el-date-picker
+          v-model="hangupTimeRange"
+          type="datetimerange"
+          size="small"
+          style="width: 340px"
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          value-format="yyyy-MM-dd HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button 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" />
+    </el-row>
+
+    <!-- 数据表格 -->
+    <el-table
+      v-loading="loading"
+      :data="tableData"
+      border
+      style="width: 100%"
+    >
+      <el-table-column align="center" label="UUID" prop="uuid" min-width="120" show-overflow-tooltip />
+      <el-table-column align="center" label="主叫号码" prop="caller" min-width="110" show-overflow-tooltip />
+       <el-table-column align="center" label="被叫号码" prop="callee" min-width="110" show-overflow-tooltip />
+      <el-table-column align="center" label="录音文件" min-width="80">
+        <template slot-scope="scope">
+          <el-tag v-if="getMediaType(scope.row.wavFile) === 'audio'" type="primary" size="small">音频</el-tag>
+          <el-tag v-else-if="getMediaType(scope.row.wavFile) === 'video'" type="success" size="small">视频</el-tag>
+          <span v-else>无</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="呼入时间" min-width="150">
+        <template slot-scope="scope">{{ formatTimestamp(scope.row.inboundTime) }}</template>
+      </el-table-column>
+      <el-table-column align="center" label="接听时间" min-width="150">
+        <template slot-scope="scope">{{ formatTimestamp(scope.row.answeredTime) }}</template>
+      </el-table-column>
+      <el-table-column align="center" label="接听分机" prop="extnum" min-width="90" />
+      <el-table-column align="center" label="接听坐席" prop="opnum" min-width="90" />
+      <el-table-column align="center" label="挂机时间" min-width="150">
+        <template slot-scope="scope">{{ formatTimestamp(scope.row.hangupTime) }}</template>
+      </el-table-column>
+      <el-table-column align="center" label="业务组" prop="groupName" min-width="100" show-overflow-tooltip />
+      <el-table-column align="center" label="通话时长" min-width="90">
+        <template slot-scope="scope">{{ formatDuration(scope.row.timeLen) }}</template>
+      </el-table-column>
+      <!-- <el-table-column align="center" label="挂机原因" min-width="120" show-overflow-tooltip>
+        <template slot-scope="scope">
+          <span
+            class="hangup-cause-cell"
+            :title="scope.row.hangupCause"
+            @dblclick="copyText(formatHangupCause(scope.row.hangupCause))"
+          >{{ formatHangupCause(scope.row.hangupCause) }}</span>
+        </template>
+      </el-table-column> -->
+      <el-table-column
+        align="center"
+        class-name="small-padding fixed-width"
+        label="操作"
+        width="160"
+      >
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.wavFileUrl"
+            size="mini"
+            type="text"
+            icon="el-icon-video-play"
+            @click="handlePlay(scope.row)"
+          >播放</el-button>
+          <el-button
+            v-if="scope.row.wavFileUrl"
+            size="mini"
+            type="text"
+            icon="el-icon-download"
+            @click="handleDownload(scope.row)"
+          >下载</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"
+    />
+
+    <!-- 播放弹窗 -->
+    <el-dialog
+      title="录音播放"
+      :visible.sync="playDialogVisible"
+      width="700px"
+      append-to-body
+      @close="handlePlayDialogClose"
+    >
+      <!-- 音频/视频播放器 -->
+      <div v-if="currentMediaType === 'audio'" class="player-wrapper">
+        <audio
+          ref="audioPlayer"
+          :src="currentWavFileUrl"
+          controls
+          style="width: 100%"
+        />
+      </div>
+      <div v-else-if="currentMediaType === 'video'" class="player-wrapper">
+        <video
+          ref="videoPlayer"
+          :src="currentWavFileUrl"
+          controls
+          style="width: 100%"
+        />
+      </div>
+
+      <!-- AI对话内容 -->
+      <div v-if="chatDialogList.length > 0" class="chat-container">
+        <div class="chat-title">AI对话内容</div>
+        <div class="chat-list">
+          <div
+            v-for="(item, index) in chatDialogList"
+            :key="index"
+            class="dialog-item"
+            :class="item.role"
+          >
+            <!-- 左侧角色(assistant / agent / kb) -->
+            <template v-if="item.role !== 'user'">
+              <span class="role-icon">
+                <i :class="getRoleIcon(item.role)" />
+              </span>
+              <span class="role-label">{{ getRoleLabel(item.role) }}</span>
+              <div class="bubble">
+                <span class="content-text">
+                  {{ item.expanded ? item.content : getTruncatedContent(item.content) }}
+                  <span
+                    v-if="item.content.length > 200"
+                    class="more-text"
+                    @click="toggleExpand(index)"
+                  >{{ item.expanded ? '[收起]' : '[更多]' }}</span>
+                </span>
+                <el-tag v-if="item.isKb" size="mini" type="warning" class="kb-tag">知识库来源</el-tag>
+              </div>
+            </template>
+            <!-- 右侧角色(user) -->
+            <template v-else>
+              <div class="bubble">
+                <span class="content-text">
+                  {{ item.expanded ? item.content : getTruncatedContent(item.content) }}
+                  <span
+                    v-if="item.content.length > 200"
+                    class="more-text"
+                    @click="toggleExpand(index)"
+                  >{{ item.expanded ? '[收起]' : '[更多]' }}</span>
+                </span>
+              </div>
+              <span class="role-icon">
+                <i class="el-icon-user" />
+              </span>
+              <span class="role-label">客户</span>
+            </template>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listInboundCdr } from '@/api/company/inboundCallManage'
+
+export default {
+  name: 'InboundCallRecord',
+  data() {
+    return {
+      // 遮罩层
+      loading: false,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 表格数据
+      tableData: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        uuid: undefined,
+        caller: undefined,
+        extnum: undefined,
+        params: {
+          timeLenStart: undefined,
+          timeLenEnd: undefined,
+          inboundTimeStart: undefined,
+          inboundTimeEnd: undefined,
+          answeredTimeStart: undefined,
+          answeredTimeEnd: undefined,
+          hangupTimeStart: undefined,
+          hangupTimeEnd: undefined
+        }
+      },
+      // 时间范围
+      inboundTimeRange: [],
+      answeredTimeRange: [],
+      hangupTimeRange: [],
+      // 播放弹窗
+      playDialogVisible: false,
+      currentWavFileUrl: '',
+      currentMediaType: '',
+      // AI对话列表
+      chatDialogList: []
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      this.loading = true
+      // 处理时间范围参数(转为epoch毫秒时间戳,与后端Long类型字段匹配)
+      if (this.inboundTimeRange && this.inboundTimeRange.length === 2) {
+        this.queryParams.params.inboundTimeStart = new Date(this.inboundTimeRange[0]).getTime()
+        this.queryParams.params.inboundTimeEnd = new Date(this.inboundTimeRange[1]).getTime()
+      } else {
+        this.queryParams.params.inboundTimeStart = undefined
+        this.queryParams.params.inboundTimeEnd = undefined
+      }
+      if (this.answeredTimeRange && this.answeredTimeRange.length === 2) {
+        this.queryParams.params.answeredTimeStart = new Date(this.answeredTimeRange[0]).getTime()
+        this.queryParams.params.answeredTimeEnd = new Date(this.answeredTimeRange[1]).getTime()
+      } else {
+        this.queryParams.params.answeredTimeStart = undefined
+        this.queryParams.params.answeredTimeEnd = undefined
+      }
+      if (this.hangupTimeRange && this.hangupTimeRange.length === 2) {
+        this.queryParams.params.hangupTimeStart = new Date(this.hangupTimeRange[0]).getTime()
+        this.queryParams.params.hangupTimeEnd = new Date(this.hangupTimeRange[1]).getTime()
+      } else {
+        this.queryParams.params.hangupTimeStart = undefined
+        this.queryParams.params.hangupTimeEnd = undefined
+      }
+      // 通话时长:前端输入秒,数据库存毫秒,显示用Math.ceil(ms/1000)
+      // Math.ceil(v/1000)==N 等价于 (N-1)*1000 < v <= N*1000,即 v∈[(N-1)*1000+1, N*1000]
+      const params = { ...this.queryParams }
+      params.params = { ...this.queryParams.params }
+      if (params.params.timeLenStart != null && params.params.timeLenStart !== undefined) {
+        const n = params.params.timeLenStart
+        params.params.timeLenStart = n > 0 ? (n - 1) * 1000 + 1 : 0
+      }
+      if (params.params.timeLenEnd != null && params.params.timeLenEnd !== undefined) {
+        params.params.timeLenEnd = params.params.timeLenEnd * 1000
+      }
+      listInboundCdr(params).then(response => {
+        this.tableData = response.rows || []
+        this.total = response.total || 0
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.inboundTimeRange = []
+      this.answeredTimeRange = []
+      this.hangupTimeRange = []
+      this.resetForm('queryForm')
+      this.queryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        uuid: undefined,
+        caller: undefined,
+        extnum: undefined,
+        params: {
+          timeLenStart: undefined,
+          timeLenEnd: undefined,
+          inboundTimeStart: undefined,
+          inboundTimeEnd: undefined,
+          answeredTimeStart: undefined,
+          answeredTimeEnd: undefined,
+          hangupTimeStart: undefined,
+          hangupTimeEnd: undefined
+        }
+      }
+      this.handleQuery()
+    },
+    /** 格式化时间戳 - 毫秒级时间戳转 yyyy-MM-dd HH:mm:ss */
+    formatTimestamp(value) {
+      if (!value || value <= 0) return '-'
+      const date = new Date(Number(value))
+      const year = date.getFullYear()
+      const month = (date.getMonth() + 1).toString().padStart(2, '0')
+      const day = date.getDate().toString().padStart(2, '0')
+      const hour = date.getHours().toString().padStart(2, '0')
+      const minute = date.getMinutes().toString().padStart(2, '0')
+      const second = date.getSeconds().toString().padStart(2, '0')
+      return `${year}-${month}-${day} ${hour}:${minute}:${second}`
+    },
+    /** 格式化通话时长 - 毫秒转 mm分ss秒 */
+    formatDuration(value) {
+      if (!value || value <= 0) return '0秒'
+      const totalSeconds = Math.ceil(value / 1000)
+      const minutes = Math.floor(totalSeconds / 60)
+      const seconds = totalSeconds % 60
+      if (minutes > 0) {
+        return `${minutes}分${seconds.toString().padStart(2, '0')}秒`
+      }
+      return `${seconds}秒`
+    },
+    /** 判断媒体类型 */
+    getMediaType(wavFile) {
+      if (!wavFile) return 'none'
+      const ext = wavFile.split('.').pop().toLowerCase()
+      if (['wav', 'mp3', 'aac'].includes(ext)) return 'audio'
+      if (['mp4', 'avi', 'mov'].includes(ext)) return 'video'
+      return 'none'
+    },
+    /** 格式化挂机原因 */
+    formatHangupCause(value) {
+      if (!value) return '-'
+      try {
+        const obj = JSON.parse(value)
+        if (obj && obj.code !== undefined) {
+          return `${obj.code}: ${obj.details || ''}`
+        }
+        return value
+      } catch (e) {
+        return value
+      }
+    },
+    /** 双击复制文本 */
+    copyText(text) {
+      if (!text || text === '-') return
+      if (navigator.clipboard && window.isSecureContext) {
+        navigator.clipboard.writeText(text).then(() => {
+          this.$message.success('复制成功')
+        }).catch(() => {
+          this.fallbackCopy(text)
+        })
+      } else {
+        this.fallbackCopy(text)
+      }
+    },
+    /** 降级复制方案 */
+    fallbackCopy(text) {
+      const textArea = document.createElement('textarea')
+      textArea.value = text
+      textArea.style.position = 'fixed'
+      textArea.style.left = '-9999px'
+      document.body.appendChild(textArea)
+      textArea.focus()
+      textArea.select()
+      try {
+        document.execCommand('copy')
+        this.$message.success('复制成功')
+      } catch (err) {
+        this.$message.error('复制失败')
+      }
+      document.body.removeChild(textArea)
+    },
+    /** 播放按钮操作 */
+    handlePlay(row) {
+      this.currentWavFileUrl = row.wavFileUrl
+      this.currentMediaType = this.getMediaType(row.wavFileUrl || row.wavFile)
+      // 解析AI对话内容
+      this.chatDialogList = this.parseChatContent(row.chatContent)
+      this.playDialogVisible = true
+    },
+    /** 解析聊天内容 */
+    parseChatContent(chatContent) {
+      if (!chatContent) return []
+      try {
+        let items = chatContent
+        // 如果是字符串,尝试解析
+        if (typeof items === 'string') {
+          items = JSON.parse(items)
+        }
+        // 可能是双重JSON字符串
+        if (typeof items === 'string') {
+          items = JSON.parse(items)
+        }
+        if (!Array.isArray(items)) return []
+        return items.filter(item => ['user', 'assistant', 'agent'].includes(item.role)).map(item => {
+          let content = item.content || ''
+          let isKb = false
+          // 检查是否包含JSON(知识库返回结果)
+          if (this.containsJson(content)) {
+            isKb = true
+            // 移除JSON部分
+            let cleaned = content
+            while (this.containsJson(cleaned)) {
+              cleaned = cleaned.replace(/\{[^{}]*\}/s, '').trim()
+            }
+            content = cleaned || content
+          }
+          return {
+            role: isKb ? 'kb' : item.role,
+            content: content,
+            isKb: isKb,
+            expanded: false
+          }
+        }).filter(item => item.content)
+      } catch (e) {
+        return []
+      }
+    },
+    /** 检查字符串是否包含JSON */
+    containsJson(input) {
+      if (!input) return false
+      return /\{.*?\}/s.test(input)
+    },
+    /** 获取截断内容 */
+    getTruncatedContent(content) {
+      if (!content || content.length <= 200) return content
+      return content.substring(0, 200) + '...'
+    },
+    /** 切换展开/收起 */
+    toggleExpand(index) {
+      this.$set(this.chatDialogList[index], 'expanded', !this.chatDialogList[index].expanded)
+    },
+    /** 获取角色图标 */
+    getRoleIcon(role) {
+      const iconMap = {
+        assistant: 'el-icon-monitor',
+        agent: 'el-icon-phone-outline',
+        kb: 'el-icon-folder-opened',
+        user: 'el-icon-user'
+      }
+      return iconMap[role] || 'el-icon-user'
+    },
+    /** 获取角色标签 */
+    getRoleLabel(role) {
+      const labelMap = {
+        assistant: 'AI',
+        agent: '坐席',
+        kb: '知识库',
+        user: '客户'
+      }
+      return labelMap[role] || role
+    },
+    /** 下载按钮操作 */
+    handleDownload(row) {
+      if (row.wavFileUrl) {
+        const link = document.createElement('a')
+        link.href = row.wavFileUrl
+        link.target = '_blank'
+        link.download = ''
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+      }
+    },
+    /** 播放弹窗关闭 */
+    handlePlayDialogClose() {
+      // 停止音频/视频播放
+      if (this.$refs.audioPlayer) {
+        this.$refs.audioPlayer.pause()
+        this.$refs.audioPlayer.currentTime = 0
+      }
+      if (this.$refs.videoPlayer) {
+        this.$refs.videoPlayer.pause()
+        this.$refs.videoPlayer.currentTime = 0
+      }
+      this.currentWavFileUrl = ''
+      this.currentMediaType = ''
+      this.chatDialogList = []
+    }
+  }
+}
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+.range-separator {
+  padding: 0 5px;
+}
+.player-wrapper {
+  margin-bottom: 16px;
+}
+/* 对话容器 */
+.chat-container {
+  margin-top: 16px;
+  border-top: 1px solid #ebeef5;
+  padding-top: 12px;
+}
+.chat-title {
+  font-size: 15px;
+  font-weight: bold;
+  margin-bottom: 12px;
+  color: #303133;
+}
+.chat-list {
+  max-height: 400px;
+  overflow-y: auto;
+  padding-right: 6px;
+}
+/* 对话气泡 */
+.dialog-item {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 12px;
+}
+.dialog-item.user {
+  justify-content: flex-end;
+}
+.dialog-item.assistant,
+.dialog-item.agent,
+.dialog-item.kb {
+  justify-content: flex-start;
+}
+.bubble {
+  display: inline-block;
+  max-width: 70%;
+  padding: 10px 14px;
+  border-radius: 12px;
+  word-break: break-word;
+  line-height: 1.6;
+  font-size: 14px;
+  color: #263238;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
+}
+.dialog-item.user .bubble {
+  background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
+  text-align: right;
+}
+.dialog-item.assistant .bubble {
+  background: linear-gradient(135deg, #f5f5f5 0%, #eeeeee 100%);
+}
+.dialog-item.agent .bubble {
+  background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
+}
+.dialog-item.kb .bubble {
+  background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
+}
+.role-icon {
+  display: flex;
+  align-items: center;
+  padding: 0 8px;
+  font-size: 18px;
+  color: #606266;
+}
+.role-label {
+  font-size: 12px;
+  color: #909399;
+  white-space: nowrap;
+  padding: 0 4px;
+  line-height: 32px;
+}
+.content-text {
+  word-break: break-word;
+}
+.more-text {
+  color: #1976d2;
+  cursor: pointer;
+  font-size: 13px;
+  margin-left: 4px;
+  white-space: nowrap;
+}
+.more-text:hover {
+  text-decoration: underline;
+}
+.kb-tag {
+  margin-top: 6px;
+  display: inline-block;
+}
+.hangup-cause-cell {
+  cursor: pointer;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-width: 100%;
+  display: inline-block;
+}
+</style>

+ 709 - 0
src/views/company/aiModel/inboundCallManage/index.vue

@@ -0,0 +1,709 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索表单 -->
+    <el-form
+      v-show="showSearch"
+      ref="queryForm"
+      :inline="true"
+      :model="queryParams"
+      label-width="100px"
+    >
+      <el-form-item label="大模型底座" prop="llmAccountId">
+        <el-select
+          v-model="queryParams.llmAccountId"
+          clearable
+          placeholder="全部"
+          size="small"
+          style="width: 200px"
+        >
+          <el-option label="全部" value="" />
+          <el-option
+            v-for="item in llmAccountList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="被叫号码" prop="callee">
+        <el-input
+          v-model="queryParams.callee"
+          clearable
+          placeholder="请输入被叫号码"
+          size="small"
+          style="width: 200px"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button 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
+          v-hasPermi="['inboundCallManage:add']"
+          icon="el-icon-plus"
+          plain
+          size="mini"
+          type="success"
+          @click="handleAdd"
+        >新增</el-button>
+      </el-col>
+      <!-- <el-col :span="1.5">
+        <el-button
+          icon="el-icon-edit"
+          plain
+          size="mini"
+          type="primary"
+          :disabled="single"
+          @click="handleEdit()"
+        >修改</el-button>
+      </el-col> -->
+      <!-- <el-col :span="1.5">
+        <el-button
+          icon="el-icon-delete"
+          plain
+          size="mini"
+          type="danger"
+          :disabled="multiple"
+          @click="handleDelete()"
+        >删除</el-button>
+      </el-col> -->
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <!-- 数据表格 -->
+    <el-table
+      v-loading="loading"
+      :data="inboundList"
+      border
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column align="center" label="名称" prop="inboundAlias" />
+      <el-table-column align="center" label="大模型底座" prop="llmAccountName" />
+      <el-table-column align="center" label="音色" prop="voiceName" />
+      <el-table-column align="center" label="被叫号码" prop="callee" />
+      <el-table-column align="center" label="服务类型" prop="serviceType" width="200">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.serviceType === 'ai'" type="primary">AI</el-tag>
+          <el-tag v-else-if="scope.row.serviceType === 'acd'" type="success">技能组</el-tag>
+          <el-tag v-else-if="scope.row.serviceType === 'ivr'" type="info">IVR</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        class-name="small-padding fixed-width"
+        label="操作"
+        width="180"
+      >
+        <template slot-scope="scope">
+          <el-button
+            v-hasPermi="['inboundCallManage:edit']"
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleEdit(scope.row)"
+          >修改</el-button>
+          <el-button
+          v-hasPermi="['inboundCallManage:delete']"
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <pagination
+      v-show="total > 0"
+      :limit.sync="queryParams.pageSize"
+      :page.sync="queryParams.pageNum"
+      :total="total"
+      @pagination="getList"
+    />
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog
+      :title="dialogTitle"
+      :visible.sync="dialogVisible"
+      width="650px"
+      append-to-body
+      @close="handleDialogClose"
+    >
+      <el-form
+        ref="form"
+        :model="form"
+        :rules="rules"
+        label-width="120px"
+      >
+        <!-- 名称 -->
+        <el-form-item label="名称" prop="inboundAlias">
+          <el-input
+            v-model="form.inboundAlias"
+            placeholder="请输入名称"
+            style="width: 400px;"
+          />
+        </el-form-item>
+        
+        <!-- 被叫号码 -->
+        <el-form-item label="被叫号码" prop="callee">
+          <el-input
+            v-model="form.callee"
+            placeholder="请输入被叫号码"
+            style="width: 400px;"
+          />
+        </el-form-item>
+        
+        <!-- 服务类型 -->
+        <el-form-item label="服务类型" prop="serviceType">
+          <el-select
+            v-model="form.serviceType"
+            placeholder="请选择服务类型"
+            style="width: 400px;"
+            @change="handleServiceTypeChange"
+          >
+            <el-option label="AI" value="ai" />
+            <el-option label="技能组" value="acd" />
+            <el-option label="IVR" value="ivr" />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 大模型底座 (AI可见) -->
+        <el-form-item v-if="form.serviceType === 'ai'" label="大模型底座" prop="llmAccountId">
+          <el-select
+            v-model="form.llmAccountId"
+            placeholder="请选择大模型底座"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in llmAccountList"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- ASR提供商 (AI可见) -->
+        <el-form-item v-if="form.serviceType === 'ai'" label="ASR提供商" prop="asrProvider">
+          <el-select
+            v-model="form.asrProvider"
+            placeholder="请选择ASR提供商"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="(value, key) in asrProviderList"
+              :key="key"
+              :label="value"
+              :value="key"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 音色来源 (AI可见) -->
+        <el-form-item v-if="form.serviceType === 'ai'" label="音色来源" prop="voiceSource">
+          <el-select
+            v-model="form.voiceSource"
+            placeholder="请选择音色来源"
+            style="width: 400px;"
+            @change="handleVoiceSourceChange"
+          >
+            <el-option
+              v-for="(value, key) in voiceSourceList"
+              :key="key"
+              :label="value"
+              :value="key"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 音色 (AI可见) -->
+        <el-form-item v-if="form.serviceType === 'ai'" label="音色" prop="voiceCode">
+          <el-select
+            v-model="form.voiceCode"
+            placeholder="请选择音色"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in voiceList"
+              :key="item.voiceCode"
+              :label="item.voiceName"
+              :value="item.voiceCode"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- AI转接类型 (AI可见) -->
+        <el-form-item v-if="form.serviceType === 'ai'" label="AI转接类型" prop="aiTransferType">
+          <el-select
+            v-model="form.aiTransferType"
+            placeholder="请选择AI转接类型"
+            style="width: 400px;"
+            @change="handleAiTransferTypeChange"
+          >
+            <el-option label="技能组" value="acd" />
+            <el-option label="分机号" value="extension" />
+            <el-option label="网关" value="gateway" />
+          </el-select>
+        </el-form-item>
+        
+        <!-- IVR下拉 (IVR可见) -->
+        <el-form-item v-if="form.serviceType === 'ivr'" label="IVR" prop="ivrId">
+          <el-select
+            v-model="form.ivrId"
+            placeholder="请选择IVR"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in ivrList"
+              :key="item.id"
+              :label="item.ivrNodeName"
+              :value="String(item.id)"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 业务组 (ACD/AI+ACD可见) -->
+        <el-form-item v-if="showBizGroup" label="业务组" prop="aiTransferGroupId">
+          <el-select
+            v-model="form.aiTransferGroupId"
+            placeholder="请选择业务组"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in bizGroupList"
+              :key="item.groupId"
+              :label="item.bizGroupName"
+              :value="String(item.groupId)"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 网关 (AI+Gateway可见) -->
+        <el-form-item v-if="showGateway" label="网关" prop="aiTransferGatewayId">
+          <el-select
+            v-model="form.aiTransferGatewayId"
+            placeholder="请选择网关"
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in gatewayList"
+              :key="item.id"
+              :label="item.gwDesc"
+              :value="String(item.id)"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <!-- 网关目标号码 (AI+Gateway可见) -->
+        <el-form-item v-if="showGateway" label="网关目标号码" prop="aiTransferGatewayDestNumber">
+          <el-input
+            v-model="form.aiTransferGatewayDestNumber"
+            placeholder="请输入网关目标号码"
+            style="width: 400px;"
+          />
+        </el-form-item>
+        
+        <!-- 分机号 (AI+Extension可见) -->
+        <el-form-item v-if="showExtension" label="分机号" prop="aiTransferExtNumber">
+          <el-input
+            v-model="form.aiTransferExtNumber"
+            placeholder="请输入分机号"
+            style="width: 400px;"
+          />
+        </el-form-item>
+        
+        <!-- 服务评价 -->
+        <el-form-item label="服务评价" prop="satisfSurveyIvrId">
+          <el-select
+            v-model="form.satisfSurveyIvrId"
+            placeholder="请选择服务评价IVR"
+            clearable
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in ivrList"
+              :key="item.id"
+              :label="item.ivrNodeName"
+              :value="String(item.id)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="呼入场景" prop="fsSceneType">
+          <el-select
+            v-model="form.fsSceneType"
+            placeholder="请选择呼入场景"
+            clearable
+            style="width: 400px;"
+          >
+            <el-option
+              v-for="item in sceneList"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="parseInt(item.dictValue)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="线路回调地址" prop="callBackUrl">
+         <el-input
+            v-model="form.callBackUrl"
+            placeholder="请输入线路回调地址"
+            style="width: 400px;"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listInboundLlm,
+  addInboundLlm,
+  updateInboundLlm,
+  delInboundLlm,
+  listLlmAccount,
+  listAsrProvider,
+  listVoiceSource,
+  listVoiceBySource,
+  listBizGroup,
+  listGateway,
+  listIvr
+} from '@/api/company/inboundCallManage'
+
+export default {
+  name: 'InboundCallManage',
+  data() {
+    return {
+      // 遮罩层
+      loading: false,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 表格数据
+      inboundList: [],
+      // 选中的数据
+      selectedRows: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        llmAccountId: undefined,
+        callee: undefined
+      },
+      // 大模型账户列表
+      llmAccountList: [],
+      // ASR提供商列表
+      asrProviderList: {},
+      // 音色来源列表
+      voiceSourceList: {},
+      // 音色列表
+      voiceList: [],
+      // 业务组列表
+      bizGroupList: [],
+      // 网关列表
+      gatewayList: [],
+      // IVR列表
+      ivrList: [],
+      // 弹窗控制
+      dialogVisible: false,
+      dialogTitle: '新增呼入大模型配置',
+      // 表单数据
+      form: {
+        id: undefined,
+        inboundAlias: undefined,
+        callee: undefined,
+        serviceType: 'ai',
+        llmAccountId: undefined,
+        asrProvider: undefined,
+        voiceSource: undefined,
+        voiceCode: undefined,
+        aiTransferType: 'acd',
+        aiTransferGroupId: undefined,
+        aiTransferGatewayId: undefined,
+        aiTransferGatewayDestNumber: undefined,
+        aiTransferExtNumber: undefined,
+        ivrId: undefined,
+        satisfSurveyIvrId: undefined,
+        fsSceneType:null,
+        callBackUrl:null
+      },
+      // 表单校验规则
+      rules: {
+        inboundAlias: [
+          { required: true, message: '请输入名称', trigger: 'blur' }
+        ],
+        callee: [
+          { required: true, message: '请输入被叫号码', trigger: 'blur' }
+        ],
+        serviceType: [
+          { required: true, message: '请选择服务类型', trigger: 'change' }
+        ],
+        fsSceneType: [
+          { required: true, message: '请选择呼入场景', trigger: 'change' }
+        ],
+        callBackUrl: [
+          { required: true, message: '请输入线路回调地址', trigger: 'blur' }
+        ],
+      },
+      //场景下拉
+      sceneList:[]
+    }
+  },
+  computed: {
+    // 是否显示业务组选择
+    showBizGroup() {
+      return this.form.serviceType === 'acd' ||
+             (this.form.serviceType === 'ai' && this.form.aiTransferType === 'acd')
+    },
+    // 是否显示网关选择
+    showGateway() {
+      return this.form.serviceType === 'ai' && this.form.aiTransferType === 'gateway'
+    },
+    // 是否显示分机号输入
+    showExtension() {
+      return this.form.serviceType === 'ai' && this.form.aiTransferType === 'extension'
+    }
+  },
+  created() {
+    this.getList()
+    this.loadDropdownData()
+    this.getDicts("task_scene_type").then((response) => {
+        this.sceneList = response.data;
+        console.log(this.sceneList);
+    });
+  },
+  methods: {
+    /** 加载下拉数据 */
+    loadDropdownData() {
+      // 大模型账户
+      listLlmAccount().then(response => {
+        this.llmAccountList = response.data || []
+      })
+      // ASR提供商
+      listAsrProvider().then(response => {
+        this.asrProviderList = response.data || {}
+      })
+      // 音色来源
+      listVoiceSource().then(response => {
+        this.voiceSourceList = response.data || {}
+      })
+      // 业务组
+      listBizGroup().then(response => {
+        this.bizGroupList = response.data || []
+      })
+      // 网关
+      listGateway().then(response => {
+        this.gatewayList = response.data || []
+      })
+      // IVR
+      listIvr().then(response => {
+        this.ivrList = response.data || []
+      })
+    },
+    /** 查询列表 */
+    getList() {
+      this.loading = true
+      listInboundLlm(this.queryParams).then(response => {
+        this.inboundList = response.rows || []
+        this.total = response.total || 0
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    /** 服务类型改变 */
+    handleServiceTypeChange() {
+      // 清空关联字段
+      this.form.llmAccountId = undefined
+      this.form.asrProvider = undefined
+      this.form.voiceSource = undefined
+      this.form.voiceCode = undefined
+      this.form.aiTransferType = 'acd'
+      this.form.ivrId = undefined
+      this.voiceList = []
+    },
+    /** 音色来源改变 */
+    handleVoiceSourceChange(val) {
+      this.form.voiceCode = undefined
+      if (val) {
+        listVoiceBySource(val).then(response => {
+          this.voiceList = response.data || []
+        })
+      } else {
+        this.voiceList = []
+      }
+    },
+    /** AI转接类型改变 */
+    handleAiTransferTypeChange() {
+      this.form.aiTransferGroupId = undefined
+      this.form.aiTransferGatewayId = undefined
+      this.form.aiTransferGatewayDestNumber = undefined
+      this.form.aiTransferExtNumber = undefined
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    /** 表格多选选中事件 */
+    handleSelectionChange(selection) {
+      this.selectedRows = selection
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset()
+      this.dialogTitle = '新增呼入大模型配置'
+      this.dialogVisible = true
+    },
+    /** 修改按钮操作 */
+    handleEdit(row) {
+      this.reset()
+      let editRow = row
+      if (!editRow) {
+        if (this.selectedRows.length !== 1) {
+          this.$message.warning('请选择一条要修改的数据')
+          return
+        }
+        editRow = this.selectedRows[0]
+      }
+      this.dialogTitle = '修改呼入大模型配置'
+      this.form = { ...editRow }
+      // 如果有音色来源,加载音色列表
+      if (this.form.voiceSource) {
+        listVoiceBySource(this.form.voiceSource).then(response => {
+          this.voiceList = response.data || []
+        })
+      }
+      this.dialogVisible = true
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      let ids = []
+      if (row) {
+        ids = [row.id]
+      } else {
+        if (this.selectedRows.length === 0) {
+          this.$message.warning('请至少选择一条要删除的数据')
+          return
+        }
+        ids = this.selectedRows.map(item => item.id)
+      }
+      this.$confirm(`此操作将永久删除${ids.length > 1 ? '这些' : '该'}记录,是否继续?`, '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        delInboundLlm(ids.join(',')).then(response => {
+          if (response.code === 200) {
+            this.$message.success('删除成功')
+            this.getList()
+          } else {
+            this.$message.error(response.msg || '删除失败')
+          }
+        })
+      }).catch(() => {})
+    },
+    /** 表单重置 */
+    reset() {
+      this.form = {
+        id: undefined,
+        inboundAlias: undefined,
+        callee: undefined,
+        serviceType: 'ai',
+        llmAccountId: undefined,
+        asrProvider: undefined,
+        voiceSource: undefined,
+        voiceCode: undefined,
+        aiTransferType: 'acd',
+        aiTransferGroupId: undefined,
+        aiTransferGatewayId: undefined,
+        aiTransferGatewayDestNumber: undefined,
+        aiTransferExtNumber: undefined,
+        ivrId: undefined,
+        satisfSurveyIvrId: undefined
+      }
+      this.voiceList = []
+      this.resetForm('form')
+    },
+    /** 弹窗关闭时清理 */
+    handleDialogClose() {
+      this.reset()
+    },
+    /** 提交表单 */
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (valid) {
+          // 处理转接数据
+          const submitData = { ...this.form }
+          if (submitData.serviceType === 'ai') {
+            if (submitData.aiTransferType === 'acd') {
+              submitData.aiTransferData = submitData.aiTransferGroupId
+            } else if (submitData.aiTransferType === 'extension') {
+              submitData.aiTransferData = submitData.aiTransferExtNumber
+            } else if (submitData.aiTransferType === 'gateway') {
+              submitData.aiTransferData = JSON.stringify({
+                gatewayId: submitData.aiTransferGatewayId,
+                destNumber: submitData.aiTransferGatewayDestNumber
+              })
+            }
+          } else if (submitData.serviceType === 'acd') {
+            submitData.aiTransferData = submitData.aiTransferGroupId
+          }
+          
+          if (submitData.id) {
+            updateInboundLlm(submitData).then(response => {
+              if (response.code === 200) {
+                this.$message.success('修改成功')
+                this.dialogVisible = false
+                this.getList()
+              } else {
+                this.$message.error(response.msg || '修改失败')
+              }
+            })
+          } else {
+            addInboundLlm(submitData).then(response => {
+              if (response.code === 200) {
+                this.$message.success('新增成功')
+                this.dialogVisible = false
+                this.getList()
+              } else {
+                this.$message.error(response.msg || '新增失败')
+              }
+            })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+</style>

+ 8 - 2
src/views/company/companyConfig/index.vue

@@ -46,7 +46,12 @@
              <el-form-item label="是否允许重复客户导入" prop="allowRepeatCustomer">
               <el-switch v-model="cidConfig.allowRepeatCustomer"></el-switch>
             </el-form-item>
-
+            <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="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>
@@ -365,7 +370,8 @@ export default {
       userIsDefaultBlack: null,
       redPacketConfig:{},
 
-      redPacketConfigForm:{}
+      redPacketConfigForm:{},
+      cidConfigForm:{}
     };
   },
   created() {

+ 129 - 1
src/views/company/companyUser/index.vue

@@ -308,6 +308,21 @@
               <el-button size="mini" type="text" icon="el-icon-key" @click="handleResetPwd(scope.row)" v-hasPermi="['company:user:resetPwd']">重置密码</el-button>
               <el-button size="mini" type="text" icon="el-icon-edit" @click="checkBindSipCallUser(scope.row)" v-if="scope.row.aiSipCallUserId==null">绑定sip角色</el-button>
               <el-button size="mini" type="text" icon="el-icon-search" @click="checkChangeSipCallUser(scope.row)" v-if="scope.row.aiSipCallUserId">修改sip角色</el-button>
+              <el-button
+                v-if="!scope.row.accountId"
+                size="mini"
+                type="text"
+                icon="el-icon-link"
+                @click="handleBindXsy(scope.row)"
+              >绑定销售易</el-button>
+
+              <el-button
+                v-if="scope.row.accountId"
+                size="mini"
+                type="text"
+                icon="el-icon-close"
+                @click="handleUnbindXsy(scope.row)"
+              >解绑销售易</el-button>
             </template>
           </el-table-column>
         </el-table>
@@ -825,6 +840,37 @@
       </div>
     </el-dialog>
 
+    <!-- 绑定销售易账号 -->
+    <el-dialog :title="xsyBindDialog.title" :visible.sync="xsyBindDialog.open" width="500px" append-to-body>
+      <el-form ref="xsyBindFormRef" :model="xsyBindForm" label-width="100px">
+          <el-form-item label="员工姓名">
+              <el-input v-model="xsyBindForm.nickName" disabled />
+          </el-form-item>
+          <el-form-item label="销售易账号" prop="accountId">
+              <el-select
+                  v-model="xsyBindForm.accountId"
+                  filterable
+                  clearable
+                  placeholder="请选择销售易账号"
+                  style="width: 100%;"
+                  :loading="xsyBindDialog.loading"
+              >
+                  <el-option
+                      v-for="item in xsyAccountList"
+                      :key="item.id"
+                      :label="item.accountName"
+                      :value="item.id"
+                  />
+              </el-select>
+          </el-form-item>
+      </el-form>
+
+      <div slot="footer" class="dialog-footer">
+          <el-button type="primary" @click="submitBindXsy">确 定</el-button>
+          <el-button @click="cancelBindXsy">取 消</el-button>
+      </div>
+    </el-dialog>
+
     <ai-sip-call-user ref="aiSipCallUser" v-show="false" @refreshParentData="getList" />
 
   </div>
@@ -864,6 +910,7 @@ import {addCodeUrl, bindSaleAndFsUser, getSaleBindUserList} from "../../../api/c
 import selectDoctor from "@/views/qw/user/selectDoctor.vue";
 import {bindCidServer,unbindCidServer} from "@/api/company/companyAiWorkflowServer";
 import AiSipCallUser from "../../aiSipCall/aiSipCallUser.vue";
+import { bindXsyAccount,unbindXsyAccount,listXsyAccount} from "@/api/xsy/xsy";
 export default {
   name: "User",
   components: {selectDoctor, Treeselect ,selectUser,AiSipCallUser},
@@ -1092,6 +1139,20 @@ export default {
       selectedFsUserIds: [],
       boundUsersList: [],
       bindUserLoading: false,
+      // 销售易绑定弹窗
+      xsyBindDialog: {
+        open: false,
+        title: '绑定销售易账号',
+        loading: false
+      },
+      // 销售易账号列表
+      xsyAccountList: [],
+      // 绑定表单
+      xsyBindForm: {
+        companyUserId: null,
+        nickName: '',
+        accountId: null
+      },
     };
 
   },
@@ -2123,7 +2184,74 @@ export default {
     },
     checkChangeSipCallUser(row){
       this.$refs.aiSipCallUser.handleUpdateById(row.aiSipCallUserId);
-    }
+    },
+    // 打开绑定销售易账号弹窗
+    handleBindXsy(row) {
+      this.xsyBindForm = {
+          companyUserId: row.userId,
+          nickName: row.nickName,
+          accountId: null
+      }
+      this.xsyAccountList = []
+      this.xsyBindDialog.open = true
+      this.xsyBindDialog.loading = true
+
+      listXsyAccount({
+          pageNum: 1,
+          pageSize: 9999
+      }).then(res => {
+          this.xsyAccountList = res.rows || []
+      }).finally(() => {
+          this.xsyBindDialog.loading = false
+      })
+    },
+
+  // 取消绑定销售易
+  cancelBindXsy() {
+      this.xsyBindDialog.open = false
+      this.xsyBindForm = {
+          companyUserId: null,
+          nickName: '',
+          accountId: null
+      }
+  },
+
+  // 提交绑定销售易
+  submitBindXsy() {
+      if (!this.xsyBindForm.accountId) {
+          this.$message.warning('请选择销售易账号')
+          return
+      }
+
+      bindXsyAccount({
+          companyUserId: this.xsyBindForm.companyUserId,
+          accountId: this.xsyBindForm.accountId
+      }).then(res => {
+          if (res.code === 200) {
+              this.msgSuccess('绑定成功')
+              this.xsyBindDialog.open = false
+              this.getList()
+          }
+      })
+  },
+
+  // 解绑销售易账号
+  handleUnbindXsy(row) {
+      this.$confirm('是否确认解绑当前员工的销售易账号?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+      }).then(() => {
+          return unbindXsyAccount({
+              companyUserId: row.userId
+          })
+      }).then(res => {
+          if (res.code === 200) {
+              this.msgSuccess('解绑成功')
+              this.getList()
+          }
+      }).catch(() => {})
+  },
   },
 }
 </script>

+ 186 - 12
src/views/company/companyVoiceRobotic/index.vue

@@ -87,7 +87,7 @@
       </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 == 1">个微</el-tag>
           <el-tag v-if="scope.row.isWeCom == 2">企微</el-tag>
         </template>
       </el-table-column>
@@ -120,6 +120,8 @@
             <el-tag v-if="scope.row.taskStatus == 3" type="success">执行完成</el-tag>
         </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">-->
 <!--        <template slot-scope="scope">-->
 <!--          <div v-loading="loadingStatus">-->
@@ -381,10 +383,10 @@
             </div>
             <el-form-item label="添加类型" prop="isWeCom">
               <el-radio-group v-model="form.isWeCom">
-                <!-- <el-radio :label="1" border>
+                <el-radio :label="1" border>
                   <i class="el-icon-pie-chart"></i>
                   个微
-                </el-radio> -->
+                </el-radio>
                 <el-radio :label="2" border>
                   <i class="el-icon-star-on"></i>
                   企微
@@ -479,6 +481,45 @@
             {{ 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="客服ID" align="center" prop="customerId"/>
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template slot-scope="scope">
             <el-button
@@ -665,7 +706,7 @@
                             <i class="el-icon-s-operation"></i>
                             当前节点:<strong>{{ record.currentNodeTypeName }}</strong>
 
-                            <el-button
+                            <!-- <el-button
                               v-if="hasContent(record)"
                               size="mini"
                               type="primary"
@@ -674,7 +715,7 @@
                               style="margin-left: 8px;"
                               @click.stop="handleShowContent(record)">
                                 查看对话内容
-                            </el-button>
+                            </el-button> -->
                             <!-- 外呼未执行标识 -->
                             <el-button
                                 v-if="record.waitCallNode"
@@ -718,6 +759,16 @@
                           <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)"
@@ -855,10 +906,11 @@ import {getDicts} from "@/api/system/dict/data";
 import { optionList } 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: "Robotic",
-  components: { draggable, customerDetails, customerSelect, qwUserSelect,qwUserSelectTwo,CallCenterPhoneBar},
+  components: {AiTagPanel, draggable, customerDetails, customerSelect, qwUserSelect,qwUserSelectTwo,CallCenterPhoneBar},
   data() {
     return {
       taskType:1,
@@ -1111,6 +1163,17 @@ export default {
     }
   },
   methods: {
+
+      getIntentionText(intention) {
+          const intentionMap = {
+              high: "高意向",
+              medium: "中意向",
+              low: "低意向",
+              none: "无意向"
+          };
+          return intentionMap[intention] || intention;
+      },
+
     getSmsTempDropList(){
       getSmsTempList().then(res=>{
         this.smsTempList = res.data;
@@ -1347,6 +1410,7 @@ export default {
     getCalleesList() {
       this.callees.loading = true;
       calleesList(this.callees.queryParams).then(response => {
+          console.log("response",response);
         this.callees.list = response.rows;
         this.callees.total = response.total;
         this.callees.loading = false;
@@ -1629,10 +1693,10 @@ export default {
       this.manualCallDialog.companyUserId = null;
       this.manualCallDialog.workflowInstanceId = null;
     },
-    handleShowContent(record) {
+     handleShowContent(record,log) {
       this.contentDialog.customerName = record.customerName || '';
       this.contentDialog.customerPhone = record.customerPhone || '';
-      this.contentDialog.content = record.contentList || '';
+      this.contentDialog.content = log.nodeContentList || '';
       this.contentDialog.visible = true;
     },
 
@@ -1660,12 +1724,12 @@ export default {
       }
     },
     hasContent(record) {
-      if (!record || !record.contentList) return false
+      if (!record || !record.nodeContentList) return false
 
       try {
-          const parsed = typeof record.contentList === 'string'
-              ? JSON.parse(record.contentList)
-              : record.contentList
+          const parsed = typeof record.nodeContentList === 'string'
+              ? JSON.parse(record.nodeContentList)
+              : record.nodeContentList
 
           if (!Array.isArray(parsed)) return false
 
@@ -2149,4 +2213,114 @@ export default {
     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>

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

@@ -0,0 +1,2308 @@
+<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" prop="createByName"/>
+        <el-table-column label="创建部门" align="center" prop="createByDeptName"/>
+<!--      <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="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="请选择任务流程">
+                <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="请选择任务流程">
+                <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">
+            <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">
+            <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" @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"/>
+        <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" 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
+                            v-if="record.intention"
+                            size="small"
+                            type="info"
+                            class="status-tag">
+                            意向度:{{ getIntentionLabel(record.intention) }}
+                          </el-tag>
+
+                          <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 }}
+                          </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"
+      />
+    </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 {
+    myListRobotic,
+  getRobotic,
+  delRobotic,
+  addRobotic,
+  updateRobotic,
+  exportRobotic,
+  calleesList,
+  statusList,
+  startRobotic,
+  stopRobotic,
+  companyUserList,
+  wxList,
+  taskRun,
+    wxListQw,
+  getSmsTempList,
+  // getCIDGroupList,
+  getExecRecords,
+  getCurrentCompanyId
+} 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 {getDicts} from "@/api/system/dict/data";
+import { optionList } from '@/api/company/companyWorkflow'
+import CallCenterPhoneBar from '../../aiSipCall/aiSipCallManualOutbound.vue'
+
+export default {
+  name: "myRobotic",
+  components: { draggable, customerDetails, customerSelect, qwUserSelect,qwUserSelectTwo,CallCenterPhoneBar},
+  data() {
+    return {
+      taskType:1,
+      taskTypeList:[{id:1,name:"普通任务"},{id:2,name:"场景任务"}],
+      currentCompanyId:null,
+      // 遮罩层
+      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
+      },
+      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;
+        this.updateStatusFun();
+      });
+    },
+    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.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;
+        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.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;
+            }
+            // if(!account.wxDialogId) {
+            //   this.msgError(`第 ${i + 1} 个账号请选择话术`);
+            //   return;
+            // }
+          }
+
+          let list = [];
+          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.form.addType != 0 ){
+          //  let firstTask = this.taskFlowList[0];
+          //   if(firstTask.key != "cellPhone"){
+          //     this.msgError("【意向】加微方式下,任务流程第一步必须为外呼!");
+          //     return;
+          //   }
+          // }
+          if(!this.form.qwUserList || this.form.qwUserList.length == 0){
+            this.msgError("请选者加微方案");
+            return;
+          }
+          this.form.taskFlow = this.taskFlowList.map(e => e.key).join();
+          if (this.form.id != null) {
+            updateRobotic(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          } else {
+            addRobotic(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 delRobotic(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(function () {
+      });
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有机器人外呼任务数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function () {
+        return exportRobotic(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;
+      });
+    },
+    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();
+      })
+    },
+    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.$nextTick(() => {
+      this.$refs.form.clearValidate();
+      })
+    },
+    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 || '',
+          intention: item.intention || ''
+        }))
+        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.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;
+    },
+     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
+      }
+    },
+    getIntentionLabel(val) {
+      if (val === null || val === undefined || val === '') {
+          return '-'
+      }
+
+      const item = (this.levelList || []).find(e => String(e.dictValue) === String(val))
+      return item ? item.dictLabel : val
+    },
+  }
+};
+</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-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>

+ 67 - 10
src/views/company/companyWorkflow/design.vue

@@ -282,7 +282,7 @@
               </el-form-item>
             </div>
              <!-- AI加微配置 -->
-            <div v-if="selectedNode.nodeType == 'AI_ADD_WX_TASK'" class="property-section">
+            <div v-if="selectedNode.nodeType == 'AI_ADD_WX_TASK_NEW'" class="property-section">
               <div class="section-title">
                 <i class="el-icon-chat-dot-round"></i>加微配置
               </div>
@@ -448,6 +448,23 @@
                   />
                 </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"
@@ -536,7 +553,7 @@
             </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_SEND_MSG_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>
@@ -575,7 +592,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>
@@ -647,7 +664,7 @@ import {
 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 { listAll } from '@/api/company/wxDialog';
 export default {
   name: 'WorkflowDesign',
   data() {
@@ -743,7 +760,13 @@ export default {
       easyCallGatewayList: [],     // 外呼线路(网关)列表
       easyCallLlmAccountList: [],  // 大模型底座列表
       easyCallVoiceCodeList: [],   // 音色列表
+      allEasyCallVoiceCodeList: [],   // 接口原始数据
       easyCallBusiGroupList: [],   // tts厂商(技能组)列表
+      voiceSourceOptions: [
+        { label: '阿里云TTS', value: 'aliyun_tts' },
+        { label: '豆包语音', value: 'doubao_vcl_tts' },
+        { label: '电信语音', value: 'chinatelecom_tts' }
+      ],
     }
   },
   created() {
@@ -761,11 +784,11 @@ export default {
     } else if (this.workflowId) {
       this.loadWorkflow()
     }
-    // listAll().then(e => {
-    //   this.wxDialogList = e.data;
-    //   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;
@@ -802,9 +825,18 @@ export default {
 
     // 处理配置变化,强制更新视图
     handleConfigChange(v) {
+      if( !!v && v === "handleVoiceSource" ){
+        let voiceSource = this.selectedNode.nodeConfig.voiceSource;
+
+        this.easyCallVoiceCodeList = this.allEasyCallVoiceCodeList.filter(e => e.voiceSource == voiceSource);
+
+        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);
@@ -885,6 +917,7 @@ export default {
           this.easyCallLlmAccountList = res.data || []
         })
         getVoiceCodeList().then(res => {
+          this.allEasyCallVoiceCodeList = res.data || []
           this.easyCallVoiceCodeList = res.data || []
         })
         getBusiGroupList().then(res => {
@@ -1009,7 +1042,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'
       }
@@ -1430,6 +1463,12 @@ export default {
     startConnect(e, node, anchor) {
         if (this.readonlyMode) return
 
+        // 结束节点不能作为连线的起点(不允许结束节点有出边)
+        if (this.isEndNodeType(node.nodeType)) {
+          this.$message.warning('结束节点不能作为连线的起点')
+          return
+        }
+
         this.connecting = true
         const anchorPos = this.getAnchorPos(node, anchor)
         this.connectStart = {
@@ -1471,6 +1510,13 @@ export default {
         e.sourceNodeKey === sourceKey && e.targetNodeKey === targetKey)
       if (exists) return
 
+      // 不允许结束节点作为连线的起点(结束节点不能有出边)
+      const sourceNode = this.nodes.find(n => n.nodeKey === sourceKey)
+      if (sourceNode && this.isEndNodeType(sourceNode.nodeType)) {
+        this.$message.warning('结束节点不能作为连线的起点')
+        return
+      }
+
       this.edges.push({
         edgeKey: 'edge_' + Date.now(),
         sourceNodeKey: sourceKey,
@@ -1659,6 +1705,17 @@ export default {
         }
       }
 
+      // 校验除结束节点外,其他节点都必须有出边(只有结束节点才能作为最后一个节点)
+      const sourceNodeKeys = new Set(this.edges.map(e => e.sourceNodeKey))
+      const tailNodes = this.nodes.filter(node =>
+        !this.isEndNodeType(node.nodeType) && !sourceNodeKeys.has(node.nodeKey)
+      )
+      if (tailNodes.length > 0) {
+        const tailNames = tailNodes.map(n => n.nodeName).join('、')
+        this.$message.warning(`以下节点没有出线连接,只有结束节点才能作为最后一个节点:${tailNames}`)
+        return
+      }
+
       // 校验短信节点和外呼节点配置
       for (const node of this.nodes) {
         // 短信节点校验:短信模版必选

+ 2 - 0
src/views/company/companyWorkflow/index.vue

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

+ 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>

+ 46 - 3
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>
@@ -134,6 +142,9 @@
             <el-option v-for="item in qwUserList" :label="item.nickName" :value="item.userId" />
           </el-select>
         </el-form-item>
+        <el-form-item label="微信备注前缀" prop="wxRemark">
+          <el-input :disabled="title=='修改个微账号'" v-model="form.wxRemark"  :maxlength="6" placeholder="请输入6位微信备注前缀(数字/字母/中文)" />
+        </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
         <el-button type="primary" @click="submitForm">确 定</el-button>
@@ -144,7 +155,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 +195,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' }
+        ]
       }
     };
   },
@@ -303,6 +332,20 @@ export default {
           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>

+ 54 - 3
src/views/components/QwUserSelect.vue

@@ -328,14 +328,65 @@ export default {
     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;
   }
+
+  .drawer-container {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+    padding: 0 16px;
+  }
+
+  .search-card {
+    margin-bottom: 12px;
+  }
+  .search-card >>> .el-card__body {
+    padding: 16px 16px 0;
+  }
+  .search-form >>> .el-form-item {
+    margin-bottom: 16px;
+  }
+  .search-form >>> .el-form-item__content {
+    width: calc(100% - 80px);
+  }
+  .search-form >>> .el-select,
+  .search-form >>> .el-date-editor {
+    width: 100%;
+  }
+
+  .table-card {
+    flex: 1;
+    overflow: hidden;
+    margin-bottom: 12px;
+  }
+  .table-card >>> .el-card__body {
+    padding: 12px 16px;
+  }
+  .card-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+
+  .footer-actions {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    padding: 12px 0;
+    border-top: 1px solid #EBEEF5;
+  }
+  .footer-actions .el-button + .el-button {
+    margin-left: 10px;
+  }
 </style>

+ 230 - 119
src/views/live/liveConfig/answer.vue

@@ -1,63 +1,65 @@
 <template>
   <div class="container-md">
     <div class="tip-box">
-      选择用于本节直播课程的题库试题,试题可用于直播间内发送
-      <el-link
-        type="primary"
-        style="margin-left: 5px;"
-        @click="handleToQuestionBank"
-      >配置题库试题 >></el-link>
+      为本直播间选择<strong>课程题库</strong>中的试题(也可先在题库中手动新增题目,再回到此处勾选添加)。
+<!--      <el-link-->
+<!--        type="primary"-->
+<!--        style="margin-left: 5px;"-->
+<!--        @click="handleToCourseQuestionBank"-->
+<!--      >打开课程题库 &gt;&gt;</el-link>-->
     </div>
 
     <el-button type="primary" icon="el-icon-plus" style="margin: 20px 0;" @click="handleAddQuestion">添加试题</el-button>
-    <!-- 试题列表表格 -->
     <el-table
       :data="questionLiveList"
       style="width: 100%"
       v-loading="loading"
     >
-      <!-- 题干列:显示试题的主要内容 -->
       <el-table-column
         prop="title"
         label="题干"
         show-overflow-tooltip
-      ></el-table-column>
-
-      <!-- 题型列:显示是单选还是多选 -->
+      />
       <el-table-column
         prop="type"
         label="题型"
+        width="100"
       >
         <template slot-scope="scope">
-          {{ scope.row.type === 1 ? '单选题' : '多选题' }}
+          {{ scope.row.type === 1 ? '单选题' : scope.row.type === 2 ? '多选题' : '—' }}
         </template>
       </el-table-column>
-
-      <!-- 创建时间列:显示试题创建的时间 -->
       <el-table-column
         prop="createTime"
         label="创建时间"
-      ></el-table-column>
-
-      <!-- 操作列:包含编辑和删除按钮 -->
+        width="170"
+      />
       <el-table-column
         label="操作"
-        width="180"
+        width="280"
         fixed="right"
       >
         <template slot-scope="scope">
-          <!-- 删除按钮:用于移除试题 -->
+          <el-button
+            type="text"
+            size="small"
+            @click="handleStartQuiz(scope.row)"
+          >开始答题</el-button>
+          <el-button
+            type="text"
+            size="small"
+            @click="handleCloseQuiz(scope.row)"
+          >结束答题</el-button>
           <el-button
             type="text"
             size="small"
             style="color: #F56C6C;"
             @click="handleDelete(scope.row)"
-          >删除</el-button>
+          >除</el-button>
         </template>
       </el-table-column>
     </el-table>
 
-    <!-- 分页组件:用于分页展示试题列表 -->
     <pagination
       v-show="questionTotal > 0"
       :total="questionTotal"
@@ -67,82 +69,85 @@
       style="margin-top: 20px;"
     />
 
-    <!-- 添加试题弹窗 -->
     <el-dialog
       title="添加试题"
       :visible.sync="questionDialogVisible"
-      width="800px"
+      width="860px"
+      append-to-body
       :close-on-click-modal="false"
       :close-on-press-escape="false"
+      @open="onAddDialogOpen"
+      @closed="onAddDialogClosed"
     >
-      <div class="dialog-content">
-        <div style="text-align: right; margin-bottom: 20px;">
-          <el-input
-            v-model="searchTitle"
-            placeholder="请输入搜索内容"
-            style="width: 300px;"
-            @input="handleQuestionSearch"
-          ></el-input>
-        </div>
+      <el-tabs v-model="addQuestionTab">
+        <el-tab-pane label="从课程题库选择" name="bank">
+          <div class="dialog-toolbar">
+            <el-input
+              v-model="searchTitle"
+              placeholder="题干关键词"
+              clearable
+              size="small"
+              style="width: 220px; margin-right: 12px;"
+              @keyup.enter.native="handleQuestionSearch"
+            />
+            <el-select
+              v-model="optionTypeFilter"
+              placeholder="题型"
+              clearable
+              size="small"
+              style="width: 120px; margin-right: 12px;"
+            >
+              <el-option label="单选题" :value="1" />
+              <el-option label="多选题" :value="2" />
+            </el-select>
+            <el-button type="primary" icon="el-icon-search" size="small" @click="handleQuestionSearch">搜索</el-button>
+          </div>
 
-        <el-table
-          ref="questionTable"
-          :data="questionList"
-          style="width: 100%"
-          v-loading="questionLoading"
-          @selection-change="handleSelectionChange"
-          @row-click="handleRowClick"
-          row-key="id"
-        >
-          <!-- 复选框列:用于多选试题 -->
-          <el-table-column
-            type="selection"
-            width="55"
-          >
-          </el-table-column>
-          <!-- 题干列:显示试题的主要内容 -->
-          <el-table-column
-            prop="title"
-            label="题干"
-            class-name="clickable-column"
-          ></el-table-column>
-          <!-- 题型列:显示单选或多选 -->
-          <el-table-column
-            prop="type"
-            label="题型"
-            class-name="clickable-column"
+          <el-table
+            ref="questionTable"
+            :data="questionList"
+            style="width: 100%"
+            v-loading="questionLoading"
+            @selection-change="handleSelectionChange"
+            @row-click="handleRowClick"
+            row-key="id"
+            max-height="420"
           >
-            <template slot-scope="scope">
-              {{ scope.row.type === 1 ? '单选题' : '多选题' }}
-            </template>
-          </el-table-column>
-          <!-- 创建人列 -->
-          <el-table-column
-            prop="createBy"
-            label="创建人"
-            class-name="clickable-column"
-          ></el-table-column>
-          <!-- 创建时间列 -->
-          <el-table-column
-            prop="createTime"
-            label="创建时间"
-            width="150"
-            class-name="clickable-column"
-          ></el-table-column>
-        </el-table>
+            <el-table-column type="selection" width="50" align="center" reserve-selection />
+            <el-table-column prop="title" label="题干" min-width="200" show-overflow-tooltip class-name="clickable-column" />
+            <el-table-column prop="type" label="题型" width="90" class-name="clickable-column">
+              <template slot-scope="scope">
+                {{ scope.row.type === 1 ? '单选' : scope.row.type === 2 ? '多选' : '—' }}
+              </template>
+            </el-table-column>
+            <el-table-column prop="createBy" label="创建人" width="100" show-overflow-tooltip class-name="clickable-column" />
+            <el-table-column prop="createTime" label="创建时间" width="160" class-name="clickable-column" />
+          </el-table>
 
-        <pagination
-          v-show="total > 0"
-          :total="total"
-          :page.sync="queryParams.pageNum"
-          :limit.sync="queryParams.pageSize"
-          @pagination="getQuestionList"
-          style="margin-top: 20px;"
-        />
-      </div>
-      <div slot="footer" class="dialog-footer">
+          <pagination
+            v-show="total > 0"
+            :total="total"
+            :page.sync="queryParams.pageNum"
+            :limit.sync="queryParams.pageSize"
+            @pagination="getQuestionList"
+            style="margin-top: 16px;"
+          />
+        </el-tab-pane>
+        <el-tab-pane label="手动维护题库" name="manual">
+          <el-alert
+            title="自定义题目请先在「课程题库」中新增或编辑;保存后回到本页「从课程题库选择」勾选添加。"
+            type="info"
+            :closable="false"
+            show-icon
+            style="margin-bottom: 16px;"
+          />
+          <el-button type="primary" plain icon="el-icon-document" @click="handleToCourseQuestionBank">前往课程题库</el-button>
+        </el-tab-pane>
+      </el-tabs>
+
+      <div slot="footer" class="dialog-footer" v-show="addQuestionTab === 'bank'">
         <div style="display: flex; justify-content: space-between; align-items: center;">
-          <span class="selected-count">当前已选择 <span style="color: #00BFFF; font-style: italic;">{{ selectedQuestions.length }}</span> 题</span>
+          <span class="selected-count">当前已选择 <span class="num">{{ selectedQuestions.length }}</span> 题(题目来自课程题库)</span>
           <div>
             <el-button @click="questionDialogVisible = false">取 消</el-button>
             <el-button type="primary" @click="confirmAddQuestion">确 定</el-button>
@@ -154,12 +159,18 @@
 </template>
 
 <script>
-
-import {addLiveQuestionLive, listLiveQuestionLive, listLiveQuestionOptionList} from "@/api/live/liveQuestionLive";
+import {
+  addLiveQuestionLive,
+  listLiveQuestionLive,
+  listLiveQuestionOptionList,
+  deleteLiveQuestionLive
+} from '@/api/live/liveQuestionLive'
 
 export default {
+  name: 'LiveConfigAnswer',
   data() {
     return {
+      addQuestionTab: 'bank',
       questionDialogVisible: false,
       questionLoading: false,
       questionTotal: 0,
@@ -168,19 +179,21 @@ export default {
       questionList: [],
       loading: true,
       searchTitle: '',
+      optionTypeFilter: null,
       total: 0,
       queryParams: {
         pageNum: 1,
         pageSize: 10,
         title: null,
+        type: null,
         liveId: null
       },
       questionParams: {
         pageNum: 1,
         pageSize: 10,
         liveId: null
-      },
-    };
+      }
+    }
   },
   created() {
     this.liveId = this.$route.params.liveId
@@ -188,73 +201,171 @@ export default {
     this.questionParams.liveId = this.liveId
     this.getLiveQuestionLiveList()
   },
+  computed: {
+    userId() {
+      return this.$store.state.user.user.userId
+    }
+  },
   methods: {
+    sendLiveQuizCmd(cmd, data) {
+      const conn = this.$store.state.liveWs[this.liveId]
+      if (!conn || !conn.ws || conn.ws.readyState !== WebSocket.OPEN) {
+        this.$message.warning('直播 WebSocket 未连接,请从直播配置页进入并保持本页不刷新')
+        return false
+      }
+      const payload = {
+        cmd,
+        liveId: Number(this.liveId),
+        userId: this.userId,
+        data: JSON.stringify(data || {})
+      }
+      conn.send(JSON.stringify(payload))
+      return true
+    },
+    handleStartQuiz(row) {
+      if (!row || row.id == null) {
+        return
+      }
+      if (this.sendLiveQuizCmd('liveQuizStart', { relId: row.id })) {
+        this.$message.success('已向直播间广播本题')
+      }
+    },
+    handleCloseQuiz(row) {
+      const data = row && row.id != null ? { relId: row.id } : {}
+      if (this.sendLiveQuizCmd('liveQuizClose', data)) {
+        this.$message.success('已广播结束答题')
+      }
+    },
+    onAddDialogOpen() {
+      this.addQuestionTab = 'bank'
+      this.searchTitle = ''
+      this.optionTypeFilter = null
+      this.queryParams.pageNum = 1
+      this.queryParams.title = null
+      this.queryParams.type = null
+      this.getQuestionList()
+    },
+    onAddDialogClosed() {
+      this.selectedQuestions = []
+      this.$nextTick(() => {
+        const t = this.$refs.questionTable
+        if (t && typeof t.clearSelection === 'function') {
+          t.clearSelection()
+        }
+      })
+    },
     handleQuestionSearch() {
       this.queryParams.pageNum = 1
-      this.queryParams.title = this.searchTitle
+      this.queryParams.title = this.searchTitle || null
+      this.queryParams.type = this.optionTypeFilter
       this.getQuestionList()
     },
     getLiveQuestionLiveList() {
       this.loading = true
       listLiveQuestionLive(this.questionParams).then(response => {
-        this.questionLiveList = response.rows
-        this.questionTotal = response.total
+        this.questionLiveList = response.rows || []
+        this.questionTotal = response.total || 0
+        this.loading = false
+      }).catch(() => {
         this.loading = false
       })
     },
-    handleToQuestionBank() {
-      this.$router.push('/live/liveQuestionBank')
+    handleToCourseQuestionBank() {
+      this.$router.push('/course/courseQuestionBank')
     },
     handleAddQuestion() {
       this.questionDialogVisible = true
-      this.getQuestionList()
     },
     getQuestionList() {
       this.questionLoading = true
-      listLiveQuestionOptionList(this.queryParams).then(response => {
-        this.questionList = response.rows
-        this.total = response.total
+      const params = {
+        ...this.queryParams,
+        liveId: this.liveId
+      }
+      listLiveQuestionOptionList(params).then(response => {
+        this.questionList = response.rows || []
+        this.total = response.total || 0
+        this.questionLoading = false
+        this.$nextTick(() => this.syncTableSelection())
+      }).catch(() => {
         this.questionLoading = false
       })
     },
+    syncTableSelection() {
+      const table = this.$refs.questionTable
+      if (!table || !this.questionList.length) return
+      const selectedIds = new Set(this.selectedQuestions.map(l => l.id))
+      this.questionList.forEach(row => {
+        table.toggleRowSelection(row, selectedIds.has(row.id))
+      })
+    },
     handleSelectionChange(selection) {
-      this.selectedQuestions = selection
+      const sel = selection || []
+      const currentIds = new Set(this.questionList.map(r => r.id))
+      const fromOtherPages = this.selectedQuestions.filter(q => !currentIds.has(q.id))
+      const merged = [...fromOtherPages, ...sel]
+      if (merged.length > 15) {
+        this.$message.warning('最多一次添加 15 题')
+        this.$nextTick(() => this.syncTableSelection())
+        return
+      }
+      this.selectedQuestions = merged
     },
     confirmAddQuestion() {
       if (this.selectedQuestions.length === 0) {
-        this.$message({
-          message: '请选择要添加的试题',
-          type: 'warning'
-        })
+        this.$message.warning('请选择要添加的试题')
         return
       }
-      // 调用添加直播间试题接口
       addLiveQuestionLive({
         liveId: this.liveId,
         questionIds: this.selectedQuestions.map(item => item.id).join(',')
-      }).then(response => {
+      }).then(() => {
+        this.$message.success('添加成功')
         this.questionDialogVisible = false
         this.getLiveQuestionLiveList()
       })
     },
-    handleRowClick(row, column) {
-      // 如果点击的是复选框列,不进行处理
-      if (column.type === 'selection') {
+    handleDelete(row) {
+      if (!row || row.id == null) return
+      this.$confirm('确认从本直播间移除该试题?(不会删除课程题库中的题目)', '提示', {
+        type: 'warning'
+      }).then(() => {
+        return deleteLiveQuestionLive({ liveId: this.liveId, ids: String(row.id) })
+      }).then(() => {
+        this.$message.success('已移除')
+        this.getLiveQuestionLiveList()
+      }).catch(() => {})
+    },
+    handleRowClick(row, column, event) {
+      if (column && column.type === 'selection') {
         return
       }
-
-      // 获取表格实例
-      const table = this.$refs.questionTable[0]
+      const table = this.$refs.questionTable
       if (!table) {
         return
       }
-
-      // 判断当前行是否已经被选中
       const isSelected = this.selectedQuestions.some(item => item.id === row.id)
-
-      // 切换选中状态
       table.toggleRowSelection(row, !isSelected)
-    },
+    }
   }
-};
+}
 </script>
+
+<style scoped>
+.tip-box {
+  line-height: 1.6;
+  color: #606266;
+}
+.dialog-toolbar {
+  margin-bottom: 12px;
+}
+.selected-count {
+  font-size: 13px;
+  color: #606266;
+}
+.selected-count .num {
+  color: #409eff;
+  font-style: italic;
+  font-weight: 600;
+}
+</style>

+ 1 - 1
src/views/live/liveConfig/index.vue

@@ -107,7 +107,7 @@ export default {
         { name: '红包配置', label: '红包配置', index: 'liveRedConf'},
         { name: '抽奖配置', label: '抽奖配置', index: 'liveLotteryConf'},
         { name: '优惠券配置', label: '优惠券配置', index: 'liveCoupon'},
-        // { name: '答题', label: '答题', index: 'answer'},
+        { name: '答题', label: '答题', index: 'answer'},
         { name: '直播商品', label: '直播商品', index: 'goods'},
         { name: '回放设置', label: '回放设置', index: 'liveReplay'},
         { name: '运营自动化', label: '运营自动化', index: 'task'},

+ 1203 - 0
src/views/live/liveStatistics/LiveStatisticsPanel.vue

@@ -0,0 +1,1203 @@
+<template>
+  <div class="app-container live-statistics-panel-root">
+    <el-alert
+      v-if="trainingPeriodId"
+      title="仅可选择当前营期下的直播间进行统计查询。"
+      type="info"
+      :closable="false"
+      show-icon
+      class="mb8"
+    />
+    <!-- 左上角:选择直播间(多选,最多展示8个,多余折叠)+ 查询按钮 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <div
+          class="live-select-input"
+          @click="openLiveSelectDialog">
+          <div class="live-select-content">
+            <template v-if="selectedLiveList.length === 0">
+              <span class="placeholder">请选择直播间</span>
+            </template>
+            <template v-else>
+              <template v-for="(item, idx) in displayedLives">
+                <span :key="item.liveId" class="live-tag">{{ item.liveName }}({{ item.liveId }})</span>
+                <span v-if="idx < displayedLives.length - 1 || selectedLiveList.length > 7" class="tag-sep">、</span>
+              </template>
+              <span v-if="selectedLiveList.length > 7" class="fold-text">等{{ selectedLiveList.length }}个</span>
+            </template>
+          </div>
+          <div class="live-select-suffix">
+            <i v-if="selectedLiveList.length > 0" class="el-icon-circle-close" @click.stop="handleClearLive"></i>
+            <i class="el-icon-arrow-down"></i>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="1">
+        <el-button
+          type="primary"
+          icon="el-icon-search"
+          size="small"
+          :loading="overviewLoading"
+          :disabled="selectedLiveList.length === 0"
+          @click="fetchOverview">
+          查询
+        </el-button>
+      </el-col>
+    </el-row>
+
+    <el-card ref="statsCard" class="box-card" shadow="never">
+      <div class="stats-main">
+        <!-- 固定导航栏:滚动时固定在顶部 -->
+        <div class="stats-nav-wrapper">
+          <div v-if="navFixed" ref="statsNavSpacer" class="stats-nav-spacer" :style="{ height: navHeight + 'px' }"/>
+          <div
+            ref="statsNavBar"
+            :class="['stats-nav-bar', { 'is-fixed': navFixed }]"
+            :style="navFixed ? navFixedStyle : {}">
+            <span
+              v-for="item in navItems"
+              :key="item.key"
+              :class="['nav-item', { active: activeNav === item.key }]"
+              @click="scrollToSection(item.key)">
+              {{ item.label }}
+            </span>
+          </div>
+        </div>
+        <!-- 四个板块竖排展示,每个板块独立卡片样式 -->
+        <el-card ref="sectionOverview" class="stats-section-card" shadow="never" data-section="overview">
+          <div slot="header" class="section-title">数据概览</div>
+          <div v-loading="overviewLoading" class="overview-content">
+            <template >
+              <!-- 直播数据 -->
+              <div class="overview-block">
+                <div class="overview-block-title">直播数据</div>
+                <el-row :gutter="16" class="overview-grid">
+                  <el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="item in overviewItems" :key="item.key">
+                    <div class="overview-item">
+                      <div class="overview-label">{{ item.label }}</div>
+                      <div class="overview-value">{{ formatOverviewValue(overviewData[item.key]) }}</div>
+                    </div>
+                  </el-col>
+                </el-row>
+              </div>
+              <!-- 互动带货数据 -->
+              <div class="overview-block">
+                <div class="overview-block-title">互动带货数据</div>
+                <el-row :gutter="16" class="overview-grid">
+                  <el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="item in interactionItems" :key="item.key">
+                    <div class="overview-item">
+                      <div class="overview-label">{{ item.label }}</div>
+                      <div class="overview-value">{{ formatOverviewValue(overviewData[item.key]) }}</div>
+                    </div>
+                  </el-col>
+                </el-row>
+              </div>
+            </template>
+          </div>
+        </el-card>
+        <el-card ref="sectionTrend" class="stats-section-card" shadow="never" data-section="trend">
+          <div slot="header" class="section-title">直播趋势</div>
+          <div v-loading="trendLoading" class="trend-chart-wrap">
+            <div  ref="trendChartRef" class="trend-chart"></div>
+          </div>
+        </el-card>
+        <el-card ref="sectionStudent" class="stats-section-card" shadow="never" data-section="student">
+          <div slot="header" class="section-title">直播间学员</div>
+          <div class="student-section">
+            <el-form :model="studentQueryParams" :inline="true" class="filter-form student-filter">
+              <el-form-item label="直播名称">
+                <el-select
+                  v-model="studentQueryParams.liveIds"
+                  multiple
+                  collapse-tags
+                  placeholder="不选则展示全部已选直播间"
+                  size="small"
+                  clearable
+                  style="width: 280px;">
+                  <el-option
+                    v-for="item in selectedLiveList"
+                    :key="item.liveId"
+                    :label="item.liveName + '(' + item.liveId + ')'"
+                    :value="item.liveId"/>
+                </el-select>
+              </el-form-item>
+              <el-form-item label="首次访问时间">
+                <el-date-picker
+                  v-model="studentFirstEntryRange"
+                  type="datetimerange"
+                  range-separator="至"
+                  start-placeholder="开始时间"
+                  end-placeholder="结束时间"
+                  value-format="yyyy-MM-dd HH:mm:ss"
+                  size="small"
+                  style="width: 340px;">
+                </el-date-picker>
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" icon="el-icon-search" size="small" :loading="studentLoading" @click="fetchStudentList">查询</el-button>
+                <el-button icon="el-icon-refresh" size="small" @click="resetStudentQuery">重置</el-button>
+              </el-form-item>
+            </el-form>
+            <el-table
+              v-loading="studentLoading"
+              :data="studentList"
+              border
+              style="width: 100%">
+              <el-table-column label="用户名" align="center" min-width="160">
+                <template slot-scope="scope">
+                  <div class="user-name-cell">
+                    <el-avatar :size="28" :src="scope.row.avatar" icon="el-icon-user-solid"/>
+                    <el-tooltip :content="scope.row.userName" placement="top">
+                      <span class="user-name-text">{{ scope.row.userName && scope.row.userName.length > 8 ? scope.row.userName.substring(0, 8) + '...' : scope.row.userName }}</span>
+                    </el-tooltip>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column label="直播间名称" align="center" prop="liveName" min-width="150" show-overflow-tooltip/>
+              <el-table-column label="销售名称" align="center" prop="salesName" min-width="100" show-overflow-tooltip/>
+              <el-table-column label="用户创建时间" align="center" prop="userCreateTime" width="170"/>
+              <el-table-column label="联系方式" align="center" prop="contact" width="140"/>
+            </el-table>
+            <pagination
+              v-show="studentTotal > 0"
+              :total="studentTotal"
+              :page.sync="studentQueryParams.pageNum"
+              :limit.sync="studentQueryParams.pageSize"
+              :auto-scroll="false"
+              @pagination="fetchStudentList"
+            />
+          </div>
+        </el-card>
+        <el-card ref="sectionCompare" class="stats-section-card" shadow="never" data-section="compare">
+          <div slot="header" class="section-title">数据对比</div>
+          <div class="compare-tabs-wrap">
+            <el-radio-group v-model="compareTab" size="small" class="compare-tabs">
+              <el-radio-button label="invite">邀课对比</el-radio-button>
+              <el-radio-button label="product">商品对比</el-radio-button>
+            </el-radio-group>
+            <!-- 邀课对比 -->
+            <div v-show="compareTab === 'invite'" class="compare-panel">
+              <el-form :model="inviteCompareQueryParams" :inline="true" class="filter-form">
+                <el-form-item label="分享人">
+                  <el-select
+                    v-model="inviteCompareQueryParams.companyUserIds"
+                    multiple
+                    collapse-tags
+                    placeholder="请选择分享人(需先选择直播间并查询)"
+                    size="small"
+                    clearable
+                    style="width: 400px;">
+                    <el-option
+                      v-for="item in inviteSalesOptions"
+                      :key="(item.companyId || '') + '_' + (item.companyUserId || '')"
+                      :label="item.salesName + (item.companyName ? '(' + item.companyName + ')' : '')"
+                      :value="(item.companyId || '') + '_' + (item.companyUserId || '')"/>
+                  </el-select>
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" icon="el-icon-search" size="small" :loading="inviteCompareLoading" @click="fetchInviteCompareList">查询</el-button>
+                  <el-button icon="el-icon-refresh" size="small" @click="resetInviteCompareQuery">重置</el-button>
+                </el-form-item>
+              </el-form>
+              <el-table
+                v-loading="inviteCompareLoading"
+                :data="inviteCompareList"
+                border
+                style="width: 100%">
+                <el-table-column label="归属公司" align="center" prop="companyName" min-width="140" show-overflow-tooltip/>
+                <el-table-column label="销售名称" align="center" prop="salesName" min-width="120" show-overflow-tooltip/>
+                <el-table-column label="邀请人数" align="center" prop="inviteCount" width="120"/>
+                <el-table-column label="已支付订单数" align="center" prop="paidOrderCount" width="140"/>
+                <el-table-column label="订单总金额" align="center" prop="totalGmv" width="140">
+                  <template slot-scope="scope">
+                    {{ formatProductMoney(scope.row.totalGmv) }}
+                  </template>
+                </el-table-column>
+              </el-table>
+              <pagination
+                v-show="inviteCompareTotal > 0"
+                :total="inviteCompareTotal"
+                :page.sync="inviteCompareQueryParams.pageNum"
+                :limit.sync="inviteCompareQueryParams.pageSize"
+                :auto-scroll="false"
+                @pagination="fetchInviteCompareList"
+              />
+            </div>
+            <!-- 商品对比 -->
+            <div v-show="compareTab === 'product'" class="compare-panel">
+              <el-form :model="productCompareQueryParams" :inline="true" class="filter-form">
+                <el-form-item label="直播间带货的产品">
+                  <el-select
+                    v-model="productCompareQueryParams.productIds"
+                    multiple
+                    collapse-tags
+                    placeholder="请选择商品(需先选择直播间并查询)"
+                    size="small"
+                    clearable
+                    style="width: 400px;">
+                    <el-option
+                      v-for="item in liveProductOptions"
+                      :key="item.productId"
+                      :label="item.productName"
+                      :value="item.productId"/>
+                  </el-select>
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" icon="el-icon-search" size="small" :loading="productCompareLoading" @click="fetchProductCompareList">查询</el-button>
+                  <el-button icon="el-icon-refresh" size="small" @click="resetProductCompareQuery">重置</el-button>
+                </el-form-item>
+              </el-form>
+              <el-table
+                v-loading="productCompareLoading"
+                :data="productCompareList"
+                border
+                style="width: 100%">
+                <el-table-column label="商品名称" align="center" prop="productName" min-width="180" show-overflow-tooltip/>
+                <el-table-column label="下单未支付人数" align="center" prop="unpaidUserCount" width="140"/>
+                <el-table-column label="成交人数" align="center" prop="paidUserCount" width="120"/>
+                <el-table-column label="成交金额" align="center" prop="totalGmv" width="140">
+                  <template slot-scope="scope">
+                    {{ formatProductMoney(scope.row.totalGmv) }}
+                  </template>
+                </el-table-column>
+              </el-table>
+              <pagination
+                v-show="productCompareTotal > 0"
+                :total="productCompareTotal"
+                :page.sync="productCompareQueryParams.pageNum"
+                :limit.sync="productCompareQueryParams.pageSize"
+                :auto-scroll="false"
+                @pagination="fetchProductCompareList"
+              />
+            </div>
+          </div>
+        </el-card>
+      </div>
+    </el-card>
+
+    <!-- 选择直播间弹窗 -->
+    <el-dialog
+      title="选择直播间"
+      :visible.sync="liveSelectDialogVisible"
+      width="900px"
+      append-to-body
+      :close-on-click-modal="false"
+      @open="handleDialogOpen"
+      @close="handleDialogClose">
+      <!-- 上面:筛选条件 -->
+      <el-form :model="liveQueryParams" ref="liveQueryForm" :inline="true" label-width="100px" class="filter-form">
+        <el-form-item label="直播名称" prop="liveName">
+          <el-input
+            v-model="liveQueryParams.liveName"
+            placeholder="请输入直播名称"
+            clearable
+            size="small"
+            style="width: 160px;"
+            @keyup.enter.native="handleLiveSearch"/>
+        </el-form-item>
+        <el-form-item label="直播时间" prop="createTimeRange">
+          <el-date-picker
+            v-model="createTimeRange"
+            type="datetimerange"
+            range-separator="至"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            size="small"
+            style="width: 340px;">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item v-if="!trainingPeriodId" label="公司名称" prop="companyName">
+          <el-input
+            v-model="liveQueryParams.companyName"
+            placeholder="请输入公司名称"
+            clearable
+            size="small"
+            style="width: 160px;"
+            @keyup.enter.native="handleLiveSearch"/>
+        </el-form-item>
+        <el-form-item label="直播状态" prop="status">
+          <el-select v-model="liveQueryParams.status" placeholder="请选择" clearable size="small" style="width: 120px;">
+            <el-option label="待直播" :value="1"></el-option>
+            <el-option label="直播中" :value="2"></el-option>
+            <el-option label="已结束" :value="3"></el-option>
+            <el-option label="直播回放中" :value="4"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="small" @click="handleLiveSearch">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="small" @click="resetLiveQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 下面:展示列表(多选,支持翻页继续选择) -->
+      <el-table
+        ref="liveTable"
+        v-loading="liveListLoading"
+        :data="liveList"
+        row-key="liveId"
+        @selection-change="handleSelectionChange"
+        border
+        max-height="400">
+        <el-table-column type="selection" width="55" align="center" reserve-selection />
+        <el-table-column label="直播ID" align="center" prop="liveId" width="100"/>
+        <el-table-column label="直播名称" align="center" prop="liveName" min-width="150" show-overflow-tooltip/>
+        <el-table-column label="直播状态" align="center" prop="status" width="120">
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.status === 1" size="small">待直播</el-tag>
+            <el-tag v-else-if="scope.row.status === 2" type="success" size="small">直播中</el-tag>
+            <el-tag v-else-if="scope.row.status === 3" type="info" size="small">已结束</el-tag>
+            <el-tag v-else-if="scope.row.status === 4" type="warning" size="small">直播回放中</el-tag>
+            <span v-else>---</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="直播公司" align="center" prop="companyName" min-width="120" show-overflow-tooltip>
+          <template slot-scope="scope">
+            {{ scope.row.companyName || '总台' }}
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="liveTotal > 0"
+        :total="liveTotal"
+        :page.sync="liveQueryParams.pageNum"
+        :limit.sync="liveQueryParams.pageSize"
+        @pagination="getLiveList"
+      />
+
+      <div slot="footer" class="dialog-footer">
+        <span class="selected-count">已选 {{ selectedLiveList.length }}/15 个</span>
+        <el-button @click="liveSelectDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="confirmSelectLive">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+import { listLive } from '@/api/live/live'
+import { listTrainingLive } from '@/api/live/trainingCamp'
+import { getLiveStatisticsOverview, getLiveEntryTrend, listLiveRoomStudents, listProductCompareStats, listInviteSalesOptions, listInviteCompareStats } from '@/api/live/liveData'
+import { listLiveGoods } from '@/api/live/liveGoods'
+
+export default {
+  name: 'LiveStatisticsPanel',
+  props: {
+    /** 若传入,则直播间选择范围仅限该营期(训练营工作台) */
+    trainingPeriodId: {
+      type: Number,
+      default: null
+    }
+  },
+  data() {
+    return {
+      // 已选择的直播间(多选)
+      selectedLiveList: [],
+      // 选择弹窗
+      liveSelectDialogVisible: false,
+      liveListLoading: false,
+      liveList: [],
+      liveTotal: 0,
+      // 直播间查询参数
+      liveQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        liveName: null,
+        companyName: null,
+        status: null,
+        beginTime: null,
+        endTime: null
+      },
+      // 创建时间范围(用于日期选择器双向绑定)
+      createTimeRange: null,
+      // 当前激活的导航
+      activeNav: 'overview',
+      // 导航配置
+      navItems: [
+        { key: 'overview', label: '数据概览' },
+        { key: 'trend', label: '直播趋势' },
+        { key: 'student', label: '直播间学员' },
+        { key: 'compare', label: '数据对比' }
+      ],
+      // 导航栏固定
+      navFixed: false,
+      navHeight: 48,
+      navFixedStyle: {},
+      // 数据概览
+      overviewData: {},
+      overviewLoading: false,
+      // 直播趋势折线图
+      trendLoading: false,
+      trendChartData: { xAxis: [], series: [] },
+      trendChart: null,
+      // 直播间学员
+      studentList: [],
+      studentTotal: 0,
+      studentLoading: false,
+      studentQueryParams: {
+        liveIds: [],
+        pageNum: 1,
+        pageSize: 10
+      },
+      studentFirstEntryRange: null,
+      // 数据对比切换 tab:invite=邀课对比,product=商品对比
+      compareTab: 'invite',
+      // 商品对比(嵌入数据对比)
+      liveProductOptions: [],
+      productCompareList: [],
+      productCompareTotal: 0,
+      productCompareLoading: false,
+      productCompareQueryParams: {
+        productIds: [],
+        liveIds: [],
+        pageNum: 1,
+        pageSize: 10
+      },
+      // 邀课对比(嵌入数据对比)
+      inviteSalesOptions: [],
+      inviteCompareList: [],
+      inviteCompareTotal: 0,
+      inviteCompareLoading: false,
+      inviteCompareQueryParams: {
+        companyUserIds: [],
+        pageNum: 1,
+        pageSize: 10
+      }
+    }
+  },
+  computed: {
+    overviewItems() {
+      return [
+        { key: 'beforeLiveUv', label: '开播前访问人数(uv)' },
+        { key: 'totalWatchUv', label: '累计观看人数(uv)' },
+        { key: 'over10MinCount', label: '停留时长超过10分钟人数' },
+        { key: 'totalWatchMinutes', label: '总观看时长(分钟)' },
+        { key: 'avgWatchMinutes', label: '平均观看时长(分钟)' },
+        { key: 'replayWatchUv', label: '累计回放观看人数(uv)' },
+        { key: 'replayVisitPv', label: '累计回放访问人数(pv)' },
+        { key: 'replayOnlyCount', label: '仅看过回放的人数' },
+        { key: 'replayTotalMinutes', label: '回放总观看时长(分钟)' },
+        { key: 'replayAvgMinutes', label: '回放平均观看时长(分钟)' },
+        { key: 'completeCount', label: '完课人数' },
+        { key: 'subscribeCount', label: '预约人数' }
+      ]
+    },
+    interactionItems() {
+      return [
+        { key: 'lotteryCount', label: '抽奖次数' },
+        { key: 'lotteryJoinCount', label: '参与抽奖人数' },
+        { key: 'lotteryWinCount', label: '中奖人数' },
+        { key: 'paidUserCount', label: '支付点击人数' },
+        { key: 'unpaidUserCount', label: '未支付人数' },
+        { key: 'totalGmv', label: '总成交金额' },
+        { key: 'totalProductQty', label: '商品总销量' }
+      ]
+    },
+    headerOffset() {
+      const fixed = this.$store.state.settings.fixedHeader
+      const tagsView = this.$store.state.settings.tagsView
+      if (fixed && tagsView) return 84
+      if (fixed) return 50
+      return 0
+    },
+    displayedLives() {
+      return this.selectedLiveList.slice(0, 7)
+    },
+    hasTrendData() {
+      return this.trendChartData.xAxis && this.trendChartData.xAxis.length > 0
+    }
+  },
+  watch: {
+    trainingPeriodId() {
+      this.handleClearLive()
+      if (this.liveSelectDialogVisible) {
+        this.liveQueryParams.pageNum = 1
+        this.getLiveList()
+      }
+    }
+  },
+  mounted() {
+    this.$nextTick(() => this.measureNavHeight())
+    window.addEventListener('scroll', this.handleNavScroll, { passive: true })
+    window.addEventListener('resize', this.handleNavScroll)
+    window.addEventListener('resize', this.handleTrendChartResize)
+    this.trendChartData = this.getDefaultTrendData()
+    this.$nextTick(() => setTimeout(() => this.renderTrendChart(), 100))
+  },
+  beforeDestroy() {
+    window.removeEventListener('scroll', this.handleNavScroll)
+    window.removeEventListener('resize', this.handleNavScroll)
+    window.removeEventListener('resize', this.handleTrendChartResize)
+    if (this.trendChart) {
+      this.trendChart.dispose()
+      this.trendChart = null
+    }
+  },
+  methods: {
+    openLiveSelectDialog() {
+      this.liveSelectDialogVisible = true
+    },
+    handleClearLive() {
+      this.selectedLiveList = []
+      this.studentList = []
+      this.studentTotal = 0
+      this.studentQueryParams.liveIds = []
+      this.studentFirstEntryRange = null
+      this.liveProductOptions = []
+      this.productCompareList = []
+      this.productCompareTotal = 0
+      this.productCompareQueryParams.productIds = []
+      if (this.trendChart) {
+        this.trendChart.dispose()
+        this.trendChart = null
+      }
+      this.trendChartData = { xAxis: [], series: [] }
+      this.overviewData = {}
+      this.$nextTick(() => {
+        const table = this.$refs.liveTable
+        if (table && typeof table.clearSelection === 'function') {
+          table.clearSelection()
+        }
+        this.handleTrendChartResize()
+      })
+    },
+    handleDialogOpen() {
+      this.getLiveList()
+    },
+    handleDialogClose() {
+      this.resetLiveQuery()
+    },
+    handleLiveSearch() {
+      this.liveQueryParams.pageNum = 1
+      this.getLiveList()
+    },
+    resetLiveQuery() {
+      this.liveQueryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        liveName: null,
+        companyName: null,
+        status: null,
+        beginTime: null,
+        endTime: null
+      }
+      this.createTimeRange = null
+    },
+    getLiveList() {
+      const params = { ...this.liveQueryParams }
+      if (this.createTimeRange && this.createTimeRange.length === 2) {
+        params.beginTime = this.createTimeRange[0]
+        params.endTime = this.createTimeRange[1]
+      }
+      this.liveListLoading = true
+      const req = this.trainingPeriodId != null
+        ? listTrainingLive({ ...params, trainingPeriodId: this.trainingPeriodId })
+        : listLive(params)
+      req.then(res => {
+        this.liveList = res.rows || []
+        this.liveTotal = res.total || 0
+        this.liveListLoading = false
+        this.$nextTick(() => this.setTableSelection())
+      }).catch(() => {
+        this.liveListLoading = false
+      })
+    },
+    handleSelectionChange(selection) {
+      if (selection && selection.length > 15) {
+        this.$message.warning('最多只能选择15个直播间')
+        this.$nextTick(() => this.setTableSelection())
+      } else {
+        this.selectedLiveList = selection || []
+      }
+    },
+    setTableSelection() {
+      this.$nextTick(() => {
+        const table = this.$refs.liveTable
+        if (!table || !this.liveList.length) return
+        const selectedIds = new Set(this.selectedLiveList.map(l => l.liveId))
+        this.liveList.forEach(row => {
+          table.toggleRowSelection(row, selectedIds.has(row.liveId))
+        })
+      })
+    },
+    confirmSelectLive() {
+      this.liveSelectDialogVisible = false
+      // 默认不选直播名称=展示全部已选直播间的人数
+    },
+    formatProductMoney(val) {
+      if (val === undefined || val === null) return '¥0.00'
+      const num = typeof val === 'number' ? val : parseFloat(val)
+      return '¥' + (isNaN(num) ? '0.00' : num.toFixed(2))
+    },
+    fetchLiveProductsForSelectedLives() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.liveProductOptions = []
+        return
+      }
+      const promises = liveIds.map(liveId => listLiveGoods({ liveId }).then(res => (res.rows || res.data?.rows || [])))
+      Promise.all(promises).then(results => {
+        const productMap = new Map()
+        results.flat().forEach(item => {
+          const pid = item.productId
+          if (pid && !productMap.has(pid)) {
+            productMap.set(pid, { productId: pid, productName: item.productName || item.name || '未知商品' })
+          }
+        })
+        this.liveProductOptions = Array.from(productMap.values())
+      }).catch(() => {
+        this.liveProductOptions = []
+      })
+    },
+    fetchProductCompareList() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.$message.warning('请先选择直播间')
+        return
+      }
+      const params = {
+        liveIds,
+        productIds: this.productCompareQueryParams.productIds,
+        pageNum: this.productCompareQueryParams.pageNum,
+        pageSize: this.productCompareQueryParams.pageSize
+      }
+      this.productCompareLoading = true
+      listProductCompareStats(params).then(res => {
+        this.productCompareList = res.rows || res.data?.rows || []
+        this.productCompareTotal = res.total || res.data?.total || 0
+        this.productCompareLoading = false
+      }).catch(() => {
+        this.productCompareLoading = false
+      })
+    },
+    resetProductCompareQuery() {
+      this.productCompareQueryParams.productIds = []
+      this.productCompareQueryParams.pageNum = 1
+      this.productCompareQueryParams.pageSize = 10
+      this.fetchProductCompareList()
+    },
+    fetchInviteSalesOptions() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.inviteSalesOptions = []
+        return
+      }
+      listInviteSalesOptions(liveIds).then(res => {
+        const data = res.data || res || []
+        this.inviteSalesOptions = Array.isArray(data) ? data : []
+      }).catch(() => {
+        this.inviteSalesOptions = []
+      })
+    },
+    fetchInviteCompareList() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.$message.warning('请先选择直播间')
+        return
+      }
+      const rawIds = this.inviteCompareQueryParams.companyUserIds || []
+      const companyUserIds = rawIds.map(v => {
+        if (typeof v === 'string' && v.includes('_')) {
+          return parseInt(v.split('_')[1], 10)
+        }
+        return typeof v === 'number' ? v : parseInt(v, 10)
+      }).filter(id => !isNaN(id))
+      const params = {
+        liveIds,
+        companyUserIds: companyUserIds.length ? companyUserIds : undefined,
+        pageNum: this.inviteCompareQueryParams.pageNum,
+        pageSize: this.inviteCompareQueryParams.pageSize
+      }
+      this.inviteCompareLoading = true
+      listInviteCompareStats(params).then(res => {
+        this.inviteCompareList = res.rows || res.data?.rows || []
+        this.inviteCompareTotal = res.total || res.data?.total || 0
+        this.inviteCompareLoading = false
+      }).catch(() => {
+        this.inviteCompareLoading = false
+      })
+    },
+    resetInviteCompareQuery() {
+      this.inviteCompareQueryParams.companyUserIds = []
+      this.inviteCompareQueryParams.pageNum = 1
+      this.inviteCompareQueryParams.pageSize = 10
+      this.fetchInviteCompareList()
+    },
+    fetchOverview() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.overviewData = {}
+        this.trendChartData = { xAxis: [], series: [] }
+        if (this.trendChart) {
+          this.trendChart.dispose()
+          this.trendChart = null
+        }
+        return
+      }
+      this.overviewLoading = true
+      getLiveStatisticsOverview(liveIds).then(res => {
+        this.overviewData = res.data || res || {}
+        this.overviewLoading = false
+        this.fetchTrend()
+        // 直播间学员:默认不选直播名称,自动查询全部已选直播间的用户
+        this.studentQueryParams.liveIds = []
+        this.studentFirstEntryRange = null
+        this.studentQueryParams.pageNum = 1
+        this.fetchStudentList()
+        // 商品对比:加载已选直播间的带货产品
+        this.fetchLiveProductsForSelectedLives()
+        // 邀课对比:加载已选直播间的分享人选项
+        this.fetchInviteSalesOptions()
+      }).catch(() => {
+        this.overviewLoading = false
+      })
+    },
+    fetchTrend() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.trendChartData = this.getDefaultTrendData()
+        this.$nextTick(() => {
+          setTimeout(() => this.renderTrendChart(), 150)
+        })
+        return
+      }
+      this.trendLoading = true
+      getLiveEntryTrend(liveIds).then(res => {
+        // 兼容多种返回格式: { code, msg, data: { xAxis, series } } 或 { data: [series] } 或 { xAxis, series }
+        const body = res && typeof res === 'object' && res.data !== undefined ? res.data : res
+        let xAxis = []
+        let series = []
+        const dataObj = body && typeof body === 'object' ? body : null
+        if (Array.isArray(body) && body.length > 0) {
+          series = body
+        } else if (dataObj && Array.isArray(dataObj.series) && dataObj.series.length > 0) {
+          xAxis = Array.isArray(dataObj.xAxis) ? dataObj.xAxis : []
+          series = dataObj.series
+        } else if (dataObj && Array.isArray(dataObj.data) && dataObj.data.length > 0) {
+          series = dataObj.data
+        } else if (dataObj && Array.isArray(dataObj.rows) && dataObj.rows.length > 0) {
+          series = dataObj.rows
+        }
+        series = Array.isArray(series) ? series : []
+        if (series.length && (!Array.isArray(xAxis) || !xAxis.length) && series[0] && Array.isArray(series[0].data)) {
+          const len = series[0].data.length
+          xAxis = ['开播前']
+          for (let m = 0; m < len - 1; m++) xAxis.push((m * 5) + 'min')
+        }
+        if (!series.length) {
+          this.trendChartData = this.getDefaultTrendData()
+        } else {
+          if (!xAxis.length) xAxis = this.getDefaultTrendData().xAxis
+          this.trendChartData = { xAxis, series }
+        }
+        this.trendLoading = false
+        this.$nextTick(() => {
+          setTimeout(() => {
+            this.renderTrendChart()
+            this.$nextTick(() => { if (this.trendChart) this.trendChart.resize() })
+          }, 80)
+        })
+      }).catch(() => {
+        this.trendChartData = this.getDefaultTrendData()
+        this.trendLoading = false
+        this.$nextTick(() => {
+          setTimeout(() => {
+            this.renderTrendChart()
+            this.$nextTick(() => {
+              if (this.trendChart) this.trendChart.resize()
+            })
+          }, 150)
+        })
+      })
+    },
+    getDefaultTrendData() {
+      const xAxis = ['开播前']
+      for (let m = 0; m <= 240; m += 5) {
+        xAxis.push(m + 'min')
+      }
+      const zeros = xAxis.map(() => 0)
+      return {
+        xAxis,
+        series: [{ name: '进入人数', data: zeros }]
+      }
+    },
+    renderTrendChart() {
+      const el = this.$refs.trendChartRef
+      if (!el) return
+      let xAxis = this.trendChartData.xAxis && Array.isArray(this.trendChartData.xAxis) ? this.trendChartData.xAxis : []
+      const series = this.trendChartData.series && Array.isArray(this.trendChartData.series) ? this.trendChartData.series : []
+      if (!series.length) return
+      if (!xAxis.length && series[0] && series[0].data) {
+        const len = series[0].data.length
+        xAxis = ['开播前']
+        for (let m = 0; m < len - 1; m++) xAxis.push((m * 5) + 'min')
+      }
+      if (!xAxis.length) xAxis = this.getDefaultTrendData().xAxis
+      if (this.trendChart) this.trendChart.dispose()
+      this.trendChart = echarts.init(el)
+      const option = {
+        title: { text: '相对直播开始时间的累计进入人数', left: 'center' },
+        tooltip: { trigger: 'axis' },
+        legend: { bottom: 0, type: 'scroll' },
+        grid: { left: '3%', right: '4%', bottom: '15%', top: 40, containLabel: true },
+        xAxis: { type: 'category', boundaryGap: false, data: xAxis },
+        yAxis: { type: 'value', name: '累计进入人数' },
+        series: series.map((s, i) => ({
+          name: s.name,
+          type: 'line',
+          smooth: true,
+          data: s.data || []
+        }))
+      }
+      this.trendChart.setOption(option)
+      this.$nextTick(() => {
+        if (this.trendChart) this.trendChart.resize()
+      })
+    },
+    handleTrendChartResize() {
+      if (this.trendChart) this.trendChart.resize()
+    },
+    fetchStudentList() {
+      const liveIds = this.studentQueryParams.liveIds && this.studentQueryParams.liveIds.length > 0
+        ? this.studentQueryParams.liveIds
+        : (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.studentList = []
+        this.studentTotal = 0
+        return
+      }
+      const params = {
+        liveIds,
+        pageNum: this.studentQueryParams.pageNum || 1,
+        pageSize: this.studentQueryParams.pageSize || 10
+      }
+      if (this.studentFirstEntryRange && this.studentFirstEntryRange.length === 2) {
+        params.firstEntryTimeBegin = this.studentFirstEntryRange[0]
+        params.firstEntryTimeEnd = this.studentFirstEntryRange[1]
+      }
+      this.studentLoading = true
+      listLiveRoomStudents(params).then(res => {
+        this.studentList = res.rows || []
+        this.studentTotal = res.total || 0
+        this.studentLoading = false
+      }).catch(() => {
+        this.studentLoading = false
+      })
+    },
+    resetStudentQuery() {
+      this.studentQueryParams.liveIds = []
+      this.studentQueryParams.pageNum = 1
+      this.studentQueryParams.pageSize = 10
+      this.studentFirstEntryRange = null
+      this.fetchStudentList()
+    },
+    formatOverviewValue(val) {
+      if (val === undefined || val === null) return '0'
+      if (typeof val === 'number' && !Number.isInteger(val)) return Number(val).toFixed(2)
+      return String(val)
+    },
+    measureNavHeight() {
+      const nav = this.$refs.statsNavBar
+      if (nav) {
+        this.navHeight = nav.offsetHeight || 48
+      }
+    },
+    handleNavScroll() {
+      const nav = this.$refs.statsNavBar
+      const card = this.$refs.statsCard
+      if (!nav || !card) return
+
+      if (this.navFixed) {
+        const spacer = this.$refs.statsNavSpacer
+        if (spacer) {
+          const rect = spacer.getBoundingClientRect()
+          if (rect.top >= this.headerOffset) {
+            this.navFixed = false
+            this.navFixedStyle = {}
+          }
+        }
+      } else {
+        const rect = nav.getBoundingClientRect()
+        if (rect.top <= this.headerOffset) {
+          const cardRect = card.$el ? card.$el.getBoundingClientRect() : card.getBoundingClientRect()
+          this.navFixedStyle = {
+            left: cardRect.left + 'px',
+            top: this.headerOffset + 'px',
+            width: cardRect.width + 'px'
+          }
+          this.navFixed = true
+        }
+      }
+    },
+    scrollToSection(key) {
+      this.activeNav = key
+      const refMap = {
+        overview: 'sectionOverview',
+        trend: 'sectionTrend',
+        student: 'sectionStudent',
+        compare: 'sectionCompare'
+      }
+      const el = this.$refs[refMap[key]]
+      if (el && el.$el) {
+        el.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
+      } else if (el) {
+        el.scrollIntoView({ behavior: 'smooth', block: 'start' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.live-select-input {
+  display: flex;
+  align-items: flex-start;
+  width: 100%;
+  min-width: 200px;
+  min-height: 32px;
+  padding: 6px 12px;
+  border: 1px solid #DCDFE6;
+  border-radius: 4px;
+  cursor: pointer;
+  background: #fff;
+  overflow: hidden;
+}
+.live-select-input:hover {
+  border-color: #C0C4CC;
+}
+.live-select-content {
+  flex: 1;
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+  align-items: center;
+  gap: 6px 8px;
+  min-height: 22px;
+  min-width: 0;
+  overflow: hidden;
+}
+.live-select-content .placeholder {
+  color: #C0C4CC;
+  font-size: 14px;
+}
+.live-tag {
+  display: inline-block;
+  padding: 0 8px;
+  height: 22px;
+  line-height: 22px;
+  font-size: 12px;
+  color: #606266;
+  background: #f4f4f5;
+  border-radius: 4px;
+}
+.tag-sep {
+  color: #909399;
+  margin: 0 2px;
+}
+.fold-text {
+  font-size: 12px;
+  color: #909399;
+}
+.live-select-suffix {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding-left: 10px;
+}
+.live-select-suffix i {
+  color: #C0C4CC;
+  font-size: 14px;
+  cursor: pointer;
+}
+.live-select-suffix i:hover {
+  color: #909399;
+}
+.stats-main {
+  padding-top: 0;
+}
+.stats-nav-wrapper {
+  position: relative;
+}
+.stats-nav-spacer {
+  flex-shrink: 0;
+}
+.stats-nav-bar {
+  position: relative;
+  z-index: 100;
+  display: flex;
+  width: 100%;
+  flex: 1 1 auto;
+  background: #fff;
+  border-bottom: 1px solid #ebeef5;
+  margin: 0 -20px 20px -20px;
+  padding: 0;
+  box-shadow: 0 1px 2px rgba(0,0,0,0.03);
+}
+.stats-nav-bar.is-fixed {
+  position: fixed;
+  margin: 0;
+  left: 0;
+  right: 0;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
+}
+.stats-nav-bar .nav-item {
+  flex: 1;
+  text-align: center;
+  padding: 14px 12px;
+  font-size: 14px;
+  color: #606266;
+  cursor: pointer;
+  border-bottom: 2px solid transparent;
+  margin-bottom: -1px;
+  transition: all 0.2s;
+}
+.stats-nav-bar .nav-item:hover {
+  color: #409EFF;
+}
+.stats-nav-bar .nav-item.active {
+  color: #409EFF;
+  font-weight: 500;
+  border-bottom-color: #409EFF;
+}
+.overview-content {
+  min-height: 120px;
+}
+.overview-grid {
+  margin-top: 8px;
+}
+.overview-item {
+  padding: 12px 16px;
+  margin-bottom: 12px;
+  background: #fafafa;
+  border-radius: 4px;
+  border-left: 3px solid #409EFF;
+  border: 1px solid #ebeef5;
+}
+.overview-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 6px;
+  line-height: 1.4;
+}
+.overview-value {
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+.overview-block {
+  margin-bottom: 24px;
+}
+.overview-block:last-child {
+  margin-bottom: 0;
+}
+.overview-block-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 12px;
+  padding-bottom: 8px;
+  border-bottom: 1px solid #ebeef5;
+}
+.stats-section-card {
+  margin-bottom: 30px;
+  /* 固定导航时:顶部 84px + 导航栏 ~48px ≈ 132,留足空间避免遮挡 */
+  scroll-margin-top: 140px;
+}
+.stats-section-card:last-child {
+  margin-bottom: 0;
+}
+.stats-section {
+  min-height: 300px;
+  padding: 20px 0;
+}
+.stats-section .section-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #303133;
+  margin-bottom: 16px;
+  padding-bottom: 12px;
+  padding-left: 10px;
+  border-bottom: 1px solid #ebeef5;
+  border-left: 3px solid #67C23A;
+}
+.box-card {
+  margin-bottom: 20px;
+}
+.filter-form {
+  margin-bottom: 16px;
+  padding-bottom: 16px;
+  border-bottom: 1px solid #ebeef5;
+}
+.dialog-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12px;
+}
+.dialog-footer .selected-count {
+  margin-right: auto;
+  font-size: 13px;
+  color: #606266;
+}
+.trend-chart-wrap {
+  min-height: 320px;
+}
+.trend-chart {
+  width: 100%;
+  height: 360px;
+}
+.student-section {
+  min-height: 200px;
+}
+.student-filter {
+  margin-bottom: 16px;
+}
+.user-name-cell {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+}
+.user-name-cell .user-name-text {
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.overview-content {
+  min-height: 120px;
+}
+.overview-grid {
+  margin-top: 0;
+}
+.overview-item {
+  padding: 12px 16px;
+  margin-bottom: 12px;
+  background: #fafafa;
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+}
+.overview-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 8px;
+  line-height: 1.4;
+}
+.overview-value {
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+.compare-tabs-wrap {
+  padding-top: 4px;
+}
+.compare-tabs {
+  margin-bottom: 16px;
+}
+.compare-panel {
+  min-height: 200px;
+}
+</style>

+ 3 - 1166
src/views/live/liveStatistics/index.vue

@@ -1,1175 +1,12 @@
 <template>
-  <div class="app-container">
-    <!-- 左上角:选择直播间(多选,最多展示8个,多余折叠)+ 查询按钮 -->
-    <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <div
-          class="live-select-input"
-          @click="openLiveSelectDialog">
-          <div class="live-select-content">
-            <template v-if="selectedLiveList.length === 0">
-              <span class="placeholder">请选择直播间</span>
-            </template>
-            <template v-else>
-              <template v-for="(item, idx) in displayedLives">
-                <span :key="item.liveId" class="live-tag">{{ item.liveName }}({{ item.liveId }})</span>
-                <span v-if="idx < displayedLives.length - 1 || selectedLiveList.length > 7" class="tag-sep">、</span>
-              </template>
-              <span v-if="selectedLiveList.length > 7" class="fold-text">等{{ selectedLiveList.length }}个</span>
-            </template>
-          </div>
-          <div class="live-select-suffix">
-            <i v-if="selectedLiveList.length > 0" class="el-icon-circle-close" @click.stop="handleClearLive"></i>
-            <i class="el-icon-arrow-down"></i>
-          </div>
-        </div>
-      </el-col>
-      <el-col :span="1">
-        <el-button
-          type="primary"
-          icon="el-icon-search"
-          size="small"
-          :loading="overviewLoading"
-          :disabled="selectedLiveList.length === 0"
-          @click="fetchOverview">
-          查询
-        </el-button>
-      </el-col>
-    </el-row>
-
-    <el-card ref="statsCard" class="box-card" shadow="never">
-      <div class="stats-main">
-        <!-- 固定导航栏:滚动时固定在顶部 -->
-        <div class="stats-nav-wrapper">
-          <div v-if="navFixed" ref="statsNavSpacer" class="stats-nav-spacer" :style="{ height: navHeight + 'px' }"/>
-          <div
-            ref="statsNavBar"
-            :class="['stats-nav-bar', { 'is-fixed': navFixed }]"
-            :style="navFixed ? navFixedStyle : {}">
-            <span
-              v-for="item in navItems"
-              :key="item.key"
-              :class="['nav-item', { active: activeNav === item.key }]"
-              @click="scrollToSection(item.key)">
-              {{ item.label }}
-            </span>
-          </div>
-        </div>
-        <!-- 四个板块竖排展示,每个板块独立卡片样式 -->
-        <el-card ref="sectionOverview" class="stats-section-card" shadow="never" data-section="overview">
-          <div slot="header" class="section-title">数据概览</div>
-          <div v-loading="overviewLoading" class="overview-content">
-            <template >
-              <!-- 直播数据 -->
-              <div class="overview-block">
-                <div class="overview-block-title">直播数据</div>
-                <el-row :gutter="16" class="overview-grid">
-                  <el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="item in overviewItems" :key="item.key">
-                    <div class="overview-item">
-                      <div class="overview-label">{{ item.label }}</div>
-                      <div class="overview-value">{{ formatOverviewValue(overviewData[item.key]) }}</div>
-                    </div>
-                  </el-col>
-                </el-row>
-              </div>
-              <!-- 互动带货数据 -->
-              <div class="overview-block">
-                <div class="overview-block-title">互动带货数据</div>
-                <el-row :gutter="16" class="overview-grid">
-                  <el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="item in interactionItems" :key="item.key">
-                    <div class="overview-item">
-                      <div class="overview-label">{{ item.label }}</div>
-                      <div class="overview-value">{{ formatOverviewValue(overviewData[item.key]) }}</div>
-                    </div>
-                  </el-col>
-                </el-row>
-              </div>
-            </template>
-          </div>
-        </el-card>
-        <el-card ref="sectionTrend" class="stats-section-card" shadow="never" data-section="trend">
-          <div slot="header" class="section-title">直播趋势</div>
-          <div v-loading="trendLoading" class="trend-chart-wrap">
-            <div  ref="trendChartRef" class="trend-chart"></div>
-          </div>
-        </el-card>
-        <el-card ref="sectionStudent" class="stats-section-card" shadow="never" data-section="student">
-          <div slot="header" class="section-title">直播间学员</div>
-          <div class="student-section">
-            <el-form :model="studentQueryParams" :inline="true" class="filter-form student-filter">
-              <el-form-item label="直播名称">
-                <el-select
-                  v-model="studentQueryParams.liveIds"
-                  multiple
-                  collapse-tags
-                  placeholder="不选则展示全部已选直播间"
-                  size="small"
-                  clearable
-                  style="width: 280px;">
-                  <el-option
-                    v-for="item in selectedLiveList"
-                    :key="item.liveId"
-                    :label="item.liveName + '(' + item.liveId + ')'"
-                    :value="item.liveId"/>
-                </el-select>
-              </el-form-item>
-              <el-form-item label="首次访问时间">
-                <el-date-picker
-                  v-model="studentFirstEntryRange"
-                  type="datetimerange"
-                  range-separator="至"
-                  start-placeholder="开始时间"
-                  end-placeholder="结束时间"
-                  value-format="yyyy-MM-dd HH:mm:ss"
-                  size="small"
-                  style="width: 340px;">
-                </el-date-picker>
-              </el-form-item>
-              <el-form-item>
-                <el-button type="primary" icon="el-icon-search" size="small" :loading="studentLoading" @click="fetchStudentList">查询</el-button>
-                <el-button icon="el-icon-refresh" size="small" @click="resetStudentQuery">重置</el-button>
-              </el-form-item>
-            </el-form>
-            <el-table
-              v-loading="studentLoading"
-              :data="studentList"
-              border
-              style="width: 100%">
-              <el-table-column label="用户名" align="center" min-width="160">
-                <template slot-scope="scope">
-                  <div class="user-name-cell">
-                    <el-avatar :size="28" :src="scope.row.avatar" icon="el-icon-user-solid"/>
-                    <el-tooltip :content="scope.row.userName" placement="top">
-                      <span class="user-name-text">{{ scope.row.userName && scope.row.userName.length > 8 ? scope.row.userName.substring(0, 8) + '...' : scope.row.userName }}</span>
-                    </el-tooltip>
-                  </div>
-                </template>
-              </el-table-column>
-              <el-table-column label="直播间名称" align="center" prop="liveName" min-width="150" show-overflow-tooltip/>
-              <el-table-column label="销售名称" align="center" prop="salesName" min-width="100" show-overflow-tooltip/>
-              <el-table-column label="用户创建时间" align="center" prop="userCreateTime" width="170"/>
-              <el-table-column label="联系方式" align="center" prop="contact" width="140"/>
-            </el-table>
-            <pagination
-              v-show="studentTotal > 0"
-              :total="studentTotal"
-              :page.sync="studentQueryParams.pageNum"
-              :limit.sync="studentQueryParams.pageSize"
-              :auto-scroll="false"
-              @pagination="fetchStudentList"
-            />
-          </div>
-        </el-card>
-        <el-card ref="sectionCompare" class="stats-section-card" shadow="never" data-section="compare">
-          <div slot="header" class="section-title">数据对比</div>
-          <div class="compare-tabs-wrap">
-            <el-radio-group v-model="compareTab" size="small" class="compare-tabs">
-              <el-radio-button label="invite">邀课对比</el-radio-button>
-              <el-radio-button label="product">商品对比</el-radio-button>
-            </el-radio-group>
-            <!-- 邀课对比 -->
-            <div v-show="compareTab === 'invite'" class="compare-panel">
-              <el-form :model="inviteCompareQueryParams" :inline="true" class="filter-form">
-                <el-form-item label="分享人">
-                  <el-select
-                    v-model="inviteCompareQueryParams.companyUserIds"
-                    multiple
-                    collapse-tags
-                    placeholder="请选择分享人(需先选择直播间并查询)"
-                    size="small"
-                    clearable
-                    style="width: 400px;">
-                    <el-option
-                      v-for="item in inviteSalesOptions"
-                      :key="(item.companyId || '') + '_' + (item.companyUserId || '')"
-                      :label="item.salesName + (item.companyName ? '(' + item.companyName + ')' : '')"
-                      :value="(item.companyId || '') + '_' + (item.companyUserId || '')"/>
-                  </el-select>
-                </el-form-item>
-                <el-form-item>
-                  <el-button type="primary" icon="el-icon-search" size="small" :loading="inviteCompareLoading" @click="fetchInviteCompareList">查询</el-button>
-                  <el-button icon="el-icon-refresh" size="small" @click="resetInviteCompareQuery">重置</el-button>
-                </el-form-item>
-              </el-form>
-              <el-table
-                v-loading="inviteCompareLoading"
-                :data="inviteCompareList"
-                border
-                style="width: 100%">
-                <el-table-column label="归属公司" align="center" prop="companyName" min-width="140" show-overflow-tooltip/>
-                <el-table-column label="销售名称" align="center" prop="salesName" min-width="120" show-overflow-tooltip/>
-                <el-table-column label="邀请人数" align="center" prop="inviteCount" width="120"/>
-                <el-table-column label="已支付订单数" align="center" prop="paidOrderCount" width="140"/>
-                <el-table-column label="订单总金额" align="center" prop="totalGmv" width="140">
-                  <template slot-scope="scope">
-                    {{ formatProductMoney(scope.row.totalGmv) }}
-                  </template>
-                </el-table-column>
-              </el-table>
-              <pagination
-                v-show="inviteCompareTotal > 0"
-                :total="inviteCompareTotal"
-                :page.sync="inviteCompareQueryParams.pageNum"
-                :limit.sync="inviteCompareQueryParams.pageSize"
-                :auto-scroll="false"
-                @pagination="fetchInviteCompareList"
-              />
-            </div>
-            <!-- 商品对比 -->
-            <div v-show="compareTab === 'product'" class="compare-panel">
-              <el-form :model="productCompareQueryParams" :inline="true" class="filter-form">
-                <el-form-item label="直播间带货的产品">
-                  <el-select
-                    v-model="productCompareQueryParams.productIds"
-                    multiple
-                    collapse-tags
-                    placeholder="请选择商品(需先选择直播间并查询)"
-                    size="small"
-                    clearable
-                    style="width: 400px;">
-                    <el-option
-                      v-for="item in liveProductOptions"
-                      :key="item.productId"
-                      :label="item.productName"
-                      :value="item.productId"/>
-                  </el-select>
-                </el-form-item>
-                <el-form-item>
-                  <el-button type="primary" icon="el-icon-search" size="small" :loading="productCompareLoading" @click="fetchProductCompareList">查询</el-button>
-                  <el-button icon="el-icon-refresh" size="small" @click="resetProductCompareQuery">重置</el-button>
-                </el-form-item>
-              </el-form>
-              <el-table
-                v-loading="productCompareLoading"
-                :data="productCompareList"
-                border
-                style="width: 100%">
-                <el-table-column label="商品名称" align="center" prop="productName" min-width="180" show-overflow-tooltip/>
-                <el-table-column label="下单未支付人数" align="center" prop="unpaidUserCount" width="140"/>
-                <el-table-column label="成交人数" align="center" prop="paidUserCount" width="120"/>
-                <el-table-column label="成交金额" align="center" prop="totalGmv" width="140">
-                  <template slot-scope="scope">
-                    {{ formatProductMoney(scope.row.totalGmv) }}
-                  </template>
-                </el-table-column>
-              </el-table>
-              <pagination
-                v-show="productCompareTotal > 0"
-                :total="productCompareTotal"
-                :page.sync="productCompareQueryParams.pageNum"
-                :limit.sync="productCompareQueryParams.pageSize"
-                :auto-scroll="false"
-                @pagination="fetchProductCompareList"
-              />
-            </div>
-          </div>
-        </el-card>
-      </div>
-    </el-card>
-
-    <!-- 选择直播间弹窗 -->
-    <el-dialog
-      title="选择直播间"
-      :visible.sync="liveSelectDialogVisible"
-      width="900px"
-      append-to-body
-      :close-on-click-modal="false"
-      @open="handleDialogOpen"
-      @close="handleDialogClose">
-      <!-- 上面:筛选条件 -->
-      <el-form :model="liveQueryParams" ref="liveQueryForm" :inline="true" label-width="100px" class="filter-form">
-        <el-form-item label="直播名称" prop="liveName">
-          <el-input
-            v-model="liveQueryParams.liveName"
-            placeholder="请输入直播名称"
-            clearable
-            size="small"
-            style="width: 160px;"
-            @keyup.enter.native="handleLiveSearch"/>
-        </el-form-item>
-        <el-form-item label="直播时间" prop="createTimeRange">
-          <el-date-picker
-            v-model="createTimeRange"
-            type="datetimerange"
-            range-separator="至"
-            start-placeholder="开始时间"
-            end-placeholder="结束时间"
-            value-format="yyyy-MM-dd HH:mm:ss"
-            size="small"
-            style="width: 340px;">
-          </el-date-picker>
-        </el-form-item>
-        <el-form-item label="公司名称" prop="companyName">
-          <el-input
-            v-model="liveQueryParams.companyName"
-            placeholder="请输入公司名称"
-            clearable
-            size="small"
-            style="width: 160px;"
-            @keyup.enter.native="handleLiveSearch"/>
-        </el-form-item>
-        <el-form-item label="直播状态" prop="status">
-          <el-select v-model="liveQueryParams.status" placeholder="请选择" clearable size="small" style="width: 120px;">
-            <el-option label="待直播" :value="1"></el-option>
-            <el-option label="直播中" :value="2"></el-option>
-            <el-option label="已结束" :value="3"></el-option>
-            <el-option label="直播回放中" :value="4"></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" icon="el-icon-search" size="small" @click="handleLiveSearch">搜索</el-button>
-          <el-button icon="el-icon-refresh" size="small" @click="resetLiveQuery">重置</el-button>
-        </el-form-item>
-      </el-form>
-
-      <!-- 下面:展示列表(多选,支持翻页继续选择) -->
-      <el-table
-        ref="liveTable"
-        v-loading="liveListLoading"
-        :data="liveList"
-        row-key="liveId"
-        @selection-change="handleSelectionChange"
-        border
-        max-height="400">
-        <el-table-column type="selection" width="55" align="center" reserve-selection />
-        <el-table-column label="直播ID" align="center" prop="liveId" width="100"/>
-        <el-table-column label="直播名称" align="center" prop="liveName" min-width="150" show-overflow-tooltip/>
-        <el-table-column label="直播状态" align="center" prop="status" width="120">
-          <template slot-scope="scope">
-            <el-tag v-if="scope.row.status === 1" size="small">待直播</el-tag>
-            <el-tag v-else-if="scope.row.status === 2" type="success" size="small">直播中</el-tag>
-            <el-tag v-else-if="scope.row.status === 3" type="info" size="small">已结束</el-tag>
-            <el-tag v-else-if="scope.row.status === 4" type="warning" size="small">直播回放中</el-tag>
-            <span v-else>---</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="直播公司" align="center" prop="companyName" min-width="120" show-overflow-tooltip>
-          <template slot-scope="scope">
-            {{ scope.row.companyName || '总台' }}
-          </template>
-        </el-table-column>
-      </el-table>
-
-      <pagination
-        v-show="liveTotal > 0"
-        :total="liveTotal"
-        :page.sync="liveQueryParams.pageNum"
-        :limit.sync="liveQueryParams.pageSize"
-        @pagination="getLiveList"
-      />
-
-      <div slot="footer" class="dialog-footer">
-        <span class="selected-count">已选 {{ selectedLiveList.length }}/15 个</span>
-        <el-button @click="liveSelectDialogVisible = false">取 消</el-button>
-        <el-button type="primary" @click="confirmSelectLive">确 定</el-button>
-      </div>
-    </el-dialog>
-  </div>
+  <live-statistics-panel />
 </template>
 
 <script>
-import * as echarts from 'echarts'
-import { listLive } from '@/api/live/live'
-import { getLiveStatisticsOverview, getLiveEntryTrend, listLiveRoomStudents, listProductCompareStats, listInviteSalesOptions, listInviteCompareStats } from '@/api/live/liveData'
-import { listLiveGoods } from '@/api/live/liveGoods'
+import LiveStatisticsPanel from './LiveStatisticsPanel.vue'
 
 export default {
   name: 'LiveStatistics',
-  data() {
-    return {
-      // 已选择的直播间(多选)
-      selectedLiveList: [],
-      // 选择弹窗
-      liveSelectDialogVisible: false,
-      liveListLoading: false,
-      liveList: [],
-      liveTotal: 0,
-      // 直播间查询参数
-      liveQueryParams: {
-        pageNum: 1,
-        pageSize: 10,
-        liveName: null,
-        companyName: null,
-        status: null,
-        beginTime: null,
-        endTime: null
-      },
-      // 创建时间范围(用于日期选择器双向绑定)
-      createTimeRange: null,
-      // 当前激活的导航
-      activeNav: 'overview',
-      // 导航配置
-      navItems: [
-        { key: 'overview', label: '数据概览' },
-        { key: 'trend', label: '直播趋势' },
-        { key: 'student', label: '直播间学员' },
-        { key: 'compare', label: '数据对比' }
-      ],
-      // 导航栏固定
-      navFixed: false,
-      navHeight: 48,
-      navFixedStyle: {},
-      // 数据概览
-      overviewData: {},
-      overviewLoading: false,
-      // 直播趋势折线图
-      trendLoading: false,
-      trendChartData: { xAxis: [], series: [] },
-      trendChart: null,
-      // 直播间学员
-      studentList: [],
-      studentTotal: 0,
-      studentLoading: false,
-      studentQueryParams: {
-        liveIds: [],
-        pageNum: 1,
-        pageSize: 10
-      },
-      studentFirstEntryRange: null,
-      // 数据对比切换 tab:invite=邀课对比,product=商品对比
-      compareTab: 'invite',
-      // 商品对比(嵌入数据对比)
-      liveProductOptions: [],
-      productCompareList: [],
-      productCompareTotal: 0,
-      productCompareLoading: false,
-      productCompareQueryParams: {
-        productIds: [],
-        liveIds: [],
-        pageNum: 1,
-        pageSize: 10
-      },
-      // 邀课对比(嵌入数据对比)
-      inviteSalesOptions: [],
-      inviteCompareList: [],
-      inviteCompareTotal: 0,
-      inviteCompareLoading: false,
-      inviteCompareQueryParams: {
-        companyUserIds: [],
-        pageNum: 1,
-        pageSize: 10
-      }
-    }
-  },
-  computed: {
-    overviewItems() {
-      return [
-        { key: 'beforeLiveUv', label: '开播前访问人数(uv)' },
-        { key: 'totalWatchUv', label: '累计观看人数(uv)' },
-        { key: 'over10MinCount', label: '停留时长超过10分钟人数' },
-        { key: 'totalWatchMinutes', label: '总观看时长(分钟)' },
-        { key: 'avgWatchMinutes', label: '平均观看时长(分钟)' },
-        { key: 'replayWatchUv', label: '累计回放观看人数(uv)' },
-        { key: 'replayVisitPv', label: '累计回放访问人数(pv)' },
-        { key: 'replayOnlyCount', label: '仅看过回放的人数' },
-        { key: 'replayTotalMinutes', label: '回放总观看时长(分钟)' },
-        { key: 'replayAvgMinutes', label: '回放平均观看时长(分钟)' },
-        { key: 'completeCount', label: '完课人数' },
-        { key: 'subscribeCount', label: '预约人数' }
-      ]
-    },
-    interactionItems() {
-      return [
-        { key: 'lotteryCount', label: '抽奖次数' },
-        { key: 'lotteryJoinCount', label: '参与抽奖人数' },
-        { key: 'lotteryWinCount', label: '中奖人数' },
-        { key: 'paidUserCount', label: '支付点击人数' },
-        { key: 'unpaidUserCount', label: '未支付人数' },
-        { key: 'totalGmv', label: '总成交金额' },
-        { key: 'totalProductQty', label: '商品总销量' }
-      ]
-    },
-    headerOffset() {
-      const fixed = this.$store.state.settings.fixedHeader
-      const tagsView = this.$store.state.settings.tagsView
-      if (fixed && tagsView) return 84
-      if (fixed) return 50
-      return 0
-    },
-    displayedLives() {
-      return this.selectedLiveList.slice(0, 7)
-    },
-    hasTrendData() {
-      return this.trendChartData.xAxis && this.trendChartData.xAxis.length > 0
-    }
-  },
-  mounted() {
-    this.$nextTick(() => this.measureNavHeight())
-    window.addEventListener('scroll', this.handleNavScroll, { passive: true })
-    window.addEventListener('resize', this.handleNavScroll)
-    window.addEventListener('resize', this.handleTrendChartResize)
-    this.trendChartData = this.getDefaultTrendData()
-    this.$nextTick(() => setTimeout(() => this.renderTrendChart(), 100))
-  },
-  beforeDestroy() {
-    window.removeEventListener('scroll', this.handleNavScroll)
-    window.removeEventListener('resize', this.handleNavScroll)
-    window.removeEventListener('resize', this.handleTrendChartResize)
-    if (this.trendChart) {
-      this.trendChart.dispose()
-      this.trendChart = null
-    }
-  },
-  methods: {
-    openLiveSelectDialog() {
-      this.liveSelectDialogVisible = true
-    },
-    handleClearLive() {
-      this.selectedLiveList = []
-      this.studentList = []
-      this.studentTotal = 0
-      this.studentQueryParams.liveIds = []
-      this.studentFirstEntryRange = null
-      this.liveProductOptions = []
-      this.productCompareList = []
-      this.productCompareTotal = 0
-      this.productCompareQueryParams.productIds = []
-      if (this.trendChart) {
-        this.trendChart.dispose()
-        this.trendChart = null
-      }
-      this.trendChartData = { xAxis: [], series: [] }
-      this.overviewData = {}
-      this.$nextTick(() => {
-        const table = this.$refs.liveTable
-        if (table && typeof table.clearSelection === 'function') {
-          table.clearSelection()
-        }
-        this.handleTrendChartResize()
-      })
-    },
-    handleDialogOpen() {
-      this.getLiveList()
-    },
-    handleDialogClose() {
-      this.resetLiveQuery()
-    },
-    handleLiveSearch() {
-      this.liveQueryParams.pageNum = 1
-      this.getLiveList()
-    },
-    resetLiveQuery() {
-      this.liveQueryParams = {
-        pageNum: 1,
-        pageSize: 10,
-        liveName: null,
-        companyName: null,
-        status: null,
-        beginTime: null,
-        endTime: null
-      }
-      this.createTimeRange = null
-    },
-    getLiveList() {
-      const params = { ...this.liveQueryParams }
-      if (this.createTimeRange && this.createTimeRange.length === 2) {
-        params.beginTime = this.createTimeRange[0]
-        params.endTime = this.createTimeRange[1]
-      }
-      this.liveListLoading = true
-      listLive(params).then(res => {
-        this.liveList = res.rows || []
-        this.liveTotal = res.total || 0
-        this.liveListLoading = false
-        this.$nextTick(() => this.setTableSelection())
-      }).catch(() => {
-        this.liveListLoading = false
-      })
-    },
-    handleSelectionChange(selection) {
-      if (selection && selection.length > 15) {
-        this.$message.warning('最多只能选择15个直播间')
-        this.$nextTick(() => this.setTableSelection())
-      } else {
-        this.selectedLiveList = selection || []
-      }
-    },
-    setTableSelection() {
-      this.$nextTick(() => {
-        const table = this.$refs.liveTable
-        if (!table || !this.liveList.length) return
-        const selectedIds = new Set(this.selectedLiveList.map(l => l.liveId))
-        this.liveList.forEach(row => {
-          table.toggleRowSelection(row, selectedIds.has(row.liveId))
-        })
-      })
-    },
-    confirmSelectLive() {
-      this.liveSelectDialogVisible = false
-      // 默认不选直播名称=展示全部已选直播间的人数
-    },
-    formatProductMoney(val) {
-      if (val === undefined || val === null) return '¥0.00'
-      const num = typeof val === 'number' ? val : parseFloat(val)
-      return '¥' + (isNaN(num) ? '0.00' : num.toFixed(2))
-    },
-    fetchLiveProductsForSelectedLives() {
-      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
-      if (!liveIds.length) {
-        this.liveProductOptions = []
-        return
-      }
-      const promises = liveIds.map(liveId => listLiveGoods({ liveId }).then(res => (res.rows || res.data?.rows || [])))
-      Promise.all(promises).then(results => {
-        const productMap = new Map()
-        results.flat().forEach(item => {
-          const pid = item.productId
-          if (pid && !productMap.has(pid)) {
-            productMap.set(pid, { productId: pid, productName: item.productName || item.name || '未知商品' })
-          }
-        })
-        this.liveProductOptions = Array.from(productMap.values())
-      }).catch(() => {
-        this.liveProductOptions = []
-      })
-    },
-    fetchProductCompareList() {
-      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
-      if (!liveIds.length) {
-        this.$message.warning('请先选择直播间')
-        return
-      }
-      const params = {
-        liveIds,
-        productIds: this.productCompareQueryParams.productIds,
-        pageNum: this.productCompareQueryParams.pageNum,
-        pageSize: this.productCompareQueryParams.pageSize
-      }
-      this.productCompareLoading = true
-      listProductCompareStats(params).then(res => {
-        this.productCompareList = res.rows || res.data?.rows || []
-        this.productCompareTotal = res.total || res.data?.total || 0
-        this.productCompareLoading = false
-      }).catch(() => {
-        this.productCompareLoading = false
-      })
-    },
-    resetProductCompareQuery() {
-      this.productCompareQueryParams.productIds = []
-      this.productCompareQueryParams.pageNum = 1
-      this.productCompareQueryParams.pageSize = 10
-      this.fetchProductCompareList()
-    },
-    fetchInviteSalesOptions() {
-      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
-      if (!liveIds.length) {
-        this.inviteSalesOptions = []
-        return
-      }
-      listInviteSalesOptions(liveIds).then(res => {
-        const data = res.data || res || []
-        this.inviteSalesOptions = Array.isArray(data) ? data : []
-      }).catch(() => {
-        this.inviteSalesOptions = []
-      })
-    },
-    fetchInviteCompareList() {
-      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
-      if (!liveIds.length) {
-        this.$message.warning('请先选择直播间')
-        return
-      }
-      const rawIds = this.inviteCompareQueryParams.companyUserIds || []
-      const companyUserIds = rawIds.map(v => {
-        if (typeof v === 'string' && v.includes('_')) {
-          return parseInt(v.split('_')[1], 10)
-        }
-        return typeof v === 'number' ? v : parseInt(v, 10)
-      }).filter(id => !isNaN(id))
-      const params = {
-        liveIds,
-        companyUserIds: companyUserIds.length ? companyUserIds : undefined,
-        pageNum: this.inviteCompareQueryParams.pageNum,
-        pageSize: this.inviteCompareQueryParams.pageSize
-      }
-      this.inviteCompareLoading = true
-      listInviteCompareStats(params).then(res => {
-        this.inviteCompareList = res.rows || res.data?.rows || []
-        this.inviteCompareTotal = res.total || res.data?.total || 0
-        this.inviteCompareLoading = false
-      }).catch(() => {
-        this.inviteCompareLoading = false
-      })
-    },
-    resetInviteCompareQuery() {
-      this.inviteCompareQueryParams.companyUserIds = []
-      this.inviteCompareQueryParams.pageNum = 1
-      this.inviteCompareQueryParams.pageSize = 10
-      this.fetchInviteCompareList()
-    },
-    fetchOverview() {
-      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
-      if (!liveIds.length) {
-        this.overviewData = {}
-        this.trendChartData = { xAxis: [], series: [] }
-        if (this.trendChart) {
-          this.trendChart.dispose()
-          this.trendChart = null
-        }
-        return
-      }
-      this.overviewLoading = true
-      getLiveStatisticsOverview(liveIds).then(res => {
-        this.overviewData = res.data || res || {}
-        this.overviewLoading = false
-        this.fetchTrend()
-        // 直播间学员:默认不选直播名称,自动查询全部已选直播间的用户
-        this.studentQueryParams.liveIds = []
-        this.studentFirstEntryRange = null
-        this.studentQueryParams.pageNum = 1
-        this.fetchStudentList()
-        // 商品对比:加载已选直播间的带货产品
-        this.fetchLiveProductsForSelectedLives()
-        // 邀课对比:加载已选直播间的分享人选项
-        this.fetchInviteSalesOptions()
-      }).catch(() => {
-        this.overviewLoading = false
-      })
-    },
-    fetchTrend() {
-      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
-      if (!liveIds.length) {
-        this.trendChartData = this.getDefaultTrendData()
-        this.$nextTick(() => {
-          setTimeout(() => this.renderTrendChart(), 150)
-        })
-        return
-      }
-      this.trendLoading = true
-      getLiveEntryTrend(liveIds).then(res => {
-        // 兼容多种返回格式: { code, msg, data: { xAxis, series } } 或 { data: [series] } 或 { xAxis, series }
-        const body = res && typeof res === 'object' && res.data !== undefined ? res.data : res
-        let xAxis = []
-        let series = []
-        const dataObj = body && typeof body === 'object' ? body : null
-        if (Array.isArray(body) && body.length > 0) {
-          series = body
-        } else if (dataObj && Array.isArray(dataObj.series) && dataObj.series.length > 0) {
-          xAxis = Array.isArray(dataObj.xAxis) ? dataObj.xAxis : []
-          series = dataObj.series
-        } else if (dataObj && Array.isArray(dataObj.data) && dataObj.data.length > 0) {
-          series = dataObj.data
-        } else if (dataObj && Array.isArray(dataObj.rows) && dataObj.rows.length > 0) {
-          series = dataObj.rows
-        }
-        series = Array.isArray(series) ? series : []
-        if (series.length && (!Array.isArray(xAxis) || !xAxis.length) && series[0] && Array.isArray(series[0].data)) {
-          const len = series[0].data.length
-          xAxis = ['开播前']
-          for (let m = 0; m < len - 1; m++) xAxis.push((m * 5) + 'min')
-        }
-        if (!series.length) {
-          this.trendChartData = this.getDefaultTrendData()
-        } else {
-          if (!xAxis.length) xAxis = this.getDefaultTrendData().xAxis
-          this.trendChartData = { xAxis, series }
-        }
-        this.trendLoading = false
-        this.$nextTick(() => {
-          setTimeout(() => {
-            this.renderTrendChart()
-            this.$nextTick(() => { if (this.trendChart) this.trendChart.resize() })
-          }, 80)
-        })
-      }).catch(() => {
-        this.trendChartData = this.getDefaultTrendData()
-        this.trendLoading = false
-        this.$nextTick(() => {
-          setTimeout(() => {
-            this.renderTrendChart()
-            this.$nextTick(() => {
-              if (this.trendChart) this.trendChart.resize()
-            })
-          }, 150)
-        })
-      })
-    },
-    getDefaultTrendData() {
-      const xAxis = ['开播前']
-      for (let m = 0; m <= 240; m += 5) {
-        xAxis.push(m + 'min')
-      }
-      const zeros = xAxis.map(() => 0)
-      return {
-        xAxis,
-        series: [{ name: '进入人数', data: zeros }]
-      }
-    },
-    renderTrendChart() {
-      const el = this.$refs.trendChartRef
-      if (!el) return
-      let xAxis = this.trendChartData.xAxis && Array.isArray(this.trendChartData.xAxis) ? this.trendChartData.xAxis : []
-      const series = this.trendChartData.series && Array.isArray(this.trendChartData.series) ? this.trendChartData.series : []
-      if (!series.length) return
-      if (!xAxis.length && series[0] && series[0].data) {
-        const len = series[0].data.length
-        xAxis = ['开播前']
-        for (let m = 0; m < len - 1; m++) xAxis.push((m * 5) + 'min')
-      }
-      if (!xAxis.length) xAxis = this.getDefaultTrendData().xAxis
-      if (this.trendChart) this.trendChart.dispose()
-      this.trendChart = echarts.init(el)
-      const option = {
-        title: { text: '相对直播开始时间的累计进入人数', left: 'center' },
-        tooltip: { trigger: 'axis' },
-        legend: { bottom: 0, type: 'scroll' },
-        grid: { left: '3%', right: '4%', bottom: '15%', top: 40, containLabel: true },
-        xAxis: { type: 'category', boundaryGap: false, data: xAxis },
-        yAxis: { type: 'value', name: '累计进入人数' },
-        series: series.map((s, i) => ({
-          name: s.name,
-          type: 'line',
-          smooth: true,
-          data: s.data || []
-        }))
-      }
-      this.trendChart.setOption(option)
-      this.$nextTick(() => {
-        if (this.trendChart) this.trendChart.resize()
-      })
-    },
-    handleTrendChartResize() {
-      if (this.trendChart) this.trendChart.resize()
-    },
-    fetchStudentList() {
-      const liveIds = this.studentQueryParams.liveIds && this.studentQueryParams.liveIds.length > 0
-        ? this.studentQueryParams.liveIds
-        : (this.selectedLiveList || []).map(l => l.liveId)
-      if (!liveIds.length) {
-        this.studentList = []
-        this.studentTotal = 0
-        return
-      }
-      const params = {
-        liveIds,
-        pageNum: this.studentQueryParams.pageNum || 1,
-        pageSize: this.studentQueryParams.pageSize || 10
-      }
-      if (this.studentFirstEntryRange && this.studentFirstEntryRange.length === 2) {
-        params.firstEntryTimeBegin = this.studentFirstEntryRange[0]
-        params.firstEntryTimeEnd = this.studentFirstEntryRange[1]
-      }
-      this.studentLoading = true
-      listLiveRoomStudents(params).then(res => {
-        this.studentList = res.rows || []
-        this.studentTotal = res.total || 0
-        this.studentLoading = false
-      }).catch(() => {
-        this.studentLoading = false
-      })
-    },
-    resetStudentQuery() {
-      this.studentQueryParams.liveIds = []
-      this.studentQueryParams.pageNum = 1
-      this.studentQueryParams.pageSize = 10
-      this.studentFirstEntryRange = null
-      this.fetchStudentList()
-    },
-    formatOverviewValue(val) {
-      if (val === undefined || val === null) return '0'
-      if (typeof val === 'number' && !Number.isInteger(val)) return Number(val).toFixed(2)
-      return String(val)
-    },
-    measureNavHeight() {
-      const nav = this.$refs.statsNavBar
-      if (nav) {
-        this.navHeight = nav.offsetHeight || 48
-      }
-    },
-    handleNavScroll() {
-      const nav = this.$refs.statsNavBar
-      const card = this.$refs.statsCard
-      if (!nav || !card) return
-
-      if (this.navFixed) {
-        const spacer = this.$refs.statsNavSpacer
-        if (spacer) {
-          const rect = spacer.getBoundingClientRect()
-          if (rect.top >= this.headerOffset) {
-            this.navFixed = false
-            this.navFixedStyle = {}
-          }
-        }
-      } else {
-        const rect = nav.getBoundingClientRect()
-        if (rect.top <= this.headerOffset) {
-          const cardRect = card.$el ? card.$el.getBoundingClientRect() : card.getBoundingClientRect()
-          this.navFixedStyle = {
-            left: cardRect.left + 'px',
-            top: this.headerOffset + 'px',
-            width: cardRect.width + 'px'
-          }
-          this.navFixed = true
-        }
-      }
-    },
-    scrollToSection(key) {
-      this.activeNav = key
-      const refMap = {
-        overview: 'sectionOverview',
-        trend: 'sectionTrend',
-        student: 'sectionStudent',
-        compare: 'sectionCompare'
-      }
-      const el = this.$refs[refMap[key]]
-      if (el && el.$el) {
-        el.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
-      } else if (el) {
-        el.scrollIntoView({ behavior: 'smooth', block: 'start' })
-      }
-    }
-  }
+  components: { LiveStatisticsPanel }
 }
 </script>
-
-<style scoped>
-.live-select-input {
-  display: flex;
-  align-items: flex-start;
-  width: 100%;
-  min-width: 200px;
-  min-height: 32px;
-  padding: 6px 12px;
-  border: 1px solid #DCDFE6;
-  border-radius: 4px;
-  cursor: pointer;
-  background: #fff;
-  overflow: hidden;
-}
-.live-select-input:hover {
-  border-color: #C0C4CC;
-}
-.live-select-content {
-  flex: 1;
-  display: flex;
-  flex-wrap: wrap;
-  align-content: flex-start;
-  align-items: center;
-  gap: 6px 8px;
-  min-height: 22px;
-  min-width: 0;
-  overflow: hidden;
-}
-.live-select-content .placeholder {
-  color: #C0C4CC;
-  font-size: 14px;
-}
-.live-tag {
-  display: inline-block;
-  padding: 0 8px;
-  height: 22px;
-  line-height: 22px;
-  font-size: 12px;
-  color: #606266;
-  background: #f4f4f5;
-  border-radius: 4px;
-}
-.tag-sep {
-  color: #909399;
-  margin: 0 2px;
-}
-.fold-text {
-  font-size: 12px;
-  color: #909399;
-}
-.live-select-suffix {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  padding-left: 10px;
-}
-.live-select-suffix i {
-  color: #C0C4CC;
-  font-size: 14px;
-  cursor: pointer;
-}
-.live-select-suffix i:hover {
-  color: #909399;
-}
-.stats-main {
-  padding-top: 0;
-}
-.stats-nav-wrapper {
-  position: relative;
-}
-.stats-nav-spacer {
-  flex-shrink: 0;
-}
-.stats-nav-bar {
-  position: relative;
-  z-index: 100;
-  display: flex;
-  width: 100%;
-  flex: 1 1 auto;
-  background: #fff;
-  border-bottom: 1px solid #ebeef5;
-  margin: 0 -20px 20px -20px;
-  padding: 0;
-  box-shadow: 0 1px 2px rgba(0,0,0,0.03);
-}
-.stats-nav-bar.is-fixed {
-  position: fixed;
-  margin: 0;
-  left: 0;
-  right: 0;
-  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
-}
-.stats-nav-bar .nav-item {
-  flex: 1;
-  text-align: center;
-  padding: 14px 12px;
-  font-size: 14px;
-  color: #606266;
-  cursor: pointer;
-  border-bottom: 2px solid transparent;
-  margin-bottom: -1px;
-  transition: all 0.2s;
-}
-.stats-nav-bar .nav-item:hover {
-  color: #409EFF;
-}
-.stats-nav-bar .nav-item.active {
-  color: #409EFF;
-  font-weight: 500;
-  border-bottom-color: #409EFF;
-}
-.overview-content {
-  min-height: 120px;
-}
-.overview-grid {
-  margin-top: 8px;
-}
-.overview-item {
-  padding: 12px 16px;
-  margin-bottom: 12px;
-  background: #fafafa;
-  border-radius: 4px;
-  border-left: 3px solid #409EFF;
-  border: 1px solid #ebeef5;
-}
-.overview-label {
-  font-size: 13px;
-  color: #909399;
-  margin-bottom: 6px;
-  line-height: 1.4;
-}
-.overview-value {
-  font-size: 18px;
-  font-weight: 600;
-  color: #303133;
-}
-.overview-block {
-  margin-bottom: 24px;
-}
-.overview-block:last-child {
-  margin-bottom: 0;
-}
-.overview-block-title {
-  font-size: 14px;
-  font-weight: 600;
-  color: #303133;
-  margin-bottom: 12px;
-  padding-bottom: 8px;
-  border-bottom: 1px solid #ebeef5;
-}
-.stats-section-card {
-  margin-bottom: 30px;
-  /* 固定导航时:顶部 84px + 导航栏 ~48px ≈ 132,留足空间避免遮挡 */
-  scroll-margin-top: 140px;
-}
-.stats-section-card:last-child {
-  margin-bottom: 0;
-}
-.stats-section {
-  min-height: 300px;
-  padding: 20px 0;
-}
-.stats-section .section-title {
-  font-size: 16px;
-  font-weight: 500;
-  color: #303133;
-  margin-bottom: 16px;
-  padding-bottom: 12px;
-  padding-left: 10px;
-  border-bottom: 1px solid #ebeef5;
-  border-left: 3px solid #67C23A;
-}
-.box-card {
-  margin-bottom: 20px;
-}
-.filter-form {
-  margin-bottom: 16px;
-  padding-bottom: 16px;
-  border-bottom: 1px solid #ebeef5;
-}
-.dialog-footer {
-  display: flex;
-  align-items: center;
-  justify-content: flex-end;
-  gap: 12px;
-}
-.dialog-footer .selected-count {
-  margin-right: auto;
-  font-size: 13px;
-  color: #606266;
-}
-.trend-chart-wrap {
-  min-height: 320px;
-}
-.trend-chart {
-  width: 100%;
-  height: 360px;
-}
-.student-section {
-  min-height: 200px;
-}
-.student-filter {
-  margin-bottom: 16px;
-}
-.user-name-cell {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 8px;
-}
-.user-name-cell .user-name-text {
-  flex: 1;
-  min-width: 0;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-.overview-content {
-  min-height: 120px;
-}
-.overview-grid {
-  margin-top: 0;
-}
-.overview-item {
-  padding: 12px 16px;
-  margin-bottom: 12px;
-  background: #fafafa;
-  border-radius: 4px;
-  border: 1px solid #ebeef5;
-}
-.overview-label {
-  font-size: 13px;
-  color: #909399;
-  margin-bottom: 8px;
-  line-height: 1.4;
-}
-.overview-value {
-  font-size: 18px;
-  font-weight: 600;
-  color: #303133;
-}
-.compare-tabs-wrap {
-  padding-top: 4px;
-}
-.compare-tabs {
-  margin-bottom: 16px;
-}
-.compare-panel {
-  min-height: 200px;
-}
-</style>

+ 302 - 0
src/views/live/trainingCamp/PeriodWorkspace.vue

@@ -0,0 +1,302 @@
+<template>
+  <div class="app-container period-workspace">
+    <el-page-header
+      class="workspace-page-header"
+      @back="$emit('back')"
+      content="返回营期列表"
+    />
+    <el-card class="period-info-card" shadow="never">
+      <div class="period-info-body">
+        <div class="period-info-main">
+          <h2 class="period-info-name">{{ workspace.periodName || '—' }}</h2>
+          <p class="period-info-line">
+            <span class="label">开始时间</span>
+            <span class="value">{{ formatPeriodTime(workspace.startTime) }}</span>
+          </p>
+          <p class="period-info-line">
+            <span class="label">结束时间</span>
+            <span class="value">{{ formatPeriodTime(workspace.endTime) }}</span>
+          </p>
+          <p v-if="workspace.campName" class="period-info-line camp">
+            <span class="label">所属训练营</span>
+            <span class="value">{{ workspace.campName }}</span>
+          </p>
+        </div>
+        <div class="period-info-cover-col">
+          <el-image
+            v-if="workspace.periodImgUrl"
+            :src="workspace.periodImgUrl"
+            fit="cover"
+            class="period-info-cover-img"
+            :preview-src-list="[workspace.periodImgUrl]"
+          >
+            <div slot="error" class="period-info-cover-placeholder">
+              <span>加载失败</span>
+            </div>
+          </el-image>
+          <div v-else class="period-info-cover-placeholder">
+            <i class="el-icon-picture-outline" />
+            <span>暂无营期图片</span>
+          </div>
+        </div>
+      </div>
+    </el-card>
+    <el-tabs v-model="activeTab" class="workspace-tabs">
+      <el-tab-pane label="直播间" name="live">
+        <el-row class="mb8">
+          <el-col :span="24" align="right">
+            <el-button
+              v-hasPermi="['live:live:add', 'live:trainingCamp:live:add']"
+              type="primary"
+              plain
+              icon="el-icon-plus"
+              size="mini"
+              @click="$emit('add-live')"
+            >新增</el-button>
+          </el-col>
+        </el-row>
+        <el-table
+          v-loading="liveLoading"
+          :data="liveList"
+          border
+          size="small"
+        >
+          <el-table-column label="直播间ID" prop="liveId" width="100" align="center" show-overflow-tooltip />
+          <el-table-column label="直播名称" prop="liveName" min-width="140" show-overflow-tooltip />
+          
+          <el-table-column label="开始时间" min-width="158" align="center" show-overflow-tooltip>
+            <template slot-scope="scope">{{ formatPeriodTime(scope.row.startTime) }}</template>
+          </el-table-column>
+          <el-table-column label="结束时间" min-width="158" align="center" show-overflow-tooltip>
+            <template slot-scope="scope">{{ formatPeriodTime(scope.row.finishTime) }}</template>
+          </el-table-column>
+          <el-table-column label="状态" width="96" align="center">
+            <template slot-scope="scope">
+              <el-tag v-if="scope.row.status == 1" size="mini">待直播</el-tag>
+              <el-tag v-else-if="scope.row.status == 2" size="mini" type="danger">直播中</el-tag>
+              <el-tag v-else-if="scope.row.status == 3" size="mini" type="info">已结束</el-tag>
+              <el-tag v-else-if="scope.row.status == 4" size="mini" type="success">回放中</el-tag>
+              <span v-else>—</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="200" align="center" fixed="right">
+            <template slot-scope="scope">
+              <el-button
+                v-hasPermi="['live:live:edit']"
+                type="text"
+                size="mini"
+                icon="el-icon-edit"
+                @click="$emit('edit-live', scope.row)"
+              >修改</el-button>
+              <el-button
+                v-hasPermi="['live:live:query']"
+                type="text"
+                size="mini"
+                @click="goLiveConfig(scope.row)"
+              >配置</el-button>
+              <el-button
+                type="text"
+                size="mini"
+                @click="openConsole(scope.row)"
+              >控制台</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <pagination
+          v-show="liveTotal > 0"
+          small
+          :total="liveTotal"
+          :page.sync="liveQuery.pageNum"
+          :limit.sync="liveQuery.pageSize"
+          @pagination="loadLiveList"
+        />
+      </el-tab-pane>
+      <el-tab-pane label="数据概览" name="overview">
+        <live-statistics-panel
+          v-if="activeTab === 'overview' && workspace.periodId"
+          :training-period-id="workspace.periodId"
+          :key="'camp-stats-' + workspace.periodId"
+        />
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+import { listTrainingLive } from '@/api/live/trainingCamp'
+import LiveStatisticsPanel from '@/views/live/liveStatistics/LiveStatisticsPanel.vue'
+
+export default {
+  name: 'PeriodWorkspace',
+  components: { LiveStatisticsPanel },
+  props: {
+    workspace: {
+      type: Object,
+      required: true,
+      default: () => ({
+        periodId: null,
+        periodName: '',
+        campName: '',
+        periodImgUrl: '',
+        startTime: null,
+        endTime: null
+      })
+    }
+  },
+  data() {
+    return {
+      activeTab: 'live',
+      liveLoading: false,
+      liveList: [],
+      liveTotal: 0,
+      liveQuery: { pageNum: 1, pageSize: 10 }
+    }
+  },
+  watch: {
+    workspace: {
+      deep: true,
+      immediate: true,
+      handler(val) {
+        if (val && val.periodId) {
+          this.liveQuery.pageNum = 1
+          this.activeTab = 'live'
+          this.loadLiveList()
+        } else {
+          this.liveList = []
+          this.liveTotal = 0
+        }
+      }
+    }
+  },
+  methods: {
+    formatPeriodTime(val) {
+      if (!val) return '—'
+      const s = String(val).replace('T', ' ')
+      if (s.length >= 19) {
+        return s.slice(0, 10) + ' ' + s.slice(11, 19)
+      }
+      return s.length >= 16 ? s.slice(0, 16) : s
+    },
+    loadLiveList() {
+      if (!this.workspace.periodId) return
+      this.liveLoading = true
+      listTrainingLive({
+        ...this.liveQuery,
+        trainingPeriodId: this.workspace.periodId
+      }).then(res => {
+        this.liveList = res.rows || []
+        this.liveTotal = res.total || 0
+        this.liveLoading = false
+      }).catch(() => {
+        this.liveLoading = false
+      })
+    },
+    /** 父组件在新建直播间成功后调用 */
+    refreshLiveList() {
+      this.loadLiveList()
+    },
+    goLiveConfig(row) {
+      this.$router.push('/live/liveConfig/' + row.liveId)
+    },
+    openConsole(row) {
+      const r = this.$router.resolve({ path: '/live/liveConsole/' + row.liveId })
+      window.open(r.href, '_blank')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.workspace-page-header {
+  margin-bottom: 16px;
+}
+.period-info-card {
+  margin-bottom: 16px;
+  border-radius: 8px;
+}
+.period-info-card >>> .el-card__body {
+  padding: 20px 24px;
+}
+.period-info-body {
+  display: flex;
+  align-items: stretch;
+  justify-content: space-between;
+  gap: 24px;
+  min-height: 132px;
+}
+.period-info-main {
+  flex: 1;
+  min-width: 0;
+}
+.period-info-name {
+  margin: 0 0 16px 0;
+  font-size: 20px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 1.3;
+}
+.period-info-line {
+  margin: 0 0 10px 0;
+  font-size: 14px;
+  color: #606266;
+  line-height: 1.5;
+}
+.period-info-line:last-child {
+  margin-bottom: 0;
+}
+.period-info-line .label {
+  display: inline-block;
+  width: 88px;
+  color: #909399;
+}
+.period-info-line .value {
+  color: #303133;
+}
+.period-info-line.camp .value {
+  font-weight: 500;
+}
+.period-info-cover-col {
+  flex-shrink: 0;
+  width: 220px;
+  height: 132px;
+}
+.period-info-cover-img {
+  width: 220px;
+  height: 132px;
+  border-radius: 8px;
+  border: 1px solid #ebeef5;
+  overflow: hidden;
+}
+.period-info-cover-img >>> .el-image__inner {
+  width: 220px;
+  height: 132px;
+  object-fit: cover;
+}
+.period-info-cover-placeholder {
+  width: 220px;
+  height: 132px;
+  border-radius: 8px;
+  border: 1px dashed #dcdfe6;
+  background: #fafafa;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  color: #c0c4cc;
+  font-size: 13px;
+}
+.period-info-cover-placeholder i {
+  font-size: 36px;
+}
+.workspace-tabs {
+  margin-top: 8px;
+}
+/* 数据概览内嵌统计页:避免双层 app-container 顶距过大 */
+.workspace-tabs >>> .live-statistics-panel-root.app-container {
+  padding-top: 0;
+}
+.mb8 {
+  margin-bottom: 8px;
+}
+</style>

+ 809 - 0
src/views/live/trainingCamp/index.vue

@@ -0,0 +1,809 @@
+<template>
+  <div class="training-camp-root">
+    <!-- 训练营列表:左侧 30% + 营期 70% -->
+    <div v-show="!periodWorkspace.periodId" class="app-container training-camp-page">
+      <el-alert
+        title="训练营直播间与普通直播相互独立:此处仅管理「训练营 → 营期」;进入营期后在「直播间」页签中管理直播。普通直播仍在「直播管理」中维护。"
+        type="info"
+        :closable="false"
+        show-icon
+        class="mb12"
+      />
+
+      <el-row :gutter="16">
+        <el-col :span="7">
+          <el-card shadow="never" class="box-card">
+            <div slot="header" class="clearfix">
+              <span>训练营</span>
+              <el-button
+                v-hasPermi="['live:trainingCamp:add']"
+                style="float: right; padding: 3px 0"
+                type="text"
+                icon="el-icon-plus"
+                @click="openCampForm()"
+              >新建</el-button>
+            </div>
+            <el-table
+              ref="campTable"
+              v-loading="campLoading"
+              :data="campList"
+              highlight-current-row
+              border
+              size="small"
+              @current-change="onCampChange"
+            >
+              <el-table-column label="名称" prop="campName" min-width="100" show-overflow-tooltip />
+              <el-table-column label="排序" prop="sortOrder" width="56" align="center" />
+              <el-table-column label="状态" width="68" align="center">
+                <template slot-scope="scope">
+                  <el-tag v-if="scope.row.status === 0" size="mini" type="success">正常</el-tag>
+                  <el-tag v-else size="mini" type="info">停用</el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="96" align="center" fixed="right">
+                <template slot-scope="scope">
+                  <el-button
+                    v-hasPermi="['live:trainingCamp:edit']"
+                    type="text"
+                    size="mini"
+                    @click.stop="openCampForm(scope.row)"
+                  >编辑</el-button>
+                  <el-button
+                    v-hasPermi="['live:trainingCamp:remove']"
+                    type="text"
+                    size="mini"
+                    @click.stop="removeCamp(scope.row)"
+                  >删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-card>
+        </el-col>
+
+        <el-col :span="17">
+          <el-card shadow="never" class="box-card box-card-period">
+            <div slot="header" class="period-header">
+              <div class="period-header-left">
+                <span>营期</span>
+                <span v-if="currentCamp" class="sub-hint">({{ currentCamp.campName }})</span>
+              </div>
+              <div class="period-header-actions">
+                <el-button
+                  type="primary"
+                  plain
+                  size="small"
+                  icon="el-icon-right"
+                  :disabled="!currentPeriod"
+                  @click="enterPeriodWorkspace(currentPeriod)"
+                >进入营期</el-button>
+                <el-button
+                  v-hasPermi="['live:trainingCamp:add']"
+                  type="primary"
+                  size="small"
+                  icon="el-icon-plus"
+                  :disabled="!currentCamp"
+                  @click="openPeriodForm()"
+                >新建营期</el-button>
+              </div>
+            </div>
+            <el-empty v-if="!currentCamp" description="请先选择训练营" :image-size="64" />
+            <el-table
+              v-else
+              ref="periodTable"
+              v-loading="periodLoading"
+              :data="periodList"
+              highlight-current-row
+              border
+              size="small"
+              @current-change="onPeriodChange"
+            >
+              <el-table-column label="营期名称" prop="periodName" min-width="120" show-overflow-tooltip />
+              <el-table-column label="营期图" width="76" align="center">
+                <template slot-scope="scope">
+                  <el-image
+                    v-if="scope.row.periodImgUrl"
+                    class="period-thumb"
+                    :src="scope.row.periodImgUrl"
+                    fit="cover"
+                    :preview-src-list="[scope.row.periodImgUrl]"
+                  />
+                  <span v-else class="period-thumb-empty">—</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="开始" prop="startTime" width="100" show-overflow-tooltip>
+                <template slot-scope="scope">{{ formatDt(scope.row.startTime) }}</template>
+              </el-table-column>
+              <el-table-column label="结束" prop="endTime" width="100" show-overflow-tooltip>
+                <template slot-scope="scope">{{ formatDt(scope.row.endTime) }}</template>
+              </el-table-column>
+              <el-table-column label="操作" width="168" align="center" fixed="right">
+                <template slot-scope="scope">
+                  <el-button
+                    type="text"
+                    size="mini"
+                    icon="el-icon-right"
+                    @click.stop="enterPeriodWorkspace(scope.row)"
+                  >进入营期</el-button>
+                  <el-button
+                    v-hasPermi="['live:trainingCamp:edit']"
+                    type="text"
+                    size="mini"
+                    @click.stop="openPeriodForm(scope.row)"
+                  >编辑</el-button>
+                  <el-button
+                    v-hasPermi="['live:trainingCamp:remove']"
+                    type="text"
+                    size="mini"
+                    @click.stop="removePeriod(scope.row)"
+                  >删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-card>
+        </el-col>
+      </el-row>
+    </div>
+
+    <period-workspace
+      v-show="periodWorkspace.periodId"
+      ref="periodWorkspaceRef"
+      :workspace="periodWorkspace"
+      @back="closePeriodWorkspace"
+      @add-live="openLiveForm"
+      @edit-live="openLiveFormEdit"
+    />
+
+    <!-- 训练营表单 -->
+    <el-dialog :title="campForm.campId ? '编辑训练营' : '新建训练营'" :visible.sync="campOpen" width="480px" append-to-body>
+      <el-form ref="campFormRef" :model="campForm" :rules="campRules" label-width="88px">
+        <el-form-item label="名称" prop="campName">
+          <el-input v-model="campForm.campName" maxlength="128" show-word-limit placeholder="训练营名称" />
+        </el-form-item>
+        <el-form-item label="排序" prop="sortOrder">
+          <el-input-number v-model="campForm.sortOrder" :min="0" :max="9999" controls-position="right" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="campForm.status">
+            <el-radio :label="0">正常</el-radio>
+            <el-radio :label="1">停用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="说明" prop="description">
+          <el-input v-model="campForm.description" type="textarea" rows="3" maxlength="512" show-word-limit />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="campOpen = false">取 消</el-button>
+        <el-button type="primary" @click="submitCamp">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 营期表单 -->
+    <el-dialog :title="periodForm.periodId ? '编辑营期' : '新建营期'" :visible.sync="periodOpen" width="520px" append-to-body>
+      <el-form ref="periodFormRef" :model="periodForm" :rules="periodRules" label-width="88px">
+        <el-form-item label="营期名称" prop="periodName">
+          <el-input v-model="periodForm.periodName" maxlength="128" show-word-limit />
+        </el-form-item>
+        <el-form-item label="营期图片" prop="periodImgUrl">
+          <image-upload v-model="periodForm.periodImgUrl" :limit="1" />
+        </el-form-item>
+        <el-form-item label="开始时间" prop="startTime">
+          <el-date-picker
+            v-model="periodForm.startTime"
+            type="datetime"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            placeholder="选择开始时间"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="结束时间" prop="endTime">
+          <el-date-picker
+            v-model="periodForm.endTime"
+            type="datetime"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            placeholder="选择结束时间"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="排序" prop="sortOrder">
+          <el-input-number v-model="periodForm.sortOrder" :min="0" :max="9999" controls-position="right" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="periodForm.status">
+            <el-radio :label="0">正常</el-radio>
+            <el-radio :label="1">停用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="说明" prop="description">
+          <el-input v-model="periodForm.description" type="textarea" rows="3" maxlength="512" show-word-limit />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="periodOpen = false">取 消</el-button>
+        <el-button type="primary" @click="submitPeriod">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 新增/修改直播间(与「直播管理」表单一致;新增走营期接口,修改走直播更新接口) -->
+    <el-dialog :title="liveUpsertMode === 'edit' ? '修改直播间' : '添加直播间'" :visible.sync="liveOpen" width="900px" append-to-body>
+      <el-form ref="liveUpsertFormRef" :model="liveUpsertForm" :rules="liveUpsertRules" label-width="80px">
+        <el-form-item label="直播名称" prop="liveName">
+          <el-input v-model="liveUpsertForm.liveName" placeholder="请输入直播名称" />
+        </el-form-item>
+        <el-form-item label="显示类型" prop="showType">
+          <el-radio-group v-model="liveUpsertForm.showType">
+            <el-radio :label="1">横屏</el-radio>
+            <el-radio :label="2">竖屏</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="直播类型" prop="liveType">
+          <el-radio-group v-model="liveUpsertForm.liveType">
+            <el-radio
+              v-for="item in liveTypeDictList"
+              :key="item.dictValue"
+              :label="parseInt(item.dictValue)"
+            >
+              {{ item.dictLabel }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="直播描述" prop="liveDesc">
+          <Editor ref="liveUpsertEditor" :height="300" @on-text-change="onLiveDescChange" />
+        </el-form-item>
+        <el-form-item label="录播视屏" prop="videoUrl" v-if="liveUpsertForm.liveType == 2">
+          <video-upload
+            :fileKey.sync="liveUpsertForm.fileKey"
+            :fileSize.sync="liveUpsertForm.fileSize"
+            :videoUrl.sync="liveUpsertForm.videoUrl"
+            :fileName.sync="liveUpsertForm.fileName"
+            :line_1.sync="liveUpsertForm.lineOne"
+            :uploadType.sync="liveUpsertForm.uploadType"
+            :isTranscode.sync="liveUpsertForm.isTranscode"
+            ref="liveUpsertVideoUpload"
+            :transcodeFileKey.sync="liveUpsertForm.transcodeFileKey"
+            @video-duration="onLiveVideoDuration"
+            @change="onLiveVideoChange"
+          />
+        </el-form-item>
+        <el-form-item label="开始时间" prop="startTime">
+          <el-date-picker
+            size="small"
+            v-model="liveUpsertForm.startTime"
+            @change="onLiveStartTimeChange"
+            type="datetime"
+            format="yyyy-MM-dd HH:mm:ss"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            :picker-options="{
+              timePickerOptions: {
+                selectableRange: '00:00:00 - 23:59:59',
+                format: 'HH:mm:ss'
+              }
+            }"
+            placeholder="选择开始时间"
+          />
+        </el-form-item>
+        <el-form-item label="结束时间" prop="finishTime" v-loading="liveTimeLoading">
+          <el-date-picker
+            size="small"
+            v-model="liveUpsertForm.finishTime"
+            type="datetime"
+            format="yyyy-MM-dd HH:mm:ss"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            :picker-options="{
+              timePickerOptions: {
+                selectableRange: '00:00:00 - 23:59:59',
+                format: 'HH:mm:ss'
+              }
+            }"
+            placeholder="视屏播放结束"
+          />
+        </el-form-item>
+        <el-form-item label="直播封面" prop="liveImgUrl">
+          <image-upload v-model="liveUpsertForm.liveImgUrl" :limit="1" />
+        </el-form-item>
+        <el-form-item label="上下架" prop="isShow">
+          <el-radio-group v-model="liveUpsertForm.isShow">
+            <el-radio :label="1">上架</el-radio>
+            <el-radio :label="2">下架</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitLive">确 定</el-button>
+        <el-button @click="cancelLiveDialog">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listTrainingCamp,
+  addTrainingCamp,
+  updateTrainingCamp,
+  delTrainingCamp,
+  listTrainingPeriod,
+  addTrainingPeriod,
+  updateTrainingPeriod,
+  delTrainingPeriod,
+  addTrainingLive,
+  getTrainingPeriod
+} from '@/api/live/trainingCamp'
+import { getLive, updateLive } from '@/api/live/live'
+import PeriodWorkspace from './PeriodWorkspace.vue'
+import Editor from '@/components/Editor/wang'
+import VideoUpload from '@/components/LiveVideoUpload/single.vue'
+
+export default {
+  name: 'LiveTrainingCamp',
+  components: { PeriodWorkspace, Editor, VideoUpload },
+  data() {
+    return {
+      campLoading: false,
+      periodLoading: false,
+      campList: [],
+      periodList: [],
+      currentCamp: null,
+      currentPeriod: null,
+      campOpen: false,
+      periodOpen: false,
+      liveOpen: false,
+      /** add | edit */
+      liveUpsertMode: 'add',
+      campForm: {},
+      periodForm: {},
+      liveTypeDictList: [],
+      liveUpsertForm: {
+        uploadType: 1,
+        isTranscode: 0,
+        transcodeFileKey: null,
+        videoUrl: null,
+        fileKey: null,
+        fileName: null,
+        fileSize: null,
+        lineOne: null
+      },
+      liveVideoUrl: '',
+      liveTimeLoading: false,
+      periodWorkspace: {
+        periodId: null,
+        periodName: '',
+        campName: '',
+        periodImgUrl: '',
+        startTime: null,
+        endTime: null
+      },
+      campRules: {
+        campName: [{ required: true, message: '请输入训练营名称', trigger: 'blur' }]
+      },
+      periodRules: {
+        periodName: [{ required: true, message: '请输入营期名称', trigger: 'blur' }]
+      },
+      liveUpsertRules: {
+        liveName: [{ required: true, message: '不能为空', trigger: 'blur' }],
+        showType: [{ required: true, message: '不能为空', trigger: 'blur' }],
+        liveType: [{ required: true, message: '不能为空', trigger: 'blur' }],
+        startTime: [{ required: true, message: '不能为空', trigger: 'blur' }],
+        liveImgUrl: [{ required: true, message: '不能为空', trigger: 'blur' }],
+        isShow: [{ required: true, message: '不能为空', trigger: 'change' }]
+      }
+    }
+  },
+  watch: {
+    'liveUpsertForm.startTime': {
+      handler(newVal) {
+        if (!newVal) return
+        const timeObj = new Date(newVal)
+        if (isNaN(timeObj.getTime())) return
+        timeObj.setSeconds(1)
+        const formattedSeconds = this.padTime(timeObj.getSeconds())
+        const year = timeObj.getFullYear()
+        const month = this.padTime(timeObj.getMonth() + 1)
+        const day = this.padTime(timeObj.getDate())
+        const hours = this.padTime(timeObj.getHours())
+        const minutes = this.padTime(timeObj.getMinutes())
+        this.liveUpsertForm.startTime = `${year}-${month}-${day} ${hours}:${minutes}:${formattedSeconds}`
+      },
+      immediate: true,
+      deep: false
+    },
+    '$route.query.periodId'(val) {
+      if (!val) {
+        this.resetWorkspaceState(false)
+        return
+      }
+      const id = Number(val)
+      if (Number.isNaN(id)) return
+      if (this.periodWorkspace.periodId === id) return
+      this.restoreWorkspaceFromRoute(id)
+    }
+  },
+  created() {
+    this.loadCamps()
+    this.getDicts('live_type').then(response => {
+      this.liveTypeDictList = response.data
+    })
+    const pid = this.$route.query.periodId
+    if (pid) {
+      const id = Number(pid)
+      if (!Number.isNaN(id)) {
+        this.$nextTick(() => this.restoreWorkspaceFromRoute(id))
+      }
+    }
+  },
+  methods: {
+    formatDt(val) {
+      if (!val) return '—'
+      const s = String(val).replace('T', ' ')
+      return s.length >= 16 ? s.slice(0, 16) : s
+    },
+    resetWorkspaceState(updateRoute) {
+      this.periodWorkspace = {
+        periodId: null,
+        periodName: '',
+        campName: '',
+        periodImgUrl: '',
+        startTime: null,
+        endTime: null
+      }
+      if (updateRoute !== false) {
+        const q = { ...this.$route.query }
+        delete q.periodId
+        this.$router.replace({ path: this.$route.path, query: q }).catch(() => {})
+      }
+    },
+    enterPeriodWorkspace(row) {
+      if (!row || !row.periodId) {
+        this.$message.warning('请先选中一行营期,或点击操作栏「进入营期」')
+        return
+      }
+      const campName = (this.currentCamp && this.currentCamp.campName) || row.campName || ''
+      this.periodWorkspace = {
+        periodId: row.periodId,
+        periodName: row.periodName || '',
+        campName,
+        periodImgUrl: row.periodImgUrl || '',
+        startTime: row.startTime || null,
+        endTime: row.endTime || null
+      }
+      this.$router.replace({
+        path: this.$route.path,
+        query: { ...this.$route.query, periodId: String(row.periodId) }
+      }).catch(() => {})
+    },
+    restoreWorkspaceFromRoute(periodId) {
+      getTrainingPeriod(periodId).then(res => {
+        const data = res.data
+        if (!data) {
+          this.$message.error('营期不存在或无权限')
+          this.resetWorkspaceState()
+          return
+        }
+        this.periodWorkspace = {
+          periodId: data.periodId,
+          periodName: data.periodName || '',
+          campName: data.campName || '',
+          periodImgUrl: data.periodImgUrl || '',
+          startTime: data.startTime || null,
+          endTime: data.endTime || null
+        }
+      }).catch(() => {
+        this.resetWorkspaceState()
+      })
+    },
+    closePeriodWorkspace() {
+      this.resetWorkspaceState(true)
+    },
+    loadCamps() {
+      this.campLoading = true
+      listTrainingCamp({ pageNum: 1, pageSize: 500 }).then(res => {
+        this.campList = res.rows || []
+        this.campLoading = false
+        this.$nextTick(() => {
+          if (this.currentCamp) {
+            const hit = this.campList.find(c => c.campId === this.currentCamp.campId)
+            if (hit) {
+              this.$refs.campTable && this.$refs.campTable.setCurrentRow(hit)
+            } else {
+              this.currentCamp = null
+              this.currentPeriod = null
+              this.periodList = []
+            }
+          }
+        })
+      }).catch(() => { this.campLoading = false })
+    },
+    onCampChange(row) {
+      this.currentCamp = row || null
+      this.currentPeriod = null
+      this.periodList = []
+      if (!row) return
+      this.loadPeriods()
+    },
+    loadPeriods() {
+      if (!this.currentCamp) return
+      this.periodLoading = true
+      listTrainingPeriod({ campId: this.currentCamp.campId, pageNum: 1, pageSize: 500 }).then(res => {
+        this.periodList = res.rows || []
+        this.periodLoading = false
+      }).catch(() => { this.periodLoading = false })
+    },
+    onPeriodChange(row) {
+      this.currentPeriod = row || null
+    },
+    openCampForm(row) {
+      this.campForm = row
+        ? { ...row }
+        : { campName: '', sortOrder: 0, status: 0, description: '' }
+      this.campOpen = true
+      this.$nextTick(() => this.$refs.campFormRef && this.$refs.campFormRef.clearValidate())
+    },
+    submitCamp() {
+      this.$refs.campFormRef.validate(valid => {
+        if (!valid) return
+        const req = this.campForm.campId ? updateTrainingCamp(this.campForm) : addTrainingCamp(this.campForm)
+        req.then(res => {
+          this.$message.success((res && res.msg) || '保存成功')
+          this.campOpen = false
+          this.loadCamps()
+        })
+      })
+    },
+    removeCamp(row) {
+      this.$confirm('确认删除训练营「' + row.campName + '」?须先清空营期。').then(() => {
+        return delTrainingCamp(row.campId)
+      }).then(res => {
+        this.$message.success((res && res.msg) || '已删除')
+        if (this.currentCamp && this.currentCamp.campId === row.campId) {
+          this.currentCamp = null
+          this.currentPeriod = null
+          this.periodList = []
+        }
+        this.loadCamps()
+      }).catch(() => {})
+    },
+    openPeriodForm(row) {
+      if (!this.currentCamp) return
+      this.periodForm = row
+        ? { ...row, campId: row.campId }
+        : {
+          campId: this.currentCamp.campId,
+          periodName: '',
+          periodImgUrl: '',
+          startTime: null,
+          endTime: null,
+          sortOrder: 0,
+          status: 0,
+          description: ''
+        }
+      this.periodOpen = true
+      this.$nextTick(() => this.$refs.periodFormRef && this.$refs.periodFormRef.clearValidate())
+    },
+    submitPeriod() {
+      this.$refs.periodFormRef.validate(valid => {
+        if (!valid) return
+        const req = this.periodForm.periodId ? updateTrainingPeriod(this.periodForm) : addTrainingPeriod(this.periodForm)
+        req.then(res => {
+          this.$message.success((res && res.msg) || '保存成功')
+          this.periodOpen = false
+          if (this.periodWorkspace.periodId && this.periodForm.periodId === this.periodWorkspace.periodId) {
+            this.periodWorkspace = {
+              ...this.periodWorkspace,
+              periodName: this.periodForm.periodName || '',
+              periodImgUrl: this.periodForm.periodImgUrl || '',
+              startTime: this.periodForm.startTime || null,
+              endTime: this.periodForm.endTime || null
+            }
+          }
+          this.loadPeriods()
+        })
+      })
+    },
+    removePeriod(row) {
+      this.$confirm('确认删除营期「' + row.periodName + '」?须先删除营期下直播间。').then(() => {
+        return delTrainingPeriod(row.periodId)
+      }).then(res => {
+        this.$message.success((res && res.msg) || '已删除')
+        if (this.currentPeriod && this.currentPeriod.periodId === row.periodId) {
+          this.currentPeriod = null
+        }
+        if (this.periodWorkspace.periodId === row.periodId) {
+          this.closePeriodWorkspace()
+        }
+        this.loadPeriods()
+      }).catch(() => {})
+    },
+    initLiveUpsertDefaults() {
+      this.liveUpsertForm = {
+        showType: 1,
+        liveType: 2,
+        isShow: 1,
+        isTranscode: 0,
+        uploadType: 1,
+        transcodeFileKey: null
+      }
+      this.liveVideoUrl = ''
+    },
+    cancelLiveDialog() {
+      this.liveOpen = false
+      this.liveUpsertMode = 'add'
+      this.initLiveUpsertDefaults()
+    },
+    padTime(n) {
+      return n < 10 ? `0${n}` : String(n)
+    },
+    formatLiveEndDate(date) {
+      const y = date.getFullYear()
+      const m = (date.getMonth() + 1).toString().padStart(2, '0')
+      const d = date.getDate().toString().padStart(2, '0')
+      const h = date.getHours().toString().padStart(2, '0')
+      const min = date.getMinutes().toString().padStart(2, '0')
+      const s = date.getSeconds().toString().padStart(2, '0')
+      return `${y}-${m}-${d} ${h}:${min}:${s}`
+    },
+    onLiveStartTimeChange() {
+      if (!this.liveUpsertForm.startTime) return
+      if (!this.liveUpsertForm.duration) return
+      const startDateTime = new Date(this.liveUpsertForm.startTime)
+      const endDateTime = new Date(startDateTime.getTime() + this.liveUpsertForm.duration * 1000)
+      this.liveUpsertForm.finishTime = this.formatLiveEndDate(endDateTime)
+    },
+    onLiveVideoDuration(duration) {
+      this.liveUpsertForm.duration = duration
+    },
+    onLiveVideoChange(videoUrl) {
+      this.liveVideoUrl = videoUrl
+      this.liveUpsertForm.videoUrl = videoUrl
+    },
+    onLiveDescChange(text) {
+      setTimeout(() => {
+        this.liveUpsertForm.liveDesc = text
+      }, 1)
+    },
+    openLiveForm() {
+      if (!this.periodWorkspace.periodId) {
+        this.$message.warning('请先进入营期')
+        return
+      }
+      this.liveUpsertMode = 'add'
+      this.initLiveUpsertDefaults()
+      this.liveOpen = true
+      this.$nextTick(() => {
+        if (this.$refs.liveUpsertFormRef) {
+          this.$refs.liveUpsertFormRef.clearValidate()
+        }
+        setTimeout(() => {
+          if (this.$refs.liveUpsertEditor) {
+            this.$refs.liveUpsertEditor.setText('')
+          }
+          if (this.$refs.liveUpsertVideoUpload) {
+            this.$refs.liveUpsertVideoUpload.reset()
+          }
+        }, 100)
+      })
+    },
+    openLiveFormEdit(row) {
+      if (!this.periodWorkspace.periodId) {
+        this.$message.warning('请先进入营期')
+        return
+      }
+      if (!row || !row.liveId) {
+        this.$message.warning('直播间信息无效')
+        return
+      }
+      this.liveUpsertMode = 'edit'
+      getLive(row.liveId).then(res => {
+        const d = res.data
+        if (!d) {
+          this.$message.error('获取直播间失败')
+          this.liveUpsertMode = 'add'
+          return
+        }
+        this.liveUpsertForm = { ...d }
+        this.liveVideoUrl = d.videoUrl || ''
+        this.liveOpen = true
+        this.$nextTick(() => {
+          if (this.$refs.liveUpsertFormRef) {
+            this.$refs.liveUpsertFormRef.clearValidate()
+          }
+          setTimeout(() => {
+            if (this.$refs.liveUpsertEditor) {
+              this.$refs.liveUpsertEditor.setText(d.liveDesc || '')
+            }
+            if (d.videoUrl) {
+              this.liveUpsertForm.videoUrl = d.videoUrl
+            }
+          }, 100)
+        })
+      }).catch(() => {
+        this.liveUpsertMode = 'add'
+      })
+    },
+    submitLive() {
+      if (this.liveUpsertForm.liveType === 2 && (!this.liveVideoUrl || this.liveVideoUrl.length === 0)) {
+        this.$message.error('请上传视频')
+        return
+      }
+      this.$refs.liveUpsertFormRef.validate(valid => {
+        if (!valid) return
+        if (!this.periodWorkspace.periodId) {
+          this.$message.warning('营期已失效,请返回列表重新进入')
+          return
+        }
+        const body = { ...this.liveUpsertForm }
+        body.videoUrl = this.liveVideoUrl
+        body.trainingPeriodId = this.periodWorkspace.periodId
+        if (this.liveUpsertMode === 'add') {
+          delete body.liveId
+          addTrainingLive(body).then(res => {
+            this.$message.success((res && res.msg) || '新增成功')
+            this.liveOpen = false
+            this.liveUpsertMode = 'add'
+            this.initLiveUpsertDefaults()
+            this.$refs.periodWorkspaceRef && this.$refs.periodWorkspaceRef.refreshLiveList()
+          })
+        } else {
+          if (!body.liveId) {
+            this.$message.error('缺少直播间ID')
+            return
+          }
+          updateLive(body).then(res => {
+            this.$message.success((res && res.msg) || '修改成功')
+            this.liveOpen = false
+            this.liveUpsertMode = 'add'
+            this.initLiveUpsertDefaults()
+            this.$refs.periodWorkspaceRef && this.$refs.periodWorkspaceRef.refreshLiveList()
+          })
+        }
+      })
+    },
+  }
+}
+</script>
+
+<style scoped>
+.training-camp-root {
+  min-height: 100%;
+}
+.training-camp-page .mb12 {
+  margin-bottom: 12px;
+}
+.box-card {
+  min-height: 420px;
+}
+.box-card-period {
+  min-height: 420px;
+}
+.period-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+.period-header-left {
+  flex: 1;
+  min-width: 0;
+}
+.period-header-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-shrink: 0;
+}
+.sub-hint {
+  color: #909399;
+  font-size: 12px;
+  margin-left: 6px;
+}
+.period-thumb {
+  width: 48px;
+  height: 48px;
+  border-radius: 4px;
+  vertical-align: middle;
+}
+.period-thumb-empty {
+  color: #c0c4cc;
+  font-size: 12px;
+}
+</style>

+ 2 - 2
src/views/qw/friendWelcome/indexNew.vue

@@ -977,8 +977,8 @@ export default {
         this.fileFrom.miniprogramTitle = item.miniprogram.title;
         this.fileFrom.miniprogramPicUrl = "待生成";
         this.fileFrom.miniprogramPage = "待生成";
-        this.fileFrom.videoId = item.miniprogram.videoId;
-        this.fileFrom.courseId = item.miniprogram.courseId;
+        this.fileFrom.videoId = item.miniprogram.videoId ? parseInt(item.miniprogram.videoId) : null
+        this.fileFrom.courseId = item.miniprogram.courseId ? parseInt(item.miniprogram.courseId) : null
         this.fileFrom.expiresDays = item.miniprogram.expiresDays;
 
         videoList(item.miniprogram.courseId).then(response => {

+ 35 - 13
src/views/watch/deviceInfo/details.vue

@@ -64,7 +64,7 @@
           </div>
         </div>
         <div class="equipment-day">
-          已绑定 <span>400</span> 天, 佩戴 <span>4365</span> 天
+          <!-- 已绑定 <span>400</span> 天, 佩戴 <span>4365</span> 天 -->
         </div>
         <div class="equipment-btn">
           <el-button type="primary" size="small">一键上传</el-button>
@@ -83,7 +83,7 @@
       </el-tabs>
       
       <keep-alive>
-        <component :ref="currentTabComponent" :is="currentTabComponent" :deviceId="deviceId" :basicInfo="basicInfo" :detailsType="detailsType">
+        <component :ref="currentTabComponent" :is="currentTabComponent" :deviceId="deviceId" :basicInfo="basicInfo" :detailsType="detailsType" :userInfo="userInfo">
         </component>
       </keep-alive>
     </el-main>
@@ -115,6 +115,8 @@ import Overview from '@/components/DeviceInfo/Overview.vue';
 import Bloodoxygen from '@/components/DeviceInfo/Bloodoxygen.vue';
 import Uricacid from '@/components/DeviceInfo/Uricacid.vue';
 import Urineketones from '@/components/DeviceInfo/Urineketones.vue';
+import PressureValue from '@/components/DeviceInfo/PressureValue.vue';
+import Report from '@/components/DeviceInfo/Report.vue';
 
 export default {
   name: "DeviceDetails",
@@ -134,7 +136,9 @@ export default {
     Overview,
     Bloodoxygen,
     Uricacid,
-    Urineketones
+    Urineketones,
+    PressureValue,
+    Report
   },
   data() {
     return {
@@ -179,10 +183,10 @@ export default {
           label: '位置',
           name: 'location'
         },
-        {
-          label: '脉搏',
-          name: 'pulse'
-        },
+        // {
+        //   label: '脉搏',
+        //   name: 'pulse'
+        // },
         {
           label: '血糖',
           name: 'bloodsugar'
@@ -191,10 +195,10 @@ export default {
           label: '血压',
           name: 'bloodpressure'
         },
-        {
-          label: '血酮',
-          name: 'urineketones'
-        },
+        // {
+        //   label: '血酮',
+        //   name: 'urineketones'
+        // },
         {
           label: '尿酸',
           name: 'uricacid'
@@ -211,6 +215,14 @@ export default {
         //   label: '血脂',
         //   name: 'bloodfat'
         // },
+        {
+          label: '压力',
+          name: 'pressureValue'
+        },
+        {
+          label: '周报',
+          name: 'report'
+        },
       ]
     };
   },
@@ -238,7 +250,9 @@ export default {
         overview: Overview,
         bloodoxygen: Bloodoxygen,
         uricacid: Uricacid,
-        urineketones: Urineketones
+        urineketones: Urineketones,
+        pressureValue:PressureValue,
+        report:Report
 
 
 
@@ -342,9 +356,12 @@ export default {
      */
     async getUserinfo(deviceId) {
       try {
-        const response = await getUser(deviceId);
+        const userId = this.$route.params.userId;
+        const selectType = this.$route.params.selectType;
+        const response = await getUser({deviceId:deviceId,userId:userId,selectType});
         if (response && response.data) {
           this.userInfo = response.data; // 确保数据被正确赋值
+          this.userInfo.userId = userId;
           // console.log("用户信息:", this.userInfo);
         } else {
           console.warn("用户信息返回为空或格式不正确:", response);
@@ -517,6 +534,11 @@ export default {
   // line-height: 160px;
   padding: 0;
 }
+::v-deep .el-main::-webkit-scrollbar {
+  width: 0;
+  display: none;  /* Chrome, Safari and Opera */
+}
+
 
 body>.el-container {
   margin-bottom: 40px;

+ 160 - 13
src/views/watch/deviceInfo/index.vue

@@ -5,10 +5,17 @@
         <el-input v-model="queryParams.doctorName" placeholder="请输入所属医生" clearable size="small"
           @keyup.enter.native="handleQuery" />
       </el-form-item>
-      <el-form-item label="绑定用户" prop="watchUserName">
+      <el-form-item label="所属员工" prop="companyUserId">
+        <el-select v-model="queryParams.companyUserId" placeholder="请选择所属员工" clearable size="small">
+          <el-option v-for="item in companyUserList" :key="item.userId" :label="item.nickName"
+            :value="item.userId">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <!-- <el-form-item label="绑定用户" prop="watchUserName">
         <el-input v-model="queryParams.watchUserName" placeholder="请输入绑定用户" clearable size="small"
           @keyup.enter.native="handleQuery" />
-      </el-form-item>
+      </el-form-item> -->
       <el-form-item label="设备编号" prop="deviceNumber">
         <el-input v-model="queryParams.deviceNumber" placeholder="请输入设备编号" clearable size="small"
           @keyup.enter.native="handleQuery" />
@@ -27,6 +34,12 @@
           </el-option>
         </el-select>
       </el-form-item>
+      <el-form-item label="健康状态" prop="isNormal">
+        <el-select v-model="queryParams.isNormal" placeholder="请选择健康状态" clearable size="small">
+          <el-option v-for="item in healthOptions" :key="item.value" :label="item.label" :value="item.value">
+          </el-option>
+        </el-select>
+      </el-form-item>
       <el-form-item>
         <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@@ -82,8 +95,29 @@
     <el-table v-loading="loading" :data="deviceInfoList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="设备id" align="center" prop="deviceId" />
+      <el-table-column label="所属员工" align="center" prop="companyUserName" />
       <el-table-column label="所属医生" align="center" prop="doctorName" />
-      <el-table-column label="绑定用户" align="center" prop="watchUserName" />
+      <el-table-column  label="绑定用户"  align="center" prop="watchUserName">
+        <!-- <template slot-scope="scope"> 
+          <div v-if="scope.row.users && scope.row.users!=null">
+              <el-link :underline="false" @click="handleDetails(scope.row.deviceId, scope.row.users.userId)">
+                {{ scope.row.users.nickName }}
+              </el-link>
+          </div>
+        </template> -->
+        
+      </el-table-column>
+      <el-table-column  label="绑定家人用户"  align="center" prop="familyUserName">
+          <!-- <template slot-scope="scope">
+            <div v-if="scope.row.familyUsers">
+              <div v-for="(item, index) in scope.row.familyUsers" :key="index">
+                <el-link :underline="false" @click="handleDetails(scope.row.deviceId, item.userId)">
+                  {{ item.nickName }}
+                </el-link>
+              </div>
+            </div>
+          </template> -->
+      </el-table-column>
       <el-table-column label="设备编号" align="center" prop="deviceNumber" width="150"/>
       <el-table-column label="电量" align="center" prop="battery" />
       <el-table-column label="信号" align="center" prop="rssi" />
@@ -97,20 +131,71 @@
           >
             最新数据
           </span>
-          <el-tag size="mini" :type="scope.row.isNormal == '异常'?'danger':'primary'">{{scope.row.isNormal}}</el-tag>
+          <el-tag size="mini" :type="scope.row.isNormal == 0?'danger':'primary'">{{ scope.row.isNormal === 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" icon="el-icon-view" @click="handleDetails(scope.row.deviceId)"
+          <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetailsAndType(scope.row)"
             v-hasPermi="['watchApi:deviceInfo:query']">查询详情</el-button>
           <el-button size="mini" type="text" icon="el-icon-phone-outline" @click="handleSoSPeople(scope.row.deviceNumber)"
             v-hasPermi="['watchApi:deviceInfo:query']">查看紧急联系人</el-button>
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+            v-hasPermi="['watchApi:deviceInfo:edit']">修改</el-button>  
         </template>
       </el-table-column>
     </el-table>
 
+    <!-- 添加或修改设备信息对话框 -->
+    <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="companyUserId">
+          <template #label>
+            <span class="required-label">所属员工</span>
+          </template>
+          <el-select v-model="form.companyUserId" placeholder="请选择所属员工" clearable size="small">
+            <el-option v-for="item in companyUserList" :key="item.userId" :label="item.nickName"
+              :value="item.userId">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="所属医生" prop="doctorId">
+          <template #label>
+            <span class="required-label">所属医生</span>
+          </template>
+          <el-select v-model="form.doctorId" placeholder="请选择所属医生" clearable size="small">
+            <el-option v-for="item in doctorList" :key="item.doctorId" :label="item.doctorName"
+              :value="item.doctorId">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="设备编号" prop="deviceNumber">
+          <el-input v-model="form.deviceNumber" placeholder="请输入设备编号" disabled/>
+        </el-form-item>
+        <el-form-item label="电量" prop="battery">
+          <el-input v-model="form.battery" placeholder="请输入电量" disabled/>
+        </el-form-item>
+        <el-form-item label="信号" prop="rssi">
+          <el-input v-model="form.rssi" placeholder="请输入信号" disabled/>
+        </el-form-item>
+        <el-form-item label="设备状态">
+          <el-radio-group v-model="form.status">
+            <el-radio :label="0">OFFLINE</el-radio>
+            <el-radio :label="1">ONLINE</el-radio>
+            <el-radio :label="2">UNACTIVE</el-radio>
+            <el-radio :label="3">DISABLE</el-radio>
+            <el-radio :label="4">NOT EXIST</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
     <!-- 弹窗展示紧急联系人列表 -->
     <el-dialog :visible.sync="sosVisible" title="紧急联系人" width="50%">
         <el-table v-loading="sosLoading" :data="sosPeopleList">
@@ -169,15 +254,15 @@
 
 <script>
 import { listDeviceInfo, getDeviceInfo, delDeviceInfo, addDeviceInfo, updateDeviceInfo, exportDeviceInfo, queryAlarm, setStatus, queryPageAlarm,queryLastHealthData,querySos} from "@/api/watch/deviceInfo";
-import { getAllUserlist } from "@/api/company/companyUser";
+import { getSelectUserList } from "@/api/company/companyUser";
 import { messageSend } from "@/api/watch/deviceInfoSet.js";
+import { listDoctor } from "@/api/doctor/doctor";
 import setUpUserInfoDialog from "@/components/DeviceInfo/SettingDialog/setUpUserInfoDialog.vue";
 import SetInfoDialog from "@/components/DeviceInfo/SettingDialog/SetInfoDialog.vue";
 import Phonebook from "@/components/DeviceInfo/SettingDialog/Phonebook.vue";
 import Clockalarm from "@/components/DeviceInfo/SettingDialog/Clockalarm.vue";
 import Sedentary from "@/components/DeviceInfo/SettingDialog/Sedentary.vue";
-// import {getInfo} from "@/api/login"
-
+import {watchDownload} from "@/api/watch/common";
 export default {
   name: "DeviceInfo",
   components: {
@@ -273,6 +358,13 @@ export default {
           { max: 240, message: '详情最多240个字节', trigger: 'blur' }
         ]
       },
+      healthOptions: [{
+          value: 0,
+          label: '异常'
+        }, {
+          value: 1,
+          label: '正常'
+        }],
       options: [{
         value: '0',
         label: 'OFFLINE'
@@ -311,6 +403,8 @@ export default {
   },
   created() {
     this.getList();
+    this.getCompanyUserList();
+    this.getDoctorList();
   },
   computed: {
     // 计算属性来控制按钮的disabled状态
@@ -319,9 +413,51 @@ export default {
     }
   },
   methods: {
+    getDoctorList(){
+      listDoctor({ doctorType:1,isAudit:1 }).then(response => {
+        this.doctorList = response.rows;
+      });
+    },
+    getCompanyUserList(){
+      getSelectUserList().then(response => {
+          this.companyUserList = response.rows;
+        }).catch(error => {
+          console.error("获取员工列表失败:", error);
+        });
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const deviceId = row.deviceId || this.ids
+      getDeviceInfo(deviceId).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改设备信息";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.deviceId != null) {
+            console.log("===============================" + JSON.stringify(this.form, null, 2))
+            updateDeviceInfo(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addDeviceInfo(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
     rowClassName({row}) {
-      if (row.status && row.status.includes('偏')) {
-        // console.log("=============================" + JSON.stringify(row.status, null, 2))
+      if (row.status && (!row.status.includes('正常') && !row.status.includes('得分') && !row.status.includes('无结果') && !row.status.includes('窦性心率'))) {
         return 'text-red';
         // return true
       }
@@ -403,9 +539,18 @@ export default {
 
       }
     },
+    handleDetailsAndType(row){
+      if(row.userId != null){
+        this.handleDetails(row.deviceId,row.userId)
+      } else if(row.familyUserId !=null){
+        this.handleDetails(row.deviceId,row.familyUserId)
+      } else {
+        this.handleDetails(row.deviceId,null);
+      }
+    },
     /** 处理查询详情按钮点击事件 */
-    handleDetails(deviceId) {
-      this.$router.push({ name: 'details', params: { deviceId } });
+    handleDetails(deviceId,userId,type) {
+      this.$router.push({ name: 'details', params: { deviceId:deviceId,userId:userId,selectType:type } });
     },
 
     /** 查询设备信息列表 */
@@ -501,7 +646,9 @@ export default {
         this.exportLoading = true;
         return exportDeviceInfo(queryParams);
       }).then(response => {
-        this.download(response.msg);
+        debugger;
+        // this.download(response.msg);
+        watchDownload(response.msg);
         this.exportLoading = false;
       }).catch(() => { });
     },

+ 46 - 14
src/views/watch/deviceInfo/my.vue

@@ -27,6 +27,12 @@
             </el-option>
           </el-select>
         </el-form-item>
+        <el-form-item label="健康状态" prop="isNormal">
+        <el-select v-model="queryParams.isNormal" placeholder="请选择健康状态" clearable size="small">
+          <el-option v-for="item in healthOptions" :key="item.value" :label="item.label" :value="item.value">
+          </el-option>
+        </el-select>
+      </el-form-item>
         <el-form-item>
           <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
           <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@@ -83,15 +89,26 @@
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="设备id" align="center" prop="deviceId" />
         <el-table-column label="所属医生" align="center" prop="doctorName" />
-        <el-table-column  label="绑定用户"  align="center">
-          <template slot-scope="scope">
-              <el-link :underline="false" prop="item.userId" v-for="(item, index) in scope.row.users"  v-if="scope.row.users"  @click="handleDetails(scope.row.deviceId,item.userId,0)">{{item.nickName}}</el-link>
-          </template>
+        <el-table-column  label="绑定用户"  align="center" prop="watchUserName">
+          <!-- <template slot-scope="scope"> 
+          <div v-if="scope.row.users && scope.row.users!=null">
+              <el-link :underline="false" @click="handleDetails(scope.row.deviceId, scope.row.users.userId)">
+                {{ scope.row.users.nickName }}
+              </el-link>
+          </div>
+        </template> -->
+          
         </el-table-column>
-        <el-table-column  label="绑定家人用户"  align="center">
-            <template slot-scope="scope">
-                <el-link :underline="false" prop="item.userId" v-for="(item, index) in scope.row.familyUsers"  v-if="scope.row.familyUsers"  @click="handleDetails(scope.row.deviceId,item.userId,1)">{{item.nickName}}</el-link>
-            </template>
+        <el-table-column  label="绑定家人用户"  align="center" prop="familyUserName">
+            <!-- <template slot-scope="scope">
+              <div v-if="scope.row.familyUsers">
+                <div v-for="(item, index) in scope.row.familyUsers" :key="index">
+                  <el-link :underline="false" @click="handleDetails(scope.row.deviceId, item.userId)">
+                    {{ item.nickName }}
+                  </el-link>
+                </div>
+              </div>
+            </template> -->
         </el-table-column>
         <el-table-column label="设备编号" align="center" prop="deviceNumber" width="150"/>
         <el-table-column label="电量" align="center" prop="battery" />
@@ -106,13 +123,13 @@
             >
               最新数据
             </span>
-            <el-tag size="mini" :type="scope.row.isNormal == '异常'?'danger':'primary'">{{scope.row.isNormal}}</el-tag>
+            <el-tag size="mini" :type="scope.row.isNormal == 0?'danger':'primary'">{{ scope.row.isNormal === 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" icon="el-icon-view" @click="handleDetails(scope.row.deviceId,null,0)"
+            <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetailsAndType(scope.row)"
               v-hasPermi="['watchApi:deviceInfo:query']">查询详情</el-button>
             <el-button size="mini" type="text" icon="el-icon-phone-outline" @click="handleSoSPeople(scope.row.deviceNumber)"
               v-hasPermi="['watchApi:deviceInfo:query']">查看紧急联系人</el-button>
@@ -186,7 +203,7 @@
   import Clockalarm from "@/components/DeviceInfo/SettingDialog/Clockalarm.vue";
   import Sedentary from "@/components/DeviceInfo/SettingDialog/Sedentary.vue";
   // import {getInfo} from "@/api/login"
-  
+  import {watchDownload} from "@/api/watch/common";
   export default {
     name: "my",
     components: {
@@ -282,6 +299,13 @@
             { max: 240, message: '详情最多240个字节', trigger: 'blur' }
           ]
         },
+        healthOptions: [{
+          value: 0,
+          label: '异常'
+        }, {
+          value: 1,
+          label: '正常'
+        }],
         options: [{
           value: '0',
           label: 'OFFLINE'
@@ -329,8 +353,7 @@
     },
     methods: {
       rowClassName({row}) {
-        if (row.status && row.status.includes('偏')) {
-          // console.log("=============================" + JSON.stringify(row.status, null, 2))
+        if (row.status && (!row.status.includes('正常') && !row.status.includes('得分') && !row.status.includes('无结果') && !row.status.includes('窦性心率'))) {
           return 'text-red';
           // return true
         }
@@ -412,6 +435,15 @@
   
         }
       },
+      handleDetailsAndType(row){
+        if(row.userId != null){
+          this.handleDetails(row.deviceId,row.userId,0)
+        } else if(row.familyUserId !=null ){
+          this.handleDetails(row.deviceId,row.familyUserId,1)
+        } else {
+          this.handleDetails(row.deviceId,null,0);
+        }
+      },
       /** 处理查询详情按钮点击事件 */
       handleDetails(deviceId,userId,type) {
         this.$router.push({ name: 'details', params: { deviceId:deviceId,userId:userId,selectType:type } });
@@ -510,7 +542,7 @@
           this.exportLoading = true;
           return exportMy(queryParams);
         }).then(response => {
-          this.download(response.msg);
+          watchDownload(response.msg);
           this.exportLoading = false;
         }).catch(() => { });
       },

+ 257 - 0
src/views/xsy/index.vue

@@ -0,0 +1,257 @@
+<template>
+  <div class="app-container">
+
+    <!-- 查询 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
+
+      <el-form-item label="ClientId">
+        <el-input v-model="queryParams.clientId" placeholder="请输入ClientId" size="small"/>
+      </el-form-item>
+
+      <el-form-item label="账号名称">
+        <el-input v-model="queryParams.accountName" placeholder="请输入账号名称"/>
+      </el-form-item>
+
+      <el-form-item label="状态">
+        <el-select v-model="queryParams.status" clearable size="small">
+          <el-option label="启用" :value="1"/>
+          <el-option label="禁用" :value="0"/>
+        </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 class="mb8">
+      <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
+      <el-button type="success" icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate">修改</el-button>
+      <el-button type="danger" icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
+    </el-row>
+
+    <!-- 表格 -->
+    <el-table v-loading="loading" :data="accountList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55"/>
+      <el-table-column prop="accountName" label="账号名称"/>
+      <el-table-column prop="id" label="ID"/>
+      <el-table-column prop="clientId" label="ClientId"/>
+      <el-table-column prop="redirectUri" label="回调地址"/>
+
+      <el-table-column label="状态">
+        <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="操作">
+        <template slot-scope="scope">
+
+          <el-button size="mini" type="text" @click="handleUpdate(scope.row)">修改</el-button>
+
+          <el-button size="mini" type="text" @click="handleDelete(scope.row)">删除</el-button>
+
+          <el-button size="mini" type="text" @click="toggleStatus(scope.row)">
+            {{scope.row.status==1?'禁用':'启用'}}
+          </el-button>
+
+          <el-button size="mini" type="text" @click="auth(scope.row.id)">
+            授权
+          </el-button>
+
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页(注意这里 total 用 length) -->
+    <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">
+
+      <el-form ref="form" :model="form">
+
+        <el-form-item label="ClientId">
+          <el-input v-model="form.clientId"/>
+        </el-form-item>
+
+        <el-form-item label="账号名称">
+          <el-input v-model="form.accountName"/>
+        </el-form-item>
+
+        <el-form-item label="ClientSecret">
+          <el-input v-model="form.clientSecret"/>
+        </el-form-item>
+
+        <el-form-item label="回调地址">
+          <el-input v-model="form.redirectUri"/>
+        </el-form-item>
+
+      </el-form>
+
+      <div slot="footer">
+        <el-button type="primary" @click="submitForm">确定</el-button>
+        <el-button @click="cancel">取消</el-button>
+      </div>
+
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import {
+  listXsyAccount,
+  addXsyAccount,
+  updateXsyAccount,
+  delXsyAccount,
+  updateXsyAccountStatus,
+  getXsyAuthUrl
+} from "@/api/xsy/xsy";
+
+export default {
+  name: "XsyAccount",
+
+  data() {
+    return {
+      loading: true,
+      accountList: [],
+      total: 0,
+      ids: [],
+      single: true,
+      multiple: true,
+      showSearch: true,
+
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        clientId: null,
+        status: null
+      },
+
+      form: {},
+      open: false,
+      title: ""
+    };
+  },
+
+  created() {
+    this.getList();
+  },
+
+  methods: {
+
+    /** 查询列表 */
+    getList() {
+      this.loading = true;
+      listXsyAccount(this.queryParams).then(res => {
+        this.accountList = res.rows || [];
+        this.total = res.total || 0;
+        this.loading = false;
+      });
+    },
+
+    /** 搜索 */
+    handleQuery() {
+      this.getList();
+    },
+
+    /** 重置 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.getList();
+    },
+
+    /** 选中 */
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+
+    /** 新增 */
+    handleAdd() {
+      this.form = {};
+      this.open = true;
+      this.title = "新增账号";
+    },
+
+    /** 修改 */
+    handleUpdate(row) {
+      this.form = row || {};
+      this.open = true;
+      this.title = "修改账号";
+    },
+
+    /** 提交 */
+    submitForm() {
+      const api = this.form.id ? updateXsyAccount : addXsyAccount;
+
+      api(this.form).then(res => {
+        if (res.code === 200) {
+          this.msgSuccess("操作成功");
+          this.open = false;
+          this.getList();
+        }
+      });
+    },
+
+    /** 删除 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+
+      this.$confirm('是否确认删除编号为"' + ids + '"的数据项?', "警告")
+        .then(() => {
+          return delXsyAccount(ids);
+        })
+        .then(() => {
+          this.msgSuccess("删除成功");
+          this.getList();
+        });
+    },
+
+    /** 状态切换 */
+    toggleStatus(row) {
+      const status = row.status === 1 ? 0 : 1;
+
+      updateXsyAccountStatus(row.id, status).then(() => {
+        this.msgSuccess("状态修改成功");
+        this.getList();
+      });
+    },
+
+    /** 授权 */
+    auth(id) {
+      const win = window.open('', '_blank');
+
+      getXsyAuthUrl(id).then(res => {
+        if (res.code === 200) {
+          win.location.href = res.msg;
+        } else {
+          win.close();
+          this.msgError(res.msg);
+        }
+      }).catch(() => {
+        win.close();
+        this.msgError("授权失败");
+      });
+    },
+
+    cancel() {
+      this.open = false;
+    }
+
+  }
+};
+</script>