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