|
|
@@ -36,8 +36,8 @@
|
|
|
<p>暂无数据</p>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 聊天窗口 -->
|
|
|
- <div v-else id="chat-container"></div>
|
|
|
+ <!-- 聊天窗口容器 -->
|
|
|
+ <div v-else id="chat-container" ref="chatContainer"></div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -60,15 +60,16 @@ export default {
|
|
|
isLoggedIn: false,
|
|
|
isReady: false,
|
|
|
msgList: [],
|
|
|
+ cursor: '',
|
|
|
+ hasMore: true,
|
|
|
+ loadingMore: false,
|
|
|
chatInstance: null,
|
|
|
_sdkInited: false,
|
|
|
defaultStaffAvatar: defaultStaffAvatar,
|
|
|
- // 从后台获取的企微配置
|
|
|
config: {
|
|
|
corpid: '',
|
|
|
agentid: '',
|
|
|
- agentSecret: '',
|
|
|
- domain:''
|
|
|
+ domain: ''
|
|
|
},
|
|
|
configReady: false,
|
|
|
};
|
|
|
@@ -76,98 +77,76 @@ export default {
|
|
|
watch: {
|
|
|
customerId(newId, oldId) {
|
|
|
if (newId && newId !== oldId && this.isLoggedIn && this.staffUserId && this.configReady) {
|
|
|
- this.reloadChat();
|
|
|
+ this.resetAndReload();
|
|
|
}
|
|
|
},
|
|
|
staffUserId(newStaffId, oldStaffId) {
|
|
|
if (newStaffId && newStaffId !== oldStaffId && this.customerId && this.isLoggedIn && this.configReady) {
|
|
|
- this.reloadChat();
|
|
|
+ this.resetAndReload();
|
|
|
}
|
|
|
+ },
|
|
|
+ customerAvatar: {
|
|
|
+ handler(newAvatar) {
|
|
|
+ if (this.chatInstance && newAvatar) {
|
|
|
+ this.chatInstance.setData({ customerAvatar: newAvatar });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ immediate: false
|
|
|
}
|
|
|
},
|
|
|
async mounted() {
|
|
|
- // 第一步:获取配置
|
|
|
- try {
|
|
|
- const configRes = await getQwSessionConfig();
|
|
|
- const configData = this._extractResponse(configRes);
|
|
|
- if (!configData.corpid || !configData.agentid) {
|
|
|
- throw new Error('配置数据不完整');
|
|
|
+ await this.initConfig();
|
|
|
+ await this.handleAuthAndLoad();
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ this.destroyChat();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // ========== 配置初始化 ==========
|
|
|
+ async initConfig() {
|
|
|
+ try {
|
|
|
+ const configRes = await getQwSessionConfig();
|
|
|
+ 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 || ''
|
|
|
+ };
|
|
|
+ this.configReady = true;
|
|
|
+ } catch (e) {
|
|
|
+ console.error('获取企微配置失败:', e);
|
|
|
+ this.$message.error('获取企微配置失败,请刷新重试');
|
|
|
}
|
|
|
- this.config = {
|
|
|
- corpid: configData.corpid,
|
|
|
- agentid: String(configData.agentid),
|
|
|
- agentSecret: configData.agentSecret || '',
|
|
|
- domain: configData.domain || ''
|
|
|
- };
|
|
|
- this.configReady = true;
|
|
|
- } catch (e) {
|
|
|
- console.error('获取企微配置失败:', e);
|
|
|
- this.$message.error('获取企微配置失败,请刷新重试');
|
|
|
- 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) {
|
|
|
- await this.loadAndRender();
|
|
|
- }
|
|
|
- } else {
|
|
|
- if (this.checkLoginState()) {
|
|
|
- this.isLoggedIn = true;
|
|
|
+ // ========== 登录与授权 ==========
|
|
|
+ async handleAuthAndLoad() {
|
|
|
+ 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) {
|
|
|
- await this.loadAndRender();
|
|
|
+ await this.loadFirstPage();
|
|
|
}
|
|
|
} else {
|
|
|
- this.$nextTick(() => this.createLoginPanel());
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- async activated() {
|
|
|
- if (this.isLoggedIn && this.configReady && this.customerId && this.staffUserId) {
|
|
|
- await this.loadAndRender();
|
|
|
- }
|
|
|
- },
|
|
|
- methods: {
|
|
|
- // 刷新并重新渲染(客户/员工切换时)
|
|
|
- async reloadChat() {
|
|
|
- this.isReady = false;
|
|
|
- await this.fetchMsgList(this.customerId, this.staffUserId);
|
|
|
- this.isReady = true;
|
|
|
- this.$nextTick(() => this.renderOrUpdateChat());
|
|
|
- },
|
|
|
- // 从接口加载数据并渲染(activated / 首次加载)
|
|
|
- async loadAndRender() {
|
|
|
- this.isReady = false;
|
|
|
- try {
|
|
|
- await this.fetchMsgList(this.customerId, this.staffUserId);
|
|
|
- this.isReady = true;
|
|
|
- await this.$nextTick();
|
|
|
- const container = document.getElementById('chat-container');
|
|
|
- if (!container || container.offsetHeight === 0) {
|
|
|
- await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
- await this.$nextTick();
|
|
|
- const retryContainer = document.getElementById('chat-container');
|
|
|
- if (!retryContainer || retryContainer.offsetHeight === 0) {
|
|
|
- console.error('容器不可见,放弃渲染');
|
|
|
- return;
|
|
|
+ if (this.checkLoginState()) {
|
|
|
+ this.isLoggedIn = true;
|
|
|
+ if (this.customerId && this.staffUserId) {
|
|
|
+ await this.loadFirstPage();
|
|
|
}
|
|
|
+ } else {
|
|
|
+ this.$nextTick(() => this.createLoginPanel());
|
|
|
}
|
|
|
- this._sdkInited = false;
|
|
|
- await this.renderOrUpdateChat();
|
|
|
- } catch (err) {
|
|
|
- console.error('初始化失败', err);
|
|
|
- this.isReady = true;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // ---------- 登录相关 ----------
|
|
|
async handleLogin(code) {
|
|
|
const res = await qwLogin({ code });
|
|
|
const resp = this._extractResponse(res);
|
|
|
@@ -176,10 +155,9 @@ export default {
|
|
|
}
|
|
|
this.isLoggedIn = true;
|
|
|
},
|
|
|
+
|
|
|
createLoginPanel() {
|
|
|
- //const redirectUri = 'http://sestest.ylrzcloud.com/companySale/companySession';
|
|
|
- const redirectUri = this.config.domain+'/companySale/companySession';
|
|
|
- console.log("回调地址:",redirectUri);
|
|
|
+ const redirectUri = this.config.domain + '/companySale/companySession';
|
|
|
ww.createWWLoginPanel({
|
|
|
el: document.getElementById('login-container'),
|
|
|
params: {
|
|
|
@@ -194,7 +172,7 @@ export default {
|
|
|
await this.handleLogin(code);
|
|
|
this.storeLoginState();
|
|
|
if (this.customerId && this.staffUserId) {
|
|
|
- await this.loadAndRender();
|
|
|
+ await this.loadFirstPage();
|
|
|
}
|
|
|
},
|
|
|
onLoginError: (err) => {
|
|
|
@@ -204,31 +182,21 @@ export default {
|
|
|
});
|
|
|
},
|
|
|
|
|
|
- // ---------- 本地存储 ----------
|
|
|
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);
|
|
|
},
|
|
|
|
|
|
- // ---------- 数据获取 ----------
|
|
|
- async fetchMsgList(customerId, staffUserId) {
|
|
|
- try {
|
|
|
- const data = await qwConversations({ customerId, staffUserId });
|
|
|
- this.msgList = data.msgList || [];
|
|
|
- } catch (e) {
|
|
|
- console.error('获取会话记录失败:', e);
|
|
|
- this.msgList = [];
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- // ---------- 签名 ----------
|
|
|
+ // ========== SDK 与签名 ==========
|
|
|
async getAgentConfigSignature() {
|
|
|
const currentUrl = window.location.href.split('#')[0];
|
|
|
const res = await qwSignature({ url: currentUrl });
|
|
|
@@ -240,7 +208,6 @@ export default {
|
|
|
};
|
|
|
},
|
|
|
|
|
|
- // ---------- SDK 初始化 ----------
|
|
|
async initSDKOnce() {
|
|
|
if (this._sdkInited) return true;
|
|
|
try {
|
|
|
@@ -259,19 +226,82 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // ---------- 渲染聊天组件 ----------
|
|
|
+ // ========== 数据拉取 ==========
|
|
|
+ async loadFirstPage() {
|
|
|
+ try {
|
|
|
+ const res = await qwConversations({
|
|
|
+ customerId: this.customerId,
|
|
|
+ staffUserId: this.staffUserId,
|
|
|
+ limit: 100,
|
|
|
+ cursor: this.cursor
|
|
|
+ });
|
|
|
+ const data = this._extractResponse(res);
|
|
|
+ if (data.errcode !== 0) {
|
|
|
+ throw new Error(data.errmsg || '拉取失败');
|
|
|
+ }
|
|
|
+ this.msgList = data.data || [];
|
|
|
+ this.cursor = data.next_cursor || '';
|
|
|
+ this.hasMore = data.has_more === 1;
|
|
|
+ this.isReady = true;
|
|
|
+ await this.$nextTick();
|
|
|
+ if (this.msgList.length > 0) {
|
|
|
+ await this.renderOrUpdateChat();
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载第一页失败', e);
|
|
|
+ this.$message.error('加载会话失败:' + e.message);
|
|
|
+ this.isReady = true;
|
|
|
+ this.msgList = [];
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async loadMore() {
|
|
|
+ if (!this.cursor || this.loadingMore || !this.hasMore) return;
|
|
|
+ this.loadingMore = true;
|
|
|
+ try {
|
|
|
+ const res = await qwConversations({
|
|
|
+ customerId: this.customerId,
|
|
|
+ staffUserId: this.staffUserId,
|
|
|
+ limit: 50,
|
|
|
+ cursor: this.cursor
|
|
|
+ });
|
|
|
+ const data = this._extractResponse(res);
|
|
|
+ if (data.errcode !== 0) {
|
|
|
+ throw new Error(data.errmsg || '拉取更多失败');
|
|
|
+ }
|
|
|
+ const newMessages = data.data || [];
|
|
|
+ if (newMessages.length > 0) {
|
|
|
+ this.msgList = [...this.msgList, ...newMessages];
|
|
|
+ this.cursor = data.next_cursor || '';
|
|
|
+ this.hasMore = data.has_more === 1;
|
|
|
+ if (this.chatInstance) {
|
|
|
+ // 直接更新整个 msgList,模板中使用原始字段 send_time_str
|
|
|
+ this.chatInstance.setData({ msgList: this.msgList });
|
|
|
+ } else {
|
|
|
+ await this.renderOrUpdateChat();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.hasMore = false;
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载更多失败', e);
|
|
|
+ this.$message.error('加载更多失败');
|
|
|
+ } finally {
|
|
|
+ this.loadingMore = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 组件渲染与更新 ==========
|
|
|
async renderOrUpdateChat() {
|
|
|
+ if (this.msgList.length === 0) return;
|
|
|
+
|
|
|
const container = document.getElementById('chat-container');
|
|
|
if (!container) return;
|
|
|
|
|
|
+ // 如果已存在实例,更新数据
|
|
|
if (this.chatInstance) {
|
|
|
- try {
|
|
|
- this.chatInstance.setData({ msgList: this.msgList });
|
|
|
- return;
|
|
|
- } catch (e) {
|
|
|
- console.warn('setData 失败,重建组件', e);
|
|
|
- this.chatInstance = null;
|
|
|
- }
|
|
|
+ this.chatInstance.setData({ msgList: this.msgList });
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
const ok = await this.initSDKOnce();
|
|
|
@@ -283,56 +313,113 @@ export default {
|
|
|
const templateData = {
|
|
|
msgList: this.msgList,
|
|
|
customerAvatar: this.customerAvatar,
|
|
|
- defaultStaffAvatar: this.defaultStaffAvatar
|
|
|
+ defaultStaffAvatar: this.defaultStaffAvatar,
|
|
|
+ hasMore: this.hasMore,
|
|
|
+ loadingMore: this.loadingMore
|
|
|
};
|
|
|
|
|
|
this.chatInstance = factory.createOpenDataFrame({
|
|
|
el: container,
|
|
|
template: `
|
|
|
- <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:red; color:white; padding:4px;">{{ msg.displayTime }}</text>
|
|
|
- </view>
|
|
|
- <view
|
|
|
- style="display: flex; flex-direction: row; {{msg.sender.type == 1 ? 'justify-content: flex-end;' : 'justify-content: flex-start;'}}">
|
|
|
- <image wx:if="{{msg.sender.type == 2}}" src="{{data.customerAvatar}}"
|
|
|
- style="width: 36px; height: 36px; border-radius: 4px; margin-right: 10px; background: #fff;"></image>
|
|
|
+ <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;">
|
|
|
+ {{ msg.send_time_str }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
<view
|
|
|
- style="max-width: 70%; padding: 10px 14px; border-radius: 8px; word-break: break-all; background: {{msg.sender.type == 1 ? '#95ec69' : '#ffffff'}}; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; align-items: center;">
|
|
|
- <ww-open-message message-id="{{msg.msgid}}" secret-key="{{msg.secretKey}}" open-type="viewMessage"/>
|
|
|
+ style="display: flex; flex-direction: row; {{msg.sender.type == 1 ? 'justify-content: flex-end;' : 'justify-content: flex-start;'}}">
|
|
|
+ <image wx:if="{{msg.sender.type == 2}}" src="{{data.customerAvatar}}"
|
|
|
+ style="width: 36px; height: 36px; border-radius: 4px; margin-right: 10px; background: #fff;"></image>
|
|
|
+ <view
|
|
|
+ style="max-width: 70%; padding: 10px 14px; border-radius: 8px; word-break: break-all; background: {{msg.sender.type == 1 ? '#95ec69' : '#ffffff'}}; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; align-items: center;">
|
|
|
+ <ww-open-message message-id="{{msg.msgid}}" secret-key="{{msg.secretKey}}" open-type="viewMessage"/>
|
|
|
+ </view>
|
|
|
+ <image wx:if="{{msg.sender.type == 1}}" src="{{data.defaultStaffAvatar}}"
|
|
|
+ style="width: 36px; height: 36px; border-radius: 4px; margin-left: 10px; background: #fff;"></image>
|
|
|
</view>
|
|
|
- <image wx:if="{{msg.sender.type == 1}}" src="{{data.defaultStaffAvatar}}"
|
|
|
- style="width: 36px; height: 36px; border-radius: 4px; margin-left: 10px; background: #fff;"></image>
|
|
|
</view>
|
|
|
- </view>
|
|
|
+ <view wx:if="{{data.hasMore && data.loadingMore}}" style="text-align: center; padding: 10px; color: #999;">
|
|
|
+ 加载更多...
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
`,
|
|
|
data: templateData,
|
|
|
+ methods: {
|
|
|
+ onScrollToLower: () => {
|
|
|
+ if (this.hasMore && !this.loadingMore) {
|
|
|
+ this.loadMore();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
error: (e) => {
|
|
|
console.error('[企微组件] 错误', e);
|
|
|
- if (e && e.errCode === 42006) {
|
|
|
+ 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());
|
|
|
}
|
|
|
},
|
|
|
- handleModal({ modalUrl }) {
|
|
|
- window.open(modalUrl, '_blank');
|
|
|
+ handleModal: ({ modalUrl, modalSize }) => {
|
|
|
+ // 外部浏览器预览多媒体
|
|
|
+ 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.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) => {
|
|
|
+ if (e.key === 'Escape') {
|
|
|
+ iframe.remove();
|
|
|
+ document.removeEventListener('keydown', onKeyDown);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ document.addEventListener('keydown', onKeyDown);
|
|
|
return true;
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
|
|
|
- // 通用提取:兼容多种若依返回结构
|
|
|
+ // ========== 重置与销毁 ==========
|
|
|
+ async resetAndReload() {
|
|
|
+ this.destroyChat();
|
|
|
+ this.msgList = [];
|
|
|
+ this.cursor = '';
|
|
|
+ this.hasMore = true;
|
|
|
+ this.loadingMore = false;
|
|
|
+ this.isReady = false;
|
|
|
+ await this.loadFirstPage();
|
|
|
+ },
|
|
|
+
|
|
|
+ destroyChat() {
|
|
|
+ if (this.chatInstance) {
|
|
|
+ this.chatInstance = null;
|
|
|
+ }
|
|
|
+ const container = document.getElementById('chat-container');
|
|
|
+ if (container) {
|
|
|
+ container.innerHTML = '';
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // ========== 工具方法 ==========
|
|
|
_extractResponse(res) {
|
|
|
if (!res) return {};
|
|
|
- // 直接就是目标对象(包含 errcode、timestamp 或 corpid)
|
|
|
if (res.errcode !== undefined || res.timestamp || res.corpid) return res;
|
|
|
- // res.data 是目标对象
|
|
|
if (res.data && (res.data.errcode !== undefined || res.data.timestamp || res.data.corpid)) return res.data;
|
|
|
- // res.data.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;
|
|
|
}
|
|
|
}
|