|
|
@@ -0,0 +1,393 @@
|
|
|
+<template>
|
|
|
+ <div class="conversation-panel">
|
|
|
+ <!-- 配置未加载 -->
|
|
|
+ <div v-if="!configReady" class="loading-tip">
|
|
|
+ <i class="el-icon-loading"></i>
|
|
|
+ <p>正在加载配置...</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 未登录 -->
|
|
|
+ <div v-else-if="!isLoggedIn" class="login-area">
|
|
|
+ <div class="login-tip">请先扫码登录企微</div>
|
|
|
+ <div id="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>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 聊天窗口 -->
|
|
|
+ <div v-else id="chat-container"></div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+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';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'ConversationPanel',
|
|
|
+ props: {
|
|
|
+ customerId: { type: String, default: null },
|
|
|
+ customerAvatar: { type: String, default: '' },
|
|
|
+ staffUserId: { type: String, default: null }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ isLoggedIn: false,
|
|
|
+ isReady: false,
|
|
|
+ msgList: [],
|
|
|
+ chatInstance: null,
|
|
|
+ _sdkInited: false,
|
|
|
+ defaultStaffAvatar: defaultStaffAvatar,
|
|
|
+ // 从后台获取的企微配置
|
|
|
+ config: {
|
|
|
+ corpid: '',
|
|
|
+ agentid: '',
|
|
|
+ agentSecret: '',
|
|
|
+ domain:''
|
|
|
+ },
|
|
|
+ configReady: false,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ customerId(newId, oldId) {
|
|
|
+ if (newId && newId !== oldId && this.isLoggedIn && this.staffUserId && this.configReady) {
|
|
|
+ this.reloadChat();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ staffUserId(newStaffId, oldStaffId) {
|
|
|
+ if (newStaffId && newStaffId !== oldStaffId && this.customerId && this.isLoggedIn && this.configReady) {
|
|
|
+ this.reloadChat();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async mounted() {
|
|
|
+ // 第一步:获取配置
|
|
|
+ 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),
|
|
|
+ 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;
|
|
|
+ if (this.customerId && this.staffUserId) {
|
|
|
+ await this.loadAndRender();
|
|
|
+ }
|
|
|
+ } 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ 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);
|
|
|
+ if (resp.errcode !== 0) {
|
|
|
+ throw new Error(resp.errmsg || '登录失败');
|
|
|
+ }
|
|
|
+ this.isLoggedIn = true;
|
|
|
+ },
|
|
|
+ createLoginPanel() {
|
|
|
+ //const redirectUri = 'http://sestest.ylrzcloud.com/companySale/companySession';
|
|
|
+ const redirectUri = this.config.domain+'/companySale/companySession';
|
|
|
+ console.log("回调地址:",redirectUri);
|
|
|
+ ww.createWWLoginPanel({
|
|
|
+ el: document.getElementById('login-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);
|
|
|
+ this.storeLoginState();
|
|
|
+ if (this.customerId && this.staffUserId) {
|
|
|
+ await this.loadAndRender();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoginError: (err) => {
|
|
|
+ console.error('登录面板错误:', err);
|
|
|
+ this.clearLoginState();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // ---------- 本地存储 ----------
|
|
|
+ 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 = [];
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // ---------- 签名 ----------
|
|
|
+ async getAgentConfigSignature() {
|
|
|
+ const currentUrl = window.location.href.split('#')[0];
|
|
|
+ const res = await qwSignature({ url: currentUrl });
|
|
|
+ const data = this._extractResponse(res);
|
|
|
+ return {
|
|
|
+ timestamp: data.timestamp,
|
|
|
+ nonceStr: data.nonceStr,
|
|
|
+ signature: data.signature
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ // ---------- SDK 初始化 ----------
|
|
|
+ async initSDKOnce() {
|
|
|
+ if (this._sdkInited) return true;
|
|
|
+ try {
|
|
|
+ await ww.register({
|
|
|
+ corpId: this.config.corpid,
|
|
|
+ agentId: this.config.agentid,
|
|
|
+ jsApiList: ['selectExternalContact', 'shareAppMessage', 'wwapp.invokeJsApiByCallInfo'],
|
|
|
+ getAgentConfigSignature: () => this.getAgentConfigSignature()
|
|
|
+ });
|
|
|
+ await ww.initOpenData();
|
|
|
+ this._sdkInited = true;
|
|
|
+ return true;
|
|
|
+ } catch (e) {
|
|
|
+ console.error('SDK 初始化失败:', e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // ---------- 渲染聊天组件 ----------
|
|
|
+ async renderOrUpdateChat() {
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const ok = await this.initSDKOnce();
|
|
|
+ if (!ok) return;
|
|
|
+
|
|
|
+ const factory = ww.createOpenDataFrameFactory();
|
|
|
+ if (!factory) return;
|
|
|
+
|
|
|
+ const templateData = {
|
|
|
+ msgList: this.msgList,
|
|
|
+ customerAvatar: this.customerAvatar,
|
|
|
+ defaultStaffAvatar: this.defaultStaffAvatar
|
|
|
+ };
|
|
|
+
|
|
|
+ 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>
|
|
|
+ <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>
|
|
|
+ </view>
|
|
|
+ `,
|
|
|
+ data: templateData,
|
|
|
+ error: (e) => {
|
|
|
+ console.error('[企微组件] 错误', e);
|
|
|
+ if (e && e.errCode === 42006) {
|
|
|
+ this.clearLoginState();
|
|
|
+ this.isLoggedIn = false;
|
|
|
+ this.isReady = false;
|
|
|
+ this.$nextTick(() => this.createLoginPanel());
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleModal({ modalUrl }) {
|
|
|
+ window.open(modalUrl, '_blank');
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 通用提取:兼容多种若依返回结构
|
|
|
+ _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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</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;
|
|
|
+}
|
|
|
+
|
|
|
+.conversation-panel {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background: #f5f6f7;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.login-area, .empty-tip, .loading-tip {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 100%;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.login-tip {
|
|
|
+ font-size: 18px;
|
|
|
+ color: #666;
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-tip, .loading-tip {
|
|
|
+ color: #999;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-tip i, .loading-tip i {
|
|
|
+ font-size: 48px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+#chat-container {
|
|
|
+ flex: 1;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: #f0f2f5;
|
|
|
+}
|
|
|
+</style>
|