Procházet zdrojové kódy

优化会话记录渲染逻辑

cgp před 6 dny
rodič
revize
97a1e395b9

+ 320 - 253
src/views/qw/companySession/ConversationPanel.vue

@@ -1,48 +1,41 @@
 <template>
   <div class="conversation-panel">
-    <!-- 配置加载 -->
-    <div v-if="!configReady && !configError && !configLoading" class="loading-tip">
+    <!-- 配置加载 -->
+    <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>企微未配置,请先配置企微应用</p>
+      <p>{{ configErrorMsg || '企微未配置,请先配置企微应用' }}</p>
     </div>
-
     <!-- 未登录 -->
     <div v-else-if="!isLoggedIn" class="login-area">
-      <div class="login-tip">请扫码登录企微</div>
-      <div id="login-container"></div>
+      <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>
+      <p>暂无会话数据</p>
     </div>
-
-    <!-- 聊天窗口容器 -->
+    <!-- 聊天窗口 -->
     <div v-else id="chat-container" ref="chatContainer"></div>
   </div>
 </template>
@@ -52,10 +45,10 @@ import * as ww from '@wecom/jssdk';
 import defaultStaffAvatar from '@/assets/images/user.png';
 import { qwLogin, qwSignature, qwConversations, getQwSessionConfig } from '@/api/qw/companySession';
 
-// ==================== 全局缓存(挂载到 window,避免 HMR 或快速重建导致缓存丢失) ====================
-window._QW_CONFIG_CACHE = window._QW_CONFIG_CACHE || new Map();     // corpId -> { config, timestamp }
-window._QW_CONFIG_PENDING = window._QW_CONFIG_PENDING || new Map(); // corpId -> Promise
-const CACHE_TTL = 5 * 60 * 1000; // 5分钟
+// ========== 全局配置缓存 ==========
+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;
@@ -63,6 +56,7 @@ function getCachedConfig(corpId) {
   if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
     return cached.config;
   }
+  window._QW_CONFIG_CACHE.delete(corpId);
   return null;
 }
 
@@ -90,43 +84,52 @@ export default {
       chatInstance: null,
       _sdkInited: false,
       defaultStaffAvatar: defaultStaffAvatar,
-      config: {
-        corpid: '',
-        agentid: '',
-        domain: ''
-      },
+      config: { corpid: '', agentid: '', domain: '' },
       configReady: false,
       configError: false,
       configLoading: false,
+      configErrorMsg: '',
       tokenCheckTimer: null,
-      // 防止短时间内重复触发 logout 导致父组件频繁重建
-      lastLogoutTime: 0
+      lastLogoutTime: 0,
+      _hasEmittedLogout: false,
+      _loginPanelCreated: false,
+      _loginPanelRetryCount: 0,
+      _loginPanelTimer: null
     };
   },
   watch: {
     customerId(newId, oldId) {
-      if (newId && newId !== oldId && this.isLoggedIn && this.staffUserId && this.configReady && this.corpId && !this.configError) {
+      if (newId && newId !== oldId && this.isLoggedIn && this.staffUserId && this.configReady && !this.configError) {
         this.resetAndReload();
       }
     },
-    staffUserId(newStaffId, oldStaffId) {
-      if (newStaffId && newStaffId !== oldStaffId && this.customerId && this.isLoggedIn && this.configReady && this.corpId && !this.configError) {
+    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;
+        this._loginPanelRetryCount = 0;
+        if (this._loginPanelTimer) {
+          clearTimeout(this._loginPanelTimer);
+          this._loginPanelTimer = null;
+        }
         this.msgList = [];
         this.cursor = '';
         this.hasMore = true;
         this.loadingMore = false;
+        this.isLoggedIn = false;
 
-        const cachedConfig = getCachedConfig(newId);
-        if (cachedConfig) {
-          this.config = cachedConfig;
+        const cached = getCachedConfig(newId);
+        if (cached) {
+          this.config = cached;
           this.configReady = true;
           this.configError = false;
           this.configLoading = false;
@@ -143,19 +146,28 @@ export default {
         }
       }
     },
-    customerAvatar: {
-      handler(newAvatar) {
-        if (this.chatInstance && newAvatar) {
-          this.chatInstance.setData({ customerAvatar: newAvatar });
+    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: false
+      immediate: true
     }
   },
   async mounted() {
-    const cachedConfig = getCachedConfig(this.corpId);
-    if (cachedConfig) {
-      this.config = cachedConfig;
+    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;
@@ -167,6 +179,7 @@ export default {
       }
     }
 
+    // 定时刷新签名
     this.tokenCheckTimer = setInterval(() => {
       if (this.isLoggedIn && this.configReady && !this.configError && this._sdkInited) {
         this.getAgentConfigSignature().catch(e => console.warn('签名刷新失败', e));
@@ -178,38 +191,36 @@ export default {
       clearInterval(this.tokenCheckTimer);
       this.tokenCheckTimer = null;
     }
+    if (this._loginPanelTimer) {
+      clearTimeout(this._loginPanelTimer);
+      this._loginPanelTimer = null;
+    }
     this.destroyChat();
   },
   methods: {
+    // ========== Storage Key ==========
     getStorageKey(corpId, suffix = 'expire') {
       if (!corpId) return null;
       return `wecom_session_${corpId}_${suffix}`;
     },
-
     checkLoginState(corpId) {
       if (!corpId) return false;
-      const expireKey = this.getStorageKey(corpId, 'expire');
-      const expire = localStorage.getItem(expireKey);
+      const expire = localStorage.getItem(this.getStorageKey(corpId, 'expire'));
       if (!expire) return false;
       return Date.now() < parseInt(expire, 10);
     },
-
     storeLoginState(corpId) {
       if (!corpId) return;
-      const expireKey = this.getStorageKey(corpId, 'expire');
-      localStorage.setItem(expireKey, (Date.now() + 115 * 60 * 1000).toString());
+      localStorage.setItem(this.getStorageKey(corpId, 'expire'), String(Date.now() + 115 * 60 * 1000));
     },
-
     clearLoginState(corpId) {
       if (!corpId) return;
-      const expireKey = this.getStorageKey(corpId, 'expire');
-      localStorage.removeItem(expireKey);
+      localStorage.removeItem(this.getStorageKey(corpId, 'expire'));
     },
 
+    // ========== 配置加载 ==========
     async initConfig() {
-      if (!this.corpId) return;
-      if (this.configReady) return;
-      if (this.configError) return;
+      if (!this.corpId || this.configReady || this.configError) return;
 
       const cached = getCachedConfig(this.corpId);
       if (cached) {
@@ -220,41 +231,56 @@ export default {
         return;
       }
 
-      // 全局并发锁
       if (window._QW_CONFIG_PENDING.has(this.corpId)) {
         return window._QW_CONFIG_PENDING.get(this.corpId);
       }
 
       this.configLoading = true;
-      const requestPromise = (async () => {
+      const promise = (async () => {
         try {
-          const configRes = await getQwSessionConfig(this.corpId);
-          const configData = this._extractResponse(configRes);
-          if (!configData.corpid || !configData.agentid) {
-            throw new Error('配置数据不完整');
+          const res = await getQwSessionConfig(this.corpId);
+
+          let configData = null;
+          if (res && res.code === 200 && res.data && res.data.corpid) {
+            configData = res.data;
+          } else if (res && res.data && res.data.corpid) {
+            configData = res.data;
+          } else if (res && res.corpid) {
+            configData = res;
+          }
+
+          if (!configData || !configData.corpid || !configData.agentid) {
+            throw new Error('配置数据不完整:缺少 corpid 或 agentid');
           }
+
           const cfg = {
             corpid: configData.corpid,
             agentid: String(configData.agentid),
             domain: configData.domain || ''
           };
+
           this.config = cfg;
           this.configReady = true;
           this.configError = false;
+          this.configErrorMsg = '';
           setCachedConfig(this.corpId, cfg);
+          console.log('[ConversationPanel] 配置加载成功:', cfg);
         } catch (e) {
-          console.error('获取企微配置失败:', 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, requestPromise);
-      return requestPromise;
+
+      window._QW_CONFIG_PENDING.set(this.corpId, promise);
+      return promise;
     },
 
+    // ========== 认证与加载主流程 ==========
     async handleAuthAndLoad() {
       if (this.configError) return;
       if (!this.configReady) {
@@ -262,12 +288,18 @@ export default {
         if (this.configError) return;
       }
 
+      const sdkOk = await this.initSDKOnce();
+      if (!sdkOk) {
+        console.error('[ConversationPanel] SDK 初始化失败');
+        return;
+      }
+
       const urlParams = new URLSearchParams(window.location.search);
       const code = urlParams.get('code');
 
       if (code) {
-        const newUrl = window.location.origin + window.location.pathname;
-        window.history.replaceState({}, '', newUrl);
+        const cleanUrl = window.location.origin + window.location.pathname;
+        window.history.replaceState({}, '', cleanUrl);
         try {
           await this.handleLogin(code);
           this.storeLoginState(this.corpId);
@@ -275,11 +307,11 @@ export default {
           if (this.customerId && this.staffUserId) {
             await this.loadFirstPage();
           }
-        } catch (loginErr) {
-          console.error('登录失败', loginErr);
+        } catch (err) {
+          console.error('[ConversationPanel] 登录失败:', err);
           this.clearLoginState(this.corpId);
           this.isLoggedIn = false;
-          this.emitLogoutWithThrottle();
+          this.safeEmitLogout();
         }
       } else {
         if (this.checkLoginState(this.corpId)) {
@@ -289,99 +321,169 @@ export default {
           }
         } else {
           this.isLoggedIn = false;
-          this.emitLogoutWithThrottle();
-          this.$nextTick(() => {
-            this.createLoginPanelWithRetry();
-          });
+          this.safeEmitLogout();
+          // 尝试创建登录面板
+          this.tryCreateLoginPanel();
         }
       }
     },
 
-    // 防止短时间内多次触发 logout 导致父组件疯狂重建
-    emitLogoutWithThrottle() {
-      const now = Date.now();
-      if (now - this.lastLogoutTime > 1000) {
-        this.lastLogoutTime = now;
-        this.$emit('logout');
+    // 尝试创建登录面板(带重试)
+    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();
+      });
+    },
+
+    // ========== 登录面板 ==========
+    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();
+        }, 1000);
+        return;
+      }
+
+      // 检查容器尺寸
+      const rect = container.getBoundingClientRect();
+      if (rect.width === 0 || rect.height === 0) {
+        console.warn('[ConversationPanel] 容器尺寸为0,等待渲染后重试');
+        this._loginPanelTimer = setTimeout(() => {
+          this._loginPanelTimer = null;
+          this.createLoginPanel();
+        }, 500);
+        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({
+          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 }) => {
+            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;
+              this.safeEmitLogout();
+            }
+          },
+          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;
+          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 resp = this._extractResponse(res);
-      if (resp.errcode !== 0) {
-        throw new Error(resp.errmsg || '登录失败');
+      const data = this._extractResponse(res);
+      if (data.errcode !== undefined && data.errcode !== 0) {
+        throw new Error(data.errmsg || '登录失败');
       }
       this.storeLoginState(this.corpId);
       this.isLoggedIn = true;
       this.$emit('login-success');
     },
 
-    createLoginPanelWithRetry(maxWait = 3000, interval = 200) {
-      const startTime = Date.now();
-      const tryCreate = () => {
-        if (!this.configReady || this.configError || !this.config.corpid || !this.config.agentid) {
-          console.warn('配置未就绪,无法创建登录面板');
-          return;
-        }
-        const container = document.getElementById('login-container');
-        if (container) {
-          this._doCreateLoginPanel(container);
-        } else if (Date.now() - startTime < maxWait) {
-          setTimeout(tryCreate, interval);
-        } else {
-          console.error('登录容器未找到,超时放弃');
-          this.$message.error('无法加载登录面板,请刷新页面重试');
-        }
-      };
-      tryCreate();
-    },
-
-    _doCreateLoginPanel(container) {
-      const redirectUri = window.location.href.split('?')[0].split('#')[0];
-      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 }) => {
-          await this.handleLogin(code);
-          if (this.customerId && this.staffUserId && this.corpId) {
-            await this.loadFirstPage();
-          }
-        },
-        onLoginError: (err) => {
-          console.error('登录面板错误:', err);
-          this.clearLoginState(this.corpId);
-          this.isLoggedIn = false;
-          this.emitLogoutWithThrottle();
-        }
-      });
-    },
-
+    // ========== SDK 签名 ==========
     async getAgentConfigSignature() {
       if (!this.configReady || this.configError) {
-        throw new Error('配置未就绪,无法获取签名');
+        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) {
-        console.error('获取签名失败,将清除登录状态', data);
+        console.error('[ConversationPanel] 签名失败,清除登录态', data);
         this.clearLoginState(this.corpId);
         this.isLoggedIn = false;
         this.isReady = false;
         this.destroyChat();
+        this._loginPanelCreated = false;
         if (!this.configError) {
-          this.emitLogoutWithThrottle();
+          this.safeEmitLogout();
         }
         throw new Error(`签名失败: ${data.errmsg}`);
       }
+
       return {
         timestamp: data.timestamp,
         nonceStr: data.nonceStr,
@@ -389,12 +491,11 @@ export default {
       };
     },
 
+    // ========== SDK 初始化 ==========
     async initSDKOnce() {
       if (this._sdkInited) return true;
-      if (!this.configReady || this.configError) {
-        console.warn('SDK初始化失败:配置未就绪');
-        return false;
-      }
+      if (!this.configReady || this.configError) return false;
+
       try {
         await ww.register({
           corpId: this.config.corpid,
@@ -404,18 +505,17 @@ export default {
         });
         await ww.initOpenData();
         this._sdkInited = true;
+        console.log('[ConversationPanel] SDK 初始化成功');
         return true;
       } catch (e) {
-        console.error('SDK 初始化失败:', e);
+        console.error('[ConversationPanel] SDK 初始化失败:', e);
         return false;
       }
     },
 
+    // ========== 会话数据加载 ==========
     async loadFirstPage() {
-      if (this.configError) {
-        this.$message.error('企微配置错误,无法加载会话');
-        return;
-      }
+      if (this.configError) return;
       try {
         const res = await qwConversations({
           customerId: this.customerId,
@@ -425,19 +525,20 @@ export default {
           corpid: this.corpId
         });
         const data = this._extractResponse(res);
-        if (data.errcode !== 0) {
+        if (data.errcode !== undefined && 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 > 0) {
           await this.renderOrUpdateChat();
         }
       } catch (e) {
-        console.error('加载第一页失败', e);
+        console.error('[ConversationPanel] 加载第一页失败', e);
         this.$message.error('加载会话失败:' + e.message);
         this.isReady = true;
         this.msgList = [];
@@ -456,7 +557,7 @@ export default {
           corpid: this.corpId
         });
         const data = this._extractResponse(res);
-        if (data.errcode !== 0) {
+        if (data.errcode !== undefined && data.errcode !== 0) {
           throw new Error(data.errmsg || '拉取更多失败');
         }
         const newMessages = data.data || [];
@@ -465,7 +566,7 @@ export default {
           this.cursor = data.next_cursor || '';
           this.hasMore = data.has_more === 1;
           if (this.chatInstance) {
-            this.chatInstance.setData({msgList: this.msgList});
+            this.chatInstance.setData({ msgList: this.msgList });
           } else {
             await this.renderOrUpdateChat();
           }
@@ -473,22 +574,20 @@ export default {
           this.hasMore = false;
         }
       } catch (e) {
-        console.error('加载更多失败', e);
-        this.$message.error('加载更多失败');
+        console.error('[ConversationPanel] 加载更多失败', e);
       } finally {
         this.loadingMore = false;
       }
     },
 
+    // ========== 渲染聊天组件 ==========
     async renderOrUpdateChat() {
-      if (this.msgList.length === 0) return;
-      if (this.configError) return;
-
+      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.msgList });
         return;
       }
 
@@ -498,43 +597,40 @@ export default {
       const factory = ww.createOpenDataFrameFactory();
       if (!factory) return;
 
-      const templateData = {
-        msgList: this.msgList,
-        customerAvatar: this.customerAvatar,
-        defaultStaffAvatar: this.defaultStaffAvatar,
-        hasMore: this.hasMore,
-        loadingMore: this.loadingMore
-      };
-
       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; font-weight:500;">
+                <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;'}}">
+                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>
+                       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;">
+                  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>
+                       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 wx:if="{{data.hasMore && data.loadingMore}}" style="text-align:center;padding:10px;color:#999;">
               加载更多...
             </view>
           </scroll-view>
         `,
-        data: templateData,
+        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) {
@@ -544,92 +640,47 @@ export default {
         },
         error: (e) => {
           console.error('[企微组件] 错误', e);
-          if (e && (e.errCode === 42006 || e.errCode === 42003 || e.errCode === 40029)) {
+          if (e && [42006, 42003, 40029].includes(e.errCode)) {
             if (!this.configError) {
               this.clearLoginState(this.corpId);
               this.isLoggedIn = false;
               this.isReady = false;
               this.destroyChat();
-              this.emitLogoutWithThrottle();
-              this.$nextTick(() => this.createLoginPanelWithRetry());
-            } else {
-              console.warn('登录态失效,但由于配置错误,不自动重新登录');
+              this._loginPanelCreated = false;
+              this.safeEmitLogout();
             }
           }
         },
-        handleModal: ({modalUrl, modalSize}) => {
+        handleModal: ({ modalUrl, modalSize }) => {
           const mask = document.createElement('div');
-          mask.style.position = 'fixed';
-          mask.style.top = '0';
-          mask.style.left = '0';
-          mask.style.width = '100%';
-          mask.style.height = '100%';
-          mask.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
-          mask.style.zIndex = 9999;
-          mask.style.display = 'flex';
-          mask.style.alignItems = 'center';
-          mask.style.justifyContent = 'center';
-
+          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.position = 'relative';
-          content.style.maxWidth = '90vw';
-          content.style.maxHeight = '90vh';
-          content.style.width = modalSize?.width ? `${modalSize.width}px` : '80%';
-          content.style.height = modalSize?.height ? `${modalSize.height}px` : '80%';
-          content.style.backgroundColor = '#fff';
-          content.style.borderRadius = '8px';
-          content.style.overflow = 'hidden';
-          content.style.boxShadow = '0 0 20px rgba(0,0,0,0.3)';
-
+          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.position = 'absolute';
-          closeBtn.style.top = '10px';
-          closeBtn.style.right = '10px';
-          closeBtn.style.zIndex = 10001;
-          closeBtn.style.width = '32px';
-          closeBtn.style.height = '32px';
-          closeBtn.style.borderRadius = '50%';
-          closeBtn.style.border = 'none';
-          closeBtn.style.backgroundColor = 'rgba(0,0,0,0.5)';
-          closeBtn.style.color = '#fff';
-          closeBtn.style.fontSize = '20px';
-          closeBtn.style.cursor = 'pointer';
-          closeBtn.style.display = 'flex';
-          closeBtn.style.alignItems = 'center';
-          closeBtn.style.justifyContent = 'center';
-          closeBtn.onclick = (e) => {
-            e.stopPropagation();
+          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);
           };
-
-          const iframe = document.createElement('iframe');
-          iframe.src = modalUrl;
-          iframe.style.width = '100%';
-          iframe.style.height = '100%';
-          iframe.style.border = 'none';
-
-          content.appendChild(iframe);
-          content.appendChild(closeBtn);
-          mask.appendChild(content);
-          document.body.appendChild(mask);
-
+          closeBtn.onclick = (e) => {
+            e.stopPropagation();
+            cleanup();
+          };
           mask.onclick = (e) => {
-            if (e.target === mask) {
-              mask.remove();
-              document.removeEventListener('keydown', escHandler);
-            }
+            if (e.target === mask) cleanup();
           };
-
           const escHandler = (e) => {
-            if (e.key === 'Escape') {
-              mask.remove();
-              document.removeEventListener('keydown', escHandler);
-            }
+            if (e.key === 'Escape') cleanup();
           };
           document.addEventListener('keydown', escHandler);
-
+          content.appendChild(iframe);
+          content.appendChild(closeBtn);
+          mask.appendChild(content);
+          document.body.appendChild(mask);
           return true;
         }
       });
@@ -650,33 +701,24 @@ export default {
         this.chatInstance = null;
       }
       const container = document.getElementById('chat-container');
-      if (container) {
-        container.innerHTML = '';
-      }
+      if (container) container.innerHTML = '';
     },
 
+    // 通用响应解包
     _extractResponse(res) {
       if (!res) return {};
-      if (res.errcode !== undefined || res.timestamp || res.corpid) return res;
-      if (res.data && (res.data.errcode !== undefined || res.data.timestamp || res.data.corpid)) return res.data;
-      if (res.data && res.data.data && (res.data.data.errcode !== undefined || res.data.data.timestamp || res.data.data.corpid)) return res.data.data;
-      return res.data || 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;
+        if (res.data.data) return res.data.data;
+      }
+      return res;
     }
   }
 };
 </script>
 
-<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 scoped>
 .conversation-panel {
   width: 100%;
   height: 100%;
@@ -686,7 +728,9 @@ export default {
   position: relative;
 }
 
-.login-area, .empty-tip, .loading-tip {
+.login-area,
+.empty-tip,
+.loading-tip {
   display: flex;
   flex-direction: column;
   align-items: center;
@@ -701,12 +745,14 @@ export default {
   margin-bottom: 24px;
 }
 
-.empty-tip, .loading-tip {
+.empty-tip,
+.loading-tip {
   color: #999;
   font-size: 16px;
 }
 
-.empty-tip i, .loading-tip i {
+.empty-tip i,
+.loading-tip i {
   font-size: 48px;
   margin-bottom: 16px;
 }
@@ -717,4 +763,25 @@ export default {
   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>

+ 85 - 46
src/views/qw/companySession/index.vue

@@ -5,7 +5,13 @@
       <div class="left-panel-content">
         <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
           <el-form-item label="企微主体" prop="corpId">
-            <el-select v-model="queryParams.corpId" placeholder="企微主体" size="small" clearable @change="handleCorpChange">
+            <el-select
+              v-model="queryParams.corpId"
+              placeholder="企微主体"
+              size="small"
+              clearable
+              @change="handleCorpChange"
+            >
               <el-option
                 v-for="corp in corpList"
                 :key="corp.corpId"
@@ -14,7 +20,6 @@
               />
             </el-select>
           </el-form-item>
-
           <el-form-item label="企微员工" prop="qwUserId">
             <el-select
               v-model="queryParams.qwUserId"
@@ -32,7 +37,6 @@
               />
             </el-select>
           </el-form-item>
-
           <el-form-item>
             <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
           </el-form-item>
@@ -60,7 +64,6 @@
             <el-table-column label="添加时间" prop="createTime" width="100px" />
           </el-table>
         </div>
-
         <pagination
           v-show="total > 0"
           :total="total"
@@ -83,21 +86,25 @@
         @logout="handleConversationLogout"
         @login-success="handleConversationLoginSuccess"
       />
+      <div v-else class="empty-tip">
+        <i class="el-icon-info"></i>
+        <p>请先选择企微主体</p>
+      </div>
     </div>
   </div>
 </template>
 
 <script>
-import {listExternalContact} from "@/api/qw/externalContact";
-import {getCompanyList} from "@/api/company/company";
-import {getAllUserlist} from "@/api/company/companyUser";
-import {allCorp} from "@/api/qw/qwCompany";
-import {listAllQwUserList} from "@/api/qw/user";
+import { listExternalContact } from "@/api/qw/externalContact";
+import { getCompanyList } from "@/api/company/company";
+import { getAllUserlist } from "@/api/company/companyUser";
+import { allCorp } from "@/api/qw/qwCompany";
+import { listAllQwUserList } from "@/api/qw/user";
 import ConversationPanel from "./ConversationPanel.vue";
 
 export default {
   name: "CompanySession",
-  components: {ConversationPanel},
+  components: { ConversationPanel },
   data() {
     return {
       loading: false,
@@ -118,7 +125,9 @@ export default {
       },
       qwCompanyList: [],
       staffList: [],
-      conversationPanelKey: 0
+      conversationPanelKey: 0,
+      isInitialized: false,
+      _logoutLock: false
     };
   },
   computed: {
@@ -129,75 +138,96 @@ export default {
     }
   },
   created() {
-    getCompanyList().then(response => {
-      this.qwCompanyList = response.data;
-      if (this.qwCompanyList && this.qwCompanyList.length > 0) {
-        this.queryParams.companyId = this.qwCompanyList[0].companyId;
-        this.getAllUserlist(this.queryParams.companyId);
-      }
-    });
-    allCorp().then(response => {
-      this.corpList = response.data || [];
-      if (this.corpList.length > 0) {
-        this.queryParams.corpId = this.corpList[0].corpId;
-        this.handleCorpChange(this.queryParams.corpId);
-      }
-    });
+    this.initData();
   },
   activated() {
-    this.conversationPanelKey++;
+    if (this.isInitialized) {
+      this.conversationPanelKey++;
+    }
   },
   methods: {
-    // ========== 子组件事件处理 ==========
-    // 当会话组件检测到登录态失效时触发,清空选中的客户和员工,并清空客户列表
+    async initData() {
+      try {
+        const [companyRes, corpRes] = await Promise.all([
+          getCompanyList(),
+          allCorp()
+        ]);
+
+        this.qwCompanyList = companyRes.data || [];
+        if (this.qwCompanyList.length > 0) {
+          this.queryParams.companyId = this.qwCompanyList[0].companyId;
+          this.getAllUserlist(this.queryParams.companyId);
+        }
+
+        this.corpList = corpRes.data || [];
+        if (this.corpList.length > 0) {
+          this.queryParams.corpId = this.corpList[0].corpId;
+          await this.fetchQwUserList(this.queryParams.corpId);
+        }
+
+        this.$nextTick(() => {
+          this.isInitialized = true;
+        });
+      } catch (e) {
+        console.error('初始化数据失败', e);
+        this.isInitialized = true;
+      }
+    },
+
     handleConversationLogout() {
+      if (this._logoutLock) return;
+      this._logoutLock = true;
+
       this.selectedCustomerId = null;
       this.selectedCustomerAvatar = null;
       this.queryParams.qwUserId = null;
       this.externalContactList = [];
       this.total = 0;
-      // 可选:强制刷新右侧面板,避免状态残留
       this.conversationPanelKey++;
+
+      setTimeout(() => {
+        this._logoutLock = false;
+      }, 1000);
     },
 
-    // 当会话组件登录成功时触发,可重新加载员工列表(如果依赖授权)
     handleConversationLoginSuccess() {
       if (this.queryParams.corpId) {
         this.fetchQwUserList(this.queryParams.corpId);
       }
-      // 如果之前有选中的员工,自动重新查询客户列表
       if (this.queryParams.qwUserId) {
         this.handleQuery();
       }
     },
 
-    // ========== 原有方法,增强切换公司时的清理逻辑 ==========
     handleCorpChange(corpId) {
-      // 清空员工选择、客户列表和选中状态
+      if (!this.isInitialized) return;
+
       this.queryParams.qwUserId = null;
       this.selectedCustomerId = null;
       this.selectedCustomerAvatar = null;
       this.externalContactList = [];
       this.total = 0;
+
       if (corpId) {
         this.fetchQwUserList(corpId);
       } else {
         this.staffList = [];
       }
-      // 强制刷新右侧会话面板(通过改变 key 值)
+
       this.conversationPanelKey++;
     },
 
     async fetchQwUserList(corpId) {
       try {
-        const res = await listAllQwUserList({corpId});
-        if (res.code === 200) {
-          this.staffList = res.data || [];
+        const res = await listAllQwUserList({ corpId });
+        if (res.code === 200 && Array.isArray(res.data)) {
+          this.staffList = res.data;
         } else if (Array.isArray(res)) {
           this.staffList = res;
+        } else if (res.data && Array.isArray(res.data.list)) {
+          this.staffList = res.data.list;
         } else {
           this.staffList = [];
-          console.warn('获取员工列表失败', res.msg);
         }
       } catch (e) {
         console.error('获取员工列表异常', e);
@@ -207,8 +237,8 @@ export default {
 
     getAllUserlist(companyId) {
       if (companyId) {
-        getAllUserlist({companyId}).then(response => {
-          this.companyUserNameList = response.data;
+        getAllUserlist({ companyId }).then(response => {
+          this.companyUserNameList = response.data || [];
         });
       }
     },
@@ -220,8 +250,8 @@ export default {
       }
       this.loading = true;
       listExternalContact(this.queryParams).then(response => {
-        this.externalContactList = response.rows;
-        this.total = response.total;
+        this.externalContactList = response.rows || [];
+        this.total = response.total || 0;
         this.loading = false;
       }).catch(() => {
         this.loading = false;
@@ -251,7 +281,6 @@ export default {
   height: calc(100vh - 90px);
   overflow: hidden;
 }
-
 .left-panel {
   width: 460px;
   min-width: 400px;
@@ -261,7 +290,6 @@ export default {
   display: flex;
   flex-direction: column;
 }
-
 .left-panel-content {
   display: flex;
   flex-direction: column;
@@ -269,13 +297,11 @@ export default {
   padding: 16px;
   box-sizing: border-box;
 }
-
 .left-table-wrapper {
   flex: 1;
   overflow: hidden;
   margin-bottom: 12px;
 }
-
 .right-panel {
   flex: 1;
   display: flex;
@@ -283,4 +309,17 @@ export default {
   overflow: hidden;
   position: relative;
 }
+.empty-tip {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  color: #999;
+  font-size: 16px;
+}
+.empty-tip i {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
 </style>