Bläddra i källkod

叮当国医-完善会话审计

cgp 1 dag sedan
förälder
incheckning
36e9f9348f

+ 18 - 0
src/api/qw/companySession.js

@@ -74,3 +74,21 @@ export function qwGroupChatOptions({ corpId, keyword }) {
   });
 }
 
+// 客户维度列表(会话审计-客户维度,按客户ID/昵称/备注搜索)
+export function qwCustomerList(params) {
+  return request({
+    url: '/weChatSpace/customerList',
+    method: 'get',
+    params
+  });
+}
+
+// 某客户关联的客服列表
+export function qwCustomerStaffList({ corpId, externalUserId }) {
+  return request({
+    url: '/weChatSpace/customerStaffList',
+    method: 'get',
+    params: { corpId, externalUserId }
+  });
+}
+

+ 5 - 1
src/views/qw/conversationNew/ConversationNew.vue

@@ -28,6 +28,9 @@
       <el-tab-pane label="按聊天内容查询" name="content">
         <ContentSearchTab :corpId="selectedCorpId" />
       </el-tab-pane>
+      <el-tab-pane label="按客户查询" name="customer">
+        <CustomerQueryTab :corpId="selectedCorpId" />
+      </el-tab-pane>
     </el-tabs>
     <div v-else class="empty-tip">请先选择企业微信主体</div>
   </div>
@@ -37,10 +40,11 @@
 import { allCorp } from "@/api/qw/qwCompany";
 import EmployeeQueryTab from "./EmployeeQueryTab.vue";
 import ContentSearchTab from "./ContentSearchTab.vue";
+import CustomerQueryTab from "./CustomerQueryTab.vue";
 
 export default {
   name: "ConversationNew",
-  components: { EmployeeQueryTab, ContentSearchTab },
+  components: { EmployeeQueryTab, ContentSearchTab, CustomerQueryTab },
   data() {
     return {
       activeTab: "employee",

+ 83 - 7
src/views/qw/conversationNew/ConversationPanelPure.vue

@@ -35,8 +35,19 @@
       <i class="el-icon-info"></i>
       <p>暂无会话数据</p>
     </div>
-    <!-- 聊天窗口 -->
-    <div v-else id="chat-container" ref="chatContainer"></div>
+    <!-- 聊天窗口(含消息类型筛选) -->
+    <div v-else class="chat-wrapper">
+      <div class="msg-type-filter">
+        <span
+          v-for="opt in msgTypeOptions"
+          :key="String(opt.value)"
+          class="filter-item"
+          :class="{ active: msgTypeFilter === opt.value }"
+          @click="handleFilterChange(opt.value)"
+        >{{ opt.label }}</span>
+      </div>
+      <div id="chat-container" ref="chatContainer"></div>
+    </div>
   </div>
 </template>
 
@@ -49,6 +60,17 @@ window._QW_CONFIG_CACHE = window._QW_CONFIG_CACHE || new Map();
 window._QW_CONFIG_PENDING = window._QW_CONFIG_PENDING || new Map();
 const CACHE_TTL = 5 * 60 * 1000;
 
+// 消息类型筛选选项(value 为 msgtype,null 表示全部)
+const MSG_TYPE_FILTER_OPTIONS = [
+  { value: null, label: '全部' },
+  { value: 1, label: '文本' },
+  { value: 6, label: '语音' },
+  { value: 2, label: '图片' },
+  { value: 7, label: '视频' },
+  { value: 4, label: '链接' },
+  { value: 22, label: '语音通话' }
+];
+
 function getCachedConfig(corpId) {
   if (!corpId) return null;
   const cached = window._QW_CONFIG_CACHE.get(corpId);
@@ -77,6 +99,8 @@ export default {
       isLoggedIn: false,
       isReady: false,
       msgList: [],
+      msgTypeFilter: null,
+      msgTypeOptions: MSG_TYPE_FILTER_OPTIONS,
       cursor: '',
       hasMore: true,
       loadingMore: false,
@@ -93,6 +117,12 @@ export default {
       _loginPanelTimer: null
     };
   },
+  computed: {
+    filteredMsgList() {
+      if (!this.msgTypeFilter) return this.msgList;
+      return this.msgList.filter(m => m.msgtype === this.msgTypeFilter);
+    }
+  },
   watch: {
     customerId(newId, oldId) {
       if (newId && newId !== oldId && this.isLoggedIn && (this.staffUserId || this.chatId) && this.configReady && !this.configError) {
@@ -432,7 +462,7 @@ export default {
           this.msgList.push(...processed);
           this.cursor = data.next_cursor || '';
           this.hasMore = data.has_more === 1;
-          if (this.chatInstance) this.chatInstance.setData({ msgList: this.msgList });
+          if (this.chatInstance) this.chatInstance.setData({ msgList: this.filteredMsgList });
           else await this.renderOrUpdateChat();
         } else {
           this.hasMore = false;
@@ -465,12 +495,25 @@ export default {
         }
       })
     },
+
+    // 消息类型筛选
+    handleFilterChange(type) {
+      this.msgTypeFilter = type;
+      if (this.chatInstance) {
+        this.chatInstance.setData({
+          msgList: this.filteredMsgList,
+          hasMore: this.hasMore,
+          loadingMore: this.loadingMore
+        });
+      }
+    },
+
     async renderOrUpdateChat() {
       if (this.msgList.length === 0 || this.configError) return;
       const container = document.getElementById('chat-container');
       if (!container) return;
       if (this.chatInstance) {
-        this.chatInstance.setData({ msgList: this.msgList });
+        this.chatInstance.setData({ msgList: this.filteredMsgList });
         return;
       }
       const ok = await this.initSDKOnce();
@@ -563,7 +606,7 @@ export default {
           }
         `,
         data: {
-          msgList: this.msgList,
+          msgList: this.filteredMsgList,
           hasMore: this.hasMore,
           loadingMore: this.loadingMore
         },
@@ -649,6 +692,37 @@ export default {
   background: #f5f6f7;
   position: relative;
 }
+.chat-wrapper {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+.msg-type-filter {
+  flex-shrink: 0;
+  display: flex;
+  gap: 4px;
+  padding: 8px 12px;
+  background: #fff;
+  border-bottom: 1px solid #e8e8e8;
+  flex-wrap: wrap;
+}
+.filter-item {
+  padding: 4px 12px;
+  font-size: 13px;
+  color: #606266;
+  cursor: pointer;
+  border-radius: 14px;
+  user-select: none;
+}
+.filter-item:hover {
+  color: #409eff;
+}
+.filter-item.active {
+  background: #409eff;
+  color: #fff;
+}
 .login-area, .empty-tip, .loading-tip {
   display: flex;
   flex-direction: column;
@@ -672,8 +746,8 @@ export default {
 }
 #chat-container {
   flex: 1;
+  min-height: 0;
   width: 100%;
-  height: 100%;
   background: #f0f2f5;
 }
 .login-container {
@@ -686,7 +760,9 @@ export default {
 </style>
 
 <style>
-#chat-container,
+#chat-container {
+  width: 100% !important;
+}
 #chat-container > div,
 #chat-container iframe {
   width: 100% !important;

+ 244 - 0
src/views/qw/conversationNew/CustomerQueryTab.vue

@@ -0,0 +1,244 @@
+<template>
+  <div class="customer-query-tab">
+    <!-- 查询表单 -->
+    <div class="query-form">
+      <el-form :model="queryParams" inline>
+        <el-form-item label="微信昵称">
+          <el-input v-model="queryParams.name" placeholder="请输入微信昵称" clearable />
+        </el-form-item>
+        <el-form-item label="微信备注">
+          <el-input v-model="queryParams.remark" placeholder="请输入微信备注" clearable />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery">查询</el-button>
+          <el-button @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 客户列表(后端分页) -->
+    <el-table
+      v-loading="tableLoading"
+      :data="customerList"
+      border
+      stripe
+      style="width: 100%"
+    >
+      <el-table-column prop="externalUserId" label="客户ID" min-width="200" type="hidden" />
+      <el-table-column prop="name" label="微信昵称" min-width="120" />
+      <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
+      <el-table-column prop="staffCount" label="关联客服数" width="100" />
+      <el-table-column label="操作" width="100">
+        <template slot-scope="scope">
+          <el-button type="text" @click="openDrawer(scope.row)">查看会话</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页组件 -->
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getCustomerList"
+    ></pagination>
+
+    <!-- 抽屉:客服列表 + 会话详情 -->
+    <el-drawer
+      :visible.sync="drawerVisible"
+      :title="(currentCustomer && currentCustomer.name ? currentCustomer.name : '') + ' 的会话'"
+      direction="rtl"
+      size="80%"
+      destroy-on-close
+    >
+      <div class="drawer-container">
+        <div class="staff-list">
+          <el-table
+            :data="staffList"
+            v-loading="staffLoading"
+            height="100%"
+            highlight-current-row
+            @row-click="handleStaffClick"
+          >
+            <el-table-column prop="qwUserName" label="客服" />
+            <el-table-column prop="remark" label="备注" show-overflow-tooltip />
+          </el-table>
+        </div>
+        <div class="conversation-panel">
+          <ConversationPanelPure
+            :corpId="corpId"
+            :customerId="currentCustomer && currentCustomer.externalUserId"
+            :staffUserId="selectedStaffUserId"
+            @logout="handleLogout"
+          />
+        </div>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { qwCustomerList, qwCustomerStaffList } from "@/api/qw/companySession";
+import ConversationPanelPure from "./ConversationPanelPure.vue";
+import Pagination from "@/components/Pagination";
+
+export default {
+  name: "CustomerQueryTab",
+  components: { ConversationPanelPure, Pagination },
+  props: {
+    corpId: { type: String, required: true }
+  },
+  watch: {
+    corpId() {
+      this.queryParams.pageNum = 1;
+      this.getCustomerList();
+    }
+  },
+  data() {
+    return {
+      queryParams: {
+        externalUserId: "",
+        name: "",
+        remark: "",
+        pageNum: 1,
+        pageSize: 10
+      },
+      customerList: [],
+      total: 0,
+      tableLoading: false,
+
+      // 抽屉相关
+      drawerVisible: false,
+      currentCustomer: null,
+      staffList: [],
+      staffLoading: false,
+      selectedStaffUserId: ''
+    };
+  },
+  mounted() {
+    this.getCustomerList();
+  },
+  methods: {
+    // 获取客户列表(后端分页)
+    async getCustomerList() {
+      this.tableLoading = true;
+      try {
+        const params = {
+          corpId: this.corpId,
+          externalUserId: this.queryParams.externalUserId || undefined,
+          name: this.queryParams.name || undefined,
+          remark: this.queryParams.remark || undefined,
+          pageNum: this.queryParams.pageNum,
+          pageSize: this.queryParams.pageSize
+        };
+        const res = await qwCustomerList(params);
+        if (res.rows) {
+          this.customerList = res.rows;
+          this.total = res.total || 0;
+        } else {
+          this.customerList = [];
+          this.total = 0;
+        }
+      } catch (e) {
+        console.error("获取客户列表失败", e);
+        this.customerList = [];
+        this.total = 0;
+      } finally {
+        this.tableLoading = false;
+      }
+    },
+
+    // 查询按钮
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getCustomerList();
+    },
+
+    // 重置查询条件
+    resetQuery() {
+      this.queryParams = {
+        externalUserId: "",
+        name: "",
+        remark: "",
+        pageNum: 1,
+        pageSize: 10
+      };
+      this.getCustomerList();
+    },
+
+    // 打开抽屉,加载该客户的客服列表
+    openDrawer(row) {
+      this.currentCustomer = row;
+      this.drawerVisible = true;
+      this.selectedStaffUserId = '';
+      this.staffList = [];
+      this.loadStaffList();
+    },
+
+    // 加载该客户关联的客服列表
+    async loadStaffList() {
+      if (!this.currentCustomer) return;
+      this.staffLoading = true;
+      try {
+        const res = await qwCustomerStaffList({
+          corpId: this.corpId,
+          externalUserId: this.currentCustomer.externalUserId
+        });
+        this.staffList = res.data || [];
+      } catch (e) {
+        console.error("获取客服列表失败", e);
+        this.staffList = [];
+      } finally {
+        this.staffLoading = false;
+      }
+    },
+
+    // 选中客服
+    handleStaffClick(row) {
+      this.selectedStaffUserId = row.userId;
+    },
+
+    // 会话登录失效处理
+    handleLogout() {
+      this.$message.warning("会话登录已失效,请刷新页面重试");
+    }
+  }
+};
+</script>
+
+<style scoped>
+.customer-query-tab {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.query-form {
+  background: #fff;
+  padding: 16px;
+  margin-bottom: 16px;
+  border-radius: 4px;
+}
+
+.drawer-container {
+  display: flex;
+  height: 100%;
+  gap: 16px;
+}
+
+.staff-list {
+  width: 320px;
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.conversation-panel {
+  flex: 1;
+  background: #f5f6f7;
+  border-radius: 8px;
+  overflow: hidden;
+}
+</style>

+ 62 - 15
src/views/qw/conversationNew/EmployeeQueryTab.vue

@@ -40,6 +40,13 @@
       <el-table-column prop="qwUserName" label="企微昵称" />
       <el-table-column prop="nickName" label="员工" />
       <el-table-column prop="departmentName" label="部门" />
+      <el-table-column prop="customerCount" label="客户数" width="80" />
+      <el-table-column prop="archiveCount" label="存档数" width="80" />
+      <el-table-column label="活跃时间" width="160">
+        <template slot-scope="scope">
+          {{ formatActiveTime(scope.row.lastActiveTime) }}
+        </template>
+      </el-table-column>
       <el-table-column label="操作" width="100">
         <template slot-scope="scope">
           <el-button type="text" @click="openDrawer(scope.row)">会话详情</el-button>
@@ -66,20 +73,34 @@
     >
       <div class="drawer-container">
         <div class="customer-list">
-          <el-table
-            :data="filteredCustomerList"
-            v-loading="customerLoading"
-            height="100%"
-            highlight-current-row
-            @row-click="handleCustomerClick"
-          >
-            <el-table-column label="头像" width="60">
-              <template slot-scope="scope">
-                <img :src="scope.row.avatar" style="width:40px;height:40px;border-radius:50%;" />
-              </template>
-            </el-table-column>
-            <el-table-column prop="name" label="客户名称" />
-          </el-table>
+          <div class="customer-search">
+            <el-input
+              v-model="customerRemark"
+              placeholder="按客户备注搜索"
+              clearable
+              size="small"
+              prefix-icon="el-icon-search"
+              @keyup.enter.native="handleRemarkSearch"
+              @clear="handleRemarkSearch"
+            />
+          </div>
+          <div class="customer-table-wrap">
+            <el-table
+              :data="filteredCustomerList"
+              v-loading="customerLoading"
+              height="100%"
+              highlight-current-row
+              @row-click="handleCustomerClick"
+            >
+              <el-table-column label="头像" width="60">
+                <template slot-scope="scope">
+                  <img :src="scope.row.avatar" style="width:40px;height:40px;border-radius:50%;" />
+                </template>
+              </el-table-column>
+              <el-table-column prop="name" label="客户名称" />
+              <el-table-column prop="remark" label="备注" show-overflow-tooltip />
+            </el-table>
+          </div>
           <pagination
             v-show="customerTotal > 0"
             :total="customerTotal"
@@ -154,6 +175,7 @@ export default {
       customerPageSize: 10,
       customerLoading: false,
       customerKeyword: "",
+      customerRemark: "",
       selectedCustomerId: null
     };
   },
@@ -251,6 +273,7 @@ export default {
       this.selectedCustomerId = null;
       this.currentStaffQwUserId = '';
       this.customerPageNum = 1;
+      this.customerRemark = '';
       this.loadCustomerList();
     },
 
@@ -263,7 +286,8 @@ export default {
           pageNum: this.customerPageNum,
           pageSize: this.customerPageSize,
           qwUserId: this.currentStaff.id,
-          corpId: this.corp?.corpId
+          corpId: this.corp?.corpId,
+          remark: this.customerRemark || undefined
         };
         const res = await listExternalContact(params);
         if (res.rows) {
@@ -286,6 +310,20 @@ export default {
       }
     },
 
+    // 备注搜索
+    handleRemarkSearch() {
+      this.customerPageNum = 1;
+      this.loadCustomerList();
+    },
+
+    // 活跃时间格式化(Unix秒 → yyyy-MM-dd HH:mm:ss)
+    formatActiveTime(timestamp) {
+      if (!timestamp) return '';
+      const d = new Date(timestamp * 1000);
+      const pad = n => String(n).padStart(2, '0');
+      return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+    },
+
     // 选中客户
     handleCustomerClick(row) {
       this.selectedCustomerId = row.externalUserId;
@@ -329,6 +367,15 @@ export default {
   margin-left: 20px;
 }
 
+.customer-search {
+  padding: 10px 0;
+}
+
+.customer-table-wrap {
+  flex: 1;
+  overflow: hidden;
+}
+
 .conversation-panel {
   flex: 1;
   background: #f5f6f7;