Procházet zdrojové kódy

Merge remote-tracking branch 'origin/master'

yjwang před 7 hodinami
rodič
revize
6fe05671fa

+ 29 - 2
src/api/company/callphone.js

@@ -3,12 +3,39 @@ import request from '@/utils/request'
 // 查询调用日志_ai打电话列表
 export function listCallphone(query) {
   return request({
-    url: '/company/callphoneLog/list',
+    url: '/company/callphoneLog/groupList',
     method: 'get',
     params: query
   })
 }
 
+// 查询调用日志_ai打电话详细
+export function listCallPhoneByRoboticId(query) {
+    return request({
+        url: '/company/callphoneLog/listByCallerIdAndRoboticId',
+        method: 'get',
+        params: query
+    })
+}
+
+
+// 获取统计数据
+export function getCallPhoneLogCount() {
+    return request({
+        url: '/company/callphoneLog/count',
+        method: 'get'
+    })
+}
+
+// 导出调用日志_ai打电话
+export function exportCallphone(query) {
+    return request({
+        url: '/company/callphoneLog/export',
+        method: 'get',
+        params: query
+    })
+}
+
 // // 查询调用日志_ai打电话详细
 // export function getCallphone(logId) {
 //   return request({
@@ -50,4 +77,4 @@ export function listCallphone(query) {
 //     method: 'get',
 //     params: query
 //   })
-// }
+// }

+ 1 - 1
src/api/company/companyVoiceRobotic.js

@@ -158,7 +158,7 @@ export function getExecRecords(query) {
 }
 export function wxListQw(params) {
   return request({
-    url: 'company/companyVoiceRobotic/wxListQw',
+    url: '/company/companyVoiceRobotic/wxListQw',
     method: 'get',
     params
   })

+ 26 - 1
src/api/company/sendmsg.js

@@ -18,6 +18,31 @@ export function listByCallerIdAndRoboticId(query) {
   })
 }
 
+// 删除调用日志_发送短信
+export function delSendmsg(logId) {
+    return request({
+        url: '/company/sendmsg/' + logId,
+        method: 'delete'
+    })
+}
+
+// 获取统计数据
+export function getSendMsgLogCount() {
+    return request({
+        url: '/company/sendmsgLog/count',
+        method: 'get'
+    })
+}
+
+// 导出调用日志_发送短信
+export function exportSendmsg(query) {
+    return request({
+        url: '/company/sendmsgLog/export',
+        method: 'get',
+        params: query
+    })
+}
+
 // // 查询调用日志_发送短信详细
 // export function getSendmsg(logId) {
 //   return request({
@@ -59,4 +84,4 @@ export function listByCallerIdAndRoboticId(query) {
 //     method: 'get',
 //     params: query
 //   })
-// }
+// }

+ 56 - 0
src/api/live/liveData.js

@@ -121,3 +121,59 @@ export function exportLiveDataCompany(data) {
     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 || {}
+//     })
+// }

+ 85 - 0
src/api/qw/acquisitionAssistant.js

@@ -0,0 +1,85 @@
+import request from '@/utils/request'
+
+// 查询获客链接列表
+export function listAssistant(query) {
+  return request({
+    url: '/qw/acquisitionAssistant/list',
+    method: 'get',
+    params: query
+  })
+}
+
+/**
+ * 获取企微用户列表(POST请求)
+ * @param {Object} params 查询参数
+ * @param {string} params.qwUserName 搜索关键词
+ * @param {number} params.pageNum 页码
+ * @param {number} params.pageSize 每页条数
+ * @param {string} params.corpId 企业ID
+ */
+export function getQwUserList(params) {
+  return request({
+    url: '/qw/acquisitionAssistant/qwUserList',
+    method: 'post',
+    data: params
+  })
+}
+
+/**
+ * 获取企微用户主体列表
+ * @param {Object} data 查询参数
+ * @param {number} data.status 状态
+ */
+export function qwUserCompanyList(data) {
+  return request({
+    url: '/qw/acquisitionAssistant/qwUserCompanyList',
+    method: 'post',
+    data: data
+  })
+}
+
+/**
+ * 批量获取企微用户详情
+ * @param {Array} qwUserTableIds 企微用户主键ID列表
+ */
+export function getQwUserListByIds(qwUserTableIds) {
+  return request({
+    url: '/qw/acquisitionAssistant/getQwUserListByIds',
+    method: 'post',
+    data: qwUserTableIds
+  })
+}
+
+// 根据linkId直接获取详情
+export function getDetailByLinkId(linkId) {
+  return request({
+    url: `/qw/acquisitionAssistant/getDetailByLinkId/${linkId}`,
+    method: 'get'
+  })
+}
+
+// 新增获客链接
+export function addAssistant(data) {
+  return request({
+    url: '/qw/acquisitionAssistant/add',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改获客链接
+export function updateAssistant(data) {
+  return request({
+    url: '/qw/acquisitionAssistant/edit',
+    method: 'post',
+    data: data
+  })
+}
+
+// 删除获客链接
+export function deleteAssistant(id) {
+  return request({
+    url: `/qw/acquisitionAssistant/delete/${id}`,
+    method: 'get'
+  })
+}

+ 34 - 1
src/router/index.js

@@ -358,7 +358,40 @@ export const constantRoutes = [
         }
       }
     ]
-  }
+  },
+    {
+        path: '/taskStatistics/sendMsgLog',
+        component: Layout,
+        hidden: true,
+        children: [
+            {
+                path: 'index',
+                component: () => import('@/views/taskStatistics/sendMsgLog/index'),
+                name: 'sendMsgLogStatistics',
+                meta: {
+                    title: '短信统计',
+                    activeMenu: '/sendMsgLogStatistics/sendMsgLog'
+                }
+            }
+        ]
+    },
+    {
+        path: '/taskStatistics/callLog',
+        component: Layout,
+        hidden: true,
+        children: [
+            {
+                path: 'index',
+                component: () => import('@/views/taskStatistics/callLog/index'),
+                name: 'callLogStatistics',
+                meta: {
+                    title: '外呼统计',
+                    activeMenu: '/callLogStatistics/callLog'
+                }
+            }
+        ]
+    }
+
 
 ]
 

+ 134 - 16
src/views/company/companyWorkflow/design.vue

@@ -289,6 +289,35 @@
               </el-form-item>
             </div>
 
+            <!-- 企微加个微配置 -->
+            <div v-if="selectedNode.nodeType == 'AI_QW_ADD_WX_TASK'" class="property-section">
+              <div class="section-title">
+                <i class="el-icon-chat-dot-round"></i>加微方式
+              </div>
+              <el-form-item label="加微方式">
+                <!-- <el-select v-model="selectedNode.nodeConfig.robot" filterable placeholder="请选择加微话术">
+                  <el-option v-for="item in robotList" :key="item.id" :label="item.name + '('+item.num+')'" :value="item.id"/>
+                </el-select> -->
+                <el-select v-model="selectedNode.nodeConfig.qwWxAddWayId" placeholder="请选择加微方式" filterable size="small">
+                  <el-option
+                    v-for="dict in qwWxAddWayOptions"
+                    :key="dict.dictValue"
+                    :label="dict.dictLabel"
+                    :value="dict.dictValue"
+                  />
+                </el-select>
+              </el-form-item>
+                <div class="section-title">
+                    <i class="el-icon-phone"></i>短信配置
+                </div>
+                <el-form-item label="短信模版">
+                    <el-select v-model="selectedNode.nodeConfig.smsTempId" filterable placeholder="请选择短信模版">
+                        <el-option v-for="item in smsTempList" :key="item.tempId" :label="item.title " :value="item.tempId"/>
+                    </el-select>
+                </el-form-item>
+            </div>
+
+
             <!-- AI外呼配置(旧版天天外呼,已停用) -->
             <!--
             <div v-if="selectedNode.nodeType == 'AI_CALL_TASK'" class="property-section">
@@ -611,7 +640,7 @@ import {
 import { getWorkflow, addWorkflow, updateWorkflow, getNodeTypes } 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() {
@@ -625,6 +654,7 @@ export default {
       }],
       editAiDisable:true,
       wxDialogList: [],
+      qwWxAddWayOptions: [],
       // 工作流ID
       workflowId: null,
       // 表单数据
@@ -711,11 +741,19 @@ export default {
     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;
+    });
+
+    // listAll().then(e => {
+    //   this.wxDialogList = e.data;
+    // })
   },
   mounted() {
     // 确保容器可获取焦点
@@ -826,7 +864,7 @@ export default {
           this.easyCallBusiGroupList = res.data || []
         })
       }
-      if (this.selectedNode.nodeType === 'AI_SEND_MSG_TASK') {
+      if (this.selectedNode.nodeType === 'AI_SEND_MSG_TASK' || this.selectedNode.nodeType === 'AI_QW_ADD_WX_TASK') {
         getSmsTempList().then(res => {
           this.smsTempList = res.data || []
         })
@@ -1117,6 +1155,14 @@ export default {
       }
       this.hideContextMenu()
     },
+    /** 判断是否是开始节点 */
+    isStartNodeType(typeCode) {
+      return typeCode === 'START' || typeCode === 'start'
+    },
+    /** 判断是否是结束节点 */
+    isEndNodeType(typeCode) {
+      return typeCode === 'END' || typeCode === 'end'
+    },
     /** 拖拽开始 */
     onDragStart(e, nodeType) {
       e.dataTransfer.setData('nodeType', JSON.stringify(nodeType))
@@ -1126,6 +1172,23 @@ export default {
       const nodeTypeStr = e.dataTransfer.getData('nodeType')
       if (!nodeTypeStr) return
       const nodeType = JSON.parse(nodeTypeStr)
+
+      // 检查开始/结束节点是否已存在(限制只能有一个)
+      if (this.isStartNodeType(nodeType.typeCode)) {
+        const existingStart = this.nodes.find(n => this.isStartNodeType(n.nodeType))
+        if (existingStart) {
+          this.$message.warning('已存在开始节点,工作流只能有一个开始节点')
+          return
+        }
+      }
+      if (this.isEndNodeType(nodeType.typeCode)) {
+        const existingEnd = this.nodes.find(n => this.isEndNodeType(n.nodeType))
+        if (existingEnd) {
+          this.$message.warning('已存在结束节点,工作流只能有一个结束节点')
+          return
+        }
+      }
+
       const rect = this.$refs.canvasContainer.getBoundingClientRect()
       const x = (e.clientX - rect.left - this.canvasOffset.x) / this.scale
       const y = (e.clientY - rect.top - this.canvasOffset.y) / this.scale
@@ -1446,9 +1509,68 @@ export default {
     /** 保存工作流 */
     handleSave() {
       if (!this.form.workflowName) {
-        this.msgWarning('请输入工作流名称')
+        this.$message.warning('请输入工作流名称')
         return
       }
+
+      // 校验开始节点和结束节点
+      const startNodes = this.nodes.filter(node => this.isStartNodeType(node.nodeType))
+      const endNodes = this.nodes.filter(node => this.isEndNodeType(node.nodeType))
+
+      if (endNodes.length === 0) {
+        this.$message.warning('工作流必须包含一个结束节点')
+        return
+      }
+      if (endNodes.length > 1) {
+        this.$message.warning('工作流只能包含一个结束节点')
+        return
+      }
+
+      // 校验所有节点都必须有连线
+      if (this.nodes.length > 0) {
+        const connectedNodeKeys = new Set()
+        this.edges.forEach(edge => {
+          connectedNodeKeys.add(edge.sourceNodeKey)
+          connectedNodeKeys.add(edge.targetNodeKey)
+        })
+        const isolatedNodes = this.nodes.filter(node => !connectedNodeKeys.has(node.nodeKey))
+        if (isolatedNodes.length > 0) {
+          const isolatedNames = isolatedNodes.map(n => n.nodeName).join('、')
+          this.$message.warning(`以下节点未连线:${isolatedNames},所有节点必须连线`)
+          return
+        }
+      }
+
+      // 校验短信节点和外呼节点配置
+      for (const node of this.nodes) {
+        // 短信节点校验:短信模版必选
+        if (node.nodeType === 'AI_SEND_MSG_TASK') {
+          if (!node.nodeConfig || !node.nodeConfig.smsTempId) {
+            this.$message.warning(`节点「${node.nodeName}」未选择短信模版`)
+            return
+          }
+        }
+        // 外呼节点校验
+        if (node.nodeType === 'AI_CALL_TASK') {
+          const callMode = node.nodeConfig ? node.nodeConfig.callMode : null
+          // callMode=2 为AI自动外呼,需要校验必选项
+          if (callMode === 2) {
+            if (!node.nodeConfig.gatewayId) {
+              this.$message.warning(`节点「${node.nodeName}」未选择外呼网关`)
+              return
+            }
+            if (!node.nodeConfig.llmAccountId) {
+              this.$message.warning(`节点「${node.nodeName}」未选择大模型`)
+              return
+            }
+            if (!node.nodeConfig.voiceCode) {
+              this.$message.warning(`节点「${node.nodeName}」未选择音色`)
+              return
+            }
+          }
+        }
+      }
+
       const nodes = JSON.parse(JSON.stringify(this.nodes))
       this.edges.forEach((edges) => {
         edges.conditionExpr = JSON.stringify(edges.conditionExprObj);
@@ -1457,10 +1579,6 @@ export default {
         node.nodeConfig = JSON.stringify(node.nodeConfig);
       })
 
-      // 查找开始节点和结束节点
-      const startNode = this.nodes.find(node => node.nodeType === 'START')
-      const endNode = this.nodes.find(node => node.nodeType === 'END')
-
       const data = {
         workflowId: this.workflowId,
         workflowName: this.form.workflowName,
@@ -1469,17 +1587,17 @@ export default {
         canvasData: JSON.stringify({ scale: this.scale, offset: this.canvasOffset }),
         nodes: nodes,
         edges: this.edges,
-        startNodeKey: startNode ? startNode.nodeKey : null,
-        endNodeKey: endNode ? endNode.nodeKey : null
+        startNodeKey: startNodes[0].nodeKey,
+        endNodeKey: endNodes[0].nodeKey
       }
 
       if (this.workflowId) {
         updateWorkflow(data).then(() => {
-          this.msgSuccess('保存成功')
+          this.$message.success('保存成功')
         })
       } else {
         addWorkflow(data).then(res => {
-          this.msgSuccess('保存成功')
+          this.$message.success('保存成功')
           this.workflowId = res.data
           this.$router.replace('/companyWx/companyWorkflow/design/' + this.workflowId)
         })

+ 5 - 0
src/views/crm/components/addOrEditCustomer.vue

@@ -212,6 +212,11 @@
                 <el-input :rows="3" v-model="form.remark" type="textarea" placeholder="请输入备注内容" maxlength="500" show-word-limit />
             </el-form-item>
         </el-form>
+        
+        <div slot="footer" class="dialog-footer">
+            <el-button @click="$emit('close')">取消</el-button>
+            <el-button type="primary" @click="submitForm">确定</el-button>
+        </div>
     </div>
 </template>
   

+ 1175 - 0
src/views/live/liveStatistics/index.vue

@@ -0,0 +1,1175 @@
+<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>
+</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'
+
+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' })
+      }
+    }
+  }
+}
+</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>

+ 1355 - 0
src/views/qw/acquisitionAssistant/index.vue

@@ -0,0 +1,1355 @@
+<template>
+    <div class="app-container">
+        <!-- 搜索栏 - 主体选择放在最前面 -->
+        <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
+            <!-- 主体选择 - 全局筛选条件 -->
+            <el-form-item label="选择主体" prop="corpId" required>
+                <el-select
+                    v-model="queryParams.corpId"
+                    placeholder="请选择主体"
+                    clearable
+                    size="small"
+                    style="width: 200px"
+                    @change="handleCorpChange"
+                >
+                    <el-option
+                        v-for="item in corpOptions"
+                        :key="item.corpId"
+                        :label="item.corpName"
+                        :value="item.corpId"
+                    />
+                </el-select>
+            </el-form-item>
+
+            <el-form-item label="链接名称" prop="linkName">
+                <el-input
+                    v-model="queryParams.linkName"
+                    placeholder="请输入链接名称"
+                    clearable
+                    size="small"
+                    @keyup.enter.native="handleQuery"
+                />
+            </el-form-item>
+            <el-form-item label="链接ID" prop="linkId">
+                <el-input
+                    v-model="queryParams.linkId"
+                    placeholder="请输入链接ID"
+                    clearable
+                    size="small"
+                    @keyup.enter.native="handleQuery"
+                />
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+                <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+                    <el-option label="正常" :value="1"/>
+                    <el-option label="已失效" :value="2"/>
+                </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"
+                    icon="el-icon-plus"
+                    size="mini"
+                    @click="handleAdd"
+                >新增
+                </el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    type="success"
+                    icon="el-icon-edit"
+                    size="mini"
+                    :disabled="single"
+                    @click="handleUpdate"
+                >修改
+                </el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button
+                    type="danger"
+                    icon="el-icon-delete"
+                    size="mini"
+                    :disabled="multiple"
+                    @click="handleDelete"
+                >删除
+                </el-button>
+            </el-col>
+        </el-row>
+
+        <!-- 数据表格 -->
+        <el-table
+            v-loading="loading"
+            :data="assistantList"
+            @selection-change="handleSelectionChange"
+        >
+            <el-table-column type="selection" width="55" align="center"/>
+            <el-table-column label="ID" prop="id" width="80" align="center"/>
+            <el-table-column label="主体" width="150" align="center">
+                <template slot-scope="scope">
+                    {{ getCorpName(scope.row.corpId) }}
+                </template>
+            </el-table-column>
+            <el-table-column label="链接ID" prop="linkId" width="200" :show-overflow-tooltip="true"/>
+            <el-table-column label="链接名称" prop="linkName" width="150" :show-overflow-tooltip="true"/>
+            <el-table-column label="链接URL" prop="url" width="200" :show-overflow-tooltip="true"/>
+            <el-table-column label="使用范围" prop="rangeDesc" width="200" :show-overflow-tooltip="true"/>
+            <el-table-column label="状态" width="80" align="center">
+                <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="企微创建时间" width="160" align="center">
+                <template slot-scope="scope">
+                    <span>{{ parseTime(scope.row.qwCreateTime) }}</span>
+                </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" width="300" fixed="right">
+                <template slot-scope="scope">
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-view"
+                        @click="handleDetail(scope.row)"
+                    >详情
+                    </el-button>
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-edit"
+                        @click="handleUpdate(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-link"
+                        @click="handleCopyLink(scope.row)"
+                    >提取链接
+                    </el-button>
+                    <!-- 新增:生成二维码按钮 -->
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-s-promotion"
+                        @click="handleGenerateQRCode(scope.row)"
+                        style="color: #67C23A;"
+                    >生成二维码
+                    </el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <!-- 分页 -->
+        <pagination
+            v-show="total>0"
+            :total="total"
+            :page.sync="queryParams.pageNum"
+            :limit.sync="queryParams.pageSize"
+            @pagination="getList"
+        />
+
+        <!-- 新增/修改对话框 -->
+        <el-dialog :title="title" :visible.sync="open" width="700px" append-to-body>
+            <el-form ref="form" :model="form" :rules="rules" label-width="140px">
+                <el-form-item label="链接名称" prop="linkName">
+                    <el-input v-model="form.linkName" placeholder="请输入链接名称" maxlength="50"/>
+                </el-form-item>
+
+                <el-form-item label="是否无需验证" prop="skipVerify">
+                    <el-radio-group v-model="form.skipVerify">
+                        <el-radio :label="true">无需验证</el-radio>   <!-- true 表示无需验证 -->
+                        <el-radio :label="false">需要验证</el-radio>  <!-- false 表示需要验证 -->
+                    </el-radio-group>
+                    <div class="el-form-item__tips" style="color: #909399; font-size: 12px; margin-top: 5px;">
+                        选择"无需验证"时,客户添加好友无需员工确认
+                    </div>
+                </el-form-item>
+
+                <el-form-item label="优先分配类型" prop="priorityType">
+                    <el-radio-group v-model="form.priorityType">
+                        <el-radio :label="0">不启用</el-radio>
+                        <el-radio :label="1">全企业范围内优先分配给有好友关系的</el-radio>
+                        <el-radio :label="2">指定范围内优先分配有好友关系的</el-radio>
+                    </el-radio-group>
+                </el-form-item>
+
+                <!-- 关联成员选择器(企微用户) -->
+                <el-form-item label="关联成员" prop="selectedUserIds">
+                    <el-select
+                        v-model="form.selectedUserIds"
+                        multiple
+                        filterable
+                        remote
+                        reserve-keyword
+                        :remote-method="searchQwUsers"
+                        :loading="userLoading"
+                        :multiple-limit="500"
+                        :disabled="!queryParams.corpId"
+                        :placeholder="getUserSelectPlaceholder"
+                        style="width: 100%"
+                        @visible-change="handleSelectVisible"
+                        @focus="loadUsersByCorp"
+                        @change="handleUserSelectionChange"
+                    >
+                        <el-option
+                            v-for="item in qwUserOptions"
+                            :key="item.id"
+                            :label="`${item.qwUserName} (${item.qwUserId})`"
+                            :value="item.id"
+                        >
+                            <span style="float: left">{{ item.qwUserName }}</span>
+                            <span style="float: right; color: #8492a6; font-size: 13px">{{ item.qwUserId }}</span>
+                            <div style="clear: both;"></div>
+                            <div style="font-size: 12px; color: #999;" v-if="item.department">
+                                部门:{{ item.departmentName || item.department }} | 本地ID:{{ item.id }}
+                            </div>
+                        </el-option>
+
+                        <!-- 加载更多 -->
+                        <div style="text-align: center; padding: 10px;" v-if="hasMoreUsers">
+                            <el-button type="text" @click="loadMoreUsers" :loading="loadingMore">加载更多</el-button>
+                        </div>
+
+                        <!-- 空状态 -->
+                        <div slot="empty" style="text-align: center; padding: 20px;">
+                            <span v-if="!queryParams.corpId">请先在搜索栏选择主体</span>
+                            <span v-else-if="searchKeyword">未找到相关成员</span>
+                            <span v-else>暂无成员数据</span>
+                        </div>
+                    </el-select>
+
+                    <!-- 显示当前选择的主体信息 -->
+                    <div v-if="queryParams.corpId" class="el-form-item__tips"
+                         style="color: #67C23A; font-size: 12px; margin-top: 5px;">
+                        当前主体:{{ getCorpName(queryParams.corpId) }}
+                    </div>
+
+                    <!-- 已选信息展示 -->
+                    <div v-if="form.selectedUserIds && form.selectedUserIds.length" class="el-form-item__tips"
+                         style="color: #909399; font-size: 12px; margin-top: 5px;">
+                        <div>已选择 {{ form.selectedUserIds.length }} 名成员</div>
+                        <el-button type="text" @click="clearSelectedUsers" size="mini">清空</el-button>
+                    </div>
+                </el-form-item>
+
+                <!-- 部门选择(如果有部门选择组件) -->
+                <el-form-item label="关联部门" prop="departmentListParam" v-if="showDepartment">
+                    <el-select
+                        v-model="form.departmentListParam"
+                        multiple
+                        filterable
+                        placeholder="请选择部门"
+                        style="width: 100%"
+                    >
+                        <el-option
+                            v-for="item in departmentOptions"
+                            :key="item.id"
+                            :label="item.departmentName"
+                            :value="item.id"
+                        />
+                    </el-select>
+                </el-form-item>
+
+                <el-form-item label="备注" prop="remark">
+                    <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" maxlength="200"/>
+                </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitForm">确 定</el-button>
+                <el-button @click="cancel">取 消</el-button>
+            </div>
+        </el-dialog>
+
+        <!-- 详情对话框 -->
+        <el-dialog title="获客链接详情" :visible.sync="detailOpen" width="700px" append-to-body>
+            <el-form ref="detailForm" :model="detailData" label-width="140px">
+                <el-form-item label="链接ID">{{ detailData.linkId }}</el-form-item>
+                <el-form-item label="链接名称">{{ detailData.linkName }}</el-form-item>
+                <el-form-item label="链接URL">
+                    <el-link :href="detailData.url" target="_blank" type="primary">{{ detailData.url }}</el-link>
+                </el-form-item>
+                <el-form-item label="链接Scheme">{{ detailData.scheme }}</el-form-item>
+                <el-form-item label="是否无需验证">
+                    <el-tag :type="detailData.skipVerify === true ? 'success' : 'info'">
+                        {{ detailData.skipVerify === true ? '无需验证' : '需要验证' }}
+                    </el-tag>
+                    <span style="margin-left: 8px; color: #909399; font-size: 12px;">
+            (skipVerify = {{ detailData.skipVerify }})
+          </span>
+                </el-form-item>
+
+                <el-form-item label="优先分配类型">
+                    <span v-if="detailData.priorityType === 0">不启用</span>
+                    <span v-else-if="detailData.priorityType === 1">全企业范围内优先分配给有好友关系的</span>
+                    <span v-else-if="detailData.priorityType === 2">指定范围内优先分配有好友关系的</span>
+                </el-form-item>
+                <el-form-item label="关联成员">
+                    <span>{{ formatDetailMembers(detailData.qwUserTableIdList) }}</span>
+                </el-form-item>
+                <el-form-item label="优先分配成员" v-if="detailData.priorityType === 2">
+                    {{ detailData.priorityUserListParam ? detailData.priorityUserListParam.join(', ') : '-' }}
+                </el-form-item>
+                <el-form-item label="使用范围描述">{{ detailData.rangeDesc || '-' }}</el-form-item>
+                <el-form-item label="状态">
+                    <el-tag :type="detailData.status === 1 ? 'success' : 'danger'">
+                        {{ detailData.status === 1 ? '正常' : '已失效' }}
+                    </el-tag>
+                </el-form-item>
+                <el-form-item label="企微创建时间">{{ parseTime(detailData.qwCreateTime) }}</el-form-item>
+                <el-form-item label="最后同步时间">{{ parseTime(detailData.syncTime) }}</el-form-item>
+                <el-form-item label="备注">{{ detailData.remark || '-' }}</el-form-item>
+            </el-form>
+        </el-dialog>
+
+        <!-- 二维码弹窗 -->
+        <el-dialog
+            title="获客链接二维码"
+            :visible.sync="qrCodeDialog.visible"
+            width="500px"
+            append-to-body
+            @closed="handleQrCodeDialogClosed"
+        >
+            <div class="qr-code-container" v-loading="qrCodeDialog.generating">
+                <div class="qr-code-wrapper" v-if="qrCodeDialog.dataUrl">
+                    <img :src="qrCodeDialog.dataUrl" alt="二维码" class="qr-code-image"/>
+                    <div class="qr-code-info">
+                        <p><strong>链接名称:</strong>{{ qrCodeDialog.linkName }}</p>
+                        <p><strong>链接ID:</strong>{{ qrCodeDialog.linkId }}</p>
+                        <p><strong>完整链接:</strong>
+                            <el-link :href="qrCodeDialog.fullUrl" target="_blank" type="primary" :underline="false">
+                                {{ qrCodeDialog.fullUrl }}
+                            </el-link>
+                        </p>
+                        <p><strong>pageParam:</strong>{{ qrCodeDialog.pageParam }}</p>
+                    </div>
+                </div>
+                <div v-else-if="!qrCodeDialog.generating" class="qr-code-empty">
+                    <el-empty description="暂无二维码"/>
+                </div>
+            </div>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="downloadQRCode" :disabled="!qrCodeDialog.dataUrl">
+                    <i class="el-icon-download"></i> 保存二维码
+                </el-button>
+                <el-button @click="qrCodeDialog.visible = false">关 闭</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import {
+    listAssistant,
+    getDetailByLinkId,
+    addAssistant,
+    updateAssistant,
+    deleteAssistant,
+    getQwUserList,
+    getQwUserListByIds,
+    qwUserCompanyList
+} from '@/api/qw/acquisitionAssistant'
+
+import QRCode from 'qrcodejs2'
+
+export default {
+    name: 'AcquisitionAssistant',
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            userLoading: false,
+            loadingMore: false,
+            // 添加部门选择器显示控制
+            showDepartment: false,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 总条数
+            total: 0,
+            // 表格数据
+            assistantList: [],
+            // 详情数据
+            detailData: {},
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            detailOpen: false,
+            // 二维码弹窗数据
+            qrCodeDialog: {
+                visible: false,
+                generating: false,
+                dataUrl: null,
+                fullUrl: '',
+                linkName: '',
+                linkId: '',
+                pageParam: '',
+                rowData: null
+            },
+            // 查询参数 - corpId作为全局筛选条件
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                linkName: '',
+                linkId: '',
+                status: '',
+                corpId: '' // 全局主体ID,所有需要corpId的接口都会用到
+            },
+            // 表单参数
+            form: {
+                // 基础信息
+                linkName: '',
+                skipVerify: true,
+                priorityType: 0,
+                remark: '',
+                corpId: '',
+                id: null,
+
+                // 关联成员相关(三套数据,各有用处)
+                userListParam: [],           // 企微用户ID数组 (用于前端显示和回显)
+                departmentListParam: [],     // 部门ID数组
+                qwUserTableIdList: [],       // 本地数据库ID数组 (需要传给后端保存)
+                userList: '',                // 完整的JSON字符串 (包含userList)
+
+                // 优先分配相关
+                priorityUserListParam: [],   // 优先分配的企微用户ID数组
+                priorityDepartmentListParam: [], // 优先分配的部门ID数组
+                priorityUserList: '',        // 优先分配的JSON字符串
+
+                // 选中的ID(用于el-select绑定)
+                selectedUserIds: []          // 与qwUserTableIdList同步,用于前端选择框显示
+            },
+            // 表单校验
+            rules: {
+                linkName: [
+                    {required: true, message: '链接名称不能为空', trigger: 'blur'}
+                ]
+            },
+            // 企业主体相关
+            corpOptions: [],
+            corpMap: new Map(),
+            // 企微用户相关
+            qwUserOptions: [],
+            searchKeyword: '',
+            userPage: 1,
+            userPageSize: 20,
+            userTotal: 0,
+            hasMoreUsers: true,
+            initialLoaded: false,
+            // 基础域名
+            baseDomain: 'https://c.ysyd.top'
+        }
+    },
+    computed: {
+        getUserSelectPlaceholder() {
+            if (!this.queryParams.corpId) {
+                return '请先在搜索栏选择主体'
+            }
+            return '请输入成员姓名进行搜索,或直接下拉选择'
+        }
+    },
+    created() {
+        this.getCorpList().then(() => {
+            // 默认选中第一个主体
+            if (this.corpOptions && this.corpOptions.length > 0) {
+                this.queryParams.corpId = this.corpOptions[0].corpId
+                console.log('默认选中主体:', this.queryParams.corpId)
+                this.getList() // 获取列表数据
+            } else {
+                this.msgWarning('请先配置企业主体')
+            }
+        })
+    },
+    methods: {
+        /** 获取完整链接 */
+        getFullUrl(row) {
+            if (!row || !row.pageParam) return `${this.baseDomain}`
+            return `${this.baseDomain}/${row.pageParam}`
+        },
+
+        /** 复制完整链接 */
+        handleCopyFullUrl(row) {
+            const fullUrl = this.getFullUrl(row)
+            this.copyToClipboard(fullUrl, '完整链接已复制')
+        },
+
+        /** 一键提取链接 */
+        handleCopyLink(row) {
+            const fullUrl = this.getFullUrl(row)
+            this.copyToClipboard(fullUrl, '链接已提取并复制到剪贴板')
+        },
+
+        /** 复制到剪贴板的通用方法 */
+        copyToClipboard(text, successMessage) {
+            // 创建临时输入框
+            const textarea = document.createElement('textarea')
+            textarea.value = text
+            textarea.style.position = 'fixed'
+            textarea.style.opacity = '0'
+            document.body.appendChild(textarea)
+            textarea.select()
+
+            try {
+                const successful = document.execCommand('copy')
+                if (successful) {
+                    this.$message({
+                        message: successMessage || '复制成功',
+                        type: 'success',
+                        duration: 2000
+                    })
+                } else {
+                    this.$message.error('复制失败,请手动复制')
+                }
+            } catch (err) {
+                console.error('复制失败:', err)
+                this.$message.error('复制失败,请手动复制')
+            } finally {
+                document.body.removeChild(textarea)
+            }
+        },
+
+        /** 生成二维码 - 使用项目自带的QRCode库 */
+        handleGenerateQRCode(row) {
+            if (!row || !row.pageParam) {
+                this.$message.warning('该链接没有pageParam参数,无法生成二维码')
+                return
+            }
+
+            const fullUrl = this.getFullUrl(row)
+
+            // 显示二维码弹窗,并显示加载中
+            this.qrCodeDialog.visible = true
+            this.qrCodeDialog.generating = true
+            this.qrCodeDialog.dataUrl = null
+            this.qrCodeDialog.fullUrl = fullUrl
+            this.qrCodeDialog.linkName = row.linkName || '-'
+            this.qrCodeDialog.linkId = row.linkId || '-'
+            this.qrCodeDialog.pageParam = row.pageParam || '-'
+            this.qrCodeDialog.rowData = row
+
+            try {
+                // 使用 setTimeout 让UI能够更新加载状态
+                setTimeout(() => {
+                    // 创建一个临时容器来生成二维码
+                    const tempDiv = document.createElement('div')
+                    tempDiv.style.position = 'absolute'
+                    tempDiv.style.left = '-9999px'
+                    tempDiv.style.top = '-9999px'
+                    document.body.appendChild(tempDiv)
+
+                    try {
+                        // 使用项目自带的 QRCode 库生成二维码
+                        const qrcode = new QRCode(tempDiv, {
+                            text: fullUrl,
+                            width: 300,
+                            height: 300,
+                            colorDark: '#000000',
+                            colorLight: '#ffffff',
+                            correctLevel: QRCode.CorrectLevel.H // 使用高容错率
+                        })
+
+                        // 等待二维码生成
+                        setTimeout(() => {
+                            try {
+                                // 从临时容器中获取生成的 canvas 或 img 元素
+                                const canvas = tempDiv.querySelector('canvas')
+                                if (canvas) {
+                                    // 转换为 data URL
+                                    this.qrCodeDialog.dataUrl = canvas.toDataURL('image/png')
+                                } else {
+                                    // 如果使用 table 方式生成,尝试其他方法
+                                    console.warn('未找到canvas元素,尝试其他方法')
+                                    this.$message.warning('二维码生成方式不支持保存为图片,请更新浏览器')
+                                }
+                            } catch (err) {
+                                console.error('获取二维码图片失败:', err)
+                                this.$message.error('生成二维码失败')
+                            } finally {
+                                // 清理临时容器
+                                document.body.removeChild(tempDiv)
+                                this.qrCodeDialog.generating = false
+                            }
+                        }, 100)
+                    } catch (err) {
+                        console.error('生成二维码失败:', err)
+                        this.$message.error('生成二维码失败')
+                        document.body.removeChild(tempDiv)
+                        this.qrCodeDialog.generating = false
+                    }
+                }, 50)
+            } catch (error) {
+                console.error('生成二维码失败:', error)
+                this.$message.error('生成二维码失败')
+                this.qrCodeDialog.generating = false
+            }
+        },
+
+        /** 下载二维码 */
+        downloadQRCode() {
+            if (!this.qrCodeDialog.dataUrl) return
+
+            // 创建下载链接
+            const link = document.createElement('a')
+            link.href = this.qrCodeDialog.dataUrl
+            link.download = `qrcode_${this.qrCodeDialog.linkId || 'link'}_${Date.now()}.png`
+            document.body.appendChild(link)
+            link.click()
+            document.body.removeChild(link)
+
+            this.$message.success('二维码已开始下载')
+        },
+
+        /** 二维码弹窗关闭后的处理 */
+        handleQrCodeDialogClosed() {
+            // 可以在这里做一些清理工作,如果需要的话
+            // 目前不需要特殊处理
+        },
+
+        /** 格式化时间 */
+        parseTime(time, pattern) {
+            if (arguments.length === 0 || !time) {
+                return '-'
+            }
+            const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+            let date
+            if (typeof time === 'object') {
+                date = time
+            } else {
+                if ((typeof time === 'string')) {
+                    if ((/^[0-9]+$/.test(time))) {
+                        // 如果是纯数字字符串,转为数字
+                        time = parseInt(time)
+                    } else {
+                        // 处理 "2026-03-19T14:05:41.000+0800" 格式
+                        time = time.replace(/-/g, '/') // 将 ISO 格式转换为兼容格式
+                    }
+                }
+                if ((typeof time === 'number') && (time.toString().length === 10)) {
+                    time = time * 1000
+                }
+                date = new Date(time)
+            }
+
+            // 如果日期无效
+            if (isNaN(date.getTime())) {
+                return '-'
+            }
+
+            const formatObj = {
+                y: date.getFullYear(),
+                m: date.getMonth() + 1,
+                d: date.getDate(),
+                h: date.getHours(),
+                i: date.getMinutes(),
+                s: date.getSeconds(),
+                a: date.getDay()
+            }
+            const timeStr = format.replace(/{([ymdhisa])+}/g, (result, key) => {
+                const value = formatObj[key]
+                // 补零
+                if (key === 'y') {
+                    return value.toString()
+                }
+                return ('00' + value).substr(-2)
+            })
+            return timeStr
+        },
+        /** 处理用户选择变化 - 同步更新所有相关字段 */
+        handleUserSelectionChange(selectedIds) {
+
+            // 1. 更新 qwUserTableIdList(需要传给后端的本地ID)
+            this.form.qwUserTableIdList = [...selectedIds]
+
+            // 2. 根据选中的本地ID,找到对应的企微用户ID,更新 userListParam
+            const qwUserIds = []
+            selectedIds.forEach(id => {
+                const user = this.qwUserOptions.find(u => u.id === id)
+                if (user) {
+                    qwUserIds.push(user.qwUserId)
+                }
+            })
+            this.form.userListParam = qwUserIds
+
+            // 3. 构建完整的 userList JSON(包含用户ID和部门ID)
+            const userListData = {
+                userList: qwUserIds,                    // 企微用户ID列表
+                departmentList: this.form.departmentListParam || [], // 部门ID列表
+                userIds: selectedIds,                    // 本地数据库ID列表
+                updateTime: new Date().getTime()
+            }
+            this.form.userList = JSON.stringify(userListData)
+
+            console.log('更新后的数据:', {
+                qwUserTableIdList: this.form.qwUserTableIdList,
+                userListParam: this.form.userListParam,
+                departmentListParam: this.form.departmentListParam,
+                userList: this.form.userList
+            })
+        },
+        /** 检查是否已选择主体 */
+        checkCorpId() {
+            if (!this.queryParams.corpId) {
+                this.msgError('请先在搜索栏选择主体')
+                return false
+            }
+            return true
+        },
+
+        /** 查询列表 */
+        getList() {
+            if (!this.checkCorpId()) {
+                this.loading = false
+                return
+            }
+
+            this.loading = true
+            const params = {...this.queryParams}
+            listAssistant(params).then(response => {
+                this.assistantList = response.rows
+                this.total = response.total
+                this.loading = false
+            }).catch(() => {
+                this.loading = false
+            })
+        },
+
+        /** 获取企业主体列表 */
+        getCorpList() {
+            return qwUserCompanyList({status: 1}).then(response => {
+                this.corpOptions = response.data || []
+                // 创建Map方便根据corpId获取corpName
+                this.corpMap.clear()
+                this.corpOptions.forEach(item => {
+                    this.corpMap.set(item.corpId, item.corpName)
+                })
+            })
+        },
+
+        /** 根据corpId获取企业名称 */
+        getCorpName(corpId) {
+            return this.corpMap.get(corpId) || corpId
+        },
+
+        /** 切换主体时的处理 */
+        handleCorpChange(corpId) {
+            console.log('切换主体:', corpId)
+
+            // 重置用户相关数据
+            this.qwUserOptions = []
+            this.initialLoaded = false
+            this.userPage = 1
+            this.hasMoreUsers = true
+            this.searchKeyword = ''
+
+            // 如果有关联成员选择框打开,清空已选成员
+            if (this.form.userListParam) {
+                this.form.userListParam = []
+            }
+
+            if (corpId) {
+                // 重新查询列表
+                this.queryParams.pageNum = 1
+                this.getList()
+                // 预加载用户数据
+                this.loadUsersByCorp()
+            } else {
+                // 如果没有选择主体,清空列表
+                this.assistantList = []
+                this.total = 0
+            }
+        },
+
+        /** 根据当前选择的主体加载用户 */
+        loadUsersByCorp() {
+            if (!this.checkCorpId()) return
+
+            const corpId = this.queryParams.corpId
+
+            // 重置分页和数据
+            this.userPage = 1
+            this.qwUserOptions = []
+            this.initialLoaded = false
+            this.searchKeyword = ''
+
+            // 调用搜索方法,传入空字符串加载所有用户
+            this.searchQwUsers('')
+        },
+
+        /** 搜索企微用户 - POST请求 */
+        searchQwUsers(query) {
+            if (!this.checkCorpId()) {
+                this.userLoading = false
+                return
+            }
+
+            if (query !== undefined) {
+                this.searchKeyword = query
+                this.userPage = 1
+            }
+
+            if (!this.searchKeyword && this.initialLoaded) {
+                return
+            }
+
+            this.userLoading = true
+
+            const params = {
+                qwUserName: this.searchKeyword || '',
+                pageNum: this.userPage,
+                pageSize: this.userPageSize,
+                corpId: this.queryParams.corpId
+            }
+
+            getQwUserList(params).then(response => {
+
+                let newUsers = []
+                if (response.data && Array.isArray(response.data)) {
+                    newUsers = response.data
+                } else if (response.rows && Array.isArray(response.rows)) {
+                    newUsers = response.rows
+                } else if (Array.isArray(response)) {
+                    newUsers = response
+                }
+
+                // 处理返回的用户数据,确保包含id字段
+                newUsers = newUsers.map(user => ({
+                    ...user,
+                    // 确保有唯一标识
+                    uniqueKey: `${user.qwUserId}_${user.corpId}`,
+                    // 确保有id字段(本地数据库ID)
+                    id: user.id || user.userId || null
+                }))
+
+                if (this.userPage === 1) {
+                    this.qwUserOptions = newUsers
+                } else {
+                    const existingKeys = new Set(
+                        this.qwUserOptions.map(u => `${u.qwUserId}_${u.corpId}`)
+                    )
+                    const uniqueNewUsers = newUsers.filter(u =>
+                        !existingKeys.has(`${u.qwUserId}_${u.corpId}`)
+                    )
+                    this.qwUserOptions = [...this.qwUserOptions, ...uniqueNewUsers]
+                }
+
+                if (response.total !== undefined) {
+                    this.userTotal = response.total
+                } else if (response.data && response.data.total) {
+                    this.userTotal = response.data.total
+                } else {
+                    this.userTotal = this.qwUserOptions.length
+                }
+
+                this.hasMoreUsers = this.qwUserOptions.length < this.userTotal
+
+                if (this.userPage === 1) {
+                    this.initialLoaded = true
+                }
+
+                this.userLoading = false
+                this.loadingMore = false
+            }).catch(error => {
+                console.error('搜索用户失败:', error)
+                this.userLoading = false
+                this.loadingMore = false
+            })
+        },
+
+        /** 处理选择器显隐变化 */
+        handleSelectVisible(visible) {
+            if (visible && this.queryParams.corpId) {
+                if (!this.initialLoaded || this.qwUserOptions.length === 0) {
+                    this.loadUsersByCorp()
+                }
+            }
+        },
+
+        /** 加载更多用户 */
+        loadMoreUsers() {
+            if (this.loadingMore || !this.hasMoreUsers) return
+
+            this.loadingMore = true
+            this.userPage++
+            this.searchQwUsers(this.searchKeyword)
+        },
+
+        /** 清空已选用户 */
+        clearSelectedUsers() {
+            this.form.selectedUserIds = []
+            this.form.qwUserTableIdList = []
+            this.form.userListParam = []
+            //部门列表可能不需要清空,根据业务需求决定
+            // this.form.departmentListParam = []
+            this.form.userList = JSON.stringify({
+                userList: [],
+                departmentList: this.form.departmentListParam || [],
+                userIds: [],
+                updateTime: new Date().getTime()
+            })
+        },
+
+        /** 加载已选中用户的详细信息 */
+        loadSelectedUsers(userIds) {
+            if (!userIds || userIds.length === 0) return
+
+            getQwUserListByIds(userIds).then(response => {
+                const selectedUsers = response.data || []
+
+                selectedUsers.forEach(user => {
+                    const exists = this.qwUserOptions.some(opt =>
+                        opt.qwUserId === user.qwUserId && opt.corpId === user.corpId
+                    )
+                    if (!exists) {
+                        this.qwUserOptions.push(user)
+                    }
+                })
+            })
+        },
+        /** 格式化详情页面的成员显示(不显示部门) */
+        formatDetailMembers(localIds) {
+            if (!localIds || !localIds.length) return '-'
+
+            const memberNames = []
+            localIds.forEach(id => {
+                // 通过本地数据库ID查找用户
+                const user = this.qwUserOptions.find(u => u.id === id)
+                if (user) {
+                    // 显示用户名和企微ID,不显示部门
+                    memberNames.push(`${user.qwUserName}[${user.qwUserId}]`)
+                } else {
+                    // 如果找不到,显示原始ID
+                    memberNames.push(`ID:${id}`)
+                }
+            })
+
+            return memberNames.join(', ')
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            if (!this.checkCorpId()) return
+            this.queryParams.pageNum = 1
+            this.getList()
+        },
+
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm')
+            // 重置时保留主体选择
+            const currentCorpId = this.queryParams.corpId
+            this.queryParams = {
+                pageNum: 1,
+                pageSize: 10,
+                linkName: '',
+                linkId: '',
+                status: '',
+                corpId: currentCorpId
+            }
+            this.handleQuery()
+        },
+
+        /** 多选框选中数据 */
+        handleSelectionChange(selection) {
+            this.ids = selection.map(item => item.id)
+            this.single = selection.length !== 1
+            this.multiple = !selection.length
+        },
+
+        /** 新增按钮操作 */
+        handleAdd() {
+            if (!this.checkCorpId()) return
+
+            this.reset()
+            this.open = true
+            this.title = '添加获客链接'
+
+            // 新增时添加关联成员必填校验
+            this.rules.selectedUserIds = [
+                {required: true, message: '请选择关联成员', trigger: 'change'}
+            ]
+
+            // 如果表单已渲染,需要重新设置校验规则
+            this.$nextTick(() => {
+                if (this.$refs.form) {
+                    this.$refs.form.clearValidate()
+                }
+            })
+
+            // 预加载用户数据
+            this.loadUsersByCorp()
+        },
+
+        /** 修改按钮操作 */
+        handleUpdate(row) {
+            if (!this.checkCorpId()) return
+
+            this.reset()
+
+            // 先查询详情
+            getDetailByLinkId(row.linkId).then(response => {
+                const data = response.data
+
+                // 基础信息赋值
+                this.form.id = data.id
+                this.form.linkId = data.linkId
+                this.form.linkName = data.linkName
+                //将字符串的"true"/"false"转换为布尔值
+                this.form.skipVerify = data.skipVerify === true || data.skipVerify === 'true'
+                this.form.priorityType = data.priorityType
+                this.form.remark = data.remark
+                this.form.corpId = data.corpId
+
+                // ========== 关联成员相关字段回显 ==========
+
+                // 1. 处理 qwUserTableIdList(本地数据库ID)
+                if (data.qwUserTableIdList) {
+                    if (typeof data.qwUserTableIdList === 'string') {
+                        try {
+                            // 处理类似 "[28462]" 的字符串
+                            const parsed = JSON.parse(data.qwUserTableIdList)
+                            this.form.qwUserTableIdList = Array.isArray(parsed) ? parsed : [parsed]
+                        } catch (e) {
+                            // 如果不是标准JSON格式,尝试按逗号分割
+                            this.form.qwUserTableIdList = data.qwUserTableIdList
+                                .replace(/[\[\]"]/g, '')  // 去除方括号和引号
+                                .split(',')
+                                .filter(id => id.trim())
+                                .map(id => parseInt(id.trim()))
+                        }
+                    } else if (Array.isArray(data.qwUserTableIdList)) {
+                        this.form.qwUserTableIdList = data.qwUserTableIdList
+                    }
+                }
+
+                // 2. 处理 userListParam(企微用户ID)- 从 userList 字段获取
+                if (data.userList) {
+                    if (typeof data.userList === 'string') {
+                        try {
+                            const parsed = JSON.parse(data.userList)
+                            // 检查 parsed 是否包含 userList 字段
+                            if (parsed && parsed.userList && Array.isArray(parsed.userList)) {
+                                this.form.userListParam = parsed.userList
+                            } else if (Array.isArray(parsed)) {
+                                this.form.userListParam = parsed
+                            } else {
+                                this.form.userListParam = []
+                            }
+                        } catch (e) {
+                            console.error('解析 userList 失败:', e)
+                            this.form.userListParam = []
+                        }
+                    } else if (Array.isArray(data.userList)) {
+                        this.form.userListParam = data.userList
+                    } else if (data.userList && data.userList.userList) {
+                        // 处理已经是对象的情况
+                        this.form.userListParam = data.userList.userList
+                    }
+                }
+
+                // 3. 处理 departmentListParam(部门ID)
+                if (data.departmentList) {
+                    if (typeof data.departmentList === 'string') {
+                        try {
+                            const parsed = JSON.parse(data.departmentList)
+                            this.form.departmentListParam = Array.isArray(parsed) ? parsed : []
+                        } catch (e) {
+                            this.form.departmentListParam = []
+                        }
+                    } else if (Array.isArray(data.departmentList)) {
+                        this.form.departmentListParam = data.departmentList
+                    }
+                }
+
+                // ========== 优先分配相关字段回显 ==========
+
+                // 4. 处理 priorityUserListParam(优先分配的企微用户ID)
+                if (data.priorityUserList) {
+                    if (typeof data.priorityUserList === 'string') {
+                        try {
+                            const parsed = JSON.parse(data.priorityUserList)
+                            this.form.priorityUserListParam = Array.isArray(parsed) ? parsed : []
+                        } catch (e) {
+                            this.form.priorityUserListParam = []
+                        }
+                    } else if (Array.isArray(data.priorityUserList)) {
+                        this.form.priorityUserListParam = data.priorityUserList
+                    }
+                }
+
+                // 5. 设置 selectedUserIds 用于前端显示(与 qwUserTableIdList 同步)
+                this.form.selectedUserIds = [...(this.form.qwUserTableIdList || [])]
+
+                console.log('回显完成:', {
+                    selectedUserIds: this.form.selectedUserIds,
+                    qwUserTableIdList: this.form.qwUserTableIdList,
+                    userListParam: this.form.userListParam,
+                    departmentListParam: this.form.departmentListParam,
+                    priorityUserListParam: this.form.priorityUserListParam
+                })
+
+                // 加载该主体的用户
+                this.loadUsersByCorp()
+
+                // 如果有已选用户,加载他们的详细信息用于显示
+                if (this.form.selectedUserIds && this.form.selectedUserIds.length > 0) {
+                    this.loadSelectedUsers(this.form.selectedUserIds)
+                }
+
+                this.open = true
+                this.title = '修改获客链接'
+            }).catch(error => {
+                console.error('获取详情失败:', error)
+                this.msgError('获取详情失败')
+            })
+        },
+
+        /** 详情按钮操作 */
+        handleDetail(row) {
+            getDetailByLinkId(row.linkId).then(response => {
+                const data = response.data
+                this.detailData = {...data}
+
+                // 解析qwUserTableIdList
+                if (data.qwUserTableIdList) {
+                    if (typeof data.qwUserTableIdList === 'string') {
+                        try {
+                            this.detailData.qwUserTableIdList = JSON.parse(data.qwUserTableIdList)
+                        } catch (e) {
+                            this.detailData.qwUserTableIdList = []
+                        }
+                    } else {
+                        this.detailData.qwUserTableIdList = data.qwUserTableIdList || []
+                    }
+                }
+
+                // 解析userList
+                if (data.userList) {
+                    try {
+                        this.detailData.userListObj = JSON.parse(data.userList)
+                    } catch (e) {
+                        this.detailData.userListObj = {}
+                    }
+                }
+
+                // 解析priorityUserList
+                if (data.priorityUserList) {
+                    try {
+                        this.detailData.priorityUserListParam = JSON.parse(data.priorityUserList)
+                    } catch (e) {
+                        this.detailData.priorityUserListParam = []
+                    }
+                }
+
+                this.detailOpen = true
+            })
+        },
+
+        /** 提交按钮 */
+        submitForm() {
+            if (!this.checkCorpId()) return
+
+            this.$refs['form'].validate(valid => {
+                if (valid) {
+                    // 验证关联成员是否超过500
+                    if (this.form.selectedUserIds && this.form.selectedUserIds.length > 500) {
+                        this.msgError('关联成员最多只能选择500人')
+                        return
+                    }
+
+                    // 构建提交的数据
+                    const submitData = {
+                        // 基础信息
+                        id: this.form.id,
+                        linkId: this.form.linkId,
+                        linkName: this.form.linkName,
+                        skipVerify: this.form.skipVerify, // 布尔值,提交时保持布尔类型
+                        priorityType: this.form.priorityType,
+                        remark: this.form.remark,
+                        corpId: this.queryParams.corpId,
+
+                        // ========== 关联成员相关 ==========
+                        // 本地数据库ID列表 - 转换为JSON字符串
+                        qwUserTableIdList: JSON.stringify(this.form.selectedUserIds || []),
+
+                        // 企微用户ID列表 - 转换为JSON字符串,与后端期望的格式一致
+                        userList: JSON.stringify(this.form.userListParam || []),
+
+                        // 部门ID列表
+                        departmentList: JSON.stringify(this.form.departmentListParam || []),
+
+                        // ========== 优先分配相关 ==========
+                        priorityUserList: JSON.stringify(this.form.priorityUserListParam || []),
+
+                        userListParam: this.form.userListParam || [],
+                        departmentListParam: this.form.departmentListParam || [],
+                        priorityUserListParam: this.form.priorityUserListParam || []
+                    }
+
+                    console.log('提交的数据:', submitData)
+
+                    if (this.form.id) {
+                        updateAssistant(submitData).then(response => {
+                            this.msgSuccess('修改成功')
+                            this.open = false
+                            this.getList()
+                        }).catch(error => {
+                            console.error('修改失败:', error)
+                        })
+                    } else {
+                        addAssistant(submitData).then(response => {
+                            this.msgSuccess('新增成功')
+                            this.open = false
+                            this.getList()
+                        }).catch(error => {
+                            console.error('新增失败:', error)
+                        })
+                    }
+                }
+            })
+        },
+
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            if (!this.checkCorpId()) return
+
+            const ids = row.id || this.ids.join(',')
+            this.$confirm('是否确认删除获客链接编号为"' + ids + '"的数据项?', '警告', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            }).then(() => {
+                return deleteAssistant(ids)
+            }).then(() => {
+                this.getList()
+                this.msgSuccess('删除成功')
+            })
+        },
+
+        /** 取消按钮 */
+        cancel() {
+            this.open = false
+            this.reset()
+        },
+
+        /** 表单重置 */
+        reset() {
+            this.form = {
+                // 基础信息
+                linkId: '',
+                linkName: '',
+                skipVerify: true,
+                priorityType: 0,
+                remark: '',
+                corpId: this.queryParams.corpId,
+                id: null,
+
+                // 关联成员相关
+                userListParam: [],           // 企微用户ID
+                departmentListParam: [],     // 部门ID
+                qwUserTableIdList: [],       // 本地数据库ID
+                userList: '{"userList":[],"departmentList":[],"userIds":[]}', // 完整JSON
+
+                // 优先分配相关
+                priorityUserListParam: [],   // 优先分配的企微用户ID
+                priorityDepartmentListParam: [], // 优先分配的部门ID
+                priorityUserList: '[]',      // 优先分配JSON
+
+                // 选中的ID(用于el-select绑定)
+                selectedUserIds: []
+            }
+
+            this.searchKeyword = ''
+            this.userPage = 1
+            this.hasMoreUsers = true
+            this.resetForm('form')
+        }
+    }
+}
+</script>
+
+<style scoped>
+.mb8 {
+    margin-bottom: 8px;
+}
+
+.el-form-item__tips {
+    font-size: 12px;
+    color: #909399;
+    margin-top: 5px;
+}
+
+/* 优化下拉选项的显示 */
+.el-select-dropdown__item {
+    height: auto;
+    padding: 8px 20px;
+}
+
+.el-select-dropdown__item span {
+    line-height: 1.5;
+}
+
+/* 二维码弹窗样式 */
+.qr-code-container {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    min-height: 350px;
+    padding: 20px 0;
+}
+
+.qr-code-wrapper {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    width: 100%;
+}
+
+.qr-code-image {
+    width: 250px;
+    height: 250px;
+    border: 1px solid #eee;
+    padding: 10px;
+    background: white;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+    margin-bottom: 20px;
+}
+
+.qr-code-info {
+    width: 100%;
+    padding: 15px;
+    background-color: #f9f9f9;
+    border-radius: 4px;
+    font-size: 14px;
+}
+
+.qr-code-info p {
+    margin: 8px 0;
+    word-break: break-all;
+}
+
+.qr-code-empty {
+    width: 100%;
+    display: flex;
+    justify-content: center;
+}
+</style>

+ 576 - 0
src/views/taskStatistics/callLog/index.vue

@@ -0,0 +1,576 @@
+<template>
+    <div class="app-container">
+        <el-row :gutter="24" class="baseInfo">
+            <!-- 总记录 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>外呼记录总数</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number total">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.recordCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
+
+            <!-- 成功数 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>外呼成功总数</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number success">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.successRecordCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
+
+            <!-- 今日发送 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>今日发送</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number today">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.todayCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
+
+            <!-- 今日成功 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>今日成功</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number today-success">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.todaySuccessCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
+
+        </el-row>
+
+        <el-form v-show="showSearch" ref="queryForm" :model="queryParams" :inline="true" label-width="96px">
+            <el-form-item prop="roboticId">
+                <el-select
+                    v-model="queryParams.roboticId"
+                    filterable
+                    clearable
+                    placeholder="请选择任务"
+                    @change="handleQuery"
+                    class="task-select"
+                >
+                    <i slot="prefix" class="el-icon-s-operation"/>
+
+                    <el-option label="所有任务" :value="null"/>
+                    <el-option
+                        v-for="item in roboticList"
+                        :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
+                    v-hasPermi="['company:addwxlog:list']"
+                    type="warning"
+                    icon="el-icon-download"
+                    size="mini"
+                    @click="handleExport"
+                >导出
+                </el-button>
+            </el-col>
+            <right-toolbar :show-search.sync="showSearch" @queryTable="handleQuery"/>
+        </el-row>
+
+        <el-table v-loading="loading" :data="companySendMsgList">
+            <!--      <el-table-column label="记录 ID" align="center" prop="logId" width="90" />-->
+            <el-table-column label="任务 ID" align="center" prop="roboticId" width="150"/>
+            <el-table-column label="任务名称" align="center" prop="name" width="250"/>
+            <el-table-column label="发送数量" align="center" prop="totalRecordCount" width="150"/>
+            <el-table-column label="发送成功数量" align="center" prop="successCount" width="150"/>
+            <el-table-column label="发送失败数量" align="center" prop="failCount" width="150"/>
+            <el-table-column label="发送中数量" align="center" prop="runningCount" width="150"/>
+            <!--      <el-table-column label="销售名称" align="center" prop="companyUserName" width="180" />-->
+            <!--      <el-table-column label="花费金额" align="center" prop="cost" />-->
+            <!--      <el-table-column label="手机号" align="center" prop="phone" />-->
+            <!--      <el-table-column label="短信模版名称" align="center" prop="smsTempName" />-->
+            <!--      <el-table-column label="内容长度" align="center" prop="contentLen" />-->
+            <!--      <el-table-column label="公司" align="center" prop="companyName" />-->
+            <!--      <el-table-column label="添加时间" align="center" prop="addTime" />-->
+
+            <!--      <el-table-column label="状态" align="center" prop="status" width="100">-->
+            <!--        <template slot-scope="scope">-->
+            <!--          <el-tag :type="getStatusType(scope.row.status)">-->
+            <!--            {{ getStatusLabel(scope.row.status) }}-->
+            <!--          </el-tag>-->
+            <!--        </template>-->
+            <!--      </el-table-column>-->
+            <!--      <el-table-column label="执行时间" align="center" prop="runTime" width="180">-->
+            <!--        <template slot-scope="scope">-->
+            <!--          <span>{{ parseTime(scope.row.runTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>-->
+            <!--        </template>-->
+            <!--      </el-table-column>-->
+
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed ="right">
+                <template slot-scope="scope">
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-view"
+                        @click="handleViewDetail(scope.row)"
+                    >
+                        查看详情
+                    </el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <el-dialog
+            :title="detailTitle"
+            :visible.sync="detailDialogVisible"
+            width="90%"
+            append-to-body
+            @close="handleDetailClose"
+        >
+            <!-- 筛选条件 -->
+            <el-form
+                :model="detailQueryParams"
+                ref="detailQueryForm"
+                :inline="true"
+                size="small"
+                label-width="70px"
+                class="detail-query-form"
+            >
+                <!--                <el-form-item label="调用时间">-->
+                <!--                    <el-date-picker-->
+                <!--                        v-model="detailTimeRange"-->
+                <!--                        type="daterange"-->
+                <!--                        range-separator="至"-->
+                <!--                        start-placeholder="开始日期"-->
+                <!--                        end-placeholder="结束日期"-->
+                <!--                        value-format="yyyy-MM-dd"-->
+                <!--                        clearable-->
+                <!--                    />-->
+                <!--                </el-form-item>-->
+
+                <el-form-item label="手机号">
+                    <el-input
+                        v-model="detailQueryParams.phone"
+                        placeholder="请输入手机号"
+                        clearable
+                        @keyup.enter.native="handleDetailQuery"
+                    />
+                </el-form-item>
+
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" @click="handleDetailQuery">
+                        查询
+                    </el-button>
+                    <el-button icon="el-icon-refresh" @click="resetDetailQuery">
+                        重置
+                    </el-button>
+                </el-form-item>
+            </el-form>
+
+            <el-table
+                v-loading="detailLoading"
+                :data="detailList"
+                border
+                style="width: 100%"
+            >
+                <el-table-column label="日志ID" align="center" prop="logId" width="90" />
+                <el-table-column label="任务名称" align="center" prop="roboticName" min-width="160" show-overflow-tooltip />
+                <el-table-column label="记录调用时间" align="center" prop="runTime" width="160" />
+                <el-table-column label="执行状态" align="center" prop="status" width="100">
+                    <template slot-scope="scope">
+                        <el-tag v-if="scope.row.status === 1" type="warning">执行中</el-tag>
+                        <el-tag v-else-if="scope.row.status === 2" type="success">执行成功</el-tag>
+                        <el-tag v-else-if="scope.row.status === 3" type="danger">执行失败</el-tag>
+                        <span v-else>--</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="客户号码" align="center" prop="callerNum" width="130" />
+                <el-table-column label="话术号码" align="center" prop="calleeNum" width="130" />
+                <el-table-column label="客户类型" align="center" prop="intention" width="120" />
+                <el-table-column label="通话时长(秒)" align="center" prop="callTime" width="120" />
+                <el-table-column label="花费金额" align="center" prop="cost" width="100" />
+                <el-table-column label="公司名称" align="center" prop="companyName" min-width="140" show-overflow-tooltip />
+                <el-table-column label="销售名称" align="center" prop="companyUserName" min-width="120" show-overflow-tooltip />
+                <el-table-column label="录音地址" align="center" min-width="120">
+                    <template slot-scope="scope">
+                        <el-link
+                            v-if="scope.row.recordPath"
+                            :href="scope.row.recordPath"
+                            type="primary"
+                            target="_blank"
+                        >
+                            查看录音
+                        </el-link>
+                        <span v-else>--</span>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <!-- 分页 -->
+            <pagination
+                v-show="detailTotal > 0"
+                :total="detailTotal"
+                :page.sync="detailQueryParams.pageNum"
+                :limit.sync="detailQueryParams.pageSize"
+                @pagination="getDetailList"
+            />
+
+            <div slot="footer" class="dialog-footer">
+                <el-button @click="detailDialogVisible = false">关 闭</el-button>
+            </div>
+        </el-dialog>
+
+        <pagination
+            v-show="total > 0"
+            :total="total"
+            :page.sync="queryParams.pageNum"
+            :limit.sync="queryParams.pageSize"
+            @pagination="getList"
+        />
+    </div>
+</template>
+
+<script>
+import {listCallphone, listCallPhoneByRoboticId, delSendmsg, getCallPhoneLogCount, exportCallphone} from '@/api/company/callphone'
+import {listAll as roboticListAll} from '@/api/company/companyVoiceRobotic'
+import {listAll as accountListAll} from '@/api/company/companyAccount'
+import CountTo from 'vue-count-to'
+
+export default {
+    name: 'WxClientStatistics',
+    components: {CountTo},
+    data() {
+        return {
+            loading: false,
+            showSearch: true,
+            total: 0,
+            companySendMsgList: [],
+            roboticList: [],
+            accountList: [],
+            count: {},
+            totalRecordCount: 0,
+            successRecordCount: 0,
+            detailDialogVisible: false,
+            detailLoading: false,
+            detailList: [],
+            detailTitle: "查看详情",
+            detailTimeRange: [],
+            detailTotal: 0,
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                roboticId: null,
+                wxAccountId: null,
+                customerIdId: null,
+                status: null,
+                time: []
+            },
+            detailQueryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                roboticId: null,
+                phone: null,
+                beginRunTime: null,
+                endRunTime: null
+            },
+            loadingCount: false
+        }
+    },
+    created() {
+        this.initOptions()
+        this.handleQuery()
+        this.getCount()
+    },
+    methods: {
+        initOptions() {
+            roboticListAll().then((res) => {
+                this.roboticList = res.data || []
+            })
+            accountListAll().then((res) => {
+                this.accountList = res.data || []
+            })
+        },
+        getQueryData() {
+            const params = JSON.parse(JSON.stringify(this.queryParams))
+            if (params.time && params.time.length === 2) {
+                params.beginTime = params.time[0]
+                params.endTime = params.time[1]
+            }
+            delete params.time
+            return params
+        },
+        getList() {
+            this.loading = true
+            listCallphone(this.getQueryData())
+                .then((response) => {
+                    this.companySendMsgList = response.rows || []
+                    this.total = response.total || 0
+                    this.successRecordCount = response.successRecordCount || 0
+                    this.totalRecordCount = response.totalRecordCount || 0
+                })
+                .finally(() => {
+                    this.loading = false
+                })
+        },
+        // statis() {
+        //   addWxStatistics(this.getQueryData()).then((res) => {
+        //     this.count = res || {}
+        //   })
+        // },
+        getStatusLabel(value) {
+            if (value === 1) return '执行中'
+            if (value === 2) return '成功'
+            if (value === 3) return '失败'
+            return '-'
+        },
+        getStatusType(value) {
+            if (value === 1) return 'warning'
+            if (value === 2) return 'success'
+            if (value === 3) return 'danger'
+            return 'info'
+        },
+        handleQuery() {
+            this.queryParams.pageNum = 1
+            // this.statis()
+            this.getList()
+        },
+        resetQuery() {
+            this.resetForm('queryForm')
+            this.handleQuery()
+        },
+        handleExport() {
+            const queryParams = this.getQueryData()
+            this.$confirm('是否确认导出查询出的短链课程看课记录数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(() => {
+                this.exportLoading = true;
+                return exportCallphone(queryParams)
+            }).then((response) => {
+                this.download(response.msg)
+                this.exportLoading = false;
+            }).catch(() => {
+            })
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const ids = row.logId || this.ids;
+            this.$confirm('是否确认删除编号为 "' + ids + '" 的加微信日志数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(function () {
+                return delSendmsg(ids);
+            }).then(() => {
+                this.getList();
+                this.msgSuccess("删除成功");
+            }).catch(function () {
+            });
+        },
+        /** 点击查看详情 */
+        handleViewDetail(row) {
+            this.detailTitle = `查看详情 - ${row.roboticName || ""}`;
+            this.detailDialogVisible = true;
+
+            this.detailQueryParams.pageNum = 1;
+            this.detailQueryParams.pageSize = 10;
+            this.detailQueryParams.callerId = row.callerId;
+            this.detailQueryParams.roboticId = row.roboticId;
+            this.detailQueryParams.phone = null;
+            this.detailQueryParams.beginRunTime = null;
+            this.detailQueryParams.endRunTime = null;
+            this.detailTimeRange = [];
+
+            this.getDetailList();
+        },
+
+        /** 查询详情分页列表 */
+        getDetailList() {
+            this.detailLoading = true;
+            listCallPhoneByRoboticId(this.detailQueryParams).then(response => {
+                this.detailList = response.rows || [];
+                this.detailTotal = response.total || 0;
+                this.detailLoading = false;
+            }).catch(() => {
+                this.detailLoading = false;
+            });
+        },
+        handleDetailQuery() {
+            this.detailQueryParams.pageNum = 1;
+
+            if (this.detailTimeRange && this.detailTimeRange.length === 2) {
+                this.detailQueryParams.beginRunTime = this.detailTimeRange[0];
+                this.detailQueryParams.endRunTime = this.detailTimeRange[1];
+            } else {
+                this.detailQueryParams.beginRunTime = null;
+                this.detailQueryParams.endRunTime = null;
+            }
+
+            this.getDetailList();
+        },
+        resetDetailQuery() {
+            this.detailTimeRange = [];
+            this.detailQueryParams.pageNum = 1;
+            this.detailQueryParams.pageSize = 10;
+            this.detailQueryParams.phone = null;
+            this.detailQueryParams.beginRunTime = null;
+            this.detailQueryParams.endRunTime = null;
+            this.getDetailList();
+        },
+        /** 获取统计数据 */
+        getCount() {
+            this.loadingCount = true;
+            getCallPhoneLogCount().then(res => {
+                this.count = res.data || {};
+                this.loadingCount = false;
+            }).catch(() => {
+                this.loadingCount = false;
+            });
+        },
+        handleDetailClose() {
+            this.detailList = [];
+            this.detailTotal = 0;
+            this.detailTimeRange = [];
+            this.detailQueryParams = {
+                pageNum: 1,
+                pageSize: 10,
+                callerId: null,
+                roboticId: null,
+                phone: null,
+                beginRunTime: null,
+                endRunTime: null
+            };
+        },
+    }
+}
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+.baseInfo {
+    margin-bottom: 30px;
+}
+
+.content {
+    .card-panel-num {
+        display: inline-block;
+        font-size: 30px;
+    }
+
+    &-time {
+        font-size: 14px;
+    }
+}
+
+.task-select .el-input__inner {
+    background-color: #ecf5ff;
+    border: 1px solid #409EFF;
+    border-radius: 20px;
+    height: 36px;
+    padding-left: 15px;
+    box-shadow: 0 2px 6px rgba(64, 158, 255, 0.1);
+    transition: all 0.2s;
+}
+
+.task-select .el-input__inner:hover {
+    border-color: #66b1ff;
+}
+
+.task-select .el-input.is-focus .el-input__inner {
+    border-color: #409EFF;
+    box-shadow: 0 0 8px rgba(64, 158, 255, 0.3);
+    background-color: #ffffff;
+}
+/* 卡片整体 */
+.stat-card {
+    border-radius: 10px;
+    transition: all 0.3s;
+}
+
+.stat-card:hover {
+    transform: translateY(-3px);
+}
+
+/* 标题 */
+.card-header {
+    font-size: 14px;
+    color: #666;
+}
+
+/* 内容区域 */
+.content {
+    height: 60px;
+    display: flex;
+    align-items: center;
+}
+
+/* 数字基础样式 */
+.card-number {
+    font-size: 28px;
+    font-weight: bold;
+}
+
+/* 🎨 颜色区分 */
+.card-number.total {
+    color: #409EFF; /* 蓝色 */
+}
+
+.card-number.success {
+    color: #67C23A; /* 绿色 */
+}
+
+.card-number.today {
+    color: #E6A23C; /* 橙色 */
+}
+
+.card-number.today-success {
+    color: #F56C6C; /* 红色 */
+}
+</style>

+ 568 - 0
src/views/taskStatistics/sendMsgLog/index.vue

@@ -0,0 +1,568 @@
+<template>
+    <div class="app-container">
+        <el-row :gutter="24" class="baseInfo">
+            <!-- 总记录 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>发送短信记录数</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number total">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.recordCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
+
+            <!-- 成功数 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>发送短信成功数</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number success">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.successRecordCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
+
+            <!-- 今日发送 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>今日发送</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number today">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.todayCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
+
+            <!-- 今日成功 -->
+            <el-col :xs="12" :sm="12" :lg="6" class="ivu-mb">
+                <el-card shadow="hover" class="stat-card">
+                    <div slot="header" class="card-header">
+                        <span>今日成功</span>
+                    </div>
+
+                    <div class="content" v-loading="loadingCount">
+                    <span class="card-number today-success">
+                      <count-to
+                          v-if="!loadingCount"
+                          :start-val="0"
+                          :end-val="count.todaySuccessCount || 0"
+                          :duration="2000"
+                      />
+                    </span>
+                    </div>
+                </el-card>
+            </el-col>
+
+        </el-row>
+
+        <el-form v-show="showSearch" ref="queryForm" :model="queryParams" :inline="true" label-width="96px">
+            <el-form-item prop="roboticId">
+                <el-select
+                    v-model="queryParams.roboticId"
+                    filterable
+                    clearable
+                    placeholder="请选择任务"
+                    @change="handleQuery"
+                    class="task-select"
+                >
+                    <i slot="prefix" class="el-icon-s-operation"/>
+
+                    <el-option label="所有任务" :value="null"/>
+                    <el-option
+                        v-for="item in roboticList"
+                        :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
+                    v-hasPermi="['company:addwxlog:list']"
+                    type="warning"
+                    icon="el-icon-download"
+                    size="mini"
+                    @click="handleExport"
+                >导出
+                </el-button>
+            </el-col>
+            <right-toolbar :show-search.sync="showSearch" @queryTable="handleQuery"/>
+        </el-row>
+
+        <el-table v-loading="loading" :data="companySendMsgList">
+            <!--      <el-table-column label="记录 ID" align="center" prop="logId" width="90" />-->
+            <el-table-column label="任务 ID" align="center" prop="roboticId" width="150"/>
+            <el-table-column label="任务名称" align="center" prop="name" width="250"/>
+            <el-table-column label="发送数量" align="center" prop="totalRecordCount" width="150"/>
+            <el-table-column label="发送成功数量" align="center" prop="successCount" width="150"/>
+            <el-table-column label="发送失败数量" align="center" prop="failCount" width="150"/>
+            <el-table-column label="发送中数量" align="center" prop="runningCount" width="150"/>
+            <!--      <el-table-column label="销售名称" align="center" prop="companyUserName" width="180" />-->
+            <!--      <el-table-column label="花费金额" align="center" prop="cost" />-->
+            <!--      <el-table-column label="手机号" align="center" prop="phone" />-->
+            <!--      <el-table-column label="短信模版名称" align="center" prop="smsTempName" />-->
+            <!--      <el-table-column label="内容长度" align="center" prop="contentLen" />-->
+            <!--      <el-table-column label="公司" align="center" prop="companyName" />-->
+            <!--      <el-table-column label="添加时间" align="center" prop="addTime" />-->
+
+            <!--      <el-table-column label="状态" align="center" prop="status" width="100">-->
+            <!--        <template slot-scope="scope">-->
+            <!--          <el-tag :type="getStatusType(scope.row.status)">-->
+            <!--            {{ getStatusLabel(scope.row.status) }}-->
+            <!--          </el-tag>-->
+            <!--        </template>-->
+            <!--      </el-table-column>-->
+            <!--      <el-table-column label="执行时间" align="center" prop="runTime" width="180">-->
+            <!--        <template slot-scope="scope">-->
+            <!--          <span>{{ parseTime(scope.row.runTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>-->
+            <!--        </template>-->
+            <!--      </el-table-column>-->
+
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed ="right">
+                <template slot-scope="scope">
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-view"
+                        @click="handleViewDetail(scope.row)"
+                    >
+                        查看详情
+                    </el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <el-dialog
+            :title="detailTitle"
+            :visible.sync="detailDialogVisible"
+            width="90%"
+            append-to-body
+            @close="handleDetailClose"
+        >
+            <!-- 筛选条件 -->
+            <el-form
+                :model="detailQueryParams"
+                ref="detailQueryForm"
+                :inline="true"
+                size="small"
+                label-width="70px"
+                class="detail-query-form"
+            >
+<!--                <el-form-item label="调用时间">-->
+<!--                    <el-date-picker-->
+<!--                        v-model="detailTimeRange"-->
+<!--                        type="daterange"-->
+<!--                        range-separator="至"-->
+<!--                        start-placeholder="开始日期"-->
+<!--                        end-placeholder="结束日期"-->
+<!--                        value-format="yyyy-MM-dd"-->
+<!--                        clearable-->
+<!--                    />-->
+<!--                </el-form-item>-->
+
+                <el-form-item label="手机号">
+                    <el-input
+                        v-model="detailQueryParams.phone"
+                        placeholder="请输入手机号"
+                        clearable
+                        @keyup.enter.native="handleDetailQuery"
+                    />
+                </el-form-item>
+
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" @click="handleDetailQuery">
+                        查询
+                    </el-button>
+                    <el-button icon="el-icon-refresh" @click="resetDetailQuery">
+                        重置
+                    </el-button>
+                </el-form-item>
+            </el-form>
+
+            <!-- 表格 -->
+            <el-table
+                v-loading="detailLoading"
+                :data="detailList"
+                border
+                style="width: 100%"
+            >
+                <el-table-column label="日志ID" align="center" prop="logId" />
+                <el-table-column label="任务id" align="center" prop="roboticId" />
+                <el-table-column label="任务名称" align="center" prop="roboticName" />
+<!--                <el-table-column label="caller_id" align="center" prop="callerId" />-->
+                <el-table-column label="记录调用时间" align="center" prop="runTime" width="160" />
+                <el-table-column label="执行状态" align="center" prop="status">
+                    <template slot-scope="scope">
+                        <el-tag v-if="scope.row.status === 1" type="warning">执行中</el-tag>
+                        <el-tag v-else-if="scope.row.status === 2" type="success">执行成功</el-tag>
+                        <el-tag v-else-if="scope.row.status === 3" type="danger">执行失败</el-tag>
+                        <span v-else>--</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="公司id" align="center" prop="companyId" />
+                <el-table-column label="公司名称" align="center" prop="companyName" />
+                <el-table-column label="销售id" align="center" prop="companyUserId" />
+                <el-table-column label="销售名称" align="center" prop="companyUserName" />
+                <el-table-column label="发送短信模板id" align="center" prop="tempId" />
+                <el-table-column label="花费金额" align="center" prop="cost" />
+                <el-table-column label="内容长度" align="center" prop="contentLen" />
+                <el-table-column label="短信模板名称" align="center" prop="smsTempName" />
+                <el-table-column label="发送手机号" align="center" prop="phone" />
+            </el-table>
+
+            <!-- 分页 -->
+            <pagination
+                v-show="detailTotal > 0"
+                :total="detailTotal"
+                :page.sync="detailQueryParams.pageNum"
+                :limit.sync="detailQueryParams.pageSize"
+                @pagination="getDetailList"
+            />
+
+            <div slot="footer" class="dialog-footer">
+                <el-button @click="detailDialogVisible = false">关 闭</el-button>
+            </div>
+        </el-dialog>
+
+        <pagination
+            v-show="total > 0"
+            :total="total"
+            :page.sync="queryParams.pageNum"
+            :limit.sync="queryParams.pageSize"
+            @pagination="getList"
+        />
+    </div>
+</template>
+
+<script>
+import {listSendmsg, listByCallerIdAndRoboticId, delSendmsg, getSendMsgLogCount, exportSendmsg} from '@/api/company/sendmsg'
+import {listAll as roboticListAll} from '@/api/company/companyVoiceRobotic'
+import {listAll as accountListAll} from '@/api/company/companyAccount'
+import CountTo from 'vue-count-to'
+
+export default {
+    name: 'WxClientStatistics',
+    components: {CountTo},
+    data() {
+        return {
+            loading: false,
+            showSearch: true,
+            total: 0,
+            companySendMsgList: [],
+            roboticList: [],
+            accountList: [],
+            count: {},
+            totalRecordCount: 0,
+            successRecordCount: 0,
+            detailDialogVisible: false,
+            detailLoading: false,
+            detailList: [],
+            detailTitle: "查看详情",
+            detailTimeRange: [],
+            detailTotal: 0,
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                roboticId: null,
+                wxAccountId: null,
+                customerIdId: null,
+                status: null,
+                time: []
+            },
+            detailQueryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                roboticId: null,
+                phone: null,
+                beginRunTime: null,
+                endRunTime: null
+            },
+            loadingCount: false
+        }
+    },
+    created() {
+        this.initOptions()
+        this.handleQuery()
+        this.getCount()
+    },
+    methods: {
+        initOptions() {
+            roboticListAll().then((res) => {
+                this.roboticList = res.data || []
+            })
+            accountListAll().then((res) => {
+                this.accountList = res.data || []
+            })
+        },
+        getQueryData() {
+            const params = JSON.parse(JSON.stringify(this.queryParams))
+            if (params.time && params.time.length === 2) {
+                params.beginTime = params.time[0]
+                params.endTime = params.time[1]
+            }
+            delete params.time
+            return params
+        },
+        getList() {
+            this.loading = true
+            listSendmsg(this.getQueryData())
+                .then((response) => {
+                    this.companySendMsgList = response.rows || []
+                    this.total = response.total || 0
+                    this.successRecordCount = response.successRecordCount || 0
+                    this.totalRecordCount = response.totalRecordCount || 0
+                })
+                .finally(() => {
+                    this.loading = false
+                })
+        },
+        // statis() {
+        //   addWxStatistics(this.getQueryData()).then((res) => {
+        //     this.count = res || {}
+        //   })
+        // },
+        getStatusLabel(value) {
+            if (value === 1) return '执行中'
+            if (value === 2) return '成功'
+            if (value === 3) return '失败'
+            return '-'
+        },
+        getStatusType(value) {
+            if (value === 1) return 'warning'
+            if (value === 2) return 'success'
+            if (value === 3) return 'danger'
+            return 'info'
+        },
+        handleQuery() {
+            this.queryParams.pageNum = 1
+            // this.statis()
+            this.getList()
+        },
+        resetQuery() {
+            this.resetForm('queryForm')
+            this.handleQuery()
+        },
+        handleExport() {
+            const queryParams = this.getQueryData()
+            this.$confirm('是否确认导出查询出的短链课程看课记录数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(() => {
+                this.exportLoading = true;
+                return exportSendmsg(queryParams)
+            }).then((response) => {
+                this.download(response.msg)
+                this.exportLoading = false;
+            }).catch(() => {
+            })
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const ids = row.logId || this.ids;
+            this.$confirm('是否确认删除编号为 "' + ids + '" 的加微信日志数据项?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+            }).then(function () {
+                return delSendmsg(ids);
+            }).then(() => {
+                this.getList();
+                this.msgSuccess("删除成功");
+            }).catch(function () {
+            });
+        },
+        /** 点击查看详情 */
+        handleViewDetail(row) {
+            this.detailTitle = `查看详情 - ${row.roboticName || ""}`;
+            this.detailDialogVisible = true;
+
+            this.detailQueryParams.pageNum = 1;
+            this.detailQueryParams.pageSize = 10;
+            this.detailQueryParams.callerId = row.callerId;
+            this.detailQueryParams.roboticId = row.roboticId;
+            this.detailQueryParams.phone = null;
+            this.detailQueryParams.beginRunTime = null;
+            this.detailQueryParams.endRunTime = null;
+            this.detailTimeRange = [];
+
+            this.getDetailList();
+        },
+
+        /** 查询详情分页列表 */
+        getDetailList() {
+            this.detailLoading = true;
+            listByCallerIdAndRoboticId(this.detailQueryParams).then(response => {
+                this.detailList = response.rows || [];
+                this.detailTotal = response.total || 0;
+                this.detailLoading = false;
+            }).catch(() => {
+                this.detailLoading = false;
+            });
+        },
+        handleDetailQuery() {
+            this.detailQueryParams.pageNum = 1;
+
+            if (this.detailTimeRange && this.detailTimeRange.length === 2) {
+                this.detailQueryParams.beginRunTime = this.detailTimeRange[0];
+                this.detailQueryParams.endRunTime = this.detailTimeRange[1];
+            } else {
+                this.detailQueryParams.beginRunTime = null;
+                this.detailQueryParams.endRunTime = null;
+            }
+
+            this.getDetailList();
+        },
+        resetDetailQuery() {
+            this.detailTimeRange = [];
+            this.detailQueryParams.pageNum = 1;
+            this.detailQueryParams.pageSize = 10;
+            this.detailQueryParams.phone = null;
+            this.detailQueryParams.beginRunTime = null;
+            this.detailQueryParams.endRunTime = null;
+            this.getDetailList();
+        },
+        /** 获取统计数据 */
+        getCount() {
+            this.loadingCount = true;
+            getSendMsgLogCount().then(res => {
+                this.count = res.data || {};
+                this.loadingCount = false;
+            }).catch(() => {
+                this.loadingCount = false;
+            });
+        },
+        handleDetailClose() {
+            this.detailList = [];
+            this.detailTotal = 0;
+            this.detailTimeRange = [];
+            this.detailQueryParams = {
+                pageNum: 1,
+                pageSize: 10,
+                callerId: null,
+                roboticId: null,
+                phone: null,
+                beginRunTime: null,
+                endRunTime: null
+            };
+        },
+    }
+}
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+.baseInfo {
+    margin-bottom: 30px;
+}
+
+.content {
+    .card-panel-num {
+        display: inline-block;
+        font-size: 30px;
+    }
+
+    &-time {
+        font-size: 14px;
+    }
+}
+
+.task-select .el-input__inner {
+    background-color: #ecf5ff;
+    border: 1px solid #409EFF;
+    border-radius: 20px;
+    height: 36px;
+    padding-left: 15px;
+    box-shadow: 0 2px 6px rgba(64, 158, 255, 0.1);
+    transition: all 0.2s;
+}
+
+.task-select .el-input__inner:hover {
+    border-color: #66b1ff;
+}
+
+.task-select .el-input.is-focus .el-input__inner {
+    border-color: #409EFF;
+    box-shadow: 0 0 8px rgba(64, 158, 255, 0.3);
+    background-color: #ffffff;
+}
+/* 卡片整体 */
+.stat-card {
+    border-radius: 10px;
+    transition: all 0.3s;
+}
+
+.stat-card:hover {
+    transform: translateY(-3px);
+}
+
+/* 标题 */
+.card-header {
+    font-size: 14px;
+    color: #666;
+}
+
+/* 内容区域 */
+.content {
+    height: 60px;
+    display: flex;
+    align-items: center;
+}
+
+/* 数字基础样式 */
+.card-number {
+    font-size: 28px;
+    font-weight: bold;
+}
+
+/* 🎨 颜色区分 */
+.card-number.total {
+    color: #409EFF; /* 蓝色 */
+}
+
+.card-number.success {
+    color: #67C23A; /* 绿色 */
+}
+
+.card-number.today {
+    color: #E6A23C; /* 橙色 */
+}
+
+.card-number.today-success {
+    color: #F56C6C; /* 红色 */
+}
+</style>

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

@@ -152,7 +152,7 @@
           </el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="Run Time" align="center" prop="runTime" width="180">
+      <el-table-column label="执行时间" align="center" prop="runTime" width="180">
         <template slot-scope="scope">
           <span>{{ parseTime(scope.row.runTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
         </template>
@@ -191,7 +191,7 @@
 
 <script>
 import { listAllAddWx, exportAddwx, delCompanyClient } from '@/api/company/addwx'
-import { addWxStatistics } from '@/api/company/companyClient'
+// import { addWxStatistics } from '@/api/company/companyClient'
 import { listAll as roboticListAll } from '@/api/company/companyVoiceRobotic'
 import { listAll as accountListAll } from '@/api/company/companyAccount'
 import CountTo from 'vue-count-to'
@@ -255,11 +255,11 @@ export default {
           this.loading = false
         })
     },
-    statis() {
-      addWxStatistics(this.getQueryData()).then((res) => {
-        this.count = res || {}
-      })
-    },
+    // statis() {
+    //   addWxStatistics(this.getQueryData()).then((res) => {
+    //     this.count = res || {}
+    //   })
+    // },
     getStatusLabel(value) {
       if (value === 1) return '执行中'
       if (value === 2) return '成功'
@@ -286,7 +286,7 @@ export default {
     },
     handleQuery() {
       this.queryParams.pageNum = 1
-      this.statis()
+      // this.statis()
       this.getList()
     },
     resetQuery() {