ConversationPanelPure.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. <template>
  2. <div class="conversation-panel">
  3. <!-- 配置加载中 -->
  4. <div v-if="configLoading" class="loading-tip">
  5. <i class="el-icon-loading"></i>
  6. <p>正在加载配置...</p>
  7. </div>
  8. <!-- 配置加载失败 / 无配置 -->
  9. <div v-else-if="configError" class="empty-tip">
  10. <i class="el-icon-warning"></i>
  11. <p>{{ configErrorMsg || '企微未配置,请先配置企微应用' }}</p>
  12. </div>
  13. <!-- 未登录 -->
  14. <div v-else-if="!isLoggedIn" class="login-area">
  15. <div class="login-tip">请扫码登录企微</div>
  16. <div id="login-container" ref="loginContainer" class="login-container"></div>
  17. </div>
  18. <!-- 已登录但未选择员工(群聊不需要员工) -->
  19. <div v-else-if="!staffUserId && !chatId" class="empty-tip">
  20. <i class="el-icon-info"></i>
  21. <p>请选择员工</p>
  22. </div>
  23. <!-- 已登录但未选择客户(群聊不需要客户) -->
  24. <div v-else-if="!customerId && !chatId" class="empty-tip">
  25. <i class="el-icon-info"></i>
  26. <p>请选择客户查看会话</p>
  27. </div>
  28. <!-- 会话加载中 -->
  29. <div v-else-if="!isReady" class="loading-tip">
  30. <i class="el-icon-loading"></i>
  31. <p>正在加载会话记录...</p>
  32. </div>
  33. <!-- 会话为空 -->
  34. <div v-else-if="msgList.length === 0" class="empty-tip">
  35. <i class="el-icon-info"></i>
  36. <p>暂无会话数据</p>
  37. </div>
  38. <!-- 聊天窗口 -->
  39. <div v-else id="chat-container" ref="chatContainer"></div>
  40. </div>
  41. </template>
  42. <script>
  43. import * as ww from '@wecom/jssdk';
  44. import { qwLogin, qwSignature, qwConversations, getQwSessionConfig } from '@/api/qw/companySession';
  45. // ========== 全局配置缓存 ==========
  46. window._QW_CONFIG_CACHE = window._QW_CONFIG_CACHE || new Map();
  47. window._QW_CONFIG_PENDING = window._QW_CONFIG_PENDING || new Map();
  48. const CACHE_TTL = 5 * 60 * 1000;
  49. function getCachedConfig(corpId) {
  50. if (!corpId) return null;
  51. const cached = window._QW_CONFIG_CACHE.get(corpId);
  52. if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
  53. return cached.config;
  54. }
  55. window._QW_CONFIG_CACHE.delete(corpId);
  56. return null;
  57. }
  58. function setCachedConfig(corpId, config) {
  59. if (!corpId) return;
  60. window._QW_CONFIG_CACHE.set(corpId, { config, timestamp: Date.now() });
  61. }
  62. export default {
  63. name: 'ConversationPanelPure',
  64. props: {
  65. corpId: { type: String, default: null },
  66. customerId: { type: String, default: null },
  67. staffUserId: { type: String, default: null },
  68. chatId: { type: String, default: null }
  69. },
  70. data() {
  71. return {
  72. isLoggedIn: false,
  73. isReady: false,
  74. msgList: [],
  75. cursor: '',
  76. hasMore: true,
  77. loadingMore: false,
  78. chatInstance: null,
  79. _sdkInited: false,
  80. config: { corpid: '', agentid: '', domain: '' },
  81. configReady: false,
  82. configError: false,
  83. configLoading: false,
  84. configErrorMsg: '',
  85. tokenCheckTimer: null,
  86. _hasEmittedLogout: false,
  87. _loginPanelCreated: false,
  88. _loginPanelTimer: null
  89. };
  90. },
  91. watch: {
  92. customerId(newId, oldId) {
  93. if (newId && newId !== oldId && this.isLoggedIn && (this.staffUserId || this.chatId) && this.configReady && !this.configError) {
  94. this.resetAndReload();
  95. }
  96. },
  97. staffUserId(newVal, oldVal) {
  98. if (newVal && newVal !== oldVal && this.customerId && this.isLoggedIn && this.configReady && !this.configError) {
  99. this.resetAndReload();
  100. }
  101. },
  102. chatId(newVal, oldVal) {
  103. if (newVal && newVal !== oldVal && this.isLoggedIn && this.configReady && !this.configError) {
  104. this.resetAndReload();
  105. }
  106. },
  107. corpId(newId, oldId) {
  108. if (newId !== oldId) {
  109. this.destroyChat();
  110. this.isReady = false;
  111. this._sdkInited = false;
  112. this._hasEmittedLogout = false;
  113. this._loginPanelCreated = false;
  114. if (this._loginPanelTimer) clearTimeout(this._loginPanelTimer);
  115. this.msgList = [];
  116. this.cursor = '';
  117. this.hasMore = true;
  118. this.loadingMore = false;
  119. this.isLoggedIn = false;
  120. const cached = getCachedConfig(newId);
  121. if (cached) {
  122. this.config = cached;
  123. this.configReady = true;
  124. this.configError = false;
  125. this.configLoading = false;
  126. this.handleAuthAndLoad();
  127. } else {
  128. this.configReady = false;
  129. this.configError = false;
  130. this.configLoading = false;
  131. this.initConfig().then(() => {
  132. if (this.configReady && !this.configError) this.handleAuthAndLoad();
  133. });
  134. }
  135. }
  136. },
  137. isLoggedIn: {
  138. handler(newVal) {
  139. if (!newVal && this.configReady && !this.configError && this._sdkInited && !this._loginPanelCreated) {
  140. this.tryCreateLoginPanel();
  141. }
  142. },
  143. immediate: true
  144. }
  145. },
  146. async mounted() {
  147. this._hasEmittedLogout = false;
  148. this._loginPanelCreated = false;
  149. const cached = getCachedConfig(this.corpId);
  150. if (cached) {
  151. this.config = cached;
  152. this.configReady = true;
  153. this.configError = false;
  154. this.configLoading = false;
  155. await this.handleAuthAndLoad();
  156. } else {
  157. await this.initConfig();
  158. if (this.configReady && !this.configError) await this.handleAuthAndLoad();
  159. }
  160. this.tokenCheckTimer = setInterval(() => {
  161. if (this.isLoggedIn && this.configReady && !this.configError && this._sdkInited) {
  162. this.getAgentConfigSignature().catch(e => console.warn('签名刷新失败', e));
  163. }
  164. }, 90 * 60 * 1000);
  165. },
  166. beforeDestroy() {
  167. if (this.tokenCheckTimer) clearInterval(this.tokenCheckTimer);
  168. if (this._loginPanelTimer) clearTimeout(this._loginPanelTimer);
  169. this.destroyChat();
  170. },
  171. methods: {
  172. // ========== Storage Key ==========
  173. getStorageKey(corpId, suffix = 'expire') {
  174. return `wecom_session_${corpId}_${suffix}`;
  175. },
  176. checkLoginState(corpId) {
  177. const expire = localStorage.getItem(this.getStorageKey(corpId, 'expire'));
  178. return expire && Date.now() < parseInt(expire, 10);
  179. },
  180. storeLoginState(corpId) {
  181. localStorage.setItem(this.getStorageKey(corpId, 'expire'), String(Date.now() + 115 * 60 * 1000));
  182. },
  183. clearLoginState(corpId) {
  184. localStorage.removeItem(this.getStorageKey(corpId, 'expire'));
  185. },
  186. // ========== 配置加载 ==========
  187. async initConfig() {
  188. if (!this.corpId || this.configReady || this.configError) return;
  189. const cached = getCachedConfig(this.corpId);
  190. if (cached) {
  191. this.config = cached;
  192. this.configReady = true;
  193. this.configError = false;
  194. this.configLoading = false;
  195. return;
  196. }
  197. if (window._QW_CONFIG_PENDING.has(this.corpId)) {
  198. return window._QW_CONFIG_PENDING.get(this.corpId);
  199. }
  200. this.configLoading = true;
  201. const promise = (async () => {
  202. try {
  203. const res = await getQwSessionConfig(this.corpId);
  204. let configData = null;
  205. if (res?.code === 200 && res.data?.corpid) configData = res.data;
  206. else if (res?.data?.corpid) configData = res.data;
  207. else if (res?.corpid) configData = res;
  208. if (!configData?.corpid || !configData?.agentid) throw new Error('配置数据不完整');
  209. const cfg = {
  210. corpid: configData.corpid,
  211. agentid: String(configData.agentid),
  212. domain: configData.domain || ''
  213. };
  214. this.config = cfg;
  215. this.configReady = true;
  216. this.configError = false;
  217. setCachedConfig(this.corpId, cfg);
  218. } catch (e) {
  219. alert("请更换企微主体!!!")
  220. console.error('[ConversationPanel] 获取配置失败:', e);
  221. this.configReady = false;
  222. this.configError = true;
  223. this.configErrorMsg = e.message || '获取配置失败';
  224. } finally {
  225. this.configLoading = false;
  226. window._QW_CONFIG_PENDING.delete(this.corpId);
  227. }
  228. })();
  229. window._QW_CONFIG_PENDING.set(this.corpId, promise);
  230. return promise;
  231. },
  232. // ========== 认证与加载 ==========
  233. async handleAuthAndLoad() {
  234. if (this.configError) return;
  235. if (!this.configReady) {
  236. await this.initConfig();
  237. if (this.configError) return;
  238. }
  239. const sdkOk = await this.initSDKOnce();
  240. if (!sdkOk) return;
  241. const urlParams = new URLSearchParams(window.location.search);
  242. const code = urlParams.get('code');
  243. if (code) {
  244. window.history.replaceState({}, '', window.location.origin + window.location.pathname);
  245. try {
  246. await this.handleLogin(code);
  247. this.storeLoginState(this.corpId);
  248. this.isLoggedIn = true;
  249. if ((this.customerId && this.staffUserId) || this.chatId) await this.loadFirstPage();
  250. } catch (err) {
  251. this.clearLoginState(this.corpId);
  252. this.isLoggedIn = false;
  253. this.safeEmitLogout();
  254. }
  255. } else {
  256. if (this.checkLoginState(this.corpId)) {
  257. this.isLoggedIn = true;
  258. if ((this.customerId && this.staffUserId) || this.chatId) await this.loadFirstPage();
  259. } else {
  260. this.isLoggedIn = false;
  261. this.safeEmitLogout();
  262. this.tryCreateLoginPanel();
  263. }
  264. }
  265. },
  266. tryCreateLoginPanel() {
  267. if (this._loginPanelCreated || !this.configReady || this.configError || !this._sdkInited || this.isLoggedIn) return;
  268. if (this._loginPanelTimer) clearTimeout(this._loginPanelTimer);
  269. this.$nextTick(() => this.createLoginPanel());
  270. },
  271. createLoginPanel() {
  272. if (this._loginPanelCreated || !this.configReady || this.configError || !this._sdkInited || this.isLoggedIn) return;
  273. let container = this.$refs.loginContainer || document.getElementById('login-container');
  274. if (!container) {
  275. this._loginPanelTimer = setTimeout(() => this.createLoginPanel(), 1000);
  276. return;
  277. }
  278. if (container.getBoundingClientRect().width === 0 || container.getBoundingClientRect().height === 0) {
  279. this._loginPanelTimer = setTimeout(() => this.createLoginPanel(), 500);
  280. return;
  281. }
  282. container.innerHTML = '';
  283. this._loginPanelCreated = true;
  284. const redirectUri = window.location.origin + window.location.pathname;
  285. try {
  286. ww.createWWLoginPanel({
  287. el: container,
  288. params: {
  289. login_type: 'CorpApp',
  290. appid: this.config.corpid,
  291. agentid: this.config.agentid,
  292. redirect_uri: redirectUri,
  293. redirect_type: 'callback',
  294. state: 'state_' + Date.now()
  295. },
  296. onLoginSuccess: async ({ code }) => {
  297. try {
  298. await this.handleLogin(code);
  299. if ((this.customerId && this.staffUserId) || this.chatId) await this.loadFirstPage();
  300. } catch (e) {
  301. this.clearLoginState(this.corpId);
  302. this.isLoggedIn = false;
  303. this._loginPanelCreated = false;
  304. this.safeEmitLogout();
  305. }
  306. },
  307. onLoginError: () => {
  308. this._loginPanelCreated = false;
  309. this._loginPanelTimer = setTimeout(() => this.createLoginPanel(), 3000);
  310. }
  311. });
  312. } catch (e) {
  313. this._loginPanelCreated = false;
  314. this._loginPanelTimer = setTimeout(() => this.createLoginPanel(), 3000);
  315. }
  316. },
  317. safeEmitLogout() {
  318. if (this._hasEmittedLogout) return;
  319. this._hasEmittedLogout = true;
  320. this.$emit('logout');
  321. },
  322. async handleLogin(code) {
  323. const res = await qwLogin({ code, corpid: this.corpId });
  324. const data = this._extractResponse(res);
  325. if (data.errcode !== 0) throw new Error(data.errmsg || '登录失败');
  326. this.storeLoginState(this.corpId);
  327. this.isLoggedIn = true;
  328. this.$emit('login-success');
  329. },
  330. async getAgentConfigSignature() {
  331. if (!this.configReady || this.configError) throw new Error('配置未就绪');
  332. const currentUrl = window.location.href.split('#')[0];
  333. const res = await qwSignature({ url: currentUrl, corpid: this.corpId });
  334. const data = this._extractResponse(res);
  335. if (data.errcode && data.errcode !== 0) {
  336. this.clearLoginState(this.corpId);
  337. this.isLoggedIn = false;
  338. this.isReady = false;
  339. this.destroyChat();
  340. this._loginPanelCreated = false;
  341. if (!this.configError) this.safeEmitLogout();
  342. throw new Error(`签名失败: ${data.errmsg}`);
  343. }
  344. return { timestamp: data.timestamp, nonceStr: data.nonceStr, signature: data.signature };
  345. },
  346. async initSDKOnce() {
  347. if (this._sdkInited) return true;
  348. if (!this.configReady || this.configError) return false;
  349. try {
  350. await ww.register({
  351. corpId: this.config.corpid,
  352. agentId: this.config.agentid,
  353. jsApiList: ['selectExternalContact', 'shareAppMessage', 'wwapp.invokeJsApiByCallInfo'],
  354. getAgentConfigSignature: () => this.getAgentConfigSignature()
  355. });
  356. await ww.initOpenData();
  357. this._sdkInited = true;
  358. return true;
  359. } catch (e) {
  360. console.error('SDK初始化失败', e);
  361. return false;
  362. }
  363. },
  364. async loadFirstPage() {
  365. if (this.configError) return;
  366. try {
  367. const res = await qwConversations({
  368. customerId: this.customerId,
  369. staffUserId: this.staffUserId,
  370. limit: 100,
  371. cursor: this.cursor,
  372. corpid: this.corpId,
  373. chatId: this.chatId || undefined
  374. });
  375. const data = this._extractResponse(res);
  376. if (data.errcode !== 0) throw new Error(data.errmsg || '拉取失败');
  377. const rawList = data.data || [];
  378. this.msgList = this.processMessages(rawList, false);
  379. this.cursor = data.next_cursor || '';
  380. this.hasMore = data.has_more === 1;
  381. this.isReady = true;
  382. await this.$nextTick();
  383. if (this.msgList.length) await this.renderOrUpdateChat();
  384. } catch (e) {
  385. console.error('加载第一页失败', e);
  386. this.$message.error('加载会话失败:' + e.message);
  387. this.isReady = true;
  388. this.msgList = [];
  389. }
  390. },
  391. async loadMore() {
  392. if (!this.cursor || this.loadingMore || !this.hasMore || this.configError) return;
  393. this.loadingMore = true;
  394. try {
  395. const res = await qwConversations({
  396. customerId: this.customerId,
  397. staffUserId: this.staffUserId,
  398. limit: 50,
  399. cursor: this.cursor,
  400. corpid: this.corpId,
  401. chatId: this.chatId || undefined
  402. });
  403. const data = this._extractResponse(res);
  404. if (data.errcode !== 0) throw new Error(data.errmsg || '拉取更多失败');
  405. const newMessages = data.data || [];
  406. if (newMessages.length) {
  407. const processed = this.processMessages(newMessages, true);
  408. this.msgList.push(...processed);
  409. this.cursor = data.next_cursor || '';
  410. this.hasMore = data.has_more === 1;
  411. if (this.chatInstance) this.chatInstance.setData({ msgList: this.msgList });
  412. else await this.renderOrUpdateChat();
  413. } else {
  414. this.hasMore = false;
  415. }
  416. } catch (e) {
  417. console.error('加载更多失败', e);
  418. } finally {
  419. this.loadingMore = false;
  420. }
  421. },
  422. processMessages(messages, isAppend = false) {
  423. const prevLastMsg = isAppend && this.msgList.length > 0
  424. ? this.msgList[this.msgList.length - 1]
  425. : null
  426. let prevDate = prevLastMsg ? (prevLastMsg.dateStr || '') : ''
  427. return messages.map(msg => {
  428. const timeStr = msg.send_time_str || ''
  429. const parts = timeStr.split(' ')
  430. const dateStr = parts[0] || ''
  431. const displayTime = parts[1] ? parts[1].substring(0, 5) : ''
  432. const showDate = !!dateStr && dateStr !== prevDate
  433. prevDate = dateStr
  434. return {
  435. ...msg,
  436. dateStr,
  437. displayTime,
  438. showDate
  439. }
  440. })
  441. },
  442. async renderOrUpdateChat() {
  443. if (this.msgList.length === 0 || this.configError) return;
  444. const container = document.getElementById('chat-container');
  445. if (!container) return;
  446. if (this.chatInstance) {
  447. this.chatInstance.setData({ msgList: this.msgList });
  448. return;
  449. }
  450. const ok = await this.initSDKOnce();
  451. if (!ok) return;
  452. const factory = ww.createOpenDataFrameFactory();
  453. if (!factory) return;
  454. this.chatInstance = factory.createOpenDataFrame({
  455. el: container,
  456. template: `
  457. <scroll-view scroll-y="{{true}}" bindscrolltolower="onScrollToLower" style="height: 100%;">
  458. <view wx:for="{{data.msgList}}" wx:for-item="msg" wx:key="msgid" style="margin-bottom: 15px;">
  459. <!-- 日期分隔线:仅跨天时显示 -->
  460. <view wx:if="{{msg.showDate}}" style="text-align: center; margin-bottom: 12px;">
  461. <span style="background:#e9e9e9; color:#888; padding:3px 12px; border-radius:12px; font-size:11px;">{{ msg.dateStr }}</span>
  462. </view>
  463. <!-- 客户/机器人消息(左对齐) -->
  464. <view wx:if="{{msg.sender.type == 2 || msg.sender.type == 3}}" style="display:flex; flex-direction:row; align-items:flex-start;">
  465. <ww-open-data wx:if="{{msg.sender.type == 2}}" type="externalUserAvatar" openid="{{msg.sender.id}}" class="msg-avatar-left" />
  466. <ww-open-data wx:if="{{msg.sender.type == 3}}" type="userAvatar" openid="{{msg.sender.id}}" class="msg-avatar-left" />
  467. <view style="display:flex; flex-direction:column; max-width:70%; margin-left:10px;">
  468. <view style="display:flex; align-items:center; margin-bottom:4px;">
  469. <ww-open-data wx:if="{{msg.sender.type == 2}}" type="externalUserName" openid="{{msg.sender.id}}" class="sender-name" />
  470. <ww-open-data wx:if="{{msg.sender.type == 3}}" type="userName" openid="{{msg.sender.id}}" class="sender-name" />
  471. <span style="font-size:11px;color:#999;margin-left:8px;">{{ msg.displayTime }}</span>
  472. </view>
  473. <view class="msg-bubble msg-bubble-left">
  474. <ww-open-message message-id="{{msg.msgid}}" secret-key="{{msg.secretKey}}" open-type="viewMessage"/>
  475. </view>
  476. </view>
  477. </view>
  478. <!-- 员工消息(右对齐) -->
  479. <view wx:if="{{msg.sender.type == 1}}" style="display:flex; flex-direction:row; justify-content:flex-end; align-items:flex-start;">
  480. <view style="display:flex; flex-direction:column; align-items:flex-end; max-width:70%; margin-right:10px;">
  481. <view style="display:flex; align-items:center; margin-bottom:4px;">
  482. <span style="font-size:11px;color:#999;margin-right:8px;">{{ msg.displayTime }}</span>
  483. <ww-open-data type="userName" openid="{{msg.sender.id}}" class="sender-name" />
  484. </view>
  485. <view class="msg-bubble msg-bubble-right">
  486. <ww-open-message message-id="{{msg.msgid}}" secret-key="{{msg.secretKey}}" open-type="viewMessage"/>
  487. </view>
  488. </view>
  489. <ww-open-data type="userAvatar" openid="{{msg.sender.id}}" class="msg-avatar-right" />
  490. </view>
  491. <!-- 未知类型消息(居中,无头像) -->
  492. <view wx:if="{{msg.sender.type != 1 && msg.sender.type != 2 && msg.sender.type != 3}}" style="display:flex; justify-content:center;">
  493. <view class="msg-bubble msg-bubble-left">
  494. <ww-open-message message-id="{{msg.msgid}}" secret-key="{{msg.secretKey}}" open-type="viewMessage"/>
  495. </view>
  496. </view>
  497. </view>
  498. <view wx:if="{{data.hasMore && data.loadingMore}}" style="text-align:center; padding:10px; color:#999;">加载更多...</view>
  499. </scroll-view>
  500. `,
  501. style: `
  502. .msg-avatar-left {
  503. width: 36px;
  504. height: 36px;
  505. border-radius: 4px;
  506. flex-shrink: 0;
  507. }
  508. .msg-avatar-right {
  509. width: 36px;
  510. height: 36px;
  511. border-radius: 4px;
  512. flex-shrink: 0;
  513. }
  514. .sender-name {
  515. font-size: 12px;
  516. color: #666;
  517. max-width: 120px;
  518. overflow: hidden;
  519. text-overflow: ellipsis;
  520. white-space: nowrap;
  521. }
  522. .msg-bubble {
  523. padding: 10px 14px;
  524. border-radius: 8px;
  525. word-break: break-all;
  526. display: flex;
  527. align-items: center;
  528. }
  529. .msg-bubble-left {
  530. background: #ffffff;
  531. box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  532. }
  533. .msg-bubble-right {
  534. background: #95ec69;
  535. box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  536. }
  537. `,
  538. data: {
  539. msgList: this.msgList,
  540. hasMore: this.hasMore,
  541. loadingMore: this.loadingMore
  542. },
  543. methods: {
  544. onScrollToLower: () => {
  545. if (this.hasMore && !this.loadingMore && !this.configError) this.loadMore();
  546. }
  547. },
  548. error: (e) => {
  549. console.error('[企微组件] 错误', e);
  550. if (e && [42006, 42003, 40029].includes(e.errCode)) {
  551. if (!this.configError) {
  552. this.clearLoginState(this.corpId);
  553. this.isLoggedIn = false;
  554. this.isReady = false;
  555. this.destroyChat();
  556. this._loginPanelCreated = false;
  557. this.safeEmitLogout();
  558. }
  559. }
  560. },
  561. handleModal: ({ modalUrl, modalSize }) => {
  562. const mask = document.createElement('div');
  563. 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;';
  564. const content = document.createElement('div');
  565. 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;`;
  566. const closeBtn = document.createElement('button');
  567. closeBtn.innerText = '✕';
  568. 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;';
  569. const iframe = document.createElement('iframe');
  570. iframe.src = modalUrl;
  571. iframe.style.cssText = 'width:100%;height:100%;border:none;';
  572. const cleanup = () => {
  573. mask.remove();
  574. document.removeEventListener('keydown', escHandler);
  575. };
  576. closeBtn.onclick = (e) => { e.stopPropagation(); cleanup(); };
  577. mask.onclick = (e) => { if (e.target === mask) cleanup(); };
  578. const escHandler = (e) => { if (e.key === 'Escape') cleanup(); };
  579. document.addEventListener('keydown', escHandler);
  580. content.appendChild(iframe);
  581. content.appendChild(closeBtn);
  582. mask.appendChild(content);
  583. document.body.appendChild(mask);
  584. return true;
  585. }
  586. });
  587. },
  588. async resetAndReload() {
  589. this.destroyChat();
  590. this.msgList = [];
  591. this.cursor = '';
  592. this.hasMore = true;
  593. this.loadingMore = false;
  594. this.isReady = false;
  595. await this.loadFirstPage();
  596. },
  597. destroyChat() {
  598. if (this.chatInstance) this.chatInstance = null;
  599. const container = document.getElementById('chat-container');
  600. if (container) container.innerHTML = '';
  601. },
  602. _extractResponse(res) {
  603. if (!res) return {};
  604. if (res.code !== undefined) return res;
  605. if (res.errcode !== undefined || res.timestamp || res.corpid || res.data) return res;
  606. if (res.data?.data) return res.data.data;
  607. return res;
  608. }
  609. }
  610. };
  611. </script>
  612. <style scoped>
  613. .conversation-panel {
  614. width: 100%;
  615. height: 100%;
  616. display: flex;
  617. flex-direction: column;
  618. background: #f5f6f7;
  619. position: relative;
  620. }
  621. .login-area, .empty-tip, .loading-tip {
  622. display: flex;
  623. flex-direction: column;
  624. align-items: center;
  625. justify-content: center;
  626. height: 100%;
  627. flex: 1;
  628. }
  629. .login-tip {
  630. font-size: 18px;
  631. color: #666;
  632. margin-bottom: 24px;
  633. }
  634. .empty-tip, .loading-tip {
  635. color: #999;
  636. font-size: 16px;
  637. }
  638. .empty-tip i, .loading-tip i {
  639. font-size: 48px;
  640. margin-bottom: 16px;
  641. }
  642. #chat-container {
  643. flex: 1;
  644. width: 100%;
  645. height: 100%;
  646. background: #f0f2f5;
  647. }
  648. .login-container {
  649. width: 320px;
  650. height: 380px;
  651. display: flex;
  652. align-items: center;
  653. justify-content: center;
  654. }
  655. </style>
  656. <style>
  657. #chat-container,
  658. #chat-container > div,
  659. #chat-container iframe {
  660. width: 100% !important;
  661. height: 100% !important;
  662. min-width: 100% !important;
  663. min-height: 100% !important;
  664. overflow: hidden;
  665. }
  666. </style>