|
|
@@ -1,11 +1,17 @@
|
|
|
<template>
|
|
|
<div class="conversation-panel">
|
|
|
<!-- 配置未加载 -->
|
|
|
- <div v-if="!configReady" class="loading-tip">
|
|
|
+ <div v-if="!configReady && !configError && !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>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 未登录 -->
|
|
|
<div v-else-if="!isLoggedIn" class="login-area">
|
|
|
<div class="login-tip">请先扫码登录企微</div>
|
|
|
@@ -46,7 +52,24 @@ import * as ww from '@wecom/jssdk';
|
|
|
import defaultStaffAvatar from '@/assets/images/user.png';
|
|
|
import { qwLogin, qwSignature, qwConversations, getQwSessionConfig } from '@/api/qw/companySession';
|
|
|
|
|
|
-const LOGIN_STORAGE_KEY = 'wecom_session_expire';
|
|
|
+// ==================== 全局缓存(挂载到 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分钟
|
|
|
+
|
|
|
+function getCachedConfig(corpId) {
|
|
|
+ if (!corpId) return null;
|
|
|
+ const cached = window._QW_CONFIG_CACHE.get(corpId);
|
|
|
+ if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
|
|
|
+ return cached.config;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+function setCachedConfig(corpId, config) {
|
|
|
+ if (!corpId) return;
|
|
|
+ window._QW_CONFIG_CACHE.set(corpId, { config, timestamp: Date.now() });
|
|
|
+}
|
|
|
|
|
|
export default {
|
|
|
name: 'ConversationPanel',
|
|
|
@@ -73,30 +96,51 @@ export default {
|
|
|
domain: ''
|
|
|
},
|
|
|
configReady: false,
|
|
|
- tokenCheckTimer: null // 声明定时器变量
|
|
|
+ configError: false,
|
|
|
+ configLoading: false,
|
|
|
+ tokenCheckTimer: null,
|
|
|
+ // 防止短时间内重复触发 logout 导致父组件频繁重建
|
|
|
+ lastLogoutTime: 0
|
|
|
};
|
|
|
},
|
|
|
watch: {
|
|
|
customerId(newId, oldId) {
|
|
|
- if (newId && newId !== oldId && this.isLoggedIn && this.staffUserId && this.configReady && this.corpId) {
|
|
|
+ if (newId && newId !== oldId && this.isLoggedIn && this.staffUserId && this.configReady && this.corpId && !this.configError) {
|
|
|
this.resetAndReload();
|
|
|
}
|
|
|
},
|
|
|
staffUserId(newStaffId, oldStaffId) {
|
|
|
- if (newStaffId && newStaffId !== oldStaffId && this.customerId && this.isLoggedIn && this.configReady && this.corpId) {
|
|
|
+ if (newStaffId && newStaffId !== oldStaffId && this.customerId && this.isLoggedIn && this.configReady && this.corpId && !this.configError) {
|
|
|
this.resetAndReload();
|
|
|
}
|
|
|
},
|
|
|
corpId(newId, oldId) {
|
|
|
- if (newId && newId !== oldId) {
|
|
|
+ if (newId !== oldId) {
|
|
|
this.destroyChat();
|
|
|
- this.clearLoginState();
|
|
|
- this.isLoggedIn = false;
|
|
|
this.isReady = false;
|
|
|
this._sdkInited = false;
|
|
|
- this.configReady = false;
|
|
|
- this.initConfig();
|
|
|
- this.handleAuthAndLoad();
|
|
|
+ this.msgList = [];
|
|
|
+ this.cursor = '';
|
|
|
+ this.hasMore = true;
|
|
|
+ this.loadingMore = false;
|
|
|
+
|
|
|
+ const cachedConfig = getCachedConfig(newId);
|
|
|
+ if (cachedConfig) {
|
|
|
+ this.config = cachedConfig;
|
|
|
+ this.configReady = true;
|
|
|
+ this.configError = false;
|
|
|
+ this.configLoading = false;
|
|
|
+ this.handleAuthAndLoad();
|
|
|
+ } else {
|
|
|
+ this.configReady = false;
|
|
|
+ this.configError = false;
|
|
|
+ this.configLoading = false;
|
|
|
+ this.initConfig().then(() => {
|
|
|
+ if (this.configReady && !this.configError) {
|
|
|
+ this.handleAuthAndLoad();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
},
|
|
|
customerAvatar: {
|
|
|
@@ -109,20 +153,23 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
async mounted() {
|
|
|
- await this.initConfig();
|
|
|
- await this.handleAuthAndLoad();
|
|
|
- console.log('configReady:', this.configReady);
|
|
|
- console.log('corpId:', this.corpId);
|
|
|
- // 每 1.5 小时检查一次登录态
|
|
|
- this.tokenCheckTimer = setInterval(async () => {
|
|
|
- if (this.isLoggedIn && this.configReady) {
|
|
|
- try {
|
|
|
- await this.getAgentConfigSignature();
|
|
|
- } catch (e) {
|
|
|
- // 签名失败会触发重新登录,清除定时器
|
|
|
- clearInterval(this.tokenCheckTimer);
|
|
|
- this.tokenCheckTimer = null;
|
|
|
- }
|
|
|
+ const cachedConfig = getCachedConfig(this.corpId);
|
|
|
+ if (cachedConfig) {
|
|
|
+ this.config = cachedConfig;
|
|
|
+ this.configReady = true;
|
|
|
+ this.configError = false;
|
|
|
+ this.configLoading = false;
|
|
|
+ await this.handleAuthAndLoad();
|
|
|
+ } else {
|
|
|
+ await this.initConfig();
|
|
|
+ if (this.configReady && !this.configError) {
|
|
|
+ await this.handleAuthAndLoad();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.tokenCheckTimer = setInterval(() => {
|
|
|
+ if (this.isLoggedIn && this.configReady && !this.configError && this._sdkInited) {
|
|
|
+ this.getAgentConfigSignature().catch(e => console.warn('签名刷新失败', e));
|
|
|
}
|
|
|
}, 90 * 60 * 1000);
|
|
|
},
|
|
|
@@ -134,68 +181,166 @@ export default {
|
|
|
this.destroyChat();
|
|
|
},
|
|
|
methods: {
|
|
|
- // ========== 配置初始化 ==========
|
|
|
+ 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);
|
|
|
+ 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());
|
|
|
+ },
|
|
|
+
|
|
|
+ clearLoginState(corpId) {
|
|
|
+ if (!corpId) return;
|
|
|
+ const expireKey = this.getStorageKey(corpId, 'expire');
|
|
|
+ localStorage.removeItem(expireKey);
|
|
|
+ },
|
|
|
+
|
|
|
async initConfig() {
|
|
|
if (!this.corpId) return;
|
|
|
- try {
|
|
|
- const configRes = await getQwSessionConfig(this.corpId);
|
|
|
- const configData = this._extractResponse(configRes);
|
|
|
- if (!configData.corpid || !configData.agentid) {
|
|
|
- throw new Error('配置数据不完整');
|
|
|
- }
|
|
|
- this.config = {
|
|
|
- corpid: configData.corpid,
|
|
|
- agentid: String(configData.agentid),
|
|
|
- domain: configData.domain || ''
|
|
|
- };
|
|
|
+ if (this.configReady) return;
|
|
|
+ if (this.configError) return;
|
|
|
+
|
|
|
+ const cached = getCachedConfig(this.corpId);
|
|
|
+ if (cached) {
|
|
|
+ this.config = cached;
|
|
|
this.configReady = true;
|
|
|
- } catch (e) {
|
|
|
- console.error('获取企微配置失败:', e);
|
|
|
- this.$message.error('获取企微配置失败,请刷新重试');
|
|
|
+ this.configError = false;
|
|
|
+ this.configLoading = false;
|
|
|
+ return;
|
|
|
}
|
|
|
+
|
|
|
+ // 全局并发锁
|
|
|
+ if (window._QW_CONFIG_PENDING.has(this.corpId)) {
|
|
|
+ return window._QW_CONFIG_PENDING.get(this.corpId);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.configLoading = true;
|
|
|
+ const requestPromise = (async () => {
|
|
|
+ try {
|
|
|
+ const configRes = await getQwSessionConfig(this.corpId);
|
|
|
+ const configData = this._extractResponse(configRes);
|
|
|
+ if (!configData.corpid || !configData.agentid) {
|
|
|
+ throw new Error('配置数据不完整');
|
|
|
+ }
|
|
|
+ const cfg = {
|
|
|
+ corpid: configData.corpid,
|
|
|
+ agentid: String(configData.agentid),
|
|
|
+ domain: configData.domain || ''
|
|
|
+ };
|
|
|
+ this.config = cfg;
|
|
|
+ this.configReady = true;
|
|
|
+ this.configError = false;
|
|
|
+ setCachedConfig(this.corpId, cfg);
|
|
|
+ } catch (e) {
|
|
|
+ console.error('获取企微配置失败:', e);
|
|
|
+ this.configReady = false;
|
|
|
+ this.configError = true;
|
|
|
+ } finally {
|
|
|
+ this.configLoading = false;
|
|
|
+ window._QW_CONFIG_PENDING.delete(this.corpId);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ window._QW_CONFIG_PENDING.set(this.corpId, requestPromise);
|
|
|
+ return requestPromise;
|
|
|
},
|
|
|
|
|
|
- // ========== 登录与授权 ==========
|
|
|
async handleAuthAndLoad() {
|
|
|
+ if (this.configError) return;
|
|
|
+ if (!this.configReady) {
|
|
|
+ await this.initConfig();
|
|
|
+ if (this.configError) 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);
|
|
|
- await this.handleLogin(code);
|
|
|
- this.storeLoginState();
|
|
|
- if (this.customerId && this.staffUserId && this.corpId) {
|
|
|
- await this.loadFirstPage();
|
|
|
+ try {
|
|
|
+ await this.handleLogin(code);
|
|
|
+ this.storeLoginState(this.corpId);
|
|
|
+ this.isLoggedIn = true;
|
|
|
+ if (this.customerId && this.staffUserId) {
|
|
|
+ await this.loadFirstPage();
|
|
|
+ }
|
|
|
+ } catch (loginErr) {
|
|
|
+ console.error('登录失败', loginErr);
|
|
|
+ this.clearLoginState(this.corpId);
|
|
|
+ this.isLoggedIn = false;
|
|
|
+ this.emitLogoutWithThrottle();
|
|
|
}
|
|
|
} else {
|
|
|
- if (this.checkLoginState()) {
|
|
|
+ if (this.checkLoginState(this.corpId)) {
|
|
|
this.isLoggedIn = true;
|
|
|
- if (this.customerId && this.staffUserId && this.corpId) {
|
|
|
+ if (this.customerId && this.staffUserId) {
|
|
|
await this.loadFirstPage();
|
|
|
}
|
|
|
} else {
|
|
|
- this.$nextTick(() => this.createLoginPanel());
|
|
|
+ this.isLoggedIn = false;
|
|
|
+ this.emitLogoutWithThrottle();
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.createLoginPanelWithRetry();
|
|
|
+ });
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
|
|
|
+ // 防止短时间内多次触发 logout 导致父组件疯狂重建
|
|
|
+ emitLogoutWithThrottle() {
|
|
|
+ const now = Date.now();
|
|
|
+ if (now - this.lastLogoutTime > 1000) {
|
|
|
+ this.lastLogoutTime = now;
|
|
|
+ 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 || '登录失败');
|
|
|
}
|
|
|
+ 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();
|
|
|
},
|
|
|
|
|
|
- createLoginPanel() {
|
|
|
- // const redirectUri = 'http://sestest.ylrzcloud.com' + '/companySale/companySession';
|
|
|
- // 若需动态获取,可使用:const redirectUri = window.location.href.split('?')[0].split('#')[0];
|
|
|
+ _doCreateLoginPanel(container) {
|
|
|
const redirectUri = window.location.href.split('?')[0].split('#')[0];
|
|
|
- console.log("重定向链接:" + redirectUri);
|
|
|
ww.createWWLoginPanel({
|
|
|
- el: document.getElementById('login-container'),
|
|
|
+ el: container,
|
|
|
params: {
|
|
|
login_type: 'CorpApp',
|
|
|
appid: this.config.corpid,
|
|
|
@@ -206,50 +351,37 @@ export default {
|
|
|
},
|
|
|
onLoginSuccess: async ({ code }) => {
|
|
|
await this.handleLogin(code);
|
|
|
- this.storeLoginState();
|
|
|
if (this.customerId && this.staffUserId && this.corpId) {
|
|
|
await this.loadFirstPage();
|
|
|
}
|
|
|
},
|
|
|
onLoginError: (err) => {
|
|
|
console.error('登录面板错误:', err);
|
|
|
- this.clearLoginState();
|
|
|
+ this.clearLoginState(this.corpId);
|
|
|
+ this.isLoggedIn = false;
|
|
|
+ this.emitLogoutWithThrottle();
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
|
|
|
- checkLoginState() {
|
|
|
- const stored = localStorage.getItem(LOGIN_STORAGE_KEY);
|
|
|
- if (!stored) return false;
|
|
|
- return Date.now() < parseInt(stored, 10);
|
|
|
- },
|
|
|
-
|
|
|
- storeLoginState() {
|
|
|
- localStorage.setItem(LOGIN_STORAGE_KEY, (Date.now() + 30 * 60 * 1000).toString());
|
|
|
- },
|
|
|
-
|
|
|
- clearLoginState() {
|
|
|
- localStorage.removeItem(LOGIN_STORAGE_KEY);
|
|
|
- },
|
|
|
-
|
|
|
- // ========== SDK 与签名 ==========
|
|
|
async getAgentConfigSignature() {
|
|
|
+ if (!this.configReady || this.configError) {
|
|
|
+ throw new Error('配置未就绪,无法获取签名');
|
|
|
+ }
|
|
|
const currentUrl = window.location.href.split('#')[0];
|
|
|
- console.log('请求签名的 URL:', currentUrl);
|
|
|
const res = await qwSignature({ url: currentUrl, corpid: this.corpId });
|
|
|
const data = this._extractResponse(res);
|
|
|
- console.log('签名响应:', data);
|
|
|
- // 如果有 errcode 且不为 0,则认为失败
|
|
|
if (data.errcode && data.errcode !== 0) {
|
|
|
- console.error('获取签名失败,将重新登录', data);
|
|
|
- this.clearLoginState();
|
|
|
+ console.error('获取签名失败,将清除登录状态', data);
|
|
|
+ this.clearLoginState(this.corpId);
|
|
|
this.isLoggedIn = false;
|
|
|
this.isReady = false;
|
|
|
this.destroyChat();
|
|
|
- this.$nextTick(() => this.createLoginPanel());
|
|
|
+ if (!this.configError) {
|
|
|
+ this.emitLogoutWithThrottle();
|
|
|
+ }
|
|
|
throw new Error(`签名失败: ${data.errmsg}`);
|
|
|
}
|
|
|
- // 否则认为成功
|
|
|
return {
|
|
|
timestamp: data.timestamp,
|
|
|
nonceStr: data.nonceStr,
|
|
|
@@ -259,6 +391,10 @@ export default {
|
|
|
|
|
|
async initSDKOnce() {
|
|
|
if (this._sdkInited) return true;
|
|
|
+ if (!this.configReady || this.configError) {
|
|
|
+ console.warn('SDK初始化失败:配置未就绪');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
try {
|
|
|
await ww.register({
|
|
|
corpId: this.config.corpid,
|
|
|
@@ -275,8 +411,11 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // ========== 数据拉取 ==========
|
|
|
async loadFirstPage() {
|
|
|
+ if (this.configError) {
|
|
|
+ this.$message.error('企微配置错误,无法加载会话');
|
|
|
+ return;
|
|
|
+ }
|
|
|
try {
|
|
|
const res = await qwConversations({
|
|
|
customerId: this.customerId,
|
|
|
@@ -306,7 +445,7 @@ export default {
|
|
|
},
|
|
|
|
|
|
async loadMore() {
|
|
|
- if (!this.cursor || this.loadingMore || !this.hasMore) return;
|
|
|
+ if (!this.cursor || this.loadingMore || !this.hasMore || this.configError) return;
|
|
|
this.loadingMore = true;
|
|
|
try {
|
|
|
const res = await qwConversations({
|
|
|
@@ -326,7 +465,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();
|
|
|
}
|
|
|
@@ -341,15 +480,15 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // ========== 组件渲染与更新 ==========
|
|
|
async renderOrUpdateChat() {
|
|
|
if (this.msgList.length === 0) return;
|
|
|
+ if (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;
|
|
|
}
|
|
|
|
|
|
@@ -373,7 +512,8 @@ export default {
|
|
|
<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; font-weight:500;">
|
|
|
{{ msg.send_time_str }}
|
|
|
</text>
|
|
|
</view>
|
|
|
@@ -397,7 +537,7 @@ export default {
|
|
|
data: templateData,
|
|
|
methods: {
|
|
|
onScrollToLower: () => {
|
|
|
- if (this.hasMore && !this.loadingMore) {
|
|
|
+ if (this.hasMore && !this.loadingMore && !this.configError) {
|
|
|
this.loadMore();
|
|
|
}
|
|
|
}
|
|
|
@@ -405,37 +545,91 @@ export default {
|
|
|
error: (e) => {
|
|
|
console.error('[企微组件] 错误', e);
|
|
|
if (e && (e.errCode === 42006 || e.errCode === 42003 || e.errCode === 40029)) {
|
|
|
- this.clearLoginState();
|
|
|
- this.isLoggedIn = false;
|
|
|
- this.isReady = false;
|
|
|
- this.destroyChat();
|
|
|
- this.$nextTick(() => this.createLoginPanel());
|
|
|
+ if (!this.configError) {
|
|
|
+ this.clearLoginState(this.corpId);
|
|
|
+ this.isLoggedIn = false;
|
|
|
+ this.isReady = false;
|
|
|
+ this.destroyChat();
|
|
|
+ this.emitLogoutWithThrottle();
|
|
|
+ this.$nextTick(() => this.createLoginPanelWithRetry());
|
|
|
+ } else {
|
|
|
+ console.warn('登录态失效,但由于配置错误,不自动重新登录');
|
|
|
+ }
|
|
|
}
|
|
|
},
|
|
|
- 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';
|
|
|
+
|
|
|
+ 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)';
|
|
|
+
|
|
|
+ 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();
|
|
|
+ mask.remove();
|
|
|
+ document.removeEventListener('keydown', escHandler);
|
|
|
+ };
|
|
|
+
|
|
|
const iframe = document.createElement('iframe');
|
|
|
iframe.src = modalUrl;
|
|
|
- iframe.style.position = 'fixed';
|
|
|
- iframe.style.top = '50%';
|
|
|
- iframe.style.left = '50%';
|
|
|
- iframe.style.transform = 'translate(-50%, -50%)';
|
|
|
- iframe.style.width = modalSize?.width ? `${modalSize.width}px` : '80%';
|
|
|
- iframe.style.height = modalSize?.height ? `${modalSize.height}px` : '80%';
|
|
|
- iframe.style.maxWidth = '90vw';
|
|
|
- iframe.style.maxHeight = '90vh';
|
|
|
- iframe.style.zIndex = 10000;
|
|
|
+ iframe.style.width = '100%';
|
|
|
+ iframe.style.height = '100%';
|
|
|
iframe.style.border = 'none';
|
|
|
- iframe.style.boxShadow = '0 0 20px rgba(0,0,0,0.3)';
|
|
|
- iframe.style.backgroundColor = '#fff';
|
|
|
- document.body.appendChild(iframe);
|
|
|
- iframe.onclick = () => iframe.remove();
|
|
|
- const onKeyDown = (e) => {
|
|
|
+
|
|
|
+ content.appendChild(iframe);
|
|
|
+ content.appendChild(closeBtn);
|
|
|
+ mask.appendChild(content);
|
|
|
+ document.body.appendChild(mask);
|
|
|
+
|
|
|
+ mask.onclick = (e) => {
|
|
|
+ if (e.target === mask) {
|
|
|
+ mask.remove();
|
|
|
+ document.removeEventListener('keydown', escHandler);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const escHandler = (e) => {
|
|
|
if (e.key === 'Escape') {
|
|
|
- iframe.remove();
|
|
|
- document.removeEventListener('keydown', onKeyDown);
|
|
|
+ mask.remove();
|
|
|
+ document.removeEventListener('keydown', escHandler);
|
|
|
}
|
|
|
};
|
|
|
- document.addEventListener('keydown', onKeyDown);
|
|
|
+ document.addEventListener('keydown', escHandler);
|
|
|
+
|
|
|
return true;
|
|
|
}
|
|
|
});
|
|
|
@@ -471,6 +665,7 @@ export default {
|
|
|
}
|
|
|
};
|
|
|
</script>
|
|
|
+
|
|
|
<style>
|
|
|
#chat-container,
|
|
|
#chat-container > div,
|