吴树波 пре 2 месеци
родитељ
комит
dadaed5001

+ 137 - 2
src/components/FloatingSoftPhone/index.vue

@@ -409,11 +409,17 @@ export default {
 
       // ===== 呼叫中心集成 =====
       ccPhoneBar: null,
+      // 标记当前 ccPhoneBar 是否为共享复用实例(非本组件创建)
+      _isSharedCCPhoneBar: false,
       ccSocketConnected: false,
       ccSocketFailed: false,
       ccConnectingPromise: null,
       ccConnectingResolve: null,
       ccConnectingReject: null,
+      // ccPhoneBar 自动重连相关
+      _ccReconnectTimer: null,
+      _ccReconnectAttempts: 0,
+      _isDestroying: false,
 
       // ===== 坐席状态 =====
       isCallingReady: false,
@@ -561,6 +567,8 @@ export default {
     this.$root.$on('dialog-call-ended', this.onDialogCallEnded);
     this.$root.$on('dialog-call-hold', this.onDialogCallHold);
     this.$root.$on('dialog-call-unhold', this.onDialogCallUnhold);
+    // 监听其他组件请求重连 ccPhoneBar 的事件
+    this.$root.$on('cc-phonebar-reconnect-requested', this.onCCPhoneBarReconnectRequested);
   },
   beforeDestroy() {
     this.destroyAllConnections();
@@ -574,6 +582,7 @@ export default {
     this.$root.$off('dialog-call-ended', this.onDialogCallEnded);
     this.$root.$off('dialog-call-hold', this.onDialogCallHold);
     this.$root.$off('dialog-call-unhold', this.onDialogCallUnhold);
+    this.$root.$off('cc-phonebar-reconnect-requested', this.onCCPhoneBarReconnectRequested);
   },
   methods: {
     // ==================== FAB按钮拖拽与吸边 ====================
@@ -949,6 +958,7 @@ export default {
 
     // ==================== 呼叫中心集成 ====================
     async initCCAndStart() {
+      this._isDestroying = false;
       this.ccSocketConnected = false;
       this.ccSocketFailed = false;
       this.isCallingReady = false;
@@ -972,6 +982,23 @@ export default {
     },
     async _doConnectCCSocket() {
       try {
+        // 优先复用已连接的共享 ccPhoneBar 实例,避免同一分机号重复登录导致 status:201 冲突
+        const shared = window.__sharedCCPhoneBar;
+        if (shared && typeof shared.getIsConnected === 'function' && shared.getIsConnected()) {
+          console.log('[FloatingSoftPhone] 检测到已连接的共享 ccPhoneBar,直接复用');
+          this.ccPhoneBar = shared;
+          this._isSharedCCPhoneBar = true;
+          this.ccSocketConnected = true;
+          this.ccSocketFailed = false;
+          this.isCallingReady = true;  // 复用时 IPCC 已连接且坐席已置忙,直接就绪
+          // 增加引用计数,防止创建者在其他组件仍使用时断开连接
+          window.__sharedCCPhoneBarRefCount = (window.__sharedCCPhoneBarRefCount || 0) + 1;
+          this._bindCCEvents();
+          if (this.ccConnectingResolve) this.ccConnectingResolve();
+          this.ccConnectingPromise = null;
+          return;
+        }
+
         const extRes = await myCallUser();
         if (extRes.code !== 200 || !extRes.data || !extRes.data.extNum) {
           throw new Error('未查询到分机号信息');
@@ -1003,6 +1030,7 @@ export default {
           heartBeatIntervalSecs: 16
         };
 
+        this._isSharedCCPhoneBar = false;
         this.ccPhoneBar = new ccPhoneBarSocket();
         this.ccPhoneBar.initConfig(callConfig);
         this._bindCCEvents();
@@ -1027,15 +1055,28 @@ export default {
     },
     _bindCCEvents() {
       this.ccPhoneBar.on(EventList.WS_CONNECTED, () => {
+        // 注册为共享实例,供其他组件复用
+        window.__sharedCCPhoneBar = this.ccPhoneBar;
+        window.__sharedCCPhoneBarRefCount = 1;  // 创建者初始引用计数为1
         this.ccSocketConnected = true;
         this.ccSocketFailed = false;
+        // 重置重连计数
+        this._ccReconnectAttempts = 0;
         if (this.ccConnectingResolve) this.ccConnectingResolve();
         this.ccConnectingPromise = null;
+        // 通知其他组件共享实例已更新(如 aiSipCallManualOutbound)
+        this.$root.$emit('cc-phonebar-reconnected', this.ccPhoneBar);
+        console.log('[FloatingSoftPhone] ccPhoneBar 连接成功,已注册为共享实例');
       });
       this.ccPhoneBar.on(EventList.WS_DISCONNECTED, () => {
+        console.log('[FloatingSoftPhone] ccPhoneBar WebSocket 断开');
         this.ccSocketConnected = false;
         this.isCallingReady = false;
         if (this.phone && this.isRegistered) this.showStatus('连接断开', 'warn');
+        // 非主动销毁时自动重连
+        if (!this._isDestroying) {
+          this._scheduleCCReconnect();
+        }
       });
       this.ccPhoneBar.on(EventList.STATUS_CHANGED, (msg) => {
         if (msg?.object) {
@@ -1121,6 +1162,58 @@ export default {
         this.showStatus('通话中', 'success');
       });
     },
+    /**
+     * ccPhoneBar 断开后自动重连
+     * 使用指数退避策略,最多重试 5 次,避免无限重连消耗资源
+     */
+    _scheduleCCReconnect() {
+      if (this._ccReconnectTimer) clearTimeout(this._ccReconnectTimer);
+      const maxAttempts = 5;
+      if (this._ccReconnectAttempts >= maxAttempts) {
+        console.log('[FloatingSoftPhone] ccPhoneBar 自动重连已达最大次数,请手动重连');
+        this.showStatus('重连失败次数过多,请点击菜单"重新连接"', 'error');
+        return;
+      }
+      this._ccReconnectAttempts++;
+      const delay = Math.min(3000 * this._ccReconnectAttempts, 15000);
+      console.log(`[FloatingSoftPhone] ccPhoneBar 将在 ${delay/1000}s 后自动重连 (${this._ccReconnectAttempts}/${maxAttempts})`);
+      this.showStatus(`${delay/1000}s 后重连...(${this._ccReconnectAttempts}/${maxAttempts})`, 'info');
+      this._ccReconnectTimer = setTimeout(async () => {
+        if (this._isDestroying || this.ccSocketConnected) return;
+        try {
+          // 清理旧的共享实例引用(旧实例已断开,不再可用)
+          if (window.__sharedCCPhoneBar === this.ccPhoneBar) {
+            window.__sharedCCPhoneBar = null;
+            window.__sharedCCPhoneBarRefCount = 0;
+          }
+          // 清理旧的 ccPhoneBar 引用
+          if (this.ccPhoneBar) {
+            this.ccPhoneBar = null;
+          }
+          this.ccConnectingPromise = null;
+          this.ccConnectingResolve = null;
+          this.ccConnectingReject = null;
+          // 重新初始化
+          await this.initCCAndStart();
+          console.log('[FloatingSoftPhone] ccPhoneBar 自动重连成功');
+        } catch (err) {
+          console.error('[FloatingSoftPhone] ccPhoneBar 自动重连失败:', err.message);
+          // 继续尝试重连
+          this._scheduleCCReconnect();
+        }
+      }, delay);
+    },
+    /**
+     * 其他组件请求重连 ccPhoneBar 的回调
+     * 如果当前已连接则忽略,否则立即触发重连(重置重连计数以加快重试)
+     */
+    onCCPhoneBarReconnectRequested() {
+      if (this.ccPhoneBar && this.ccPhoneBar.getIsConnected()) return;
+      console.log('[FloatingSoftPhone] 收到 ccPhoneBar 重连请求');
+      // 重置重连计数,立即开始重连
+      this._ccReconnectAttempts = 0;
+      this._scheduleCCReconnect();
+    },
     setupDefaultAccount(extNum, extPass) {
       if (!extNum || !extPass) return;
       extNum = String(extNum).trim();
@@ -1303,6 +1396,12 @@ export default {
 
       const phoneNumber = this.plaintextRealNumber || this.dialNumber.trim();
       if (!phoneNumber || phoneNumber.length < 3) { this.showStatus('请输入正确的号码', 'warn'); return; }
+      // 检查 ccPhoneBar 是否已连接,未连接时触发重连
+      if (!this.ccPhoneBar || !this.ccPhoneBar.getIsConnected()) {
+        this.showStatus('电话未连接,正在重连...', 'warn');
+        this._scheduleCCReconnect();
+        return;
+      }
       // 通知弹窗组件通过其phoneBar执行外呼(走弹窗完整的通话记录、客户信息流程)
       // 不再通过浮动电话自己的ccPhoneBar.call()拨号,避免创建独立通话
       this.$root.$emit('floating-softphone-call-triggered', phoneNumber);
@@ -1505,10 +1604,21 @@ export default {
     },
     async resetReconnectState() {
       this.showStatus('正在重置...', 'info');
+      // 清除自动重连定时器和计数
+      if (this._ccReconnectTimer) { clearTimeout(this._ccReconnectTimer); this._ccReconnectTimer = null; }
+      this._ccReconnectAttempts = 0;
       if (this.volumeTimerId) { clearTimeout(this.volumeTimerId); this.volumeTimerId = null; }
       if (this.statusTimerId) { clearTimeout(this.statusTimerId); this.statusTimerId = null; }
       if (this.phone) { this.phone.destroy(); this.phone = null; }
-      if (this.ccPhoneBar) { try { this.ccPhoneBar.disconnect(); } catch(e) {} this.ccPhoneBar = null; }
+      // 手动重置时清理共享实例引用
+      if (this.ccPhoneBar) {
+        if (window.__sharedCCPhoneBar === this.ccPhoneBar) {
+          window.__sharedCCPhoneBar = null;
+          window.__sharedCCPhoneBarRefCount = 0;
+        }
+        try { this.ccPhoneBar.disconnect(); } catch(e) {}
+        this.ccPhoneBar = null;
+      }
       this.ccSocketConnected = false;
       this.ccSocketFailed = false;
       this.isCallingReady = false;
@@ -1534,12 +1644,37 @@ export default {
 
     // ==================== 清理 ====================
     destroyAllConnections() {
+      this._isDestroying = true;
+      // 清除自动重连定时器
+      if (this._ccReconnectTimer) { clearTimeout(this._ccReconnectTimer); this._ccReconnectTimer = null; }
+      this._ccReconnectAttempts = 0;
       this.clearAllTimers();
       if (this.phone) { try { this.phone.destroy(); } catch(e) {} this.phone = null; }
-      if (this.ccPhoneBar) { try { this.ccPhoneBar.disconnect(); } catch(e) {} this.ccPhoneBar = null; }
+      if (this.ccPhoneBar) {
+        // 递减引用计数
+        if (window.__sharedCCPhoneBarRefCount > 0) {
+          window.__sharedCCPhoneBarRefCount--;
+        }
+        if (this._isSharedCCPhoneBar) {
+          // 复用者:只需递减引用计数,不断开连接
+          console.log('[销毁] ccPhoneBar 是共享实例,跳过断开,剩余引用:', window.__sharedCCPhoneBarRefCount);
+        } else if (window.__sharedCCPhoneBarRefCount > 0) {
+          // 创建者:仍有其他组件在使用,不断开连接
+          console.log('[销毁] ccPhoneBar 仍有其他组件引用,跳过断开,剩余引用:', window.__sharedCCPhoneBarRefCount);
+        } else {
+          // 创建者:无其他组件引用,安全断开
+          try { this.ccPhoneBar.disconnect(); } catch(e) {}
+          if (window.__sharedCCPhoneBar === this.ccPhoneBar) {
+            window.__sharedCCPhoneBar = null;
+          }
+        }
+        this.ccPhoneBar = null;
+      }
       this.callUuidMap = {};
       this.currentCallUuid = '';
       this.resetAllStates();
+      // 通知其他组件共享实例已销毁
+      this.$root.$emit('cc-phonebar-destroyed');
     },
     clearAllTimers() {
       if (this.volumeTimerId) { clearTimeout(this.volumeTimerId); this.volumeTimerId = null; }

+ 172 - 50
src/views/aiSipCall/aiSipCallManualOutbound.vue

@@ -530,6 +530,10 @@ export default {
         return {
             // 工具条对象
             phoneBar: null,
+            // 标记当前 phoneBar 是否为共享复用实例(非本组件创建)
+            _isSharedPhoneBar: false,
+            // 共享 phoneBar 时跟踪注册的事件回调,用于销毁时清理
+            _phoneBarCallbackMap: null,
 
             // 配置参数
             scriptServer: DefaultConfig.scriptServer,
@@ -688,13 +692,36 @@ export default {
         // 监听浮动软电话的挂机/保持委托事件
         this.$root.$on('floating-softphone-hangup-triggered', this.onFloatingPhoneHangupTriggered);
         this.$root.$on('floating-softphone-hold-triggered', this.onFloatingPhoneHoldTriggered);
+        // 监听共享 ccPhoneBar 重连事件:FloatingSoftPhone 重连后更新本组件的 phoneBar 引用
+        this.$root.$on('cc-phonebar-reconnected', this.onCCPhoneBarReconnected);
+        // 监听共享 ccPhoneBar 销毁事件
+        this.$root.$on('cc-phonebar-destroyed', this.onCCPhoneBarDestroyed);
     },
 
     beforeDestroy() {
         // 断开 WebSocket 连接
+        // 如果 phoneBar 是共享实例(由其他组件创建),不主动断开,避免影响其他组件
         if (this.phoneBar) {
-            console.log('[beforeDestroy] 断开 WebSocket 连接');
-            this.phoneBar.disconnect();
+            // 递减引用计数
+            if (window.__sharedCCPhoneBarRefCount > 0) {
+                window.__sharedCCPhoneBarRefCount--;
+            }
+            if (this._isSharedPhoneBar) {
+                // 复用者:清理事件回调并跳过断开
+                console.log('[beforeDestroy] phoneBar 是共享实例,清理事件回调并跳过断开,剩余引用:', window.__sharedCCPhoneBarRefCount);
+                this._cleanupPhoneBarListeners();
+            } else if (window.__sharedCCPhoneBarRefCount > 0) {
+                // 创建者:仍有其他组件在使用,不断开连接
+                console.log('[beforeDestroy] phoneBar 仍有其他组件引用,跳过断开,剩余引用:', window.__sharedCCPhoneBarRefCount);
+            } else {
+                // 创建者:无其他组件引用,安全断开
+                console.log('[beforeDestroy] 断开 WebSocket 连接');
+                this.phoneBar.disconnect();
+                if (window.__sharedCCPhoneBar === this.phoneBar) {
+                    window.__sharedCCPhoneBar = null;
+                }
+            }
+            this.phoneBar = null;
         }
         // 移除键盘事件监听
         document.removeEventListener('keyup', this._escKeyHandler);
@@ -708,6 +735,8 @@ export default {
         this.$root.$off('floating-softphone-call-triggered', this.onFloatingPhoneCallTriggered);
         this.$root.$off('floating-softphone-hangup-triggered', this.onFloatingPhoneHangupTriggered);
         this.$root.$off('floating-softphone-hold-triggered', this.onFloatingPhoneHoldTriggered);
+        this.$root.$off('cc-phonebar-reconnected', this.onCCPhoneBarReconnected);
+        this.$root.$off('cc-phonebar-destroyed', this.onCCPhoneBarDestroyed);
         if (this.recordTimer) {
             clearTimeout(this.recordTimer);
             this.recordTimer = null;
@@ -715,14 +744,85 @@ export default {
     },
 
     methods: {
+        /**
+         * 在 phoneBar 上注册事件回调,同时跟踪回调引用以便销毁时清理
+         * 防止共享 phoneBar 时组件销毁后回调残留导致内存泄漏和重复执行
+         */
+        _phoneBarOn(eventKey, callback) {
+            this.phoneBar.on(eventKey, callback);
+            if (!this._phoneBarCallbackMap) this._phoneBarCallbackMap = {};
+            if (!this._phoneBarCallbackMap[eventKey]) this._phoneBarCallbackMap[eventKey] = [];
+            this._phoneBarCallbackMap[eventKey].push(callback);
+        },
+        /**
+         * 清理本组件在共享 phoneBar 上注册的所有事件回调
+         */
+        _cleanupPhoneBarListeners() {
+            if (!this._phoneBarCallbackMap || !this.phoneBar) return;
+            for (const key of Object.keys(this._phoneBarCallbackMap)) {
+                const callbacks = this._phoneBarCallbackMap[key];
+                callbacks.forEach(cb => {
+                    try { this.phoneBar.off(key, cb); } catch(e) {}
+                });
+            }
+            this._phoneBarCallbackMap = null;
+        },
+
+        /**
+         * 共享 ccPhoneBar 重连后的回调:更新 phoneBar 引用并重新注册事件监听
+         * 解决 FloatingSoftPhone 自动重连创建新 ccPhoneBar 后,本组件仍持有旧引用的问题
+         */
+        onCCPhoneBarReconnected(newPhoneBar) {
+            console.log('[电话工具条] 检测到共享 ccPhoneBar 重连,更新引用并重新注册事件');
+            // 清理旧 phoneBar 上的事件回调
+            this._cleanupPhoneBarListeners();
+            // 更新引用
+            this.phoneBar = newPhoneBar;
+            this._isSharedPhoneBar = true;
+            // 重新注册事件监听
+            this.setupEventListeners();
+            // 同步 UI 状态
+            this.onlineButtonText = '签出';
+            this.callStatus = '已签入';
+            this.agentStatus = '忙碌';
+            this.loginTime = new Date().toLocaleTimeString();
+        },
+
+        /**
+         * 共享 ccPhoneBar 销毁后的回调:清理引用和 UI 状态
+         */
+        onCCPhoneBarDestroyed() {
+            console.log('[电话工具条] 检测到共享 ccPhoneBar 已销毁,清理引用');
+            this._cleanupPhoneBarListeners();
+            this.phoneBar = null;
+            this._isSharedPhoneBar = false;
+            this.callStatus = '没有连接';
+            this.agentStatus = '空闲';
+            this.onlineButtonText = '签入';
+        },
+
         /**
          * 初始化电话工具条
+         * 优先复用已连接的共享 ccPhoneBar 实例,避免同一分机号重复登录导致 status:201 冲突
          */
         initPhoneBar() {
-            // 创建工具条对象
+            // 检查是否已有共享的 ccPhoneBar 实例(由 FloatingSoftPhone 或 softPhone 注册)
+            const shared = window.__sharedCCPhoneBar;
+            if (shared && typeof shared.getIsConnected === 'function' && shared.getIsConnected()) {
+                console.log('[电话工具条] 检测到已连接的共享 ccPhoneBar,直接复用,避免 status:201 冲突');
+                this.phoneBar = shared;
+                this._isSharedPhoneBar = true;
+                // 增加引用计数,防止创建者在其他组件仍使用时断开连接
+                window.__sharedCCPhoneBarRefCount = (window.__sharedCCPhoneBarRefCount || 0) + 1;
+                // 复用实例时直接同步连接状态,无需重新走连接流程
+                this.onlineButtonText = '签出';
+                this.callStatus = '已签入';
+                this.agentStatus = '忙碌';
+                return;
+            }
+            // 没有可复用的实例,创建新的
+            this._isSharedPhoneBar = false;
             this.phoneBar = new ccPhoneBarSocket();
-
-            // 先调用 myCallUser 获取 extNum
             this.loadExtNumAndInit();
         },
 
@@ -758,7 +858,10 @@ export default {
                         ipccServer: this.ipccServerUrl,
                         gatewayList: response.data.gatewayList,
                         gatewayEncrypted: false,
-                        extPassword: response.data.encryptPsw
+                        extPassword: response.data.encryptPsw,
+                        enableWss: true,
+                        enableHeartBeat: true,
+                        heartBeatIntervalSecs: 16
                     };
 
                     console.log('初始化工具条基础配置参数:', callConfig);
@@ -818,7 +921,7 @@ export default {
          */
         setupEventListeners() {
             // websocket 通信对象断开事件
-            this.phoneBar.on(EventList.WS_DISCONNECTED, (msg) => {
+            this._phoneBarOn(EventList.WS_DISCONNECTED, (msg) => {
                 console.log("websocket 通信对象断开事件:" + msg);
                 this.updatePhoneBar(msg, EventList.WS_DISCONNECTED);
                 this.showTransferArea = false;
@@ -838,7 +941,7 @@ export default {
                 }
             });
 
-            this.phoneBar.on(EventList.OUTBOUND_START, (msg) => {
+            this._phoneBarOn(EventList.OUTBOUND_START, (msg) => {
                 console.log('outbound_start 事件:' + msg);
                 this.showChatContainer = true;
                 this.showCustomerForm = true;
@@ -846,7 +949,7 @@ export default {
                 this.markExecuteSuccess();
             });
 
-            this.phoneBar.on(EventList.REQUEST_ARGS_ERROR, (msg) => {
+            this._phoneBarOn(EventList.REQUEST_ARGS_ERROR, (msg) => {
                 console.log('request_args_error事件:' + msg);
                 this.updatePhoneBar(msg, EventList.REQUEST_ARGS_ERROR);
                 this.callExecuteFailed = true;
@@ -855,7 +958,7 @@ export default {
             });
 
             if (EventList.SERVER_ERROR) {
-                this.phoneBar.on(EventList.SERVER_ERROR, (msg) => {
+                this._phoneBarOn(EventList.SERVER_ERROR, (msg) => {
                     this.updatePhoneBar(msg, EventList.SERVER_ERROR);
                     this.callExecuteFailed = true;
                     this.callFailReason = '服务器内部错误';
@@ -864,7 +967,7 @@ export default {
             }
 
             if (EventList.CALLER_BUSY) {
-                this.phoneBar.on(EventList.CALLER_BUSY, (msg) => {
+                this._phoneBarOn(EventList.CALLER_BUSY, (msg) => {
                     this.updatePhoneBar(msg, EventList.CALLER_BUSY);
                     this.callExecuteFailed = true;
                     this.callFailReason = '分机忙';
@@ -873,7 +976,7 @@ export default {
             }
 
             if (EventList.CALLER_NOT_LOGIN) {
-                this.phoneBar.on(EventList.CALLER_NOT_LOGIN, (msg) => {
+                this._phoneBarOn(EventList.CALLER_NOT_LOGIN, (msg) => {
                     this.updatePhoneBar(msg, EventList.CALLER_NOT_LOGIN);
                     this.callExecuteFailed = true;
                     this.callFailReason = '分机未登录';
@@ -882,7 +985,7 @@ export default {
             }
 
             if (EventList.CALLER_RESPOND_TIMEOUT) {
-                this.phoneBar.on(EventList.CALLER_RESPOND_TIMEOUT, (msg) => {
+                this._phoneBarOn(EventList.CALLER_RESPOND_TIMEOUT, (msg) => {
                     this.updatePhoneBar(msg, EventList.CALLER_RESPOND_TIMEOUT);
                     this.callExecuteFailed = true;
                     this.callFailReason = '分机应答超时';
@@ -891,13 +994,16 @@ export default {
             }
 
             // 用户已在其他设备登录
-            this.phoneBar.on(EventList.USER_LOGIN_ON_OTHER_DEVICE, (msg) => {
+            this._phoneBarOn(EventList.USER_LOGIN_ON_OTHER_DEVICE, (msg) => {
                 this.updatePhoneBar(msg, EventList.USER_LOGIN_ON_OTHER_DEVICE);
                 alert(EventListWithTextInfo.USER_LOGIN_ON_OTHER_DEVICE.msg);
             });
 
-            this.phoneBar.on(EventList.WS_CONNECTED, (msg) => {
+            this._phoneBarOn(EventList.WS_CONNECTED, (msg) => {
                 console.log('WebSocket连接成功事件:' + msg);
+                // 注册为共享实例,供其他组件复用
+                window.__sharedCCPhoneBar = this.phoneBar;
+                window.__sharedCCPhoneBarRefCount = 1;  // 创建者初始引用计数为1
                 this.loginTime = new Date().toLocaleTimeString();
                 this.callStatus = '已签入';
                 this.agentStatus = '忙碌';
@@ -908,7 +1014,7 @@ export default {
                 }
             });
 
-            this.phoneBar.on(EventList.CALLEE_RINGING, (msg) => {
+            this._phoneBarOn(EventList.CALLEE_RINGING, (msg) => {
                 console.log('被叫振铃事件:' + msg.content);
                 this.updatePhoneBar(msg, EventList.CALLEE_RINGING);
                 this.markExecuteSuccess();
@@ -916,7 +1022,7 @@ export default {
                 this.$root.$emit('dialog-call-ringing');
             });
 
-            this.phoneBar.on(EventList.CALLER_ANSWERED, (msg) => {
+            this._phoneBarOn(EventList.CALLER_ANSWERED, (msg) => {
                 console.log('主叫接通事件:' + msg);
                 this.agentStatus = '通话中';
                 if (msg && msg.object && msg.object.uuid) {
@@ -928,7 +1034,7 @@ export default {
                 this.$root.$emit('dialog-call-talking');
             });
 
-            this.phoneBar.on(EventList.CALLER_HANGUP, (msg) => {
+            this._phoneBarOn(EventList.CALLER_HANGUP, (msg) => {
                 console.log('主叫挂断事件:' + msg);
                 this.agentStatus = '通话结束';
                 this.canSendVideoReInvite = false;
@@ -947,7 +1053,7 @@ export default {
                 }
             });
 
-            this.phoneBar.on(EventList.CALLEE_ANSWERED, (msg) => {
+            this._phoneBarOn(EventList.CALLEE_ANSWERED, (msg) => {
                 console.log('被叫接通事件:' + msg);
                 if (msg && msg.object && msg.object.uuid) {
                     this.currentCallUuid = msg.object.uuid;
@@ -958,7 +1064,7 @@ export default {
                 this.$root.$emit('dialog-call-talking');
             });
 
-            this.phoneBar.on(EventList.CALLEE_HANGUP, (msg) => {
+            this._phoneBarOn(EventList.CALLEE_HANGUP, (msg) => {
                 console.log('被叫挂断事件:' + msg);
                 this.showTransferArea = false;
                 this.showConferenceArea = false;
@@ -974,7 +1080,7 @@ export default {
                 }
             });
 
-            this.phoneBar.on(EventList.STATUS_CHANGED, (msg) => {
+            this._phoneBarOn(EventList.STATUS_CHANGED, (msg) => {
                 console.log('座席状态改变事件:' + msg);
 
                 // 确保 msg.object 存在才设置
@@ -991,17 +1097,17 @@ export default {
                 }
             });
 
-            this.phoneBar.on(EventList.ACD_GROUP_QUEUE_NUMBER, (msg) => {
+            this._phoneBarOn(EventList.ACD_GROUP_QUEUE_NUMBER, (msg) => {
                 console.log('当前排队人数消息事件:' + msg);
                 this.queueNumber = msg.object.queue_number;
             });
 
-            this.phoneBar.on(EventList.ON_AUDIO_CALL_CONNECTED, (msg) => {
+            this._phoneBarOn(EventList.ON_AUDIO_CALL_CONNECTED, (msg) => {
                 console.log('音频通话已建立事件:' + msg);
                 this.canSendVideoReInvite = true;
             });
 
-            this.phoneBar.on(EventList.CUSTOMER_CHANNEL_HOLD, (msg) => {
+            this._phoneBarOn(EventList.CUSTOMER_CHANNEL_HOLD, (msg) => {
                 console.log('客户通话已保持事件:' + msg);
                 this.callStatus = '通话已保持';
                 this.showHoldBtn = false;
@@ -1010,7 +1116,7 @@ export default {
                 this.$root.$emit('dialog-call-hold');
             });
 
-            this.phoneBar.on(EventList.CUSTOMER_CHANNEL_UNHOLD, (msg) => {
+            this._phoneBarOn(EventList.CUSTOMER_CHANNEL_UNHOLD, (msg) => {
                 console.log('客户通话已接回事件:' + msg);
                 this.callStatus = '客户通话已接回';
                 this.showHoldBtn = true;
@@ -1019,29 +1125,29 @@ export default {
                 this.$root.$emit('dialog-call-unhold');
             });
 
-            this.phoneBar.on(EventList.CUSTOMER_ON_HOLD_HANGUP, (msg) => {
+            this._phoneBarOn(EventList.CUSTOMER_ON_HOLD_HANGUP, (msg) => {
                 console.log('保持的通话已挂机事件:' + msg);
                 this.showHoldBtn = true;
                 this.showUnHoldBtn = false;
                 this.callStatus = '保持的通话已挂机.';
             });
 
-            this.phoneBar.on(EventList.ON_VIDEO_CALL_CONNECTED, (msg) => {
+            this._phoneBarOn(EventList.ON_VIDEO_CALL_CONNECTED, (msg) => {
                 console.log('视频通话已建立事件:' + msg);
                 this.canSendVideoFile = true;
             });
 
-            this.phoneBar.on(EventList.INNER_CONSULTATION_START, (msg) => {
+            this._phoneBarOn(EventList.INNER_CONSULTATION_START, (msg) => {
                 console.log('咨询开始事件:' + msg);
                 this.callStatus = '咨询开始.';
             });
 
-            this.phoneBar.on(EventList.INNER_CONSULTATION_STOP, (msg) => {
+            this._phoneBarOn(EventList.INNER_CONSULTATION_STOP, (msg) => {
                 console.log('咨询结束事件:' + msg);
                 this.callStatus = '咨询结束.';
             });
 
-            this.phoneBar.on(EventList.TRANSFER_CALL_SUCCESS, (msg) => {
+            this._phoneBarOn(EventList.TRANSFER_CALL_SUCCESS, (msg) => {
                 console.log('电话转接成功事件:' + msg);
                 // 转接成功后隐藏转接/咨询区域(与 source 一致)
                 this.showTransferArea = false;
@@ -1049,7 +1155,7 @@ export default {
                 this.externalPhoneNumber = '';
             });
 
-            this.phoneBar.on(EventList.AGENT_STATUS_DATA_CHANGED, (msg) => {
+            this._phoneBarOn(EventList.AGENT_STATUS_DATA_CHANGED, (msg) => {
                 console.log('agent_status_data_changed 事件:' + msg);
 
                 // 解析并更新坐席列表数据
@@ -1074,7 +1180,7 @@ export default {
             });
 
             // 客户通话等待相关事件
-            this.phoneBar.on(EventList.CUSTOMER_CHANNEL_CALL_WAIT, (msg) => {
+            this._phoneBarOn(EventList.CUSTOMER_CHANNEL_CALL_WAIT, (msg) => {
                 console.log('客户电话等待中事件:' + msg);
                 // 隐藏会议区域和客户信息表单(与 source 一致)
                 this.showConferenceArea = false;
@@ -1086,7 +1192,7 @@ export default {
                 this.showTransferArea = true;
             });
 
-            this.phoneBar.on(EventList.CUSTOMER_CHANNEL_OFF_CALL_WAIT, (msg) => {
+            this._phoneBarOn(EventList.CUSTOMER_CHANNEL_OFF_CALL_WAIT, (msg) => {
                 console.log('等待的电话已接回事件:' + msg);
                 // 隐藏转接/咨询区域
                 this.showStopCallWait = false;
@@ -1095,7 +1201,7 @@ export default {
                 this.callStatus = '等待的电话已接回.';
             });
 
-            this.phoneBar.on(EventList.INNER_CONSULTATION_START, (msg) => {
+            this._phoneBarOn(EventList.INNER_CONSULTATION_START, (msg) => {
                 console.log('咨询已开始事件:' + msg);
                 // 隐藏会议区域和客户信息表单(与 source 一致)
                 this.showConferenceArea = false;
@@ -1106,7 +1212,7 @@ export default {
                 this.showDoTransferBtn = false;
             });
 
-            this.phoneBar.on(EventList.INNER_CONSULTATION_STOP, (msg) => {
+            this._phoneBarOn(EventList.INNER_CONSULTATION_STOP, (msg) => {
                 console.log('咨询已结束事件:' + msg);
                 // 隐藏转接/咨询区域
                 this.showTransferCallWait = false;
@@ -1114,7 +1220,7 @@ export default {
                 this.callStatus = '咨询结束.';
             });
 
-            this.phoneBar.on(EventList.CUSTOMER_ON_CALL_WAIT_HANGUP, (msg) => {
+            this._phoneBarOn(EventList.CUSTOMER_ON_CALL_WAIT_HANGUP, (msg) => {
                 console.log('等待的客户已挂机事件:' + msg);
                 // 隐藏转接/咨询区域
                 this.showStopCallWait = false;
@@ -1124,17 +1230,17 @@ export default {
             });
 
             // 会议相关事件
-            this.phoneBar.on(EventList.CONFERENCE_MEMBER_ANSWERED, (msg) => {
+            this._phoneBarOn(EventList.CONFERENCE_MEMBER_ANSWERED, (msg) => {
                 console.log('会议成员已经接通事件:' + msg);
                 this.updateConferenceMemberStatus(msg.object.phone, '通话中', 'green');
             });
 
-            this.phoneBar.on(EventList.CONFERENCE_MEMBER_HANGUP, (msg) => {
+            this._phoneBarOn(EventList.CONFERENCE_MEMBER_HANGUP, (msg) => {
                 console.log('会议成员已经挂机事件:' + msg);
                 this.updateConferenceMemberHangup(msg.object.phone, msg.object);
             });
 
-            this.phoneBar.on(EventList.CONFERENCE_MODERATOR_ANSWERED, (msg) => {
+            this._phoneBarOn(EventList.CONFERENCE_MODERATOR_ANSWERED, (msg) => {
                 console.log('电话会议开始,主持人已接通事件:' + msg);
                 // 隐藏转接/咨询区域和客户信息表单(与 source 一致)
                 this.showTransferArea = false;
@@ -1142,13 +1248,13 @@ export default {
                 this.onConferenceStart();
             });
 
-            this.phoneBar.on(EventList.CONFERENCE_MODERATOR_HANGUP, (msg) => {
+            this._phoneBarOn(EventList.CONFERENCE_MODERATOR_HANGUP, (msg) => {
                 console.log('电话会议结束,主持人已挂机事件:' + msg);
                 // 会议结束后隐藏会议区域
                 this.onConferenceEnd();
             });
 
-            this.phoneBar.on(EventList.CONFERENCE_MEMBER_MUTED_SUCCESS, (msg) => {
+            this._phoneBarOn(EventList.CONFERENCE_MEMBER_MUTED_SUCCESS, (msg) => {
                 console.log('会议成员已被禁言事件:' + msg);
                 const memberPhone = msg.object.phone;
                 const member = this.conferenceMembers.find(m => m.phone === memberPhone);
@@ -1157,7 +1263,7 @@ export default {
                 }
             });
 
-            this.phoneBar.on(EventList.CONFERENCE_MEMBER_UNMUTED_SUCCESS, (msg) => {
+            this._phoneBarOn(EventList.CONFERENCE_MEMBER_UNMUTED_SUCCESS, (msg) => {
                 console.log('会议成员解除禁言成功事件:' + msg);
                 const memberPhone = msg.object.phone;
                 const member = this.conferenceMembers.find(m => m.phone === memberPhone);
@@ -1166,7 +1272,7 @@ export default {
                 }
             });
 
-            this.phoneBar.on(EventList.CONFERENCE_MEMBER_VMUTED_SUCCESS, (msg) => {
+            this._phoneBarOn(EventList.CONFERENCE_MEMBER_VMUTED_SUCCESS, (msg) => {
                 console.log('会议成员已被禁用视频事件:' + msg);
                 const memberPhone = msg.object.phone;
                 const member = this.conferenceMembers.find(m => m.phone === memberPhone);
@@ -1175,7 +1281,7 @@ export default {
                 }
             });
 
-            this.phoneBar.on(EventList.CONFERENCE_MEMBER_UNVMUTED_SUCCESS, (msg) => {
+            this._phoneBarOn(EventList.CONFERENCE_MEMBER_UNVMUTED_SUCCESS, (msg) => {
                 console.log('会议成员启用视频成功事件:' + msg);
                 const memberPhone = msg.object.phone;
                 const member = this.conferenceMembers.find(m => m.phone === memberPhone);
@@ -1185,19 +1291,19 @@ export default {
             });
 
             // ASR 相关事件
-            this.phoneBar.on(EventList.ASR_PROCESS_STARTED, (msg) => {
+            this._phoneBarOn(EventList.ASR_PROCESS_STARTED, (msg) => {
                 this.chatMessages = [];
             });
 
-            this.phoneBar.on(EventList.ASR_RESULT_GENERATE, (msg) => {
+            this._phoneBarOn(EventList.ASR_RESULT_GENERATE, (msg) => {
                 this.handleAsrMessage(msg);
             });
 
-            this.phoneBar.on(EventList.ASR_PROCESS_END_CUSTOMER, (msg) => {
+            this._phoneBarOn(EventList.ASR_PROCESS_END_CUSTOMER, (msg) => {
                 this.handleAsrMessage(msg);
             });
 
-            this.phoneBar.on(EventList.ASR_PROCESS_END_AGENT, (msg) => {
+            this._phoneBarOn(EventList.ASR_PROCESS_END_AGENT, (msg) => {
                 this.handleAsrMessage(msg);
             });
         },
@@ -1388,8 +1494,24 @@ export default {
                 return;
             }
             if (!this.phoneBar || !this.phoneBar.getIsConnected()) {
-                this.$message.warning('弹窗电话未连接,请先签入');
-                return;
+                // 检查共享实例是否已更新(FloatingSoftPhone 可能已自动重连)
+                const shared = window.__sharedCCPhoneBar;
+                if (shared && shared !== this.phoneBar && typeof shared.getIsConnected === 'function' && shared.getIsConnected()) {
+                    console.log('[浮动电话触发] 检测到共享 ccPhoneBar 已更新,切换引用');
+                    this._cleanupPhoneBarListeners();
+                    this.phoneBar = shared;
+                    this._isSharedPhoneBar = true;
+                    this.setupEventListeners();
+                    this.onlineButtonText = '签出';
+                    this.callStatus = '已签入';
+                    this.agentStatus = '忙碌';
+                } else {
+                    console.log('[浮动电话触发] phoneBar 未连接,请求重连');
+                    this.$message.warning('电话正在重连,请稍后再试');
+                    // 通知 FloatingSoftPhone 触发重连
+                    this.$root.$emit('cc-phonebar-reconnect-requested');
+                    return;
+                }
             }
             // 检查外呼按钮状态
             const callBtn = document.getElementById('callBtn');

+ 61 - 9
src/views/aiSipCall/softPhone.vue

@@ -413,6 +413,8 @@ export default {
 
       // 呼叫中心集成
       ccPhoneBar: null,
+      // 标记当前 ccPhoneBar 是否为共享复用实例(非本组件创建)
+      _isSharedCCPhoneBar: false,
       ccSocketConnected: false,
       ccSocketFailed: false,
       ccConnectingPromise: null,
@@ -635,6 +637,23 @@ export default {
     },
     async _doConnectCCSocket() {
       try {
+        // 优先复用已连接的共享 ccPhoneBar 实例,避免同一分机号重复登录导致 status:201 冲突
+        const shared = window.__sharedCCPhoneBar;
+        if (shared && typeof shared.getIsConnected === 'function' && shared.getIsConnected()) {
+          console.log('[SoftPhone] 检测到已连接的共享 ccPhoneBar,直接复用');
+          this.ccPhoneBar = shared;
+          this._isSharedCCPhoneBar = true;
+          this.ccSocketConnected = true;
+          this.ccSocketFailed = false;
+          this.isCallingReady = true;  // 复用时 IPCC 已连接且坐席已置忙,直接就绪
+          // 增加引用计数,防止创建者在其他组件仍使用时断开连接
+          window.__sharedCCPhoneBarRefCount = (window.__sharedCCPhoneBarRefCount || 0) + 1;
+          this._bindCCEvents();
+          if (this.ccConnectingResolve) this.ccConnectingResolve();
+          this.ccConnectingPromise = null;
+          return;
+        }
+
         // 获取分机信息
         const extRes = await myCallUser();
         if (extRes.code !== 200 || !extRes.data || !extRes.data.extNum) {
@@ -673,6 +692,7 @@ export default {
         };
 
         // 初始化并连接
+        this._isSharedCCPhoneBar = false;
         this.ccPhoneBar = new ccPhoneBarSocket();
         this.ccPhoneBar.initConfig(callConfig);
 
@@ -707,6 +727,9 @@ export default {
       // WebSocket连接事件
       this.ccPhoneBar.on(EventList.WS_CONNECTED, () => {
         console.log('[IPCC] WebSocket已连接');
+        // 注册为共享实例,供其他组件复用
+        window.__sharedCCPhoneBar = this.ccPhoneBar;
+        window.__sharedCCPhoneBarRefCount = 1;  // 创建者初始引用计数为1
         this.ccSocketConnected = true;
         this.ccSocketFailed = false;
         if (this.ccConnectingResolve) this.ccConnectingResolve();
@@ -1437,10 +1460,23 @@ export default {
 
       // 断开 IPCC 连接
       if (this.ccPhoneBar) {
-        console.log('[重置] 断开IPCC连接');
-        try {
-          this.ccPhoneBar.disconnect();
-        } catch(e) {}
+        // 递减引用计数
+        if (window.__sharedCCPhoneBarRefCount > 0) {
+          window.__sharedCCPhoneBarRefCount--;
+        }
+        if (this._isSharedCCPhoneBar) {
+          console.log('[重置] ccPhoneBar 是共享实例,跳过断开');
+        } else if (window.__sharedCCPhoneBarRefCount > 0) {
+          console.log('[重置] ccPhoneBar 仍有其他组件引用,跳过断开');
+        } else {
+          console.log('[重置] 断开IPCC连接');
+          try {
+            this.ccPhoneBar.disconnect();
+          } catch(e) {}
+          if (window.__sharedCCPhoneBar === this.ccPhoneBar) {
+            window.__sharedCCPhoneBar = null;
+          }
+        }
         this.ccPhoneBar = null;
       }
 
@@ -1508,11 +1544,27 @@ export default {
 
       // 断开 IPCC 连接
       if (this.ccPhoneBar) {
-        console.log('[销毁] 断开IPCC连接');
-        try {
-          this.ccPhoneBar.disconnect();
-        } catch(e) {
-          console.error('[销毁] IPCC连接断开失败:', e);
+        // 递减引用计数
+        if (window.__sharedCCPhoneBarRefCount > 0) {
+          window.__sharedCCPhoneBarRefCount--;
+        }
+        if (this._isSharedCCPhoneBar) {
+          // 复用者:只需递减引用计数,不断开连接
+          console.log('[销毁] ccPhoneBar 是共享实例,跳过断开,剩余引用:', window.__sharedCCPhoneBarRefCount);
+        } else if (window.__sharedCCPhoneBarRefCount > 0) {
+          // 创建者:仍有其他组件在使用,不断开连接
+          console.log('[销毁] ccPhoneBar 仍有其他组件引用,跳过断开,剩余引用:', window.__sharedCCPhoneBarRefCount);
+        } else {
+          // 创建者:无其他组件引用,安全断开
+          console.log('[销毁] 断开IPCC连接');
+          try {
+            this.ccPhoneBar.disconnect();
+          } catch(e) {
+            console.error('[销毁] IPCC连接断开失败:', e);
+          }
+          if (window.__sharedCCPhoneBar === this.ccPhoneBar) {
+            window.__sharedCCPhoneBar = null;
+          }
         }
         this.ccPhoneBar = null;
       }