浏览代码

修改会话审计页面布局

cgp 1 天之前
父节点
当前提交
de9fc914ad

+ 8 - 0
src/api/company/company.js

@@ -141,3 +141,11 @@ export function saveSidebarCompanyImage(data) {
     data: data
   })
 }
+
+export function queryCompanyListByCompanyIds(data) {
+  return request({
+    url: '/company/company/queryCompanyListByCompanyIds',
+    method: 'post',
+    data: data
+  })
+}

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

@@ -38,3 +38,12 @@ export function getQwSessionConfig(corpid) {
   });
 }
 
+// 关键词搜索消息
+export function qwSearchMsg(params) {
+  return request({
+    url: '/weChatSpace/searchMsg',
+    method: 'post',
+    data: params
+  });
+}
+

+ 9 - 0
src/api/qw/qwUser.js

@@ -25,3 +25,12 @@ export function getQwList(params) {
     data: params
   })
 }
+
+// 根据条件动态查询企微用户列表
+export function selectQwUserListByCondition(data) {
+  return request({
+    url: '/qw/user/selectQwUserListByCondition',
+    method: 'post',
+    data: data
+  })
+}

+ 6 - 0
src/router/index.js

@@ -289,6 +289,12 @@ export const constantRoutes = [
         meta: { title: '企微会话' }
       }
     ]
+  },
+  {
+    path: '/qw/conversationNew',
+    component: () => import('@/views/qw/conversationNew/ConversationNew'),
+    name: 'ConversationNew',
+    meta: { title: '会话分析', icon: 'el-icon-chat-line-round' }
   }
 ]
 

+ 173 - 44
src/views/qw/companySession/ConversationPanel.vue

@@ -31,19 +31,49 @@
       <p>正在加载会话记录...</p>
     </div>
     <!-- 会话为空 -->
-    <div v-else-if="msgList.length === 0" class="empty-tip">
+    <div v-else-if="msgList.length === 0 && !searchMode" class="empty-tip">
       <i class="el-icon-info"></i>
       <p>暂无会话数据</p>
     </div>
-    <!-- 聊天窗口 -->
-    <div v-else id="chat-container" ref="chatContainer"></div>
+    <!-- 搜索结果为空 -->
+    <div v-else-if="searchMode && searchResultList.length === 0" class="empty-tip">
+      <i class="el-icon-info"></i>
+      <p>未找到相关消息</p>
+    </div>
+    <!-- 聊天窗口(正常会话或搜索结果) -->
+    <template v-else>
+      <div class="search-bar">
+        <el-input
+          v-model="searchKeyword"
+          placeholder="搜索聊天内容(文本、文件名称等)"
+          size="small"
+          clearable
+          @keyup.enter.native="doSearch"
+        >
+          <el-button
+            slot="append"
+            icon="el-icon-search"
+            :loading="searchLoading"
+            @click="doSearch"
+          >搜索</el-button>
+        </el-input>
+        <el-button
+          v-if="searchMode"
+          type="text"
+          size="small"
+          @click="clearSearch"
+          style="margin-left: 8px;"
+        >清除搜索</el-button>
+      </div>
+      <div id="chat-container" ref="chatContainer"></div>
+    </template>
   </div>
 </template>
 
 <script>
 import * as ww from '@wecom/jssdk';
 import defaultStaffAvatar from '@/assets/images/user.png';
-import { qwLogin, qwSignature, qwConversations, getQwSessionConfig } from '@/api/qw/companySession';
+import { qwLogin, qwSignature, qwConversations, getQwSessionConfig, qwSearchMsg } from '@/api/qw/companySession';
 
 // ========== 全局配置缓存 ==========
 window._QW_CONFIG_CACHE = window._QW_CONFIG_CACHE || new Map();
@@ -94,23 +124,31 @@ export default {
       _hasEmittedLogout: false,
       _loginPanelCreated: false,
       _loginPanelRetryCount: 0,
-      _loginPanelTimer: null
+      _loginPanelTimer: null,
+      // 搜索相关
+      searchKeyword: '',
+      searchLoading: false,
+      searchMode: false,
+      searchResultList: [],
+      searchCursor: '',
+      searchHasMore: false
     };
   },
   watch: {
     customerId(newId, oldId) {
       if (newId && newId !== oldId && this.isLoggedIn && this.staffUserId && this.configReady && !this.configError) {
+        this.clearSearch();
         this.resetAndReload();
       }
     },
     staffUserId(newVal, oldVal) {
       if (newVal && newVal !== oldVal && this.customerId && this.isLoggedIn && this.configReady && !this.configError) {
+        this.clearSearch();
         this.resetAndReload();
       }
     },
     corpId(newId, oldId) {
       if (newId !== oldId) {
-        // 重置所有状态
         this.destroyChat();
         this.isReady = false;
         this._sdkInited = false;
@@ -121,6 +159,9 @@ export default {
           clearTimeout(this._loginPanelTimer);
           this._loginPanelTimer = null;
         }
+        this.searchMode = false;
+        this.searchKeyword = '';
+        this.searchResultList = [];
         this.msgList = [];
         this.cursor = '';
         this.hasMore = true;
@@ -151,7 +192,6 @@ export default {
         this.chatInstance.setData({ customerAvatar: newAvatar });
       }
     },
-    // 监听登录状态变化,尝试创建登录面板
     isLoggedIn: {
       handler(newVal) {
         if (!newVal && this.configReady && !this.configError && this._sdkInited && !this._loginPanelCreated) {
@@ -179,7 +219,6 @@ export default {
       }
     }
 
-    // 定时刷新签名
     this.tokenCheckTimer = setInterval(() => {
       if (this.isLoggedIn && this.configReady && !this.configError && this._sdkInited) {
         this.getAgentConfigSignature().catch(e => console.warn('签名刷新失败', e));
@@ -239,7 +278,6 @@ export default {
       const promise = (async () => {
         try {
           const res = await getQwSessionConfig(this.corpId);
-
           let configData = null;
           if (res && res.code === 200 && res.data && res.data.corpid) {
             configData = res.data;
@@ -322,31 +360,21 @@ export default {
         } else {
           this.isLoggedIn = false;
           this.safeEmitLogout();
-          // 尝试创建登录面板
           this.tryCreateLoginPanel();
         }
       }
     },
 
-    // 尝试创建登录面板(带重试)
     tryCreateLoginPanel() {
       if (this._loginPanelCreated) return;
       if (!this.configReady || this.configError || !this._sdkInited) {
-        console.log('[ConversationPanel] 条件不满足,等待重试', {
-          configReady: this.configReady,
-          configError: this.configError,
-          sdkInited: this._sdkInited
-        });
         return;
       }
       if (this.isLoggedIn) return;
-
-      // 清除之前的定时器
       if (this._loginPanelTimer) {
         clearTimeout(this._loginPanelTimer);
         this._loginPanelTimer = null;
       }
-
       this.$nextTick(() => {
         this.createLoginPanel();
       });
@@ -355,22 +383,18 @@ export default {
     // ========== 登录面板 ==========
     createLoginPanel() {
       if (this._loginPanelCreated) {
-        console.log('[ConversationPanel] 登录面板已创建,跳过');
         return;
       }
       if (!this.configReady || this.configError || !this._sdkInited) {
-        console.log('[ConversationPanel] 登录面板条件不满足');
         return;
       }
       if (this.isLoggedIn) return;
 
-      // 获取容器
       let container = this.$refs.loginContainer;
       if (!container) {
         container = document.getElementById('login-container');
       }
       if (!container) {
-        console.error('[ConversationPanel] 登录容器未找到,1秒后重试');
         this._loginPanelTimer = setTimeout(() => {
           this._loginPanelTimer = null;
           this.createLoginPanel();
@@ -378,10 +402,8 @@ export default {
         return;
       }
 
-      // 检查容器尺寸
       const rect = container.getBoundingClientRect();
       if (rect.width === 0 || rect.height === 0) {
-        console.warn('[ConversationPanel] 容器尺寸为0,等待渲染后重试');
         this._loginPanelTimer = setTimeout(() => {
           this._loginPanelTimer = null;
           this.createLoginPanel();
@@ -389,14 +411,9 @@ export default {
         return;
       }
 
-      // 清空容器内容,避免残留
       container.innerHTML = '';
-
       this._loginPanelCreated = true;
       const redirectUri = window.location.origin + window.location.pathname;
-      console.log('[ConversationPanel] 创建登录面板, redirect_uri:', redirectUri);
-      console.log('[ConversationPanel] corpid:', this.config.corpid);
-      console.log('[ConversationPanel] agentid:', this.config.agentid);
 
       try {
         ww.createWWLoginPanel({
@@ -410,14 +427,12 @@ export default {
             state: 'state_' + Date.now()
           },
           onLoginSuccess: async ({ code }) => {
-            console.log('[ConversationPanel] 登录成功, code:', code);
             try {
               await this.handleLogin(code);
               if (this.customerId && this.staffUserId) {
                 await this.loadFirstPage();
               }
             } catch (e) {
-              console.error('[ConversationPanel] 登录回调处理失败', e);
               this.clearLoginState(this.corpId);
               this.isLoggedIn = false;
               this._loginPanelCreated = false;
@@ -425,18 +440,14 @@ export default {
             }
           },
           onLoginError: (err) => {
-            console.error('[ConversationPanel] 登录面板错误:', err);
             this._loginPanelCreated = false;
-            // 错误后尝试重试
             this._loginPanelTimer = setTimeout(() => {
               this._loginPanelTimer = null;
               this.createLoginPanel();
             }, 3000);
           }
         });
-        console.log('[ConversationPanel] createWWLoginPanel 调用成功');
       } catch (e) {
-        console.error('[ConversationPanel] createWWLoginPanel 异常:', e);
         this._loginPanelCreated = false;
         this._loginPanelTimer = setTimeout(() => {
           this._loginPanelTimer = null;
@@ -472,7 +483,6 @@ export default {
       const data = this._extractResponse(res);
 
       if (data.errcode && data.errcode !== 0) {
-        console.error('[ConversationPanel] 签名失败,清除登录态', data);
         this.clearLoginState(this.corpId);
         this.isLoggedIn = false;
         this.isReady = false;
@@ -505,7 +515,6 @@ export default {
         });
         await ww.initOpenData();
         this._sdkInited = true;
-        console.log('[ConversationPanel] SDK 初始化成功');
         return true;
       } catch (e) {
         console.error('[ConversationPanel] SDK 初始化失败:', e);
@@ -580,14 +589,19 @@ export default {
       }
     },
 
-    // ========== 渲染聊天组件 ==========
+    // ========== 渲染聊天组件(原有,仅用于正常会话) ==========
     async renderOrUpdateChat() {
       if (this.msgList.length === 0 || this.configError) return;
+      await this._renderChatWithData(this.msgList, this.hasMore);
+    },
+
+    // ========== 公共渲染方法 ==========
+    async _renderChatWithData(msgList, hasMore = false) {
       const container = document.getElementById('chat-container');
       if (!container) return;
 
       if (this.chatInstance) {
-        this.chatInstance.setData({ msgList: this.msgList });
+        this.chatInstance.setData({ msgList, hasMore, loadingMore: this.loadingMore });
         return;
       }
 
@@ -625,16 +639,18 @@ export default {
           </scroll-view>
         `,
         data: {
-          msgList: this.msgList,
+          msgList: msgList,
           customerAvatar: this.customerAvatar,
           defaultStaffAvatar: this.defaultStaffAvatar,
-          hasMore: this.hasMore,
+          hasMore: hasMore,
           loadingMore: this.loadingMore
         },
         methods: {
           onScrollToLower: () => {
-            if (this.hasMore && !this.loadingMore && !this.configError) {
-              this.loadMore();
+            if (this.searchMode) {
+              if (this.searchHasMore && !this.searchLoading) this.loadMoreSearch();
+            } else {
+              if (this.hasMore && !this.loadingMore && !this.configError) this.loadMore();
             }
           }
         },
@@ -687,6 +703,7 @@ export default {
     },
 
     async resetAndReload() {
+      this.clearSearch();
       this.destroyChat();
       this.msgList = [];
       this.cursor = '';
@@ -704,9 +721,110 @@ export default {
       if (container) container.innerHTML = '';
     },
 
+    // ========== 关键词搜索 ==========
+    async doSearch() {
+      if (!this.searchKeyword || this.searchKeyword.length < 2) {
+        this.$message.warning('关键词至少2个字符');
+        return;
+      }
+      if (!this.staffUserId || !this.customerId) {
+        this.$message.warning('请先选择员工和客户');
+        return;
+      }
+      this.searchLoading = true;
+      try {
+        const res = await qwSearchMsg({
+          corpId: this.corpId,
+          queryWord: this.searchKeyword,
+          chatType: 1,
+          staffUserId: this.staffUserId,
+          customerId: this.customerId,
+          limit: 50
+        });
+        // 统一解包响应
+        const result = this._extractResponse(res);
+        if (result.code === 200 && result.data) {
+          const searchData = result.data;
+          this.searchMode = true;
+          this.searchResultList = searchData.data || [];
+          this.searchCursor = searchData.nextCursor || '';
+          this.searchHasMore = searchData.hasMore === 1;
+          await this.renderSearchResult();
+          if (this.searchResultList.length === 0) {
+            this.$message.info('未找到相关消息');
+          }
+        } else {
+          this.$message.error(result.msg || '搜索失败');
+        }
+      } catch (e) {
+        console.error('搜索异常', e);
+        this.$message.error('搜索异常');
+      } finally {
+        this.searchLoading = false;
+      }
+    },
+
+    async loadMoreSearch() {
+      if (!this.searchCursor || this.searchLoading || !this.searchHasMore) return;
+      this.searchLoading = true;
+      try {
+        const res = await qwSearchMsg({
+          corpId: this.corpId,
+          queryWord: this.searchKeyword,
+          chatType: 1,
+          staffUserId: this.staffUserId,
+          customerId: this.customerId,
+          limit: 50,
+          cursor: this.searchCursor
+        });
+        const result = this._extractResponse(res);
+        if (result.code === 200 && result.data) {
+          const searchData = result.data;
+          const newMessages = searchData.data || [];
+          if (newMessages.length > 0) {
+            this.searchResultList = [...this.searchResultList, ...newMessages];
+            this.searchCursor = searchData.nextCursor || '';
+            this.searchHasMore = searchData.hasMore === 1;
+            if (this.chatInstance) {
+              this.chatInstance.setData({ msgList: this.searchResultList, hasMore: this.searchHasMore });
+            }
+          } else {
+            this.searchHasMore = false;
+          }
+        }
+      } catch (e) {
+        console.error('加载更多搜索结果失败', e);
+      } finally {
+        this.searchLoading = false;
+      }
+    },
+
+    clearSearch() {
+      if (!this.searchMode) return;
+      this.searchMode = false;
+      this.searchKeyword = '';
+      this.searchResultList = [];
+      this.searchCursor = '';
+      this.searchHasMore = false;
+      // 恢复原有聊天数据
+      if (this.msgList.length > 0) {
+        this.renderOrUpdateChat();
+      } else if (this.isReady && this.customerId && this.staffUserId) {
+        this.loadFirstPage();
+      }
+    },
+
+    async renderSearchResult() {
+      if (this.searchResultList.length === 0) return;
+      await this._renderChatWithData(this.searchResultList, this.searchHasMore);
+    },
+
     // 通用响应解包
     _extractResponse(res) {
       if (!res) return {};
+      // 如果已经是标准 AjaxResult 格式 { code, msg, data }
+      if (res.code !== undefined) return res;
+      // 兼容直接返回数据的情况
       if (res.errcode !== undefined || res.timestamp || res.corpid || res.data) return res;
       if (res.data) {
         if (res.data.errcode !== undefined || res.data.timestamp || res.data.corpid || res.data.data) return res.data;
@@ -784,4 +902,15 @@ export default {
   min-height: 100% !important;
   overflow: hidden;
 }
+
+.search-bar {
+  padding: 12px 16px;
+  background: #fff;
+  border-bottom: 1px solid #e8e8e8;
+  display: flex;
+  align-items: center;
+}
+.search-bar .el-input {
+  flex: 1;
+}
 </style>

+ 167 - 0
src/views/qw/conversationNew/ContentSearchTab.vue

@@ -0,0 +1,167 @@
+<template>
+  <div class="content-search-tab">
+    <div class="search-form">
+      <el-form :model="searchForm" ref="searchForm" label-width="100px">
+        <el-form-item label="聊天内容" required>
+          <el-input v-model="searchForm.queryWord" placeholder="请输入关键词(至少2个字符)" clearable />
+        </el-form-item>
+        <el-form-item label="聊天类型">
+          <el-select v-model="searchForm.chatType" placeholder="请选择" clearable>
+            <el-option label="单聊" :value="1" />
+            <el-option label="群聊" :value="2" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="发送时间">
+          <el-date-picker
+            v-model="dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            :picker-options="pickerOptions"
+            value-format="timestamp"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="doSearch" :loading="searching">查询</el-button>
+          <el-button @click="resetForm">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div class="search-result">
+      <!-- 暂时显示空白,后续可展示搜索结果列表 -->
+      <div class="result-placeholder">
+        <i class="el-icon-search"></i>
+        <p>查询结果将在这里展示(后续版本完善)</p>
+      </div>
+      <!--
+        后续可加 ConversationPanel 展示会话详情,需传递员工ID和客户ID
+        但搜索接口返回的是消息列表,需要聚合按会话分组,暂不实现
+      -->
+    </div>
+  </div>
+</template>
+
+<script>
+import { qwSearchMsg } from "@/api/qw/companySession";
+import ConversationPanelPure from './ConversationPanelPure.vue';
+import Pagination from "@/components/Pagination";
+
+export default {
+  name: "ContentSearchTab",
+  components: { ConversationPanelPure, Pagination },
+  props: {
+    corpId: { type: String, required: true },
+  },
+  data() {
+    return {
+      searchForm: {
+        queryWord: "",
+        chatType: null,
+        startTime: null,
+        endTime: null,
+      },
+      dateRange: [],
+      searching: false,
+      pickerOptions: {
+        disabledDate(time) {
+          // 只能选择当前日期到30天前
+          const today = new Date();
+          today.setHours(0, 0, 0, 0);
+          const thirtyDaysAgo = new Date();
+          thirtyDaysAgo.setDate(today.getDate() - 30);
+          thirtyDaysAgo.setHours(0, 0, 0, 0);
+          return time.getTime() > today.getTime() || time.getTime() < thirtyDaysAgo.getTime();
+        },
+      },
+    };
+  },
+  watch: {
+    dateRange(val) {
+      if (val && val.length === 2) {
+        this.searchForm.startTime = val[0];
+        this.searchForm.endTime = val[1];
+      } else {
+        this.searchForm.startTime = null;
+        this.searchForm.endTime = null;
+      }
+    },
+  },
+  methods: {
+    async doSearch() {
+      if (!this.searchForm.queryWord || this.searchForm.queryWord.length < 2) {
+        this.$message.warning("聊天内容关键词至少2个字符");
+        return;
+      }
+      if (!this.corpId) {
+        this.$message.warning("请先选择企微主体");
+        return;
+      }
+      this.searching = true;
+      try {
+        const params = {
+          corpId: this.corpId,
+          queryWord: this.searchForm.queryWord,
+          chatType: this.searchForm.chatType || undefined,
+          startTime: this.searchForm.startTime ? Math.floor(this.searchForm.startTime / 1000) : undefined,
+          endTime: this.searchForm.endTime ? Math.floor(this.searchForm.endTime / 1000) : undefined,
+          limit: 50,
+        };
+        // 仅调用接口验证,暂不处理返回结果
+        const res = await qwSearchMsg(params);
+        if (res.code === 200) {
+          this.$message.success(`搜索完成,共 ${res.data?.data?.length || 0} 条消息`);
+          // 后续可在此处理结果展示,目前仅为演示
+        } else {
+          this.$message.error(res.msg || "搜索失败");
+        }
+      } catch (e) {
+        console.error("搜索异常", e);
+        this.$message.error("搜索异常");
+      } finally {
+        this.searching = false;
+      }
+    },
+    resetForm() {
+      this.searchForm = {
+        queryWord: "",
+        chatType: null,
+        startTime: null,
+        endTime: null,
+      };
+      this.dateRange = [];
+    },
+  },
+};
+</script>
+
+<style scoped>
+.content-search-tab {
+  height: 100%;
+}
+.search-form {
+  background: #fff;
+  padding: 16px;
+  border-radius: 4px;
+  margin-bottom: 16px;
+}
+.search-result {
+  background: #fff;
+  border-radius: 4px;
+  padding: 24px;
+  min-height: 400px;
+}
+.result-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #c0c4cc;
+  padding: 60px 0;
+}
+.result-placeholder i {
+  font-size: 64px;
+  margin-bottom: 16px;
+}
+</style>

+ 109 - 0
src/views/qw/conversationNew/ConversationNew.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="conversation-new">
+    <div class="header-bar">
+      <span class="label">企微主体:</span>
+      <el-select
+        v-model="selectedCorpId"
+        placeholder="请选择企微主体"
+        clearable
+        filterable
+        @change="handleCorpChange"
+        style="width: 300px"
+      >
+        <el-option
+          v-for="corp in corpList"
+          :key="corp.corpId"
+          :label="corp.corpName"
+          :value="corp.corpId"
+        />
+      </el-select>
+      <div class="tips" v-if="!selectedCorpId">请先选择企微主体,才能使用会话功能</div>
+    </div>
+
+    <el-tabs v-model="activeTab" type="border-card" v-if="selectedCorpId">
+      <el-tab-pane label="按员工查询" name="employee">
+        <!-- 传递完整的 corp 对象 -->
+        <EmployeeQueryTab :corp="currentCorp" />
+      </el-tab-pane>
+      <el-tab-pane label="按聊天内容查询" name="content">
+        <ContentSearchTab :corpId="selectedCorpId" />
+      </el-tab-pane>
+    </el-tabs>
+    <div v-else class="empty-tip">请先选择企业微信主体</div>
+  </div>
+</template>
+
+<script>
+import { allCorp } from "@/api/qw/qwCompany";
+import EmployeeQueryTab from "./EmployeeQueryTab.vue";
+import ContentSearchTab from "./ContentSearchTab.vue";
+
+export default {
+  name: "ConversationNew",
+  components: { EmployeeQueryTab, ContentSearchTab },
+  data() {
+    return {
+      activeTab: "employee",
+      selectedCorpId: "",
+      corpList: [],
+    };
+  },
+  computed: {
+    currentCorp() {
+      return this.corpList.find(corp => corp.corpId === this.selectedCorpId) || null;
+    }
+  },
+  created() {
+    this.fetchCorpList();
+  },
+  methods: {
+    async fetchCorpList() {
+      try {
+        const res = await allCorp();
+        this.corpList = res.data || [];
+        if (this.corpList.length > 0) {
+          this.selectedCorpId = this.corpList[0].corpId;
+        }
+      } catch (e) {
+        console.error("获取企业主体列表失败", e);
+      }
+    },
+    handleCorpChange() {
+      // 子组件会通过 watch 监听 corp 变化
+    },
+  },
+};
+</script>
+
+<style scoped>
+.conversation-new {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 16px;
+  background: #f0f2f5;
+  box-sizing: border-box;
+}
+.header-bar {
+  margin-bottom: 16px;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+.label {
+  font-size: 14px;
+  color: #606266;
+}
+.tips {
+  color: #909399;
+  font-size: 14px;
+}
+.empty-tip {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #999;
+  font-size: 16px;
+}
+</style>

+ 613 - 0
src/views/qw/conversationNew/ConversationPanelPure.vue

@@ -0,0 +1,613 @@
+<template>
+  <div class="conversation-panel">
+    <!-- 配置加载中 -->
+    <div v-if="configLoading" class="loading-tip">
+      <i class="el-icon-loading"></i>
+      <p>正在加载配置...</p>
+    </div>
+    <!-- 配置加载失败 / 无配置 -->
+    <div v-else-if="configError" class="empty-tip">
+      <i class="el-icon-warning"></i>
+      <p>{{ configErrorMsg || '企微未配置,请先配置企微应用' }}</p>
+    </div>
+    <!-- 未登录 -->
+    <div v-else-if="!isLoggedIn" class="login-area">
+      <div class="login-tip">请扫码登录企微</div>
+      <div id="login-container" ref="loginContainer" class="login-container"></div>
+    </div>
+    <!-- 已登录但未选择员工 -->
+    <div v-else-if="!staffUserId" class="empty-tip">
+      <i class="el-icon-info"></i>
+      <p>请选择员工</p>
+    </div>
+    <!-- 已登录但未选择客户 -->
+    <div v-else-if="!customerId" class="empty-tip">
+      <i class="el-icon-info"></i>
+      <p>请选择客户查看会话</p>
+    </div>
+    <!-- 会话加载中 -->
+    <div v-else-if="!isReady" class="loading-tip">
+      <i class="el-icon-loading"></i>
+      <p>正在加载会话记录...</p>
+    </div>
+    <!-- 会话为空 -->
+    <div v-else-if="msgList.length === 0" class="empty-tip">
+      <i class="el-icon-info"></i>
+      <p>暂无会话数据</p>
+    </div>
+    <!-- 聊天窗口 -->
+    <div v-else id="chat-container" ref="chatContainer"></div>
+  </div>
+</template>
+
+<script>
+import * as ww from '@wecom/jssdk';
+import defaultStaffAvatar from '@/assets/images/user.png';
+import { qwLogin, qwSignature, qwConversations, getQwSessionConfig } from '@/api/qw/companySession';
+
+// ========== 全局配置缓存 ==========
+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;
+
+function getCachedConfig(corpId) {
+  if (!corpId) return null;
+  const cached = window._QW_CONFIG_CACHE.get(corpId);
+  if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
+    return cached.config;
+  }
+  window._QW_CONFIG_CACHE.delete(corpId);
+  return null;
+}
+
+function setCachedConfig(corpId, config) {
+  if (!corpId) return;
+  window._QW_CONFIG_CACHE.set(corpId, { config, timestamp: Date.now() });
+}
+
+export default {
+  name: 'ConversationPanelPure',
+  props: {
+    corpId: { type: String, default: null },
+    customerId: { type: String, default: null },
+    customerAvatar: { type: String, default: '' },
+    staffUserId: { type: String, default: null }
+  },
+  data() {
+    return {
+      isLoggedIn: false,
+      isReady: false,
+      msgList: [],
+      cursor: '',
+      hasMore: true,
+      loadingMore: false,
+      chatInstance: null,
+      _sdkInited: false,
+      defaultStaffAvatar: defaultStaffAvatar,
+      config: { corpid: '', agentid: '', domain: '' },
+      configReady: false,
+      configError: false,
+      configLoading: false,
+      configErrorMsg: '',
+      tokenCheckTimer: null,
+      _hasEmittedLogout: false,
+      _loginPanelCreated: false,
+      _loginPanelTimer: null
+    };
+  },
+  watch: {
+    customerId(newId, oldId) {
+      if (newId && newId !== oldId && this.isLoggedIn && this.staffUserId && this.configReady && !this.configError) {
+        this.resetAndReload();
+      }
+    },
+    staffUserId(newVal, oldVal) {
+      if (newVal && newVal !== oldVal && this.customerId && this.isLoggedIn && this.configReady && !this.configError) {
+        this.resetAndReload();
+      }
+    },
+    corpId(newId, oldId) {
+      if (newId !== oldId) {
+        this.destroyChat();
+        this.isReady = false;
+        this._sdkInited = false;
+        this._hasEmittedLogout = false;
+        this._loginPanelCreated = false;
+        if (this._loginPanelTimer) clearTimeout(this._loginPanelTimer);
+        this.msgList = [];
+        this.cursor = '';
+        this.hasMore = true;
+        this.loadingMore = false;
+        this.isLoggedIn = false;
+
+        const cached = getCachedConfig(newId);
+        if (cached) {
+          this.config = cached;
+          this.configReady = true;
+          this.configError = false;
+          this.configLoading = false;
+          this.handleAuthAndLoad();
+        } else {
+          this.configReady = false;
+          this.configError = false;
+          this.configLoading = false;
+          this.initConfig().then(() => {
+            if (this.configReady && !this.configError) this.handleAuthAndLoad();
+          });
+        }
+      }
+    },
+    customerAvatar(newAvatar) {
+      if (this.chatInstance && newAvatar) {
+        this.chatInstance.setData({ customerAvatar: newAvatar });
+      }
+    },
+    isLoggedIn: {
+      handler(newVal) {
+        if (!newVal && this.configReady && !this.configError && this._sdkInited && !this._loginPanelCreated) {
+          this.tryCreateLoginPanel();
+        }
+      },
+      immediate: true
+    }
+  },
+  async mounted() {
+    this._hasEmittedLogout = false;
+    this._loginPanelCreated = false;
+
+    const cached = getCachedConfig(this.corpId);
+    if (cached) {
+      this.config = cached;
+      this.configReady = true;
+      this.configError = false;
+      this.configLoading = false;
+      await this.handleAuthAndLoad();
+    } else {
+      await this.initConfig();
+      if (this.configReady && !this.configError) await this.handleAuthAndLoad();
+    }
+
+    this.tokenCheckTimer = setInterval(() => {
+      if (this.isLoggedIn && this.configReady && !this.configError && this._sdkInited) {
+        this.getAgentConfigSignature().catch(e => console.warn('签名刷新失败', e));
+      }
+    }, 90 * 60 * 1000);
+  },
+  beforeDestroy() {
+    if (this.tokenCheckTimer) clearInterval(this.tokenCheckTimer);
+    if (this._loginPanelTimer) clearTimeout(this._loginPanelTimer);
+    this.destroyChat();
+  },
+  methods: {
+    // ========== Storage Key ==========
+    getStorageKey(corpId, suffix = 'expire') {
+      return `wecom_session_${corpId}_${suffix}`;
+    },
+    checkLoginState(corpId) {
+      const expire = localStorage.getItem(this.getStorageKey(corpId, 'expire'));
+      return expire && Date.now() < parseInt(expire, 10);
+    },
+    storeLoginState(corpId) {
+      localStorage.setItem(this.getStorageKey(corpId, 'expire'), String(Date.now() + 115 * 60 * 1000));
+    },
+    clearLoginState(corpId) {
+      localStorage.removeItem(this.getStorageKey(corpId, 'expire'));
+    },
+
+    // ========== 配置加载 ==========
+    async initConfig() {
+      if (!this.corpId || this.configReady || this.configError) return;
+
+      const cached = getCachedConfig(this.corpId);
+      if (cached) {
+        this.config = cached;
+        this.configReady = true;
+        this.configError = false;
+        this.configLoading = false;
+        return;
+      }
+
+      if (window._QW_CONFIG_PENDING.has(this.corpId)) {
+        return window._QW_CONFIG_PENDING.get(this.corpId);
+      }
+
+      this.configLoading = true;
+      const promise = (async () => {
+        try {
+          const res = await getQwSessionConfig(this.corpId);
+          let configData = null;
+          if (res?.code === 200 && res.data?.corpid) configData = res.data;
+          else if (res?.data?.corpid) configData = res.data;
+          else if (res?.corpid) configData = res;
+
+          if (!configData?.corpid || !configData?.agentid) throw new Error('配置数据不完整');
+
+          const cfg = {
+            corpid: configData.corpid,
+            agentid: String(configData.agentid),
+            domain: configData.domain || ''
+          };
+          this.config = cfg;
+          this.configReady = true;
+          this.configError = false;
+          setCachedConfig(this.corpId, cfg);
+        } catch (e) {
+          console.error('[ConversationPanel] 获取配置失败:', e);
+          this.configReady = false;
+          this.configError = true;
+          this.configErrorMsg = e.message || '获取配置失败';
+        } finally {
+          this.configLoading = false;
+          window._QW_CONFIG_PENDING.delete(this.corpId);
+        }
+      })();
+      window._QW_CONFIG_PENDING.set(this.corpId, promise);
+      return promise;
+    },
+
+    // ========== 认证与加载 ==========
+    async handleAuthAndLoad() {
+      if (this.configError) return;
+      if (!this.configReady) {
+        await this.initConfig();
+        if (this.configError) return;
+      }
+      const sdkOk = await this.initSDKOnce();
+      if (!sdkOk) return;
+
+      const urlParams = new URLSearchParams(window.location.search);
+      const code = urlParams.get('code');
+      if (code) {
+        window.history.replaceState({}, '', window.location.origin + window.location.pathname);
+        try {
+          await this.handleLogin(code);
+          this.storeLoginState(this.corpId);
+          this.isLoggedIn = true;
+          if (this.customerId && this.staffUserId) await this.loadFirstPage();
+        } catch (err) {
+          this.clearLoginState(this.corpId);
+          this.isLoggedIn = false;
+          this.safeEmitLogout();
+        }
+      } else {
+        if (this.checkLoginState(this.corpId)) {
+          this.isLoggedIn = true;
+          if (this.customerId && this.staffUserId) await this.loadFirstPage();
+        } else {
+          this.isLoggedIn = false;
+          this.safeEmitLogout();
+          this.tryCreateLoginPanel();
+        }
+      }
+    },
+
+    tryCreateLoginPanel() {
+      if (this._loginPanelCreated || !this.configReady || this.configError || !this._sdkInited || this.isLoggedIn) return;
+      if (this._loginPanelTimer) clearTimeout(this._loginPanelTimer);
+      this.$nextTick(() => this.createLoginPanel());
+    },
+
+    createLoginPanel() {
+      if (this._loginPanelCreated || !this.configReady || this.configError || !this._sdkInited || this.isLoggedIn) return;
+      let container = this.$refs.loginContainer || document.getElementById('login-container');
+      if (!container) {
+        this._loginPanelTimer = setTimeout(() => this.createLoginPanel(), 1000);
+        return;
+      }
+      if (container.getBoundingClientRect().width === 0 || container.getBoundingClientRect().height === 0) {
+        this._loginPanelTimer = setTimeout(() => this.createLoginPanel(), 500);
+        return;
+      }
+      container.innerHTML = '';
+      this._loginPanelCreated = true;
+      const redirectUri = window.location.origin + window.location.pathname;
+      try {
+        ww.createWWLoginPanel({
+          el: container,
+          params: {
+            login_type: 'CorpApp',
+            appid: this.config.corpid,
+            agentid: this.config.agentid,
+            redirect_uri: redirectUri,
+            redirect_type: 'callback',
+            state: 'state_' + Date.now()
+          },
+          onLoginSuccess: async ({ code }) => {
+            try {
+              await this.handleLogin(code);
+              if (this.customerId && this.staffUserId) await this.loadFirstPage();
+            } catch (e) {
+              this.clearLoginState(this.corpId);
+              this.isLoggedIn = false;
+              this._loginPanelCreated = false;
+              this.safeEmitLogout();
+            }
+          },
+          onLoginError: () => {
+            this._loginPanelCreated = false;
+            this._loginPanelTimer = setTimeout(() => this.createLoginPanel(), 3000);
+          }
+        });
+      } catch (e) {
+        this._loginPanelCreated = false;
+        this._loginPanelTimer = setTimeout(() => this.createLoginPanel(), 3000);
+      }
+    },
+
+    safeEmitLogout() {
+      if (this._hasEmittedLogout) return;
+      this._hasEmittedLogout = true;
+      this.$emit('logout');
+    },
+
+    async handleLogin(code) {
+      const res = await qwLogin({ code, corpid: this.corpId });
+      const data = this._extractResponse(res);
+      if (data.errcode !== 0) throw new Error(data.errmsg || '登录失败');
+      this.storeLoginState(this.corpId);
+      this.isLoggedIn = true;
+      this.$emit('login-success');
+    },
+
+    async getAgentConfigSignature() {
+      if (!this.configReady || this.configError) throw new Error('配置未就绪');
+      const currentUrl = window.location.href.split('#')[0];
+      const res = await qwSignature({ url: currentUrl, corpid: this.corpId });
+      const data = this._extractResponse(res);
+      if (data.errcode && data.errcode !== 0) {
+        this.clearLoginState(this.corpId);
+        this.isLoggedIn = false;
+        this.isReady = false;
+        this.destroyChat();
+        this._loginPanelCreated = false;
+        if (!this.configError) this.safeEmitLogout();
+        throw new Error(`签名失败: ${data.errmsg}`);
+      }
+      return { timestamp: data.timestamp, nonceStr: data.nonceStr, signature: data.signature };
+    },
+
+    async initSDKOnce() {
+      if (this._sdkInited) return true;
+      if (!this.configReady || this.configError) return false;
+      try {
+        await ww.register({
+          corpId: this.config.corpid,
+          agentId: this.config.agentid,
+          jsApiList: ['selectExternalContact', 'shareAppMessage', 'wwapp.invokeJsApiByCallInfo'],
+          getAgentConfigSignature: () => this.getAgentConfigSignature()
+        });
+        await ww.initOpenData();
+        this._sdkInited = true;
+        return true;
+      } catch (e) {
+        console.error('SDK初始化失败', e);
+        return false;
+      }
+    },
+
+    async loadFirstPage() {
+      if (this.configError) return;
+      try {
+        const res = await qwConversations({
+          customerId: this.customerId,
+          staffUserId: this.staffUserId,
+          limit: 100,
+          cursor: this.cursor,
+          corpid: this.corpId
+        });
+        const data = this._extractResponse(res);
+        if (data.errcode !== 0) throw new Error(data.errmsg || '拉取失败');
+        this.msgList = data.data || [];
+        this.cursor = data.next_cursor || '';
+        this.hasMore = data.has_more === 1;
+        this.isReady = true;
+        await this.$nextTick();
+        if (this.msgList.length) await this.renderOrUpdateChat();
+      } catch (e) {
+        console.error('加载第一页失败', e);
+        this.$message.error('加载会话失败:' + e.message);
+        this.isReady = true;
+        this.msgList = [];
+      }
+    },
+
+    async loadMore() {
+      if (!this.cursor || this.loadingMore || !this.hasMore || this.configError) return;
+      this.loadingMore = true;
+      try {
+        const res = await qwConversations({
+          customerId: this.customerId,
+          staffUserId: this.staffUserId,
+          limit: 50,
+          cursor: this.cursor,
+          corpid: this.corpId
+        });
+        const data = this._extractResponse(res);
+        if (data.errcode !== 0) throw new Error(data.errmsg || '拉取更多失败');
+        const newMessages = data.data || [];
+        if (newMessages.length) {
+          this.msgList.push(...newMessages);
+          this.cursor = data.next_cursor || '';
+          this.hasMore = data.has_more === 1;
+          if (this.chatInstance) this.chatInstance.setData({ msgList: this.msgList });
+          else await this.renderOrUpdateChat();
+        } else {
+          this.hasMore = false;
+        }
+      } catch (e) {
+        console.error('加载更多失败', e);
+      } finally {
+        this.loadingMore = false;
+      }
+    },
+
+    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 });
+        return;
+      }
+      const ok = await this.initSDKOnce();
+      if (!ok) return;
+      const factory = ww.createOpenDataFrameFactory();
+      if (!factory) return;
+
+      this.chatInstance = factory.createOpenDataFrame({
+        el: container,
+        template: `
+          <scroll-view scroll-y="{{true}}" bindscrolltolower="onScrollToLower" style="height: 100%;">
+            <view wx:for="{{data.msgList}}" wx:for-item="msg" wx:key="msgid" style="margin-bottom: 15px;">
+              <view style="text-align: center; margin-bottom: 8px;">
+                <text style="background:#e9e9e9; color:#333; padding:4px 12px; border-radius:16px; font-size:12px;">
+                  {{ msg.send_time_str }}
+                </text>
+              </view>
+              <view style="display:flex; flex-direction:row; {{msg.sender.type == 1 ? 'justify-content:flex-end;' : 'justify-content:flex-start;'}}">
+                <image wx:if="{{msg.sender.type == 2}}" src="{{data.customerAvatar}}" style="width:36px;height:36px;border-radius:4px;margin-right:10px;background:#fff;"></image>
+                <view style="max-width:70%;padding:10px 14px;border-radius:8px;word-break:break-all;background:{{msg.sender.type == 1 ? '#95ec69' : '#ffffff'}};box-shadow:0 1px 3px rgba(0,0,0,0.1);display:flex;align-items:center;">
+                  <ww-open-message message-id="{{msg.msgid}}" secret-key="{{msg.secretKey}}" open-type="viewMessage"/>
+                </view>
+                <image wx:if="{{msg.sender.type == 1}}" src="{{data.defaultStaffAvatar}}" style="width:36px;height:36px;border-radius:4px;margin-left:10px;background:#fff;"></image>
+              </view>
+            </view>
+            <view wx:if="{{data.hasMore && data.loadingMore}}" style="text-align:center;padding:10px;color:#999;">加载更多...</view>
+          </scroll-view>
+        `,
+        data: {
+          msgList: this.msgList,
+          customerAvatar: this.customerAvatar,
+          defaultStaffAvatar: this.defaultStaffAvatar,
+          hasMore: this.hasMore,
+          loadingMore: this.loadingMore
+        },
+        methods: {
+          onScrollToLower: () => {
+            if (this.hasMore && !this.loadingMore && !this.configError) this.loadMore();
+          }
+        },
+        error: (e) => {
+          console.error('[企微组件] 错误', e);
+          if (e && [42006, 42003, 40029].includes(e.errCode)) {
+            if (!this.configError) {
+              this.clearLoginState(this.corpId);
+              this.isLoggedIn = false;
+              this.isReady = false;
+              this.destroyChat();
+              this._loginPanelCreated = false;
+              this.safeEmitLogout();
+            }
+          }
+        },
+        handleModal: ({ modalUrl, modalSize }) => {
+          const mask = document.createElement('div');
+          mask.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;';
+          const content = document.createElement('div');
+          content.style.cssText = `position:relative;max-width:90vw;max-height:90vh;width:${modalSize?.width || 80}%;height:${modalSize?.height || 80}%;background:#fff;border-radius:8px;overflow:hidden;`;
+          const closeBtn = document.createElement('button');
+          closeBtn.innerText = '✕';
+          closeBtn.style.cssText = 'position:absolute;top:10px;right:10px;z-index:10001;width:32px;height:32px;border-radius:50%;border:none;background:rgba(0,0,0,0.5);color:#fff;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;';
+          const iframe = document.createElement('iframe');
+          iframe.src = modalUrl;
+          iframe.style.cssText = 'width:100%;height:100%;border:none;';
+          const cleanup = () => {
+            mask.remove();
+            document.removeEventListener('keydown', escHandler);
+          };
+          closeBtn.onclick = (e) => { e.stopPropagation(); cleanup(); };
+          mask.onclick = (e) => { if (e.target === mask) cleanup(); };
+          const escHandler = (e) => { if (e.key === 'Escape') cleanup(); };
+          document.addEventListener('keydown', escHandler);
+          content.appendChild(iframe);
+          content.appendChild(closeBtn);
+          mask.appendChild(content);
+          document.body.appendChild(mask);
+          return true;
+        }
+      });
+    },
+
+    async resetAndReload() {
+      this.destroyChat();
+      this.msgList = [];
+      this.cursor = '';
+      this.hasMore = true;
+      this.loadingMore = false;
+      this.isReady = false;
+      await this.loadFirstPage();
+    },
+
+    destroyChat() {
+      if (this.chatInstance) this.chatInstance = null;
+      const container = document.getElementById('chat-container');
+      if (container) container.innerHTML = '';
+    },
+
+    _extractResponse(res) {
+      if (!res) return {};
+      if (res.code !== undefined) return res;
+      if (res.errcode !== undefined || res.timestamp || res.corpid || res.data) return res;
+      if (res.data?.data) return res.data.data;
+      return res;
+    }
+  }
+};
+</script>
+
+<style scoped>
+.conversation-panel {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  background: #f5f6f7;
+  position: relative;
+}
+.login-area, .empty-tip, .loading-tip {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  flex: 1;
+}
+.login-tip {
+  font-size: 18px;
+  color: #666;
+  margin-bottom: 24px;
+}
+.empty-tip, .loading-tip {
+  color: #999;
+  font-size: 16px;
+}
+.empty-tip i, .loading-tip i {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
+#chat-container {
+  flex: 1;
+  width: 100%;
+  height: 100%;
+  background: #f0f2f5;
+}
+.login-container {
+  width: 320px;
+  height: 380px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+</style>
+
+<style>
+#chat-container,
+#chat-container > div,
+#chat-container iframe {
+  width: 100% !important;
+  height: 100% !important;
+  min-width: 100% !important;
+  min-height: 100% !important;
+  overflow: hidden;
+}
+</style>

+ 375 - 0
src/views/qw/conversationNew/EmployeeQueryTab.vue

@@ -0,0 +1,375 @@
+<template>
+  <div class="employee-query-tab">
+    <!-- 公司选择 -->
+    <div class="query-form">
+      <el-form :model="queryParams" inline>
+        <el-form-item label="所属公司" required>
+          <el-select
+            v-model="selectedCompanyId"
+            placeholder="请选择公司"
+            clearable
+            filterable
+            @change="handleCompanyChange"
+            :loading="companyLoading"
+          >
+            <el-option
+              v-for="company in companyList"
+              :key="company.companyId"
+              :label="company.companyName"
+              :value="company.companyId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="员工">
+          <el-select
+            v-model="queryParams.userId"
+            filterable
+            remote
+            reserve-keyword
+            placeholder="请输入员工名称"
+            :remote-method="remoteSearchStaff"
+            :loading="staffLoading"
+            clearable
+          >
+            <el-option
+              v-for="item in staffOptions"
+              :key="item.userId"
+              :label="item.nickName || item.qwUserName"
+              :value="item.userId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="企微昵称">
+          <el-input v-model="queryParams.qwUserName" 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="staffList"
+      border
+      stripe
+      style="width: 100%"
+    >
+      <el-table-column prop="nickName" label="员工" />
+      <el-table-column prop="qwUserName" label="企微昵称" />
+      <el-table-column prop="departmentName" label="部门" />
+      <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="getStaffList"
+    ></pagination>
+
+    <!-- 抽屉:客户列表 + 会话详情 -->
+    <el-drawer
+      :visible.sync="drawerVisible"
+      :title="(currentStaff && (currentStaff.nickName || currentStaff.qwUserName) ? (currentStaff.nickName || currentStaff.qwUserName) : '') + ' 的客户会话'"
+      direction="rtl"
+      size="80%"
+      destroy-on-close
+    >
+      <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-column prop="qwUserName" label="销售昵称" />
+          </el-table>
+          <pagination
+            v-show="customerTotal > 0"
+            :total="customerTotal"
+            :page.sync="customerPageNum"
+            :limit.sync="customerPageSize"
+            @pagination="loadCustomerList"
+            layout="prev, pager, next"
+          ></pagination>
+        </div>
+        <div class="conversation-panel">
+          <ConversationPanelPure
+            :corpId="corp && corp.corpId"
+            :customerId="selectedCustomerId"
+            :customerAvatar="selectedCustomerAvatar"
+            :staffUserId="currentStaff && currentStaff.qwUserId"
+            @logout="handleLogout"
+          />
+        </div>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { getAllUserlist } from "@/api/company/companyUser";
+import { selectQwUserListByCondition } from "@/api/qw/qwUser";
+import { listExternalContact } from "@/api/qw/externalContact";
+import { queryCompanyListByCompanyIds } from "@/api/company/company";
+import ConversationPanelPure from "./ConversationPanelPure.vue";
+import Pagination from "@/components/Pagination";
+
+export default {
+  name: "EmployeeQueryTab",
+  components: { ConversationPanelPure, Pagination },
+  props: {
+    corp: { type: Object, default: null } // { corpId, companyIds, corpName }
+  },
+  data() {
+    return {
+      // 公司相关
+      companyList: [],
+      selectedCompanyId: null,
+      companyLoading: false,
+
+      // 员工查询参数(分页)
+      queryParams: {
+        userId: null,
+        qwUserName: "",
+        pageNum: 1,
+        pageSize: 10
+      },
+      staffList: [],        // 当前页数据
+      total: 0,             // 总记录数
+      tableLoading: false,
+      staffOptions: [],     // 下拉框选项(远程搜索用)
+      staffLoading: false,
+
+      // 抽屉相关
+      drawerVisible: false,
+      currentStaff: null,
+      customerList: [],
+      customerTotal: 0,
+      customerPageNum: 1,
+      customerPageSize: 10,
+      customerLoading: false,
+      customerKeyword: "",
+      selectedCustomerId: null,
+      selectedCustomerAvatar: ""
+    };
+  },
+  watch: {
+    corp: {
+      immediate: true,
+      handler(newCorp) {
+        if (newCorp && newCorp.companyIds) {
+          this.loadCompanyList(newCorp.companyIds);
+        } else {
+          this.companyList = [];
+          this.selectedCompanyId = null;
+          this.clearStaffData();
+        }
+      }
+    }
+  },
+  computed: {
+    filteredCustomerList() {
+      if (!this.customerKeyword) return this.customerList;
+      return this.customerList.filter(c => c.name && c.name.includes(this.customerKeyword));
+    }
+  },
+  methods: {
+    // 根据 companyIds 加载公司列表
+    async loadCompanyList(companyIdsStr) {
+      if (!companyIdsStr) return;
+      const ids = companyIdsStr.split(',').map(Number).filter(id => !isNaN(id));
+      if (ids.length === 0) return;
+      this.companyLoading = true;
+      try {
+        const res = await queryCompanyListByCompanyIds(ids);
+        if (res.code === 200) {
+          this.companyList = res.companyList || [];
+          if (this.companyList.length > 0) {
+            this.selectedCompanyId = this.companyList[0].companyId;
+            await this.onCompanyChanged();
+          }
+        } else {
+          this.companyList = [];
+        }
+      } catch (e) {
+        console.error("加载公司列表失败", e);
+      } finally {
+        this.companyLoading = false;
+      }
+    },
+
+    // 切换公司时调用
+    async handleCompanyChange(companyId) {
+      if (!companyId) {
+        this.clearStaffData();
+        return;
+      }
+      this.queryParams = { userId: null, qwUserName: "", pageNum: 1, pageSize: 10 };
+      await this.onCompanyChanged();
+    },
+
+    // 公司变更后的核心处理:重新加载员工下拉选项,并刷新列表第一页
+    async onCompanyChanged() {
+      if (!this.selectedCompanyId) return;
+      await this.loadStaffOptions();
+      await this.getStaffList();
+    },
+
+    // 加载全部员工(用于下拉选项)
+    async loadStaffOptions() {
+      if (!this.selectedCompanyId) return;
+      this.staffLoading = true;
+      try {
+        const res = await getAllUserlist({ companyId: this.selectedCompanyId });
+        const data = res.data || [];
+        this.staffOptions = data.map(item => ({
+          userId: item.userId,
+          nickName: item.nickName,
+          qwUserName: item.qwUserName,
+          qwUserId: item.qwUserId
+        }));
+      } catch (e) {
+        console.error("加载员工下拉选项失败", e);
+        this.staffOptions = [];
+      } finally {
+        this.staffLoading = false;
+      }
+    },
+
+    // 远程搜索员工(前端过滤)
+    async remoteSearchStaff(query) {
+      if (!this.selectedCompanyId) return;
+      if (!query) {
+        await this.loadStaffOptions();
+        return;
+      }
+      const filtered = this.staffOptions.filter(item =>
+        (item.nickName && item.nickName.includes(query)) ||
+        (item.qwUserName && item.qwUserName.includes(query))
+      );
+      this.staffOptions = filtered;
+    },
+
+    // 获取员工列表(后端分页)
+    async getStaffList() {
+      if (!this.selectedCompanyId) {
+        this.staffList = [];
+        this.total = 0;
+        return;
+      }
+      this.tableLoading = true;
+      try {
+        const params = {
+          companyId: this.selectedCompanyId,
+          userId: this.queryParams.userId,
+          qwUserName: this.queryParams.qwUserName,
+          pageNum: this.queryParams.pageNum,
+          pageSize: this.queryParams.pageSize
+        };
+        const res = await selectQwUserListByCondition(params);
+        if (res.code === 200 && res.data) {
+          this.staffList = res.data.list || [];
+          this.total = res.data.total || 0;
+        } else {
+          this.staffList = [];
+          this.total = 0;
+        }
+      } catch (e) {
+        console.error("获取员工列表失败", e);
+        this.staffList = [];
+        this.total = 0;
+      } finally {
+        this.tableLoading = false;
+      }
+    },
+
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getStaffList();
+    },
+
+    resetQuery() {
+      this.queryParams = {
+        userId: null,
+        qwUserName: "",
+        pageNum: 1,
+        pageSize: 10
+      };
+      this.getStaffList();
+      this.loadStaffOptions();
+    },
+
+    clearStaffData() {
+      this.staffOptions = [];
+      this.staffList = [];
+      this.total = 0;
+      this.queryParams = {userId: null, qwUserName: "", pageNum: 1, pageSize: 10};
+    },
+
+    openDrawer(row) {
+      this.currentStaff = row;
+      this.drawerVisible = true;
+      this.selectedCustomerId = null;
+      this.selectedCustomerAvatar = "";
+      this.customerPageNum = 1;
+      this.loadCustomerList();
+    },
+
+    async loadCustomerList() {
+      if (!this.currentStaff) return;
+      this.customerLoading = true;
+      try {
+        const params = {
+          pageNum: this.customerPageNum,
+          pageSize: this.customerPageSize,
+          qwUserId: this.currentStaff.id,
+          corpId: this.corp && this.corp.corpId
+        };
+        const res = await listExternalContact(params);
+        if (res.rows) {
+          this.customerList = res.rows;
+          this.customerTotal = res.total || 0;
+        } else {
+          this.customerList = [];
+          this.customerTotal = 0;
+        }
+      } catch (e) {
+        console.error("获取客户列表失败", e);
+      } finally {
+        this.customerLoading = false;
+      }
+    },
+
+    handleCustomerClick(row) {
+      this.selectedCustomerId = row.externalUserId;
+      this.selectedCustomerAvatar = row.avatar;
+    },
+
+    handleLogout() {
+      this.$message.warning("会话登录已失效,请刷新页面重试");
+    }
+  }
+};
+</script>
+
+<style scoped>
+/* 原有样式 */
+</style>