Explorar el Código

Merge remote-tracking branch 'origin/master'

yuhongqi hace 1 semana
padre
commit
10d567b5c1

+ 6 - 0
.env.prod-sxsm

@@ -31,6 +31,9 @@ VUE_APP_VIDEO_URL = https://sxsmvolcengine.ylrztop.com
 #火山云视频点播空间名
 VUE_APP_HSY_SPACE = sxsm-2114522511
 
+#直播解码路径
+VUE_APP_LIVE_PATH = /live
+
 # 开发环境配置
 ENV = 'development'
 
@@ -42,3 +45,6 @@ VUE_APP_COURSE_DEFAULT = 1
 
 # 路由懒加载
 VUE_CLI_BABEL_TRANSPILE_MODULES = true
+
+#上面地址是为了解决跨越,用nginx进行了转发
+VUE_APP_LIVE_WS_URL = wss://websocket.mxzjkwbvk.cn/ws

+ 15 - 32
src/api/qw/companySession.js

@@ -1,9 +1,7 @@
-// src/api/qw/companySession.js
 // 统一封装企微会话相关接口
 
 import request from '@/utils/request';
 
-// ---------- 登录 & 签名(走 SCRM 后端) ----------
 
 /**
  * 企微扫码登录(用 code 换取用户信息)
@@ -40,45 +38,30 @@ export function getQwSessionConfig() {
 }
 
 
-// ---------- 会话记录(暂走测试服务器代理) ----------
+// ---------- 会话记录 ----------
 
 /**
- * 获取会话记录列表
+ * 获取会话记录列表 调用中转接口
  * @param {object} params
  * @returns {Promise}
  */
-// export async function qwConversations({ seq = 0, limit = 50, customerId, staffUserId } = {}) {
-//   const BASE = '/wecom-api';
-//   let url = `${BASE}/api/conversations?seq=${seq}&limit=${limit}&customerId=${customerId}`;
-//   if (staffUserId) {
-//     url += `&staffUserId=${staffUserId}`;
-//   }
-//   const res = await fetch(url);
-//   const data = await res.json();
-//   if (data.errcode !== 0) {
-//     throw new Error(data.errmsg || '获取会话失败');
-//   }
-//   return data;
-// }
 
-// 改用 request 调用中转接口
-export async function qwConversations(params = {}) {
+export async function qwConversations({ customerId, staffUserId, limit = 50, cursor = '', timeout = 30 }) {
+  const params = {
+    limit,
+    timeout,
+    customerId,
+    staffUserId
+  };
+  // 只有 cursor 非空时才传递(后端接口判断空字符串时不传)
+  if (cursor) {
+    params.cursor = cursor;
+  }
   const res = await request({
     url: '/weChatSpace/conversations',
     method: 'get',
-    params: {
-      seq: params.seq || 0,
-      limit: params.limit || 50,
-      proxy: params.proxy || 0,
-      timeout: params.timeout || 30,
-      customerId: params.customerId,
-      staffUserId: params.staffUserId
-    }
+    params
   });
-  const resp = res.data || res;
-  if (resp.errcode !== 0) {
-    throw new Error(resp.errmsg || '获取会话失败');
-  }
-  return resp;
+  return res;
 }
 

+ 6 - 1
src/views/course/coursePlaySourceConfig/index.vue

@@ -354,7 +354,12 @@
         <el-form-item label="msgDataFormat" prop="msgDataFormat">
           <el-input v-model="form.msgDataFormat" placeholder="请输入msgDataFormat" />
         </el-form-item>
-
+        <el-form-item label="客服电话" prop="customerNum">
+          <el-input v-model="form.customerNum" placeholder="请输入客服电话" />
+        </el-form-item>
+        <el-form-item label="备案号" prop="recordNumber">
+          <el-input v-model="form.recordNumber" placeholder="请输入备案号" />
+        </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
         <el-button type="primary" @click="showOpenPlatformWarning">确 定</el-button>

+ 214 - 127
src/views/qw/companySession/ConversationPanel.vue

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

+ 48 - 6
src/views/system/config/config.vue

@@ -3088,6 +3088,7 @@
 
       <el-tab-pane label="企微专区会话配置" name="qw.sessionConfig">
         <el-form ref="form41" :model="form41" label-width="160px">
+          <!-- 原有的固定配置保持不变 -->
           <el-form-item label="企业ID" prop="corpid">
             <el-input v-model="form41.corpid" placeholder="请输入企业ID" style="width:400px"></el-input>
           </el-form-item>
@@ -3103,12 +3104,45 @@
           <el-form-item label="专区程序id" prop="programId">
             <el-input v-model="form41.programId" placeholder="请输入专区程序id" style="width:400px"></el-input>
           </el-form-item>
-          <el-form-item label="程序查询会话能力id" prop="fetchConversationAbilityId">
-            <el-input v-model="form41.fetchConversationAbilityId" placeholder="请输入查询会话能力id" style="width:400px"></el-input>
+          <el-form-item label="会话密钥(私钥)" prop="privateKey" required>
+            <el-input v-model="form41.privateKey" type="textarea" :rows="6" placeholder="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" style="width:600px"></el-input>
           </el-form-item>
-          <el-form-item label="程序能力action" prop="abilityAction">
-            <el-input v-model="form41.abilityAction" placeholder="请输入专区程序能力action" style="width:400px"></el-input>
+
+          <!-- 新增:动态能力ID配置区 -->
+          <el-divider content-position="left">能力ID配置</el-divider>
+          <div v-for="(item, index) in form41.abilityIds" :key="index" style="margin-bottom: 10px;">
+            <el-form-item
+              :label="'能力标识 (Key)'"
+              :prop="'abilityIds.' + index + '.key'"
+              :rules="{ required: true, message: '能力标识不能为空', trigger: 'blur' }"
+              style="display: inline-block; width: 35%;"
+            >
+              <el-input v-model="item.key" placeholder="例如:invokeSyncMsg"></el-input>
+            </el-form-item>
+
+            <el-form-item
+              :label="'能力值 (Value)'"
+              :prop="'abilityIds.' + index + '.value'"
+              :rules="{ required: true, message: '能力值不能为空', trigger: 'blur' }"
+              style="display: inline-block; width: 35%; margin-left: 2%;"
+            >
+              <el-input v-model="item.value" placeholder="例如:invoke_sync_msg"></el-input>
+            </el-form-item>
+
+            <el-button
+              type="danger"
+              icon="el-icon-delete"
+              circle
+              @click="removeAbilityId(index)"
+              style="margin-left: 10px;"
+            ></el-button>
+          </div>
+
+          <el-form-item>
+            <el-button type="primary" icon="el-icon-plus" @click="addAbilityId">添加能力ID</el-button>
           </el-form-item>
+          <!-- 动态能力ID配置区结束 -->
+
           <div class="footer">
             <el-button type="primary" @click="submitQwSessionConfig41">提交</el-button>
           </div>
@@ -3342,9 +3376,9 @@ export default {
         agentid: '',
         domain: '',
         agentSecret: '',
-        fetchConversationAbilityId: '',
         programId: '',
-        abilityAction: '',
+        privateKey: '',
+        abilityIds: [] //用于存放动态的 key-value 数组
       },
       storeProductScrmColumns:[],
       storeScrmColumns: [],
@@ -4392,6 +4426,14 @@ export default {
 
       this.saveConfig40();
     },
+    // 新增一行能力配置
+    addAbilityId() {
+      this.form41.abilityIds.push({ key: '', value: '' });
+    },
+    // 删除指定行
+    removeAbilityId(index) {
+      this.form41.abilityIds.splice(index, 1);
+    },
     submitQwSessionConfig41() {
       const param = {
         configId: this.configId,