Просмотр исходного кода

恢复老版本,新增一些小功能

吴树波 1 месяц назад
Родитель
Сommit
a12257ba4d
34 измененных файлов с 770 добавлено и 4106 удалено
  1. 71 396
      src/api/aiSipCall/softPhone.js
  2. BIN
      src/assets/来电铃声.mp3
  3. 0 2383
      src/components/FloatingSoftPhone/index.vue
  4. 0 103
      src/components/ManualCallDialog/index.vue
  5. 0 3
      src/layout/index.vue
  6. 0 6
      src/router/index.js
  7. 0 71
      src/utils/ccPhoneBarShared.js
  8. 10 0
      src/utils/common.js
  9. 0 229
      src/utils/incomingCallAttention.js
  10. 125 411
      src/views/aiSipCall/aiSipCallManualOutbound.vue
  11. 49 95
      src/views/aiSipCall/softPhone.vue
  12. 21 215
      src/views/company/companyVoiceRobotic/handleManualAnswered.vue
  13. 68 33
      src/views/company/companyVoiceRobotic/index.vue
  14. 67 33
      src/views/company/companyVoiceRobotic/myIndex.vue
  15. 1 32
      src/views/company/wxAccount/index.vue
  16. 22 2
      src/views/crm/components/AppendCustomerSelect.vue
  17. 22 2
      src/views/crm/components/CustomerSelect.vue
  18. 6 2
      src/views/crm/components/addOrEditCustomer.vue
  19. 5 5
      src/views/crm/components/addVisitStatus.vue
  20. 21 2
      src/views/crm/components/customerAssignList.vue
  21. 20 7
      src/views/crm/components/customerCallLogList.vue
  22. 21 1
      src/views/crm/components/customerContacts.vue
  23. 6 2
      src/views/crm/components/customerDetails.vue
  24. 21 1
      src/views/crm/components/customerSmsLogsList.vue
  25. 9 3
      src/views/crm/customer/assist.vue
  26. 25 14
      src/views/crm/customer/customerAll.vue
  27. 18 2
      src/views/crm/customer/full.vue
  28. 8 2
      src/views/crm/customer/index.vue
  29. 3 2
      src/views/crm/customer/line.vue
  30. 29 7
      src/views/crm/customer/manualOutboundCallLog.vue
  31. 55 33
      src/views/crm/customer/my.vue
  32. 45 7
      src/views/crm/customer/myManualOutboundCallLog.vue
  33. 21 1
      src/views/crm/customerContacts/index.vue
  34. 1 1
      src/views/qw/externalContact/mycustomer.vue

+ 71 - 396
src/api/aiSipCall/softPhone.js

@@ -2,56 +2,9 @@
 // 本模块负责 SIP 注册、音频处理、DTMF、保持/接回等,不包含外呼逻辑(由 ccPhoneBarSocket 实现)
 
 import * as JsSIP from 'jssip';
-import incomingRingAudio from '@/assets/来电铃声.mp3';
 
-// ========== 回铃音 / 来电铃声 URL ==========
+// ========== 回铃音 URL(使用本地音频文件) ==========
 export const RINGBACK_AUDIO_URL = '/assets/voice/ringback.wav';
-export const INCOMING_RING_AUDIO_URL = incomingRingAudio;
-
-/** 当前活跃的 WebPhone 实例,供控制台测试使用 */
-let activeWebPhoneInstance = null;
-
-// ========== 默认配置常量(IPCC 与 JsSIP 严格分离) ==========
-
-/** IPCC(呼叫中心)服务器默认配置 */
-export const IPCC_DEFAULTS = {
-  SERVER_PROD: 'sip.ylrzcloud.com',
-  SERVER_LOCAL: '129.28.164.235',
-  PORT_LOCAL: 1081,
-  CONNECT_TIMEOUT: 15000,
-  HEARTBEAT_INTERVAL: 16
-};
-
-/** JsSIP(软电话)SIP 默认配置 */
-export const JS_SIP_DEFAULTS = {
-  SERVER: 'wss://sip.ylrzcloud.com:8443', // 线上环境
-  DOMAIN: 'sip.ylrzcloud.com',
-  TRANSPORT: 'wss',
-  USER_AGENT: 'JsSIP',
-  SESSION_EXPIRES: 180,
-  MIN_SESSION_EXPIRES: 90,
-  SPEAKER_VOLUME: 0.8,
-  MIC_VOLUME: 0.8,
-  RECONNECT_INTERVAL: 15,
-  RECONNECT_TOTAL_DURATION: 60000
-};
-
-/**
- * 将字符串转换为 Base64(安全,使用 TextEncoder 替代已弃用的 unescape)
- */
-const toBase64 = (str) => {
-  const bytes = new TextEncoder().encode(str);
-  return btoa(String.fromCharCode(...bytes));
-};
-
-/**
- * 从 Base64 解码为字符串(安全,使用 TextDecoder 替代已弃用的 escape)
- */
-const fromBase64 = (b64) => {
-  const binary = atob(b64);
-  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
-  return new TextDecoder().decode(bytes);
-};
 
 /**
  * 简单的密码混淆(非加密,仅避免明文暴露)
@@ -62,12 +15,14 @@ const encodePassword = (pwd) => {
   try {
     const timestamp = Date.now().toString(36);
     const pwdStr = String(pwd);
-    const encoded = toBase64(pwdStr);
+    const encoded = btoa(unescape(encodeURIComponent(pwdStr)));
     return `${timestamp}:${encoded}`;
   } catch (e) {
+    console.error('[密码] 编码失败');
     try {
       return btoa(String(pwd));
     } catch (fallbackError) {
+      console.error('[密码] 备选编码失败');
       return '';
     }
   }
@@ -80,8 +35,9 @@ const decodePassword = (encoded) => {
     if (encoded.includes(':')) {
       base64Part = encoded.split(':')[1];
     }
-    return fromBase64(base64Part);
+    return decodeURIComponent(escape(atob(base64Part)));
   } catch (e) {
+    console.warn('[密码] 解码失败,可能是旧格式');
     return '';
   }
 };
@@ -150,12 +106,12 @@ export class ProfileManager {
       users: {},
       user: '',
       reconnect: true,
-      reconnect_interval: JS_SIP_DEFAULTS.RECONNECT_INTERVAL,
-      user_agent: JS_SIP_DEFAULTS.USER_AGENT,
-      session_expires: JS_SIP_DEFAULTS.SESSION_EXPIRES,
-      min_session_expires: JS_SIP_DEFAULTS.MIN_SESSION_EXPIRES,
-      speaker_volume: JS_SIP_DEFAULTS.SPEAKER_VOLUME,
-      mic_volume: JS_SIP_DEFAULTS.MIC_VOLUME,
+      reconnect_interval: 15,
+      user_agent: 'JsSIP',
+      session_expires: 180,
+      min_session_expires: 120,
+      speaker_volume: 0.8,
+      mic_volume: 0.8,
       speaker_paused: false,
       mic_paused: false,
       auto_answer: false,
@@ -176,14 +132,13 @@ export class ProfileManager {
       this.profile.reconnect_interval = reconnectInterval;
       this.save();
     }
-    const currentUser = this.getCurrentUserProfile() || {};
     const settings = {
       user_agent: this.profile.user_agent,
       session_expires: this.profile.session_expires,
       min_session_expires: this.profile.min_session_expires,
       stun: this.profile.stun,
       ice_server: this.profile.ice_server,
-      auto_answer: currentUser.auto_answer !== undefined ? currentUser.auto_answer : this.profile.auto_answer,
+      auto_answer: this.profile.auto_answer,
       reconnect: this.profile.reconnect,
       reconnect_interval: reconnectInterval
     };
@@ -197,10 +152,10 @@ export class ProfileManager {
 
   resetSettings() {
     this.profile.reconnect = true;
-    this.profile.reconnect_interval = JS_SIP_DEFAULTS.RECONNECT_INTERVAL;
-    this.profile.user_agent = JS_SIP_DEFAULTS.USER_AGENT;
-    this.profile.session_expires = JS_SIP_DEFAULTS.SESSION_EXPIRES;
-    this.profile.min_session_expires = JS_SIP_DEFAULTS.MIN_SESSION_EXPIRES;
+    this.profile.reconnect_interval = 15;
+    this.profile.user_agent = 'JsSIP';
+    this.profile.session_expires = 180;
+    this.profile.min_session_expires = 120;
     this.profile.auto_answer = false;
     this.profile.stun = false;
     this.profile.ice_server = '';
@@ -235,31 +190,14 @@ export class ProfileManager {
 
   updateUser(userId, updatedProfile) {
     if (this.profile.users[userId]) {
-      const newUser = updatedProfile.user;
-      const newDomain = updatedProfile.domain;
-      const newUserId = `${newUser}@${newDomain}`;
-
-      if (newUserId !== userId) {
-        // user 或 domain 改变了,删除旧条目并用新 key 创建
-        const merged = {
-          ...this.profile.users[userId],
-          ...updatedProfile,
-          user: newUser,
-          domain: newDomain
-        };
-        delete this.profile.users[userId];
-        this.profile.users[newUserId] = merged;
-        if (this.profile.user === userId) {
-          this.profile.user = newUserId;
-        }
-      } else {
-        this.profile.users[userId] = {
-          ...this.profile.users[userId],
-          ...updatedProfile,
-          user: newUser,
-          domain: newDomain
-        };
-      }
+      const existing = this.profile.users[userId];
+      const merged = {
+        ...existing,
+        ...updatedProfile,
+        user: existing.user,
+        domain: existing.domain
+      };
+      this.profile.users[userId] = merged;
       this.save();
     }
   }
@@ -301,23 +239,18 @@ export class WebPhone {
     this.reconnectEnabled = settings.reconnect;
     this.reconnectAttempts = 0;
     this.reconnectStartTime = null;
-    this.reconnectTotalDuration = JS_SIP_DEFAULTS.RECONNECT_TOTAL_DURATION;
+    this.reconnectTotalDuration = 1 * 60 * 1000; // 1分钟
     this.isReconnecting = false;
     this.reconnectTimerId = null;
     this._isHandlingDisconnect = false;
 
-    // 外呼回铃音
+    // 使用 Base64 回铃音
     this.ringbackMedia = new Audio(RINGBACK_AUDIO_URL);
     this.ringbackMedia.loop = true;
-    // 来电铃声
-    this.incomingRingMedia = new Audio(INCOMING_RING_AUDIO_URL);
-    this.incomingRingMedia.loop = true;
-    this._simulatedIncoming = false;
     this.remoteMedia = new Audio();
     this.localMedia = new Audio();
-    this.peerConnection = null;
-    this.localStream = null;
 
+    // this.applyWebSocketPatch();
     this.initUA();
   }
 
@@ -364,64 +297,49 @@ export class WebPhone {
 
   initUA() {
     if (!this.profile.server || !this.profile.user || !this.profile.domain) {
-      console.error('[jsSip] 配置不完整');
+      console.error('[SIP] 配置不完整');
       this.emit('OnStatusMessage', { type: 'error', text: '配置不完整' });
       return;
     }
-    // JsSIP 3.x 底层创建 WebSocket 时已自动使用 'sip' 子协议(RFC 7118)
-    const socket = new JsSIP.WebSocketInterface(this.profile.server);
-
-    // 修复 JsSIP 3.x 的 via_transport 问题:
-    // 对于 WSS 连接,JsSIP 设置 via_transport = 'WSS'(WebSocketInterface.js:23)
-    // 但 RFC 3261/7118 中 Via header transport 应用 'WS'(不分是否 TLS)
-    // 若服务器不识别 'WSS' 会静默丢弃 REGISTER 请求,导致超时
-    if (String(this.profile.server || '').startsWith('wss://')) {
-      socket.via_transport = 'WS';
-      console.log('[jsSip] WSS 连接: 修正 via_transport WSS → WS(RFC 7118)');
-    }
+    const socket = new JsSIP.WebSocketInterface(this.profile.server, { protocols: [] });
+    socket.via_transport = this.profile.transport || 'wss';
 
     const user = String(this.profile.user || '');
+    const domain = String(this.profile.domain || '');
     const displayName = this.profile.display_name ? String(this.profile.display_name) : '';
     const password = this.profile.password ? String(this.profile.password) : '';
     const server = String(this.profile.server || '');
-    // SIP over WebSocket 的 transport 始终为 'ws'(RFC 7118),与是否 WSS/TLS 无关
-    const transport = 'ws';
+    const transport = String(this.profile.transport || 'wss');
 
-    const domain = String(this.profile.domain || '');
     if (!user || !domain || !password) {
-      console.error('[jsSip] 账号配置缺失:', { user, domain, hasPassword: !!password });
+      console.error('[SIP] 账号配置缺失:', { user, domain, hasPassword: !!password });
       this.emit('OnStatusMessage', { type: 'error', text: '账号配置不完整,请检查登录名、域名和密码' });
       return;
     }
 
-    // 检测混合内容问题:HTTP 页面使用 WSS 连接会被浏览器阻止
-    if (server.startsWith('wss://') && window.location.protocol === 'http:') {
-      console.warn('[jsSip] 警告: 页面通过 HTTP 加载,但 SIP 服务器使用 WSS 协议。浏览器会阻止混合内容,请使用 HTTPS 或改用 WS 协议');
-      this.emit('OnStatusMessage', { type: 'warn', text: 'HTTP页面使用WSS会被浏览器阻止' });
-    }
-
     const uri = new JsSIP.URI('sip', user, domain);
-    const contactUriStr = `sip:${user}@${domain};transport=${transport}`;
+    const contactUriStr = `sip:${user}@${domain};transport=ws`;
 
     this.configuration = {
       sockets: [socket],
       authorization_user: user,
-      user_agent: this.settings.user_agent || JS_SIP_DEFAULTS.USER_AGENT,
+      hack_ip_in_contact: true,
+      user_agent: this.settings.user_agent || 'JsSIP',
       display_name: displayName || undefined,
-      // 启用会话定时器,防止长时间通话被中间代理断开
       session_timers: true,
-      session_timers_expires: this.settings.session_expires || JS_SIP_DEFAULTS.SESSION_EXPIRES,
-      session_timers_min_se: this.settings.min_session_expires || JS_SIP_DEFAULTS.MIN_SESSION_EXPIRES,
       no_answer_timeout: 60,
-      register: true,
       uri: uri.toAor(),
       contact_uri: contactUriStr,
-      // 始终使用 password 模式进行认证
-      // 移除旧的 ha1 启发式判断(password.length === 32),避免错误地将32位密码当成HA1
-      password: password
     };
 
-    console.log(`[jsSip] UA配置完成: ${user}@${domain}, 服务器: ${server}, 认证方式: password`);
+    if (password && password.length === 32) {
+      this.configuration.ha1 = password;
+      this.configuration.realm = domain;
+    } else {
+      this.configuration.password = password;
+    }
+
+    console.log(`[SIP] UA配置完成: ${user}@${domain}`);
   }
 
   createUA() {
@@ -436,19 +354,12 @@ export class WebPhone {
     this.ua.on('registrationExpiring', this.registrationExpiring.bind(this));
     this.ua.on('newRTCSession', this.newRTCSession.bind(this));
     this.ua.on('newMessage', this.newMessage.bind(this));
-    this.ua.on('transportError', this.transportError.bind(this));
   }
 
   On(event, callback) {
     this.events[event] = callback;
   }
 
-  Off(event) {
-    if (this.events[event]) {
-      delete this.events[event];
-    }
-  }
-
   emit(event, ...args) {
     if (this.events[event]) {
       try {
@@ -471,7 +382,6 @@ export class WebPhone {
   }
 
   Start(reconnect, isReconnect = false) {
-    activeWebPhoneInstance = this;
     console.log(`[SIP] 启动 ${reconnect ? '启用重连' : '禁用重连'} ${isReconnect ? '(重连模式)' : ''}`);
     this.reconnectEnabled = reconnect;
     if (this.ua) {
@@ -564,31 +474,20 @@ export class WebPhone {
 
   // ---- JsSIP 事件处理 ----
   connecting() {
-    console.log('[jsSip] 连接中...');
-    this.emit('OnStatusMessage', { type: 'info', text: 'jsSip连接中...' });
+    console.log('[SIP] 连接中...');
+    this.emit('OnStatusMessage', { type: 'info', text: '连接中...' });
   }
   connected() {
-    console.log('[jsSip] 已连接,开始注册');
+    console.log('[SIP] 已连接,开始注册');
     this.Register();
-    this.emit('OnStatusMessage', { type: 'success', text: 'jsSip开始注册' });
+    this.emit('OnStatusMessage', { type: 'success', text: '已连接' });
   }
   disconnected(e) {
     if (this._isHandlingDisconnect) return;
     this._isHandlingDisconnect = true;
-    const reason = e?.cause || e?.message || '未知原因';
-    const code = e?.code || '';
-    if (code !== '') {
-      console.error(`[SIP] 连接断开: ${reason}`, {
-        code,
-        cause: e?.cause,
-        message: e?.message,
-        server: this.profile?.server || 'unknown',
-        fullEvent: e
-      });
-    }
-
+    console.log('[SIP] 连接断开');
     this.emit('OnRegister', { registered: false });
-    this.emit('OnStatusMessage', { type: 'error', text: 'WSS断开: ' + reason });
+    this.emit('OnStatusMessage', { type: 'error', text: '连接断开' });
     if (this.ua) {
       try { this.ua.stop(); } catch (err) {}
       this.ua = null;
@@ -597,74 +496,27 @@ export class WebPhone {
     setTimeout(() => { this._isHandlingDisconnect = false; }, 1000);
   }
   registered() {
+    console.log('[SIP] 连接成功');
     this.resetReconnectState();
     this.emit('OnRegister', { registered: true });
-    this.emit('OnStatusMessage', { type: 'success', text: '连接' });
+    this.emit('OnStatusMessage', { type: 'success', text: '连接成功' });
     this.SetQueueIn();
   }
   unregistered() {
+    console.log('[SIP] 已注销');
     this.emit('OnRegister', { registered: false });
-    this.emit('OnStatusMessage', { type: 'info', text: 'jsSip已注销' });
+    this.emit('OnStatusMessage', { type: 'info', text: '已注销' });
   }
   registrationFailed(e) {
-    const cause = e?.cause || e?.message || '未知原因';
-    const statusCode = e?.response?.status_code || '';
-    console.error('[jsSip] 注册失败:', {
-      cause,
-      status_code: statusCode,
-      response: e?.response,
-      server: this.profile?.server || 'unknown',
-      user: this.profile?.user || 'unknown',
-      domain: this.profile?.domain || 'unknown'
-    });
-
-    // 根据失败原因给出更明确的中文提示
-    let errorText = '注册失败: ' + cause;
-    if (cause === 'Connection Error') {
-      errorText = '注册失败: 无法连接到SIP服务器,请检查服务器地址 ' + (this.profile?.server || '') + ' 是否可访问';
-    } else if (cause.includes('403') || cause.includes('Forbidden') || cause.includes('401') || cause.includes('Unauthorized')) {
-      errorText = '注册失败: 认证失败(code:' + statusCode + '),请检查分机号和密码是否正确';
-    } else if (cause.includes('404') || cause.includes('Not Found')) {
-      errorText = '注册失败: 用户不存在(code:404),请检查分机号是否正确';
-    } else if (cause.includes('408') || cause.includes('Timeout') || cause.includes('timeout')) {
-      errorText = '注册失败: 注册请求超时,服务器无响应';
-    }
-
+    console.error('[SIP] 注册失败:', e.cause || '未知原因');
     this.emit('OnRegister', { registered: false });
-    this.emit('OnStatusMessage', { type: 'error', text: errorText });
+    this.emit('OnStatusMessage', { type: 'error', text: '注册失败: ' + (e.cause || '未知原因') });
     if (!this.isReconnecting && this.reconnectEnabled) this.scheduleReconnect();
   }
   registrationExpiring() {
-    console.log('[jsSip] 注册即将过期,重新注册');
+    console.log('[SIP] 注册即将过期,重新注册');
     this.Register();
   }
-  transportError(err) {
-    // 详细输出 WebSocket 错误信息,帮助排查连接问题
-    const errorCode = err?.code || '';
-    const errorMessage = err?.message || '';
-    const errorReason = err?.reason || '';
-    console.error('[SIP] WebSocket传输错误:', {
-      code: errorCode,
-      message: errorMessage,
-      reason: errorReason,
-      server: this.profile?.server || 'unknown',
-      fullError: err
-    });
-
-    let detail = errorMessage || errorReason || JSON.stringify(err);
-    let errorText = 'WSS连接失败: ' + detail;
-
-    if (errorMessage.includes('SecurityError') || errorMessage.includes('mixed content') || errorMessage.includes('Mixed Content')) {
-      errorText = 'WSS连接被浏览器阻止: 页面是HTTP协议,无法连接安全的WSS服务。请使用HTTPS访问页面或改用WS协议';
-    } else if (errorCode === 1006 || errorMessage.includes('close with code 1006')) {
-      errorText = 'WSS连接异常关闭(code 1006): 服务器无响应或SSL证书错误, 服务地址:' + (this.profile?.server || 'unknown');
-    } else if (errorCode === 1005 || errorMessage.includes('close with code 1005')) {
-      errorText = 'WSS连接被拒绝(code 1005): 服务器不接受WebSocket连接, 请检查服务地址和端口';
-    }
-
-    this.emit('OnStatusMessage', { type: 'error', text: errorText });
-    if (!this.isReconnecting && this.reconnectEnabled) this.scheduleReconnect();
-  }
 
   // 外呼功能由 ccPhoneBarSocket 实现,WebPhone 不再实现 Call 方法
   // 但保留 Answer、Terminate、ToggleHold、ToggleMicPhone 等方法供来电和通话控制
@@ -673,16 +525,12 @@ export class WebPhone {
   }
 
   Terminate(code) {
-    const currentSession = this.session;
-    if (currentSession) {
-      if (code) currentSession.terminate({ status_code: code });
-      else currentSession.terminate();
+    if (this.session) {
+      if (code) this.session.terminate({ status_code: code });
+      else this.session.terminate();
     }
     if (this.sessionCloseTimerId) clearTimeout(this.sessionCloseTimerId);
-    // 捕获当前session引用,定时器回调只在session未变时执行清理
-    this.sessionCloseTimerId = setTimeout(() => {
-      if (this.session === currentSession) this.sessionClosed(true, '');
-    }, 1000);
+    this.sessionCloseTimerId = setTimeout(() => this.sessionClosed(true, ''), 1000);
   }
 
   ToggleHold() {
@@ -702,18 +550,10 @@ export class WebPhone {
   SetSpeaker(paused, volume) {
     this.remoteMedia.volume = paused ? 0 : volume;
     this.ringbackMedia.volume = paused ? 0 : volume;
-    if (this.incomingRingMedia) {
-      this.incomingRingMedia.volume = paused ? 0 : volume;
-    }
   }
 
   SetMicPhone(paused, volume) {
     this.localMedia.volume = paused ? 0 : volume;
-    if (this.localStream) {
-      this.localStream.getAudioTracks().forEach((track) => {
-        track.enabled = !paused;
-      });
-    }
   }
 
   SetQueueIn() {
@@ -801,83 +641,24 @@ export class WebPhone {
     }
   }
 
-  pauseIncomingRing() {
-    if (this.incomingRingMedia && !this.incomingRingMedia.paused) {
-      this.incomingRingMedia.pause();
-      console.log('[音频] 暂停来电铃声');
-    }
-  }
-
-  playIncomingRing() {
-    if (this.incomingRingMedia && this.incomingRingMedia.paused) {
-      this.incomingRingMedia.currentTime = 0;
-      this.incomingRingMedia.play().catch(e => console.warn('[音频] 来电铃声播放失败:', e));
-      console.log('[音频] 播放来电铃声');
-    }
-  }
-
-  /**
-   * 控制台模拟来电(触发 UI 事件 + 播放铃声)
-   * @param {string} caller 模拟来电号码
-   */
-  simulateIncomingCall(caller = '13800138000') {
-    this._simulatedIncoming = true;
-    activeWebPhoneInstance = this;
-    this.emit('OnSessionCreated', {
-      outgoing: false,
-      callee: caller,
-      province: '测试',
-      city: ''
-    });
-    this.emit('OnRing', {
-      outgoing: false,
-      caller,
-      province: '测试',
-      city: ''
-    });
-    this.playIncomingRing();
-    console.log(`[测试] 模拟来电: ${caller},执行 __testSoftPhoneStopIncomingCall() 停止`);
-  }
-
-  /** 停止控制台模拟来电 */
-  stopSimulatedIncomingCall() {
-    this.pauseIncomingRing();
-    if (this._simulatedIncoming) {
-      this._simulatedIncoming = false;
-      this.emit('OnSessionClosed', { succeeded: false, reason: 'test_rejected' });
-      console.log('[测试] 已停止模拟来电');
-    }
-  }
-
   newRTCSession(event) {
     console.log('[通话] 新会话创建');
     if (this.session) { event.session.terminate({ status_code: 486 }); return; }
-    // 清除上一次通话可能残留的定时器,防止其回调误清当前session
-    if (this.sessionCloseTimerId) { clearTimeout(this.sessionCloseTimerId); this.sessionCloseTimerId = null; }
     this.session = event.session;
-    // 捕获当前session引用,避免旧session的异步事件回调误清新session
-    const sessionRef = this.session;
     this.session.on('progress', (e) => {
       this.emit('OnRing', {
-        outgoing: sessionRef.direction === 'outgoing',
-        caller: sessionRef.remote_identity?.uri?.user || '',
+        outgoing: this.session.direction === 'outgoing',
         province: e.response?.getHeader('X-Province') || '',
         city: e.response?.getHeader('X-City') || ''
       });
     });
     this.session.on('confirmed', () => {
       this.pauseRingback();
-      this.pauseIncomingRing();
-      this._simulatedIncoming = false;
-      this.emit('OnAnswered', sessionRef.direction === 'outgoing');
+      this.emit('OnAnswered', this.session.direction === 'outgoing');
       this.startCallTimer();
     });
-    this.session.on('ended', () => {
-      if (this.session === sessionRef) this.sessionClosed(true, '');
-    });
-    this.session.on('failed', (e) => {
-      if (this.session === sessionRef) this.sessionClosed(false, e.cause);
-    });
+    this.session.on('ended', () => this.sessionClosed(true, ''));
+    this.session.on('failed', (e) => this.sessionClosed(false, e.cause));
     this.session.on('peerconnection', (pc) => {
       if (pc && typeof pc.addTrack === 'function') {
         this.registerRemoteMedia(pc);
@@ -895,11 +676,8 @@ export class WebPhone {
       province: event.request?.getHeader('X-Province') || '',
       city: event.request?.getHeader('X-City') || ''
     });
-    if (outgoing) {
-      this.playRingback();
-    } else {
-      this.playIncomingRing();
-    }
+    if (!outgoing && this.settings.auto_answer) this.Answer();
+    if (outgoing) this.playRingback();
   }
 
   newMessage(event) {
@@ -914,9 +692,6 @@ export class WebPhone {
       return;
     }
 
-    // 保存 peerConnection 引用
-    this.peerConnection = connection;
-
     // 监听远程流
     connection.ontrack = (e) => {
       if (!this.remoteMedia.srcObject) {
@@ -928,31 +703,9 @@ export class WebPhone {
       }
     };
 
-    // 如果已有本地流,先清理
-    if (this.localStream) {
-      console.log('[音频] 清理旧的本地流');
-      this.localStream.getTracks().forEach(track => {
-        track.stop();
-        if (this.peerConnection) {
-          try {
-            this.peerConnection.removeTrack(track, this.localStream);
-          } catch (err) {
-            // ignore
-          }
-        }
-      });
-      this.localStream = null;
-    }
-
-    // 清理 localMedia 的旧 srcObject
-    if (this.localMedia.srcObject) {
-      this.localMedia.srcObject = null;
-    }
-
     // 获取并添加本地流
     navigator.mediaDevices.getUserMedia({ audio: true })
       .then(stream => {
-        this.localStream = stream;
         this.localMedia.srcObject = stream;
         stream.getTracks().forEach(track => {
           try {
@@ -971,42 +724,12 @@ export class WebPhone {
 
   sessionClosed(succeed, reason) {
     if (!this.session) return;
-
-    console.log('[通话] 会话关闭,开始清理资源');
     this.session = null;
     if (this.callTimerId) clearInterval(this.callTimerId);
     this.pauseRingback();
-    this.pauseIncomingRing();
-    this._simulatedIncoming = false;
-
-    // 清理远程媒体
-    if (this.remoteMedia.srcObject) {
-      this.remoteMedia.srcObject.getTracks().forEach(t => t.stop());
-      this.remoteMedia.srcObject = null;
-    }
-
-    // 清理本地媒体
-    if (this.localMedia.srcObject) {
-      this.localMedia.srcObject.getTracks().forEach(t => t.stop());
-      this.localMedia.srcObject = null;
-    }
-
-    // 清理本地流引用
-    if (this.localStream) {
-      this.localStream.getTracks().forEach(t => t.stop());
-      this.localStream = null;
-    }
-
-    // 清理 peerConnection
-    if (this.peerConnection) {
-      try {
-        this.peerConnection.close();
-      } catch (err) {
-        // ignore
-      }
-      this.peerConnection = null;
-    }
 
+    if (this.remoteMedia.srcObject) this.remoteMedia.srcObject.getTracks().forEach(t => t.stop());
+    if (this.localMedia.srcObject) this.localMedia.srcObject.getTracks().forEach(t => t.stop());
     this.emit('OnSessionClosed', { succeeded: succeed, reason });
   }
 
@@ -1041,16 +764,11 @@ export class WebPhone {
       } catch (e) {}
     };
     cleanupAudio(this.ringbackMedia);
-    cleanupAudio(this.incomingRingMedia);
     cleanupAudio(this.remoteMedia);
     cleanupAudio(this.localMedia);
     this.ringbackMedia = null;
-    this.incomingRingMedia = null;
     this.remoteMedia = null;
     this.localMedia = null;
-    if (activeWebPhoneInstance === this) {
-      activeWebPhoneInstance = null;
-    }
     if (this.audioCtx) {
       try { if (this.audioCtx.state !== 'closed') this.audioCtx.close(); } catch (e) {}
       this.audioCtx = null;
@@ -1068,47 +786,4 @@ export class WebPhone {
   }
 }
 
-/** 获取当前活跃的 WebPhone 实例 */
-export function getActiveWebPhone() {
-  return activeWebPhoneInstance;
-}
-
-// 控制台测试方法(在浏览器 DevTools Console 中执行)
-if (typeof window !== 'undefined') {
-  window.__testSoftPhoneIncomingCall = function(caller = '13800138000') {
-    const phone = getActiveWebPhone();
-    if (!phone) {
-      console.warn('[测试] 软电话未初始化,请先打开页面并等待软电话连接成功');
-      return;
-    }
-    phone.simulateIncomingCall(caller);
-  };
-  window.__testSoftPhoneStopIncomingCall = function() {
-    const phone = getActiveWebPhone();
-    if (!phone) {
-      console.warn('[测试] 软电话未初始化');
-      return;
-    }
-    phone.stopSimulatedIncomingCall();
-  };
-  window.__testSoftPhonePlayRing = function() {
-    const phone = getActiveWebPhone();
-    if (!phone) {
-      console.warn('[测试] 软电话未初始化');
-      return;
-    }
-    phone.playIncomingRing();
-    console.log('[测试] 仅播放铃声,执行 __testSoftPhoneStopIncomingCall() 停止');
-  };
-}
-
-export default {
-  WebPhone,
-  ProfileManager,
-  checkMicrophonePermission,
-  RINGBACK_AUDIO_URL,
-  INCOMING_RING_AUDIO_URL,
-  getActiveWebPhone,
-  IPCC_DEFAULTS,
-  JS_SIP_DEFAULTS
-};
+export default { WebPhone, ProfileManager, checkMicrophonePermission, RINGBACK_AUDIO_URL };

BIN
src/assets/来电铃声.mp3


+ 0 - 2383
src/components/FloatingSoftPhone/index.vue

@@ -1,2383 +0,0 @@
-<template>
-  <div class="floating-softphone">
-    <!-- 来电醒目提示(右下角固定,高于面板层级) -->
-    <transition name="call-bubble-fade">
-      <div class="incoming-call-alert"
-           v-if="isIncomingRinging"
-           @click="onIncomingAlertClick">
-        <div class="incoming-call-alert__ripple"></div>
-        <div class="incoming-call-alert__content">
-          <div class="incoming-call-alert__icon-wrap">
-            <i class="material-icons">phone_in_talk</i>
-          </div>
-          <div class="incoming-call-alert__info">
-            <span class="incoming-call-alert__title">来电响铃中</span>
-            <span class="incoming-call-alert__number">{{ incomingCallDisplayNumber }}</span>
-            <span class="incoming-call-alert__hint">点击卡片打开软电话</span>
-          </div>
-          <div class="incoming-call-alert__actions">
-            <button class="incoming-call-alert__btn answer" title="接听" @click.stop="answerIncomingCall">
-              <i class="material-icons">call</i>
-            </button>
-            <button class="incoming-call-alert__btn reject" title="拒接" @click.stop="rejectIncomingCall">
-              <i class="material-icons">call_end</i>
-            </button>
-          </div>
-        </div>
-      </div>
-    </transition>
-
-    <!-- 右下角FAB触发按钮(可拖拽、自动吸边、可折叠) -->
-    <div class="softphone-fab"
-         :class="{
-           'fab-active': panelVisible,
-           'fab-connected': isRegistered,
-           'fab-collapsed': fabIsCollapsed,
-           'fab-left': fabOnLeftEdge,
-           'fab-ringing': callStatus === 'ringing'
-         }"
-         :style="fabStyle"
-         @mousedown="onFabDragStart"
-         @click="onFabClick">
-      <i class="material-icons">{{ fabIconName }}</i>
-      <span class="fab-badge" v-if="callStatus !== 'idle'"></span>
-    </div>
-
-    <!-- 可拖拽的软电话面板 -->
-    <transition name="softphone-fade">
-      <div class="softphone-panel"
-           v-show="panelVisible"
-           :style="panelStyle"
-           ref="panel">
-        <div class="dialer">
-          <!-- 状态栏(拖拽手柄) -->
-          <div class="status-bar drag-handle" @mousedown="startDrag">
-            <!-- 左侧区域:头像下拉 + 呼叫状态图标 -->
-            <div class="status-left">
-              <div class="user-avatar-dropdown" v-click-outside="closeDropdown">
-                <i class="material-icons user-avatar-icon"
-                   :class="{ 'network-available': isRegistered, 'no-network': !isConnected }"
-                   @click.stop="toggleDropdown"
-                   @mousedown.stop
-                   title="点击切换账号">account_circle</i>
-                <div class="dropdown-menu" v-show="dropdownVisible">
-                  <div class="dropdown-group">
-                    <a href="#" class="dropdown-item" v-for="(userProfile, userId) in userList" :key="userId" @click.prevent="switchAccount(userId)" :title="'切换到: ' + userProfile.note">
-                      <i class="material-icons" v-if="currentUserId === userId">check</i>
-                      <i class="material-icons" v-else style="visibility: hidden;">check</i>
-                      {{ userProfile.note }}
-                    </a>
-                  </div>
-                  <div class="dropdown-group">
-                    <a href="#" class="dropdown-item" @click.prevent="openEditAccountDialog" title="编辑当前账号信息"><i class="material-icons">edit</i>编辑账号</a>
-                    <a href="#" class="dropdown-item" @click.prevent="openAddAccountDialog" title="添加新的SIP账号"><i class="material-icons">add</i>添加账号</a>
-                    <a href="#" class="dropdown-item" @click.prevent="confirmDeleteAccount" title="删除当前账号"><i class="material-icons">delete</i>删除账号</a>
-                  </div>
-                  <div class="dropdown-group">
-                    <a href="#" class="dropdown-item" @click.prevent="resetSettings" title="恢复默认设置"><i class="material-icons">settings_backup_restore</i>清空设置</a>
-                    <a href="#" class="dropdown-item" @click.prevent="resetReconnectState" title="重新连接服务器"><i class="material-icons">autorenew</i>重新连接</a>
-                  </div>
-                </div>
-              </div>
-              <i class="material-icons call-status-icon"
-                 v-show="callStatus !== 'idle'"
-                 :class="{ inprogress: callStatus === 'ringing', 'ringing-icon': callStatus === 'ringing' }"
-                 title="通话中">call</i>
-            </div>
-
-            <!-- 中间区域:用户名居中显示 -->
-            <div class="status-center">
-              <span class="display-user"
-                    :class="{ 'network-available': isRegistered }"
-                    :title="currentUserDisplay">{{ currentUserDisplay }}</span>
-            </div>
-
-            <!-- 右侧区域:麦克风、扬声器、网络状态、最小化 -->
-            <div class="status-right">
-              <div class="volume-control-group" @mouseenter="showMicSlider" @mouseleave="startHideSliderTimer" @mousedown.stop>
-                <i class="material-icons microphone-icon"
-                   :class="{ muted: isMicMuted, 'connection-success': isConnected && isRegistered, 'connection-failed': !isConnected }"
-                   @click="toggleMuteMic"
-                   :title="isMicMuted ? '取消静音' : '静音'">mic</i>
-                <div class="volume-slider-container mic-volume-slider" v-show="micSliderVisible" @mouseenter="cancelHideSliderTimer" @mouseleave="startHideSliderTimer">
-                  <input type="range" min="0" max="1" step="0.01" v-model="micVolume" @input="changeMicVolume" class="volume-slider">
-                </div>
-              </div>
-              <div class="volume-control-group" @mouseenter="showSpeakerSlider" @mouseleave="startHideSliderTimer" @mousedown.stop>
-                <i class="material-icons speaker-icon"
-                   :class="{ muted: isSpeakerMuted, 'connection-success': isConnected && isRegistered, 'connection-failed': !isConnected }"
-                   @click="toggleMuteSpeaker"
-                   :title="isSpeakerMuted ? '取消静音' : '静音'">volume_up</i>
-                <div class="volume-slider-container speaker-volume-slider" v-show="speakerSliderVisible" @mouseenter="cancelHideSliderTimer" @mouseleave="startHideSliderTimer">
-                  <input type="range" min="0" max="1" step="0.01" v-model="speakerVolume" @input="changeSpeakerVolume" class="volume-slider">
-                </div>
-              </div>
-              <i class="material-icons network-icon"
-                 :class="{ 'no-network': !isConnected, 'network-available': isConnected && isRegistered, 'network-connecting': isConnected && !isRegistered }"
-                 :title="!isConnected ? '未连接' : (isRegistered ? '已注册' : '连接中')"
-                 @mousedown.stop>signal_cellular_alt</i>
-              <i class="material-icons minimize-btn" @click.stop="hidePanel" @mousedown.stop title="最小化">remove</i>
-            </div>
-          </div>
-
-          <!-- 拨号显示屏与删除按钮 -->
-          <div class="display-wrapper">
-            <!-- 输入框:支持光标定位和键盘删除 -->
-            <input type="text"
-                   class="dialer-display"
-                   v-model="displayText"
-                   @keydown="handleKeydown"
-                   @input="handleInput"
-                   placeholder="输入电话号码"
-                   ref="dialerInput">
-            <i class="material-icons delete-icon"
-               @click="deleteAtCursor"
-               title="删除">backspace</i>
-          </div>
-
-          <!-- 归属地与计时器 -->
-          <div class="container" v-show="province" title="来电归属地">
-            <span class="province">{{ province }}</span>
-          </div>
-          <div class="container" title="通话时长">
-            <span class="call-timer" v-show="callDuration !== '00:00'">{{ callDuration }}</span>
-          </div>
-
-          <!-- 拨号键盘 -->
-          <div class="dialer-keypad">
-            <button class="dialer-button"
-                    v-for="digit in dialKeys"
-                    :key="digit"
-                    @click="onDigitClick(digit)"
-                    :title="callStatus === 'talking' ? '发送DTMF: ' + digit : '输入: ' + digit">{{ digit }}</button>
-          </div>
-
-          <!-- 呼叫按钮组:空闲=绿色外呼;外呼振铃/通话中=中间挂断;来电振铃=左右接听/拒接;接通=左保持/中挂断/右转移 -->
-          <div class="call-buttons">
-            <button class="call-button call-left-button"
-                    :class="{ hidden: !showSideCallButtons, normal: callStatus === 'talking' && leftButtonNormal }"
-                    @click="onLeftButtonClick"
-                    :title="getLeftButtonTitle()">
-              <i class="material-icons">{{ leftButtonIcon }}</i>
-            </button>
-            <button class="call-button call-hangup-button"
-                    :class="[getHangupButtonClass(), { hidden: !showCenterCallButton }]"
-                    @click="onHangupClick"
-                    :title="getHangupButtonTitle()">
-              <i class="material-icons">{{ hangupButtonIcon }}</i>
-            </button>
-            <button class="call-button call-right-button"
-                    :class="{ hidden: !showSideCallButtons, normal: callStatus === 'talking' && rightButtonNormal, hangup: isIncomingRinging }"
-                    @click="onRightButtonClick"
-                    :title="getRightButtonTitle()">
-              <i class="material-icons">{{ rightButtonIcon }}</i>
-            </button>
-          </div>
-
-          <!-- 底部状态栏 -->
-          <div class="status-footer">
-            <div class="status-footer-left">
-              <div class="status-bar-message"
-                   :class="statusType"
-                   v-if="statusText"
-                   :title="statusText">{{ statusText }}</div>
-              <div class="reconnect-failed" v-if="reconnectFailed" title="重连超时,请尝试手动重新连接">
-                <span>重连超时</span>
-              </div>
-            </div>
-            <div class="version-ribbon" title="软电话版本">v1.0.0</div>
-          </div>
-        </div>
-
-        <!-- 添加/编辑账号模态框 -->
-        <div class="modal" v-show="accountDialogVisible" @click.self="accountDialogVisible = false">
-          <div class="modal-header">
-            <i class="material-icons">{{ accountDialogTitle === '添加账号' ? 'add' : 'edit' }}</i>
-            <span>{{ accountDialogTitle }}</span>
-          </div>
-          <div class="modal-content">
-            <form @submit.prevent="saveAccount">
-              <div class="form-group">
-                <input type="text" v-model="accountForm.note" placeholder="备注">
-              </div>
-              <div class="form-group">
-                <input type="text" v-model="accountForm.server" placeholder="服务(wss://sip.ylrzcloud.com:8443)" required>
-              </div>
-              <div class="form-group">
-                <input type="text" v-model="accountForm.username" placeholder="用户名">
-              </div>
-              <div class="form-group">
-                <input type="text" v-model="accountForm.domain" placeholder="域名" required>
-              </div>
-              <div class="form-group">
-                <input type="text" v-model="accountForm.loginName" placeholder="登录名" required>
-              </div>
-              <div class="form-group">
-                <input :type="showPassword ? 'text' : 'password'" v-model="accountForm.password" placeholder="密码" required>
-                <i class="material-icons password-toggle" @click="showPassword = !showPassword">{{ showPassword ? 'visibility_off' : 'visibility' }}</i>
-              </div>
-              <div class="form-group">
-                <select v-model="accountForm.transport">
-                  <option value="wss">Transport (WSS)</option>
-                  <option value="ws">Transport (WS)</option>
-                </select>
-              </div>
-              <div class="form-buttons">
-                <button type="button" class="cancel-button" @click="accountDialogVisible = false">取消</button>
-                <button type="submit" class="add-button">保存</button>
-              </div>
-            </form>
-          </div>
-        </div>
-
-        <!-- 设置模态框 -->
-        <div class="modal" v-show="settingsDialogVisible" @click.self="settingsDialogVisible = false">
-          <div class="modal-header">
-            <i class="material-icons">settings</i>
-            <span>SIP 设置</span>
-          </div>
-          <div class="modal-content">
-            <form @submit.prevent="saveSettings">
-              <div class="form-group">
-                <label class="form-label">User Agent</label>
-                <input type="text" v-model="settingsForm.userAgent" placeholder="例如: JsSIP" required>
-              </div>
-              <div class="form-group">
-                <label class="form-label">Session Expires</label>
-                <input type="number" v-model.number="settingsForm.sessionExpires" placeholder="例如: 180" required min="60">
-              </div>
-              <div class="form-group">
-                <label class="form-label">Min Session Expires</label>
-                <input type="number" v-model.number="settingsForm.minSessionExpires" placeholder="例如: 120" required min="30">
-              </div>
-              <div class="form-group">
-                <label class="form-label">启用 STUN</label>
-                <select v-model="settingsForm.stun">
-                  <option :value="false">否</option>
-                  <option :value="true">是</option>
-                </select>
-              </div>
-              <div class="form-group">
-                <label class="form-label">ICE Server</label>
-                <input type="text" v-model="settingsForm.iceServer" placeholder="例如: stun:stun.l.google.com:19302">
-              </div>
-              <div class="form-group">
-                <label class="form-label">自动重连</label>
-                <select v-model="settingsForm.reconnect">
-                  <option :value="true">是</option>
-                  <option :value="false">否</option>
-                </select>
-              </div>
-              <div class="form-group" v-if="settingsForm.reconnect">
-                <label class="form-label">重连间隔(秒)</label>
-                <input type="number" v-model.number="settingsForm.reconnectInterval" placeholder="例如: 15" min="5" max="300">
-              </div>
-              <div class="form-buttons">
-                <button type="button" class="cancel-button" @click="settingsDialogVisible = false">取消</button>
-                <button type="submit" class="add-button">保存</button>
-              </div>
-            </form>
-          </div>
-        </div>
-      </div>
-    </transition>
-  </div>
-</template>
-
-<script>
-import { WebPhone, ProfileManager, checkMicrophonePermission, IPCC_DEFAULTS, JS_SIP_DEFAULTS } from '@/api/aiSipCall/softPhone.js';
-import ccPhoneBarSocket from '@/assets/callCenterPhoneBarSdk/ccPhoneBarSocket.js';
-import { EventList, VideoLevels, AgentStatusEnum } from '@/assets/callCenterPhoneBarSdk/constants.js';
-import { myCallUser, getToolbarBasicParam } from '@/api/aiSipCall/aiSipCallUser.js';
-import { syncByUuid } from '@/api/aiSipCall/aiSipCallOutboundCdr.js';
-import {
-  beginCCPhoneBarInit,
-  finishCCPhoneBarInitFailed,
-  finishCCPhoneBarInitSuccess,
-  getConnectedSharedCCPhoneBar,
-  incrementSharedCCPhoneBarRef
-} from '@/utils/ccPhoneBarShared';
-import {
-  startIncomingCallAttention,
-  stopIncomingCallAttention,
-  requestIncomingCallNotificationPermission
-} from '@/utils/incomingCallAttention';
-
-// IPCC 和 JsSIP 默认配置已从 softPhone.js 统一导入,此处仅作别名引用
-const IPCC_CONFIG = IPCC_DEFAULTS;
-const JS_SIP_CONFIG = JS_SIP_DEFAULTS;
-
-const VOLUME_CONFIG = {
-  DEFAULT: 0.8,
-  HIDE_DELAY: 1000
-};
-
-const UI_STATE = {
-  IDLE: 'idle',
-  RINGING: 'ringing',
-  TALKING: 'talking'
-};
-
-const FAB_SIZE = 56;
-const FAB_RING_SIZE = 64;
-const FAB_EDGE_OFFSET = 8;
-const FAB_MIN_VISIBLE = 28;
-const FAB_SAFE_MARGIN = 16;
-
-export default {
-  name: 'FloatingSoftPhone',
-  directives: {
-    'click-outside': {
-      bind(el, binding, vnode) {
-        el.clickOutsideEvent = function(event) {
-          if (!(el === event.target || el.contains(event.target))) {
-            vnode.context[binding.expression]();
-          }
-        };
-        document.body.addEventListener('click', el.clickOutsideEvent);
-      },
-      unbind(el) {
-        document.body.removeEventListener('click', el.clickOutsideEvent);
-      }
-    }
-  },
-  data() {
-    return {
-      // ===== 面板与拖拽状态 =====
-      panelVisible: false,
-      isDragging: false,
-      panelX: null,
-      panelY: null,
-      dragOffsetX: 0,
-      dragOffsetY: 0,
-
-      // ===== FAB按钮拖拽与吸边 =====
-      fabX: null,
-      fabY: null,
-      fabIsDragging: false,
-      fabDragOffsetX: 0,
-      fabDragOffsetY: 0,
-      fabIsCollapsed: false,  // 是否折叠到边缘(半隐藏)
-      fabDragMoved: false,  // 拖拽过程中是否产生了位移(区分点击和拖拽)
-      fabEdge: 'right',     // 贴边方向:left | right
-      fabYRatio: 0.85,      // 相对视口高度的纵向位置(0~1)
-      _resizeTimer: null,
-
-      // ===== 拨号与显示 =====
-      dialNumber: '',
-      callDuration: '00:00',
-      province: '',
-      isContentFit: true,
-
-      // ===== 网络与注册状态 =====
-      isRegistered: false,
-      isConnected: false,
-      callStatus: UI_STATE.IDLE,
-      isIncomingCall: false,  // 振铃阶段是否为来电(非外呼)
-      incomingCaller: '',  // 来电号码
-
-      // ===== 音频控制 =====
-      speakerVolume: VOLUME_CONFIG.DEFAULT,
-      micVolume: VOLUME_CONFIG.DEFAULT,
-      isSpeakerMuted: false,
-      isMicMuted: false,
-      speakerSliderVisible: false,
-      micSliderVisible: false,
-      volumeTimerId: null,
-
-      // ===== UI按钮状态 =====
-      showLeftButton: false,
-      showRightButton: false,
-      leftButtonNormal: false,
-      rightButtonNormal: false,
-      rightButtonHangup: false,
-
-      // ===== 用户账号管理 =====
-      dropdownVisible: false,
-      userList: {},
-      currentUserId: '',
-      currentUserDisplay: '',
-
-      // ===== 账号对话框 =====
-      accountDialogVisible: false,
-      accountDialogTitle: '添加账号',
-      isEditMode: false,
-      editingUserId: null,
-      accountForm: {
-        note: '',
-        server: JS_SIP_CONFIG.SERVER,
-        username: '',
-        domain: JS_SIP_CONFIG.DOMAIN,
-        loginName: '',
-        password: '',
-        transport: JS_SIP_CONFIG.TRANSPORT
-      },
-      showPassword: false,
-
-      // ===== 设置对话框 =====
-      settingsDialogVisible: false,
-      settingsForm: {
-        userAgent: 'JsSIP',
-        sessionExpires: 180,
-        minSessionExpires: 120,
-        stun: false,
-        iceServer: '',
-        autoAnswer: false,
-        reconnect: true,
-        reconnectInterval: 15
-      },
-
-      // ===== 拨号键盘 =====
-      dialKeys: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'],
-
-      // ===== SIP电话实例 =====
-      phone: null,
-      profileManager: null,
-
-      // ===== 状态提示 =====
-      statusText: '',
-      statusType: 'info',
-      statusTimerId: null,
-      isReconnecting: false,
-      reconnectFailed: false,
-
-      // ===== 呼叫中心集成 =====
-      ccPhoneBar: null,
-      // 标记当前 ccPhoneBar 是否为共享复用实例(非本组件创建)
-      _isSharedCCPhoneBar: false,
-      ccSocketConnected: false,
-      ccSocketFailed: false,
-      ccConnectingPromise: null,
-      ccConnectingResolve: null,
-      ccConnectingReject: null,
-      // ccPhoneBar 自动重连相关
-      _ccReconnectTimer: null,
-      _ccReconnectAttempts: 0,
-      _ccLoginConflict: false,
-      _initCCPromise: null,
-      _isDestroying: false,
-      _ccEventsBoundFor: null,
-
-      // ===== 坐席状态 =====
-      isCallingReady: false,
-      isOnHold: false,
-
-      // ===== 通话记录 =====
-      currentCallUuid: '',
-      callUuidMap: {},
-
-      // ===== 号码存储与展示 =====
-      plaintextRealNumber: '',  // 存储的真实号码(用于拨号)
-      displayText: '',  // 展示文本(脱敏格式或原始输入)
-
-      // ===== 委托通话标记 =====
-      delegatedCallActive: false,  // 是否有通话委托给弹窗的phoneBar
-      isOutboundInProgress: false,  // 外呼进行中(含 SIP 回振坐席阶段,需自动接听)
-      incomingJsipCall: false,  // 是否为JsSIP直接来电(转人工等)
-      pendingManualNavigation: false  // 转人工来电结束后是否需要跳转到通话列表
-    };
-  },
-  computed: {
-    // 真实号码(用于拨号)
-    realNumber() {
-      return this.plaintextRealNumber || '';
-    },
-    isEnabled() {
-      return true;
-    },
-    isOnSoftPhonePage() {
-      return this.$route && this.$route.path && this.$route.path.includes('/softPhone');
-    },
-    panelStyle() {
-      if (this.panelX === null) {
-        return { position: 'fixed', bottom: '90px', right: '24px', zIndex: 9999 };
-      }
-      return {
-        position: 'fixed',
-        left: this.panelX + 'px',
-        top: this.panelY + 'px',
-        zIndex: 9999
-      };
-    },
-    fabStyle() {
-      if (this.fabX === null) {
-        return { position: 'fixed', bottom: '24px', right: '24px', zIndex: 9998 };
-      }
-      return {
-        position: 'fixed',
-        left: this.fabX + 'px',
-        top: this.fabY + 'px',
-        zIndex: 9998
-      };
-    },
-    /** 来电提示显示的号码 */
-    incomingCallDisplayNumber() {
-      if (this.incomingCaller) {
-        return this.maskNumber(this.incomingCaller);
-      }
-      if (this.displayText) {
-        return this.displayText;
-      }
-      return '未知号码';
-    },
-    hangupButtonIcon() {
-      return this.callStatus !== 'idle' ? 'call_end' : 'phone';
-    },
-    /** 来电振铃时不用 close 图标,避免与面板关闭态混淆且吸边时易被裁切 */
-    fabIconName() {
-      if (this.isIncomingRinging) {
-        return 'phone_in_talk';
-      }
-      return this.panelVisible ? 'close' : 'phone';
-    },
-    fabOnLeftEdge() {
-      if (this.fabEdge === 'left') return true;
-      if (this.fabEdge === 'right') return false;
-      if (this.fabX === null) return false;
-      return this.fabX + FAB_SIZE / 2 < this.getViewportSize().width / 2;
-    },
-    /** 来电振铃(左右接听/拒接,隐藏中间按钮) */
-    isIncomingRinging() {
-      return this.callStatus === UI_STATE.RINGING && this.isIncomingCall;
-    },
-    /** 中间按钮:空闲外呼 / 外呼振铃挂断 / 通话中挂断 */
-    showCenterCallButton() {
-      return this.callStatus === UI_STATE.IDLE
-        || (this.callStatus === UI_STATE.RINGING && !this.isIncomingCall)
-        || this.callStatus === UI_STATE.TALKING;
-    },
-    /** 左右侧按钮:来电振铃 或 通话中 */
-    showSideCallButtons() {
-      return this.isIncomingRinging || this.callStatus === UI_STATE.TALKING;
-    },
-    leftButtonIcon() {
-      if (this.isIncomingRinging) return 'phone';
-      if (this.callStatus === 'talking') {
-        if (this.ccPhoneBar && this.ccSocketConnected) {
-          return this.isOnHold ? 'play_arrow' : 'pause';
-        }
-        return this.phone && this.phone.IsOnHold() ? 'play_arrow' : 'pause';
-      }
-      return '';
-    },
-    rightButtonIcon() {
-      if (this.isIncomingRinging) return 'call_end';
-      if (this.callStatus === 'talking') return 'call_split';
-      return '';
-    }
-  },
-  watch: {
-    speakerVolume(val) {
-      if (this.phone) this.phone.SetSpeaker(this.isSpeakerMuted, val);
-    },
-    micVolume(val) {
-      if (this.phone) this.phone.SetMicPhone(this.isMicMuted, val);
-    },
-    dialNumber() {
-      this.updateContentAlignment();
-    },
-    callStatus(val) {
-      if (val === 'ringing' || val === 'talking') {
-        this.panelVisible = true;
-        this.fabIsCollapsed = false;  // 来电/通话时自动展开FAB
-        this.$nextTick(() => this.ensureFabFullyVisible());
-      }
-    },
-    isIncomingRinging(val) {
-      if (val) {
-        this.fabIsCollapsed = false;
-        this.$nextTick(() => this.ensureFabFullyVisible());
-        startIncomingCallAttention({
-          caller: this.incomingCallDisplayNumber,
-          body: `来电号码:${this.incomingCallDisplayNumber},请尽快接听`
-        });
-      } else {
-        stopIncomingCallAttention();
-      }
-    },
-    '$route.path'(newPath, oldPath) {
-      if (this.isOnSoftPhonePage) {
-        this.destroyAllConnections();
-      } else if (oldPath && oldPath.includes('/softPhone')) {
-        this.initCCAndStart();
-      }
-    }
-  },
-  async mounted() {
-    this.profileManager = new ProfileManager();
-    const profile = this.profileManager.getProfile();
-    this.userList = profile.users || {};
-    this.currentUserId = profile.user || '';
-    if (this.currentUserId && this.userList[this.currentUserId]) {
-      this.currentUserDisplay = this.userList[this.currentUserId].note || this.userList[this.currentUserId].user;
-    }
-    const settings = this.profileManager.getSettings();
-    this.settingsForm = { ...settings };
-    this.speakerVolume = profile.speaker_volume !== undefined ? profile.speaker_volume : 0.8;
-    this.micVolume = profile.mic_volume !== undefined ? profile.mic_volume : 0.8;
-    this.isSpeakerMuted = profile.speaker_paused || false;
-    this.isMicMuted = profile.mic_paused || false;
-
-    // 加载面板位置
-    this.loadPosition();
-    // 加载FAB按钮位置
-    this.loadFabPosition();
-    this._boundViewportResize = this.handleWindowResize.bind(this);
-    window.addEventListener('resize', this._boundViewportResize);
-    if (window.visualViewport) {
-      window.visualViewport.addEventListener('resize', this._boundViewportResize);
-      window.visualViewport.addEventListener('scroll', this._boundViewportResize);
-    }
-    this.$nextTick(() => {
-      if (this.$el && this.$el.parentNode && this.$el.parentNode !== document.body) {
-        document.body.appendChild(this.$el);
-      }
-      this.adjustFabForViewport();
-    });
-
-    // 如果不在原softPhone页面,初始化连接
-    if (!this.isOnSoftPhonePage) {
-      await this.initCCAndStart();
-    }
-
-    window.addEventListener('beforeunload', this.handleBeforeUnload);
-
-    // 监听全局外部拨号事件
-    this.$root.$on('floating-softphone-dial', this.dialExternal);
-    // 监听弹窗phoneBar的通话状态同步事件
-    this.$root.$on('dialog-call-ringing', this.onDialogCallRinging);
-    this.$root.$on('dialog-call-talking', this.onDialogCallTalking);
-    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() {
-    stopIncomingCallAttention();
-    this.destroyAllConnections();
-    window.removeEventListener('beforeunload', this.handleBeforeUnload);
-    if (this._boundViewportResize) {
-      window.removeEventListener('resize', this._boundViewportResize);
-      if (window.visualViewport) {
-        window.visualViewport.removeEventListener('resize', this._boundViewportResize);
-        window.visualViewport.removeEventListener('scroll', this._boundViewportResize);
-      }
-    }
-    if (this._resizeTimer) {
-      clearTimeout(this._resizeTimer);
-      this._resizeTimer = null;
-    }
-    this.clearAllTimers();
-    this.removeEventListeners();
-    this.$root.$off('floating-softphone-dial', this.dialExternal);
-    this.$root.$off('dialog-call-ringing', this.onDialogCallRinging);
-    this.$root.$off('dialog-call-talking', this.onDialogCallTalking);
-    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: {
-    getViewportSize() {
-      const vv = window.visualViewport;
-      return {
-        width: vv ? vv.width : window.innerWidth,
-        height: vv ? vv.height : window.innerHeight
-      };
-    },
-    getFabSize() {
-      return this.callStatus === UI_STATE.RINGING ? FAB_RING_SIZE : FAB_SIZE;
-    },
-    ensureFabFullyVisible() {
-      const fabSize = this.getFabSize();
-      const { width, height } = this.getViewportSize();
-      if (this.fabX === null) {
-        this.fabX = width - fabSize - FAB_SAFE_MARGIN;
-        this.fabY = Math.max(0, Math.min(
-          this.fabYRatio != null ? this.fabYRatio * height : height * 0.85,
-          height - fabSize
-        ));
-        return;
-      }
-      this.fabX = Math.max(FAB_SAFE_MARGIN, Math.min(this.fabX, width - fabSize - FAB_SAFE_MARGIN));
-      this.fabY = Math.max(0, Math.min(this.fabY, height - fabSize));
-      this.syncFabRelativeFromAbsolute();
-    },
-    applyFabFromRelative(edge, yRatio) {
-      const { width, height } = this.getViewportSize();
-      this.fabEdge = edge === 'left' ? 'left' : 'right';
-      this.fabYRatio = Math.max(0, Math.min(1, yRatio));
-      this.fabX = this.fabEdge === 'left'
-        ? -FAB_EDGE_OFFSET
-        : width - FAB_SIZE + FAB_EDGE_OFFSET;
-      this.fabY = Math.max(0, Math.min(this.fabYRatio * height, height - FAB_SIZE));
-    },
-    syncFabRelativeFromAbsolute() {
-      const { width, height } = this.getViewportSize();
-      this.fabEdge = this.fabX + FAB_SIZE / 2 < width / 2 ? 'left' : 'right';
-      this.fabYRatio = height > 0 ? Math.max(0, Math.min(1, this.fabY / height)) : 0.85;
-    },
-    adjustFabForViewport() {
-      if (this.fabX === null && !this.fabEdge) return;
-      const { width, height } = this.getViewportSize();
-      if (this.fabIsCollapsed && !this.panelVisible) {
-        const edge = this.fabEdge || (this.fabOnLeftEdge() ? 'left' : 'right');
-        const yRatio = this.fabYRatio != null ? this.fabYRatio : (height > 0 ? this.fabY / height : 0.85);
-        this.applyFabFromRelative(edge, yRatio);
-        return;
-      }
-      const fabSize = this.getFabSize();
-      const minX = FAB_SAFE_MARGIN;
-      const maxX = width - fabSize - FAB_SAFE_MARGIN;
-      this.fabX = Math.max(minX, Math.min(this.fabX, maxX));
-      this.fabY = Math.max(0, Math.min(this.fabY, height - fabSize));
-      this.syncFabRelativeFromAbsolute();
-    },
-
-    // ==================== FAB按钮拖拽与吸边 ====================
-    onFabDragStart(e) {
-      if (e.button !== 0) return;
-      this.fabIsDragging = true;
-      this.fabDragMoved = false;
-      const fabEl = e.currentTarget;
-      const rect = fabEl.getBoundingClientRect();
-      this.fabDragOffsetX = e.clientX - rect.left;
-      this.fabDragOffsetY = e.clientY - rect.top;
-      // 如果还没有位置信息,用当前rect初始化
-      if (this.fabX === null) {
-        this.fabX = rect.left;
-        this.fabY = rect.top;
-      }
-      document.addEventListener('mousemove', this.onFabDrag);
-      document.addEventListener('mouseup', this.onFabDragEnd);
-      document.body.style.userSelect = 'none';
-      e.preventDefault();
-    },
-    onFabDrag(e) {
-      if (!this.fabIsDragging) return;
-      this.fabDragMoved = true;
-      const { width, height } = this.getViewportSize();
-      let x = e.clientX - this.fabDragOffsetX;
-      let y = e.clientY - this.fabDragOffsetY;
-      // 允许超出边缘一半(折叠效果)
-      x = Math.max(-FAB_SIZE / 2, Math.min(x, width - FAB_SIZE / 2));
-      y = Math.max(0, Math.min(y, height - FAB_SIZE));
-      this.fabX = x;
-      this.fabY = y;
-      // 拖拽时取消折叠状态
-      this.fabIsCollapsed = false;
-    },
-    onFabDragEnd() {
-      this.fabIsDragging = false;
-      document.removeEventListener('mousemove', this.onFabDrag);
-      document.removeEventListener('mouseup', this.onFabDragEnd);
-      document.body.style.userSelect = '';
-      // 吸附到最近的边缘
-      this.snapFabToEdge();
-      this.saveFabPosition();
-    },
-    snapFabToEdge() {
-      if (this.fabX === null && !this.fabEdge) return;
-      const { width, height } = this.getViewportSize();
-      const centerX = this.fabX != null ? this.fabX + FAB_SIZE / 2 : (this.fabEdge === 'left' ? 0 : width);
-      const edge = centerX < width / 2 ? 'left' : 'right';
-      const yRatio = height > 0 ? this.fabY / height : (this.fabYRatio || 0.85);
-      this.applyFabFromRelative(edge, yRatio);
-      // 面板打开时不折叠,避免影响操作
-      this.fabIsCollapsed = !this.panelVisible;
-    },
-    onIncomingAlertClick() {
-      this.panelVisible = true;
-      this.fabIsCollapsed = false;
-    },
-    answerIncomingCall() {
-      if (this.phone) {
-        this.phone.Answer();
-      }
-      this.panelVisible = true;
-      this.fabIsCollapsed = false;
-    },
-    rejectIncomingCall() {
-      this.endCall();
-    },
-    onFabClick(e) {
-      // 用户点击软电话时尝试申请桌面通知权限
-      requestIncomingCallNotificationPermission();
-      // 如果拖拽过程中产生了位移,不触发点击
-      if (this.fabDragMoved) {
-        this.fabDragMoved = false;
-        return;
-      }
-      // 折叠状态:展开并打开面板
-      if (this.fabIsCollapsed) {
-        this.fabIsCollapsed = false;
-        this.panelVisible = true;
-        this.$nextTick(() => {
-          const { width, height } = this.getViewportSize();
-          this.fabX = this.fabEdge === 'left' ? 16 : width - FAB_SIZE - 16;
-          this.fabY = Math.max(0, Math.min(this.fabYRatio * height, height - FAB_SIZE));
-        });
-        return;
-      }
-      this.togglePanel();
-    },
-    saveFabPosition() {
-      if (this.fabX === null) return;
-      this.syncFabRelativeFromAbsolute();
-      localStorage.setItem('FloatingSoftPhoneFabPosition', JSON.stringify({
-        edge: this.fabEdge,
-        yRatio: this.fabYRatio,
-        x: this.fabX,
-        y: this.fabY
-      }));
-    },
-    loadFabPosition() {
-      try {
-        const saved = JSON.parse(localStorage.getItem('FloatingSoftPhoneFabPosition'));
-        if (saved) {
-          if (saved.edge != null && saved.yRatio != null) {
-            this.applyFabFromRelative(saved.edge, saved.yRatio);
-          } else if (saved.x != null && saved.y != null) {
-            const { width, height } = this.getViewportSize();
-            const edge = (saved.x + FAB_SIZE / 2) < width / 2 ? 'left' : 'right';
-            const yRatio = height > 0 ? Math.max(0, Math.min(1, saved.y / height)) : 0.85;
-            this.applyFabFromRelative(edge, yRatio);
-          }
-        }
-      } catch (e) { /* ignore */ }
-      if (this.fabX === null) {
-        this.applyFabFromRelative('right', 0.85);
-      } else {
-        this.adjustFabForViewport();
-      }
-      // 初始加载时折叠贴边
-      this.fabIsCollapsed = true;
-    },
-
-    // ==================== 外部拨号接口 ====================
-    /**
-     * 外部调用拨号:存储号码并脱敏展示
-     * 号码由调用方通过服务端API获取已解密的明文号码
-     * @param {Object} params - { phone: string, customerName?: string }
-     */
-    dialExternal({ phone, customerName }) {
-      if (!phone) return;
-      this.dialNumber = phone;
-      this.plaintextRealNumber = phone;
-      this.displayText = this.maskNumber(phone);
-      this.panelVisible = true;
-      if (customerName) {
-        this.showStatus(`外呼: ${customerName}`, 'info');
-      }
-    },
-
-    // ==================== 面板控制 ====================
-    togglePanel() {
-      if (this.panelVisible && this.callStatus !== 'idle') {
-        this.$message.warning('通话中无法收起软电话');
-        return;
-      }
-      this.panelVisible = !this.panelVisible;
-      // 面板关闭后自动折叠FAB,面板打开时展开FAB
-      this.fabIsCollapsed = !this.panelVisible && this.fabX !== null;
-      this.$nextTick(() => this.adjustFabForViewport());
-    },
-    hidePanel() {
-      if (this.callStatus !== 'idle') {
-        this.$message.warning('通话中无法收起软电话');
-        return;
-      }
-      this.panelVisible = false;
-      this.fabIsCollapsed = this.fabX !== null;
-      this.$nextTick(() => this.adjustFabForViewport());
-    },
-
-    // ==================== 拖拽功能 ====================
-    startDrag(e) {
-      if (e.button !== 0) return;
-      // 不拖拽下拉菜单和按钮
-      if (e.target.closest('.user-avatar-dropdown') || e.target.closest('.minimize-btn') || e.target.closest('.volume-control-group')) return;
-      this.isDragging = true;
-      const rect = this.$refs.panel.getBoundingClientRect();
-      this.dragOffsetX = e.clientX - rect.left;
-      this.dragOffsetY = e.clientY - rect.top;
-      document.addEventListener('mousemove', this.onDrag);
-      document.addEventListener('mouseup', this.endDrag);
-      document.body.style.userSelect = 'none';
-      e.preventDefault();
-    },
-    onDrag(e) {
-      if (!this.isDragging) return;
-      const panel = this.$refs.panel;
-      if (!panel) return;
-      const { width, height } = this.getViewportSize();
-      let x = e.clientX - this.dragOffsetX;
-      let y = e.clientY - this.dragOffsetY;
-      const maxX = width - panel.offsetWidth;
-      const maxY = height - panel.offsetHeight;
-      x = Math.max(0, Math.min(x, maxX));
-      y = Math.max(0, Math.min(y, maxY));
-      this.panelX = x;
-      this.panelY = y;
-    },
-    endDrag() {
-      this.isDragging = false;
-      document.removeEventListener('mousemove', this.onDrag);
-      document.removeEventListener('mouseup', this.endDrag);
-      document.body.style.userSelect = '';
-      this.savePosition();
-    },
-    savePosition() {
-      if (this.panelX !== null) {
-        localStorage.setItem('FloatingSoftPhonePosition', JSON.stringify({
-          x: this.panelX, y: this.panelY
-        }));
-      }
-    },
-    loadPosition() {
-      try {
-        const saved = JSON.parse(localStorage.getItem('FloatingSoftPhonePosition'));
-        if (saved && saved.x !== null && saved.x !== undefined) {
-          this.panelX = saved.x;
-          this.panelY = saved.y;
-          this.$nextTick(() => this.clampPosition());
-        }
-      } catch (e) { /* ignore */ }
-    },
-    clampPosition() {
-      if (this.panelX === null) return;
-      this.$nextTick(() => {
-        const panel = this.$refs.panel;
-        if (!panel) return;
-        const { width, height } = this.getViewportSize();
-        const maxX = width - panel.offsetWidth;
-        const maxY = height - panel.offsetHeight;
-        this.panelX = Math.max(0, Math.min(this.panelX, maxX));
-        this.panelY = Math.max(0, Math.min(this.panelY, maxY));
-      });
-    },
-    handleWindowResize() {
-      if (this._resizeTimer) clearTimeout(this._resizeTimer);
-      this._resizeTimer = setTimeout(() => {
-        this.clampPosition();
-        this.adjustFabForViewport();
-      }, 80);
-    },
-
-    // ==================== 号码管理 ====================
-    /** 生成脱敏格式:前3 + **** + 后3 */
-    maskNumber(number) {
-      if (!number || number.length < 7) return number || '';
-      return number.substring(0, 3) + '****' + number.substring(number.length - 3);
-    },
-    /** 清除全部号码 */
-    clearNumber() {
-      this.dialNumber = '';
-      this.plaintextRealNumber = '';
-      this.displayText = '';
-    },
-    /**
-     * 键盘按下事件:处理删除和数字输入
-     * 允许:Backspace/Delete(原生行为删除displayText中的字符)
-     * 允许:数字0-9、*、#(追加到displayText)
-     * 拦截:其他所有输入
-     */
-    handleKeydown(event) {
-      const key = event.key;
-      // 允许的控制键
-      if (['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'Tab'].includes(key)) {
-        // 通话中 Backspace/Delete 不操作
-        if ((key === 'Backspace' || key === 'Delete') && this.callStatus === 'talking') {
-          event.preventDefault();
-          return;
-        }
-        // 允许原生行为(光标移动和删除)
-        return;
-      }
-      // Enter 触发拨号/挂机
-      if (key === 'Enter') {
-        event.preventDefault();
-        this.onHangupClick();
-        return;
-      }
-      // 数字和符号:只在无真实号码时允许输入
-      if ((key >= '0' && key <= '9') || key === '*' || key === '#') {
-        if (this.realNumber) {
-          // 已有真实号码(脱敏状态),不允许追加
-          event.preventDefault();
-          return;
-        }
-        // 通话中发送DTMF
-        if (this.callStatus === 'talking' && this.phone) {
-          this.phone.SendDTMF(key);
-          this.phone.PlayDtmfTone(key);
-          event.preventDefault();
-          return;
-        }
-        // 允许原生输入(会被handleInput捕获同步到dialNumber)
-        return;
-      }
-      // 其他字符一律拦截
-      event.preventDefault();
-    },
-    /**
-     * 输入变化事件:同步displayText到dialNumber(无脱敏状态时)
-     */
-    handleInput() {
-      if (!this.realNumber) {
-        // 无真实号码时,displayText就是dialNumber
-        this.dialNumber = this.displayText;
-      }
-      // 有真实号码时(脱敏状态),displayText变化意味着用户在删除字符
-      // 这里不需要额外操作,因为删除是通过原生Backspace完成的
-    },
-    /**
-     * 右侧删除按钮:模拟在光标位置删除
-     */
-    deleteAtCursor() {
-      const input = this.$refs.dialerInput;
-      if (!input || !this.displayText) return;
-      const pos = input.selectionStart;
-      if (pos === null || pos === 0) {
-        // 没有光标位置或在开头,删末尾
-        this.displayText = this.displayText.slice(0, -1);
-      } else {
-        // 删除光标前一位
-        this.displayText = this.displayText.slice(0, pos - 1) + this.displayText.slice(pos);
-        this.$nextTick(() => {
-          input.setSelectionRange(pos - 1, pos - 1);
-        });
-      }
-      // 同步dialNumber
-      if (!this.realNumber) {
-        this.dialNumber = this.displayText;
-      }
-    },
-
-    // ==================== UI事件 ====================
-    getLeftButtonTitle() {
-      if (this.isIncomingRinging) return '接听来电';
-      if (this.callStatus === 'talking') return this.isOnHold ? '恢复通话' : '保持通话';
-      return '';
-    },
-    getHangupButtonTitle() {
-      if (this.callStatus !== 'idle') return '结束通话';
-      if (this.isRegistered && this.ccSocketConnected && this.isCallingReady) return '发起外呼';
-      // 委托模式下:有号码时显示可拨号
-      if (this.realNumber) return '发起外呼';
-      return '未就绪';
-    },
-    getRightButtonTitle() {
-      if (this.isIncomingRinging) return '拒绝来电';
-      if (this.callStatus === 'talking') return '呼叫转移';
-      return '';
-    },
-    getHangupButtonClass() {
-      if (this.callStatus !== 'idle') return 'hangup';
-      if (this.isRegistered && this.ccSocketConnected && this.isCallingReady) return 'call-ready';
-      // 委托模式下:有号码时可拨号(实际由弹窗phoneBar执行)
-      if (this.realNumber) return 'call-ready';
-      return 'disabled';
-    },
-    updateContentAlignment() {
-      this.$nextTick(() => {
-        const inputEl = this.$refs.dialerInput;
-        if (!inputEl) return;
-        this.isContentFit = inputEl.scrollWidth <= inputEl.clientWidth;
-      });
-    },
-    scrollInputToEnd() {
-      const inputEl = this.$refs.dialerInput;
-      if (!inputEl) return;
-      this.$nextTick(() => {
-        inputEl.focus();
-        inputEl.setSelectionRange(this.dialNumber.length, this.dialNumber.length);
-        inputEl.scrollLeft = inputEl.scrollWidth - inputEl.clientWidth;
-      });
-    },
-    restoreCursorAfterDelete(start, end) {
-      const inputEl = this.$refs.dialerInput;
-      if (!inputEl) return;
-      this.$nextTick(() => {
-        inputEl.focus();
-        inputEl.setSelectionRange(start, end);
-        this.updateContentAlignment();
-      });
-    },
-    deleteCharByCursor() {
-      const inputEl = this.$refs.dialerInput;
-      if (!inputEl) return;
-      if (document.activeElement !== inputEl) {
-        inputEl.focus();
-        inputEl.setSelectionRange(this.dialNumber.length, this.dialNumber.length);
-      }
-      const cursorPos = inputEl.selectionStart || this.dialNumber.length;
-      if (cursorPos > 0) {
-        this.dialNumber = this.dialNumber.slice(0, cursorPos - 1) + this.dialNumber.slice(cursorPos);
-        this.restoreCursorAfterDelete(cursorPos - 1, cursorPos - 1);
-      }
-    },
-    showStatus(text, type = 'info') {
-      if (this.statusTimerId) { clearTimeout(this.statusTimerId); this.statusTimerId = null; }
-      this.statusText = text;
-      this.statusType = type;
-      if (text === '就绪') return;
-      this.statusTimerId = setTimeout(() => {
-        if (this.isRegistered && this.ccSocketConnected && this.isCallingReady) {
-          this.statusText = '就绪';
-          this.statusType = 'success';
-        } else {
-          this.statusText = '';
-        }
-        this.statusTimerId = null;
-      }, 10000);
-    },
-
-    // ==================== 呼叫中心集成 ====================
-    async initCCAndStart() {
-      if (this._ccLoginConflict) {
-        this.showStatus('账号已在其他窗口登录,请关闭多余软电话后点击重新连接', 'error');
-        return;
-      }
-      if (this._initCCPromise) {
-        return this._initCCPromise;
-      }
-
-      const shared = getConnectedSharedCCPhoneBar();
-      if (shared) {
-        this._isDestroying = false;
-        if (this.ccPhoneBar !== shared) {
-          this.ccPhoneBar = shared;
-          this._isSharedCCPhoneBar = true;
-          incrementSharedCCPhoneBarRef();
-        }
-        this.ccSocketConnected = true;
-        this.ccSocketFailed = false;
-        this._bindCCEvents();
-        if (!this.phone || !this.isRegistered) {
-          await this.startPhone();
-        }
-        return;
-      }
-
-      this._initCCPromise = (async () => {
-        this._isDestroying = false;
-        this.ccSocketFailed = false;
-        this.isCallingReady = false;
-        try {
-          await this.ensureCCSocketConnect();
-          await this.startPhone();
-        } catch (err) {
-          this.ccSocketFailed = true;
-          this.showStatus(`初始化失败: ${err.message}`, 'error');
-          throw err;
-        }
-      })();
-
-      try {
-        await this._initCCPromise;
-      } finally {
-        this._initCCPromise = null;
-      }
-    },
-    ensureCCSocketConnect() {
-      if (this.ccSocketConnected) return Promise.resolve();
-      if (this.ccConnectingPromise) return this.ccConnectingPromise;
-      this.ccConnectingPromise = new Promise((resolve, reject) => {
-        this.ccConnectingResolve = resolve;
-        this.ccConnectingReject = reject;
-        this._doConnectCCSocket();
-      });
-      return this.ccConnectingPromise;
-    },
-    async _doConnectCCSocket() {
-      try {
-        const shared = getConnectedSharedCCPhoneBar();
-        if (shared) {
-          console.log('[FloatingSoftPhone] 检测到已连接的共享 ccPhoneBar,直接复用');
-          this.ccPhoneBar = shared;
-          this._isSharedCCPhoneBar = true;
-          this.ccSocketConnected = true;
-          this.ccSocketFailed = false;
-          this.isCallingReady = true;
-          incrementSharedCCPhoneBarRef();
-          this._bindCCEvents();
-          if (this.ccConnectingResolve) this.ccConnectingResolve();
-          this.ccConnectingPromise = null;
-          return;
-        }
-
-        beginCCPhoneBarInit();
-
-        const extRes = await myCallUser();
-        if (extRes.code !== 200 || !extRes.data || !extRes.data.extNum) {
-          throw new Error('未查询到分机号信息');
-        }
-        const { extNum, extPass, gatewayIds: myGateway } = extRes.data;
-        this.setupDefaultAccount(extNum, extPass);
-        const basicRes = await getToolbarBasicParam({ extNum, myGateway });
-        if (basicRes.code !== 0) throw new Error(basicRes.message || '获取配置失败');
-        const configData = basicRes.data;
-        if (!configData.loginToken) throw new Error('登录令牌无效');
-
-        // 将 loginToken 拼接到 SIP 服务器地址上(去掉旧的 loginToken 再拼接新的)
-        if (this.currentUserId && this.userList[this.currentUserId]) {
-          const baseUrl = this.userList[this.currentUserId].server.split('?')[0];
-          this.userList[this.currentUserId].server = `${baseUrl}?loginToken=${configData.loginToken}`;
-        }
-
-        const callConfig = {
-          useDefaultUi: false,
-          loginToken: configData.loginToken,
-          ipccServer: IPCC_CONFIG.SERVER_PROD,
-          gatewayList: configData.gatewayList,
-          gatewayEncrypted: false,
-          extPassword: configData.encryptPsw,
-          extnum: extNum,
-          opnum: configData.opNum || configData.userName,
-          enableWss: true,
-          enableHeartBeat: true,
-          heartBeatIntervalSecs: 16
-        };
-
-        this._isSharedCCPhoneBar = false;
-        this.ccPhoneBar = new ccPhoneBarSocket();
-        this.ccPhoneBar.initConfig(callConfig);
-        this._bindCCEvents();
-        this.ccPhoneBar.connect();
-
-        const timeoutId = setTimeout(() => {
-          if (!this.ccSocketConnected && this.ccConnectingReject) {
-            this.ccConnectingReject(new Error('连接超时'));
-          }
-        }, IPCC_CONFIG.CONNECT_TIMEOUT);
-
-        const originalResolve = this.ccConnectingResolve;
-        this.ccConnectingResolve = () => {
-          clearTimeout(timeoutId);
-          if (originalResolve) originalResolve();
-        };
-      } catch (err) {
-        finishCCPhoneBarInitFailed(err);
-        if (this.ccConnectingReject) this.ccConnectingReject(err);
-        this.ccConnectingPromise = null;
-        throw err;
-      }
-    },
-    _bindCCEvents() {
-      if (this._ccEventsBoundFor === this.ccPhoneBar) return;
-      this._ccEventsBoundFor = this.ccPhoneBar;
-      this.ccPhoneBar.on(EventList.WS_CONNECTED, () => {
-        finishCCPhoneBarInitSuccess(this.ccPhoneBar);
-        window.__sharedCCPhoneBar = this.ccPhoneBar;
-        window.__sharedCCPhoneBarRefCount = 1;
-        this.ccSocketConnected = true;
-        this.ccSocketFailed = false;
-        this._ccLoginConflict = false;
-        this._ccReconnectAttempts = 0;
-        if (this.ccConnectingResolve) this.ccConnectingResolve();
-        this.ccConnectingPromise = null;
-        this.$root.$emit('cc-phonebar-reconnected', this.ccPhoneBar);
-        console.log('[FloatingSoftPhone] ccPhoneBar 连接成功,已注册为共享实例');
-      });
-      this.ccPhoneBar.on(EventList.USER_LOGIN_ON_OTHER_DEVICE, (msg) => {
-        console.warn('[FloatingSoftPhone] 检测到重复登录 status:201', msg);
-        this._ccLoginConflict = true;
-        this._ccReconnectAttempts = 999;
-        if (this._ccReconnectTimer) {
-          clearTimeout(this._ccReconnectTimer);
-          this._ccReconnectTimer = null;
-        }
-        finishCCPhoneBarInitFailed(new Error('user_logined_on_other_device'));
-        this.showStatus('软电话已在其他窗口登录,请关闭多余页面后重新连接', 'error');
-      });
-      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._ccLoginConflict) {
-          this._scheduleCCReconnect();
-        }
-      });
-      this.ccPhoneBar.on(EventList.STATUS_CHANGED, (msg) => {
-        if (msg?.object) {
-          const statusCode = msg.object.status;
-          const busyStatuses = [AgentStatusEnum.BUSY, AgentStatusEnum.BUSY_REST, AgentStatusEnum.BUSY_MEETING, AgentStatusEnum.BUSY_TRAINING];
-          this.isCallingReady = busyStatuses.includes(statusCode);
-        }
-      });
-      this.ccPhoneBar.on(EventList.REQUEST_ARGS_ERROR, () => {
-        if (!this.ccSocketConnected && this.ccConnectingReject) this.ccConnectingReject(new Error('请求参数错误'));
-      });
-      this.ccPhoneBar.on(EventList.SERVER_ERROR, () => {
-        if (!this.ccSocketConnected && this.ccConnectingReject) this.ccConnectingReject(new Error('服务器错误'));
-      });
-      this.ccPhoneBar.on(EventList.OUTBOUND_START, (msg) => {
-        this.isOutboundInProgress = true;
-        this.isIncomingCall = false;
-        this.callStatus = UI_STATE.RINGING;
-        this.showLeftButton = false;
-        this.showRightButton = false;
-        this.showStatus('拨号中...', 'info');
-        if (msg?.object?.uuid) {
-          this.currentCallUuid = msg.object.uuid;
-          this.callUuidMap[msg.object.uuid] = { startTime: Date.now(), phoneNumber: this.dialNumber, status: 'outbound_start' };
-        }
-      });
-      this.ccPhoneBar.on(EventList.CALLEE_RINGING, () => {
-        this.isIncomingCall = false;
-        this.callStatus = UI_STATE.RINGING;
-        this.showLeftButton = false;
-        this.showRightButton = false;
-        this.showStatus('振铃中...', 'info');
-        if (this.currentCallUuid && this.callUuidMap[this.currentCallUuid]) {
-          this.callUuidMap[this.currentCallUuid].status = 'ringing';
-        }
-      });
-
-      const handleCallAnswered = (msg) => {
-        if (msg?.object?.uuid) {
-          const realUuid = msg.object.uuid;
-          if (!this.currentCallUuid || this.currentCallUuid !== realUuid) {
-            if (this.currentCallUuid && this.callUuidMap[this.currentCallUuid]) {
-              this.callUuidMap[realUuid] = this.callUuidMap[this.currentCallUuid];
-              delete this.callUuidMap[this.currentCallUuid];
-            } else if (!this.currentCallUuid) {
-              this.callUuidMap[realUuid] = { startTime: Date.now(), phoneNumber: this.dialNumber, status: 'answered' };
-            }
-            this.currentCallUuid = realUuid;
-          }
-        }
-        this.callStatus = UI_STATE.TALKING;
-        this.isIncomingCall = false;
-        this.showLeftButton = true;
-        this.showRightButton = true;
-        this.leftButtonNormal = true;
-        this.rightButtonNormal = true;
-        this.showStatus('通话中', 'success');
-        if (this.currentCallUuid && this.callUuidMap[this.currentCallUuid]) {
-          this.callUuidMap[this.currentCallUuid].status = 'answered';
-          this.callUuidMap[this.currentCallUuid].answerTime = Date.now();
-        }
-      };
-      this.ccPhoneBar.on(EventList.CALLER_ANSWERED, handleCallAnswered);
-      this.ccPhoneBar.on(EventList.CALLEE_ANSWERED, handleCallAnswered);
-
-      const handleCallHangup = () => {
-        const callUuid = this.currentCallUuid;
-        if (callUuid && this.callUuidMap[callUuid]) {
-          this.callUuidMap[callUuid].status = 'ended';
-          this.callUuidMap[callUuid].endTime = Date.now();
-        }
-        // 如果是转人工来电,挂断时保留导航意图给onSessionClosed使用
-        if (this.incomingJsipCall) {
-          this.pendingManualNavigation = true;
-        }
-        this._resetCallState();
-        this.showStatus('已挂机', 'info');
-        if (this.ccPhoneBar && this.ccSocketConnected) {
-          this.ccPhoneBar.setStatus(AgentStatusEnum.BUSY);
-        }
-        this._handleCallEnd(callUuid);
-      };
-      this.ccPhoneBar.on(EventList.CALLER_HANGUP, handleCallHangup);
-      this.ccPhoneBar.on(EventList.CALLEE_HANGUP, handleCallHangup);
-
-      this.ccPhoneBar.on(EventList.CUSTOMER_CHANNEL_HOLD, () => {
-        this.isOnHold = true;
-        this.showStatus('暂停通话中', 'warn');
-      });
-      this.ccPhoneBar.on(EventList.CUSTOMER_CHANNEL_UNHOLD, () => {
-        this.isOnHold = false;
-        this.showStatus('通话中', 'success');
-      });
-    },
-    /**
-     * ccPhoneBar 断开后自动重连
-     * 使用指数退避策略,最多重试 5 次,避免无限重连消耗资源
-     */
-    _scheduleCCReconnect() {
-      if (this._ccLoginConflict) return;
-      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;
-          }
-          if (this.ccPhoneBar) {
-            try { this.ccPhoneBar.disconnect(); } catch (e) { /* ignore */ }
-            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();
-      extPass = String(extPass).trim();
-      if (!extNum || !extPass) return;
-
-      let existingUserId = null;
-      for (const [id, user] of Object.entries(this.userList)) {
-        if (user.user === extNum) { existingUserId = id; break; }
-      }
-
-      if (existingUserId) {
-        const updatedProfile = {
-          ...this.userList[existingUserId],
-          note: extNum, display_name: extNum, password: extPass, user: extNum,
-          domain: JS_SIP_CONFIG.DOMAIN,
-          server: JS_SIP_CONFIG.SERVER,
-          transport: JS_SIP_CONFIG.TRANSPORT
-        };
-        this.profileManager.updateUser(existingUserId, updatedProfile);
-        if (this.currentUserId !== existingUserId) {
-          this.profileManager.switchUser(existingUserId);
-          this.currentUserId = existingUserId;
-        }
-      } else {
-        const newProfile = {
-          note: extNum, user: extNum, domain: JS_SIP_CONFIG.DOMAIN,
-          password: extPass, display_name: extNum,
-          server: JS_SIP_CONFIG.SERVER, transport: JS_SIP_CONFIG.TRANSPORT
-        };
-        this.profileManager.addUser(newProfile);
-        const updatedProfile = this.profileManager.getProfile();
-        this.userList = updatedProfile.users;
-        for (const [id, user] of Object.entries(this.userList)) {
-          if (user.user === extNum) { this.currentUserId = id; this.profileManager.switchUser(id); break; }
-        }
-      }
-
-      if (this.currentUserId && this.userList[this.currentUserId]) {
-        this.currentUserDisplay = this.userList[this.currentUserId].note || this.userList[this.currentUserId].user;
-      }
-      const profile = this.profileManager.getProfile();
-      this.speakerVolume = profile.speaker_volume ?? VOLUME_CONFIG.DEFAULT;
-      this.micVolume = profile.mic_volume ?? VOLUME_CONFIG.DEFAULT;
-      this.isSpeakerMuted = profile.speaker_paused || false;
-      this.isMicMuted = profile.mic_paused || false;
-    },
-    async startPhone() {
-      if (!this.ccSocketConnected) { this.showStatus('未连接', 'error'); return; }
-      const userProfile = this.profileManager.getCurrentUserProfile();
-      if (!userProfile) { this.showStatus('无可用账号', 'warn'); return; }
-      if (!userProfile.user || !userProfile.domain || !userProfile.password) { this.showStatus('账号配置错误', 'error'); return; }
-
-      await checkMicrophonePermission();
-
-      if (this.phone) { this.phone.destroy(); this.phone = null; }
-
-      const settings = this.profileManager.getSettings();
-      this.phone = new WebPhone(userProfile, settings);
-      this.phone.On('OnRegister', this.onRegisterEvent);
-      this.phone.On('OnSessionCreated', this.onSessionCreated);
-      this.phone.On('OnRing', this.onRing);
-      this.phone.On('OnAnswered', this.onAnswered);
-      this.phone.On('OnSessionClosed', this.onSessionClosed);
-      this.phone.On('OnCallTimer', this.onCallTimer);
-      this.phone.On('OnStatusMessage', this.onStatusMessage);
-      this.phone.On('OnReconnectStatus', this.onReconnectStatus);
-      this.phone.Start(settings.reconnect);
-    },
-    onReconnectStatus({ isReconnecting, failed }) {
-      this.isReconnecting = isReconnecting;
-      this.reconnectFailed = failed;
-      if (failed) this.showStatus('连接超时', 'error');
-      else if (isReconnecting) this.showStatus('重连中...', 'info');
-    },
-    onStatusMessage({ type, text }) { this.showStatus(text, type); },
-    onRegisterEvent(event) {
-      this.isRegistered = event.registered;
-      this.isConnected = event.registered;
-      if (event.registered) {
-        this.currentUserDisplay = this.userList[this.currentUserId]?.note || this.userList[this.currentUserId]?.user || '';
-        this.showStatus('就绪', 'success');
-        if (this.ccPhoneBar && this.ccSocketConnected) this.ccPhoneBar.setStatus(AgentStatusEnum.BUSY);
-      } else if (!this.isReconnecting) {
-        this.showStatus('未注册', 'warn');
-      }
-    },
-    onSessionCreated(event) {
-      // 不再自动接听,由来电振铃 UI 等待用户手动操作
-    },
-    onRing(event) {
-      if (!event.outgoing) {
-        // 外呼时呼叫中心回振坐席 SIP,自动接听,不展示来电 UI
-        if (this.isOutboundInProgress) {
-          if (this.phone) this.phone.Answer();
-          return;
-        }
-        this.isIncomingCall = true;
-        this.incomingJsipCall = true;
-        this.incomingCaller = event.caller || '';
-        this.callStatus = UI_STATE.RINGING;
-        this.province = `${event.province || ''}${event.city ? '-' + event.city : ''}`;
-        this.showLeftButton = true;
-        this.showRightButton = true;
-        this.rightButtonHangup = true;
-        if (this.incomingCaller) {
-          this.displayText = this.maskNumber(this.incomingCaller);
-        }
-        this.showStatus('来电振铃中...', 'info');
-      }
-    },
-    onAnswered() {
-      this.isIncomingCall = false;
-      this.callStatus = UI_STATE.TALKING;
-      this.showLeftButton = true;
-      this.showRightButton = true;
-      this.leftButtonNormal = true;
-      this.rightButtonNormal = true;
-      this.rightButtonHangup = false;
-      this.showStatus('通话中', 'success');
-    },
-    _resetCallState() {
-      if (this.callStatus !== UI_STATE.IDLE) {
-        this.callStatus = UI_STATE.IDLE;
-        this.isIncomingCall = false;
-        this.isOnHold = false;
-        this.province = '';
-        this.callDuration = '00:00';
-        this.showLeftButton = false;
-        this.showRightButton = false;
-        this.leftButtonNormal = false;
-        this.rightButtonNormal = false;
-        this.rightButtonHangup = false;
-      }
-      this.delegatedCallActive = false;
-      this.isOutboundInProgress = false;
-      this.incomingJsipCall = false;
-      this.incomingCaller = '';
-      // 注意:不在_resetCallState中重置pendingManualNavigation,它由onSessionClosed消费
-    },
-    onSessionClosed(event) {
-      // 转人工来电(incomingJsipCall)或主动设置的导航标记,挂断后都跳转
-      const isTestRejected = event && event.reason === 'test_rejected';
-      const shouldNavigate = !isTestRejected && (this.incomingJsipCall || this.pendingManualNavigation);
-      this.showStatus('已挂机', 'info');
-      this._resetCallState();
-      // 转人工来电挂断后,跳转到转人工通话列表页面
-      if (shouldNavigate) {
-        this.pendingManualNavigation = false;
-        this.$router.push({ path: '/companyWx/companyWorkflow/manual', query: { t: Date.now() } });
-        this.$message({ message: '数据正在加载请稍后', type: 'info', duration: 3000 });
-      }
-    },
-    onCallTimer(time) { this.callDuration = time; },
-
-    // ==================== 弹窗通话状态同步 ====================
-    onDialogCallRinging(payload) {
-      const incoming = !!(payload && payload.incoming);
-      this.isIncomingCall = incoming;
-      this.callStatus = UI_STATE.RINGING;
-      this.showLeftButton = incoming;
-      this.showRightButton = incoming;
-      this.rightButtonHangup = incoming;
-      this.delegatedCallActive = true;
-      this.showStatus(incoming ? '来电振铃中...' : '振铃中...', 'info');
-      if (incoming && this.phone) {
-        this.phone.playIncomingRing();
-      }
-    },
-    onDialogCallTalking() {
-      if (this.phone) {
-        this.phone.pauseIncomingRing();
-      }
-      this.isIncomingCall = false;
-      this.callStatus = UI_STATE.TALKING;
-      this.showLeftButton = true;
-      this.showRightButton = true;
-      this.leftButtonNormal = true;
-      this.rightButtonNormal = true;
-      this.rightButtonHangup = false;
-      this.showStatus('通话中', 'success');
-    },
-    onDialogCallEnded() {
-      if (this.phone) {
-        this.phone.pauseIncomingRing();
-      }
-      this._resetCallState();
-      this.showStatus('已挂机', 'info');
-    },
-    onDialogCallHold() {
-      this.isOnHold = true;
-      this.showStatus('暂停通话中', 'warn');
-    },
-    onDialogCallUnhold() {
-      this.isOnHold = false;
-      this.showStatus('通话中', 'success');
-    },
-
-    // ==================== 外呼/挂机 ====================
-    async makeCall() {
-      if (this.callStatus !== UI_STATE.IDLE) { this.showStatus('请先结束当前通话', 'warn'); return; }
-
-      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;
-      }
-      if (!this.isCallingReady) {
-        this.showStatus('坐席未就绪,请稍候', 'warn');
-        return;
-      }
-
-      // 人工外呼弹窗打开时,委托弹窗 phoneBar(含通话记录、工作流等业务逻辑)
-      if (window.__floatingPhoneCallDelegateActive) {
-        this.isOutboundInProgress = true;
-        this.$root.$emit('floating-softphone-call-triggered', phoneNumber);
-        this.delegatedCallActive = true;
-        this.showStatus('拨号中...', 'info');
-        return;
-      }
-
-      // 普通页面:使用浮动软电话自身的 ccPhoneBar 直接外呼
-      try {
-        this.delegatedCallActive = false;
-        this.isOutboundInProgress = true;
-        this.ccPhoneBar.call(phoneNumber, 'audio', VideoLevels.HD.levelId);
-      } catch (err) {
-        this.isOutboundInProgress = false;
-        console.error('[FloatingSoftPhone] 外呼失败:', err);
-        this.showStatus('拨号失败', 'error');
-      }
-    },
-    endCall() {
-      if (this.callStatus === UI_STATE.IDLE) { this.showStatus('当前无通话', 'warn'); return; }
-      // 委托通话时,通知弹窗挂机
-      if (this.delegatedCallActive) {
-        this.$root.$emit('floating-softphone-hangup-triggered');
-        return;
-      }
-      // JsSIP直接来电(转人工等):同时通知呼叫中心挂机(确保客户方断开)并终止SIP会话
-      if (this.incomingJsipCall) {
-        this.pendingManualNavigation = true;
-        if (this.ccPhoneBar && this.ccSocketConnected) {
-          try { this.ccPhoneBar.hangup(); } catch (err) { /* ignore */ }
-        }
-        if (this.phone) {
-          try { this.phone.Terminate(); } catch (err) { this.showStatus('挂机失败', 'error'); }
-        }
-      } else if (this.ccPhoneBar && this.ccSocketConnected) {
-        try { this.ccPhoneBar.hangup(); } catch (err) { this.showStatus('挂机失败', 'error'); }
-      } else if (this.phone) {
-        try { this.phone.Terminate(); } catch (err) { this.showStatus('挂机失败', 'error'); }
-      } else {
-        this.showStatus('无法挂机:未连接', 'error');
-      }
-    },
-    onDigitClick(digit) {
-      if (this.callStatus === 'talking' && this.phone) {
-        this.phone.SendDTMF(digit);
-        this.phone.PlayDtmfTone(digit);
-        return;
-      }
-      // 如果已有真实号码(从外部传入的脱敏号码),不允许追加
-      if (this.realNumber) return;
-      this.dialNumber += digit;
-      this.displayText += digit;
-    },
-    onHangupClick() {
-      if (this.callStatus !== UI_STATE.IDLE) this.endCall();
-      else this.makeCall();
-    },
-    onLeftButtonClick() {
-      if (this.isIncomingRinging) {
-        if (this.phone) this.phone.Answer();
-      } else if (this.callStatus === UI_STATE.TALKING) {
-        // 委托通话时,通知弹窗执行保持/恢复
-        if (this.delegatedCallActive) {
-          this.$root.$emit('floating-softphone-hold-triggered');
-          return;
-        }
-        if (this.ccPhoneBar && this.ccSocketConnected) {
-          try { if (this.isOnHold) this.ccPhoneBar.unHoldCall(); else this.ccPhoneBar.holdCall(); }
-          catch (err) { this.showStatus('操作失败', 'error'); }
-        } else if (this.phone) {
-          try { this.phone.ToggleHold(); } catch (err) { this.showStatus('操作失败', 'error'); }
-        }
-      }
-    },
-    onRightButtonClick() {
-      if (this.isIncomingRinging) this.endCall();
-      else if (this.callStatus === UI_STATE.TALKING) this.showStatus('呼叫转移功能暂未实现', 'info');
-    },
-
-    // ==================== 音频控制 ====================
-    toggleMuteMic() {
-      if (!this.phone) return;
-      this.isMicMuted = !this.isMicMuted;
-      this.phone.ToggleMicPhone();
-      if (!this.isMicMuted) this.micVolume = this.profileManager.getProfile().mic_volume || 0.8;
-    },
-    toggleMuteSpeaker() {
-      if (!this.phone) return;
-      this.isSpeakerMuted = !this.isSpeakerMuted;
-      this.phone.SetSpeaker(this.isSpeakerMuted, this.speakerVolume);
-      if (!this.isSpeakerMuted) this.speakerVolume = this.profileManager.getProfile().speaker_volume || 0.8;
-    },
-    changeMicVolume(event) { this.micVolume = parseFloat(event.target.value); },
-    changeSpeakerVolume(event) { this.speakerVolume = parseFloat(event.target.value); },
-    showMicSlider() { this.cancelHideSliderTimer(); this.speakerSliderVisible = false; this.micSliderVisible = true; },
-    showSpeakerSlider() { this.cancelHideSliderTimer(); this.micSliderVisible = false; this.speakerSliderVisible = true; },
-    startHideSliderTimer() {
-      if (this.volumeTimerId) clearTimeout(this.volumeTimerId);
-      this.volumeTimerId = setTimeout(() => { this.micSliderVisible = this.speakerSliderVisible = false; }, 1000);
-    },
-    cancelHideSliderTimer() { if (this.volumeTimerId) { clearTimeout(this.volumeTimerId); this.volumeTimerId = null; } },
-
-    // ==================== 账号管理 ====================
-    toggleDropdown() { this.dropdownVisible = !this.dropdownVisible; },
-    closeDropdown() { this.dropdownVisible = false; },
-    async switchAccount(userId) {
-      if (userId === this.currentUserId) { this.closeDropdown(); return; }
-      this.profileManager.switchUser(userId);
-      this.currentUserId = userId;
-      const user = this.userList[userId];
-      this.currentUserDisplay = user.note || user.user;
-      if (this.phone) { this.phone.destroy(); this.phone = null; }
-      if (!this.ccSocketConnected) { this.showStatus('呼叫中心连接已断开', 'error'); return; }
-      await this.startPhone();
-      this.closeDropdown();
-    },
-    openAddAccountDialog() {
-      this.isEditMode = false;
-      this.editingUserId = null;
-      this.accountDialogTitle = '添加账号';
-      this.accountForm = { note: '', server: JS_SIP_CONFIG.SERVER, username: '', domain: JS_SIP_CONFIG.DOMAIN, loginName: '', password: '', transport: JS_SIP_CONFIG.TRANSPORT };
-      this.showPassword = false;
-      this.accountDialogVisible = true;
-    },
-    openEditAccountDialog() {
-      const profile = this.profileManager.getCurrentUserProfile();
-      if (!profile) return;
-      this.isEditMode = true;
-      this.editingUserId = this.currentUserId;
-      this.accountDialogTitle = '编辑账号';
-      this.accountForm = {
-        note: profile.note || profile.display_name,
-        server: profile.server,
-        username: profile.display_name,
-        domain: profile.domain,
-        loginName: profile.user,
-        password: profile.password,
-        transport: profile.transport || 'wss'
-      };
-      this.showPassword = false;
-      this.accountDialogVisible = true;
-    },
-    saveAccount() {
-      try {
-        if (!this.accountForm.loginName || !this.accountForm.domain || !this.accountForm.password) {
-          this.$message.warning('登录名、域名和密码为必填项');
-          return;
-        }
-        if (this.isEditMode && this.editingUserId) {
-          const updatedProfile = {
-            note: this.accountForm.note, server: this.accountForm.server,
-            display_name: this.accountForm.username, password: this.accountForm.password,
-            transport: this.accountForm.transport,
-            user: this.userList[this.editingUserId].user, domain: this.userList[this.editingUserId].domain
-          };
-          this.profileManager.updateUser(this.editingUserId, updatedProfile);
-          this.showStatus('账号已更新', 'success');
-        } else {
-          const profile = {
-            note: this.accountForm.note, user: this.accountForm.loginName,
-            domain: this.accountForm.domain, password: this.accountForm.password,
-            display_name: this.accountForm.username, server: this.accountForm.server,
-            transport: this.accountForm.transport
-          };
-          this.profileManager.addUser(profile);
-          this.showStatus('账号已添加', 'success');
-        }
-        this.userList = this.profileManager.getProfile().users;
-        this.currentUserId = this.profileManager.getProfile().user;
-        if (this.currentUserId && this.userList[this.currentUserId]) {
-          this.currentUserDisplay = this.userList[this.currentUserId].note || this.userList[this.currentUserId].user;
-        }
-        this.accountDialogVisible = false;
-        if (this.phone) { this.phone.destroy(); this.phone = null; }
-        if (this.ccSocketConnected) this.startPhone();
-      } catch (err) {
-        this.$message.error(err.message);
-      }
-    },
-    confirmDeleteAccount() {
-      this.$confirm('删除账号将清除本地配置,确认删除?', '提示', {
-        confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
-      }).then(() => {
-        this.profileManager.deleteCurrentUser();
-        const newProfile = this.profileManager.getProfile();
-        this.userList = newProfile.users || {};
-        this.currentUserId = newProfile.user || '';
-        this.currentUserDisplay = (this.currentUserId && this.userList[this.currentUserId])
-          ? (this.userList[this.currentUserId].note || this.userList[this.currentUserId].user) : '';
-        if (this.phone) { this.phone.destroy(); this.phone = null; }
-        if (this.ccSocketConnected) this.startPhone();
-        this.showStatus('账号已删除', 'info');
-      }).catch(() => {});
-    },
-    saveSettings() {
-      this.profileManager.updateSettings({
-        user_agent: this.settingsForm.userAgent, session_expires: this.settingsForm.sessionExpires,
-        min_session_expires: this.settingsForm.minSessionExpires, stun: this.settingsForm.stun,
-        ice_server: this.settingsForm.iceServer, auto_answer: false,
-        reconnect: this.settingsForm.reconnect, reconnect_interval: this.settingsForm.reconnectInterval
-      });
-      this.settingsDialogVisible = false;
-      if (this.phone) { this.phone.destroy(); this.phone = null; }
-      this.startPhone();
-      this.showStatus('设置已保存', 'success');
-    },
-    resetSettings() {
-      this.profileManager.resetSettings();
-      const settings = this.profileManager.getSettings();
-      this.settingsForm = { ...settings };
-      this.showStatus('设置已重置', 'success');
-    },
-    async resetReconnectState() {
-      this.showStatus('正在重置...', 'info');
-      this._ccLoginConflict = false;
-      // 清除自动重连定时器和计数
-      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) {
-        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;
-      this.isReconnecting = false;
-      this.reconnectFailed = false;
-      this.isConnected = false;
-      this.isRegistered = false;
-      this.callStatus = UI_STATE.IDLE;
-      if (this.ccConnectingPromise) {
-        if (this.ccConnectingReject) { try { this.ccConnectingReject(new Error('用户主动重置')); } catch(e) {} }
-        this.ccConnectingPromise = null;
-        this.ccConnectingResolve = null;
-        this.ccConnectingReject = null;
-      }
-      this.dropdownVisible = false;
-      try {
-        await this.initCCAndStart();
-        this.showStatus('重连成功', 'success');
-      } catch (err) {
-        this.showStatus('重连失败', 'error');
-      }
-    },
-
-    // ==================== 清理 ====================
-    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) {
-        // 递减引用计数
-        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._ccEventsBoundFor = null;
-      }
-      this.callUuidMap = {};
-      this.currentCallUuid = '';
-      this.resetAllStates();
-      // 通知其他组件共享实例已销毁
-      this.$root.$emit('cc-phonebar-destroyed');
-    },
-    clearAllTimers() {
-      if (this.volumeTimerId) { clearTimeout(this.volumeTimerId); this.volumeTimerId = null; }
-      if (this.statusTimerId) { clearTimeout(this.statusTimerId); this.statusTimerId = null; }
-    },
-    resetAllStates() {
-      this.ccSocketConnected = false;
-      this.ccSocketFailed = false;
-      this.isCallingReady = false;
-      this.isReconnecting = false;
-      this.reconnectFailed = false;
-      this.isConnected = false;
-      this.isRegistered = false;
-      this.callStatus = UI_STATE.IDLE;
-      this.isOnHold = false;
-      this.dropdownVisible = false;
-      this.accountDialogVisible = false;
-      this.settingsDialogVisible = false;
-      this.micSliderVisible = false;
-      this.speakerSliderVisible = false;
-    },
-    removeEventListeners() {
-      if (this.phone) {
-        try {
-          this.phone.Off('OnRegister', this.onRegisterEvent);
-          this.phone.Off('OnSessionCreated', this.onSessionCreated);
-          this.phone.Off('OnRing', this.onRing);
-          this.phone.Off('OnAnswered', this.onAnswered);
-          this.phone.Off('OnSessionClosed', this.onSessionClosed);
-          this.phone.Off('OnCallTimer', this.onCallTimer);
-          this.phone.Off('OnStatusMessage', this.onStatusMessage);
-          this.phone.Off('OnReconnectStatus', this.onReconnectStatus);
-        } catch(e) {}
-      }
-    },
-    handleBeforeUnload() { this.destroyAllConnections(); },
-
-    // ==================== 通话记录 ====================
-    _handleCallEnd(callUuid) {
-      if (!callUuid) return;
-      setTimeout(() => {
-        syncByUuid({ uuid: callUuid }).then(() => {
-          const uuidKeys = Object.keys(this.callUuidMap);
-          if (uuidKeys.length > 10) delete this.callUuidMap[uuidKeys[0]];
-        }).catch(() => {});
-      }, 10000);
-    }
-  }
-};
-</script>
-
-<style scoped>
-@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
-
-/* ===== FAB按钮 ===== */
-.softphone-fab {
-  position: fixed;
-  bottom: 24px;
-  right: 24px;
-  width: 56px;
-  height: 56px;
-  border-radius: 50%;
-  background: #006CFF;
-  color: #fff;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  cursor: pointer;
-  box-shadow: 0 4px 12px rgba(0, 108, 255, 0.4);
-  z-index: 9998;
-  transition: transform 0.3s ease, box-shadow 0.2s, background 0.2s, width 0.3s ease, border-radius 0.3s ease, opacity 0.3s ease;
-}
-.softphone-fab:hover {
-  transform: scale(1.08);
-  box-shadow: 0 6px 16px rgba(0, 108, 255, 0.5);
-}
-.softphone-fab.fab-active {
-  background: #555;
-}
-.softphone-fab.fab-connected:not(.fab-active) {
-  background: #4caf50;
-  box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
-}
-/* 折叠状态:缩小并半隐藏到边缘 */
-.softphone-fab.fab-collapsed {
-  width: 36px;
-  height: 56px;
-  border-radius: 18px 0 0 18px;
-  opacity: 0.6;
-  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
-}
-.softphone-fab.fab-collapsed.fab-left {
-  border-radius: 0 18px 18px 0;
-}
-.softphone-fab.fab-collapsed:hover {
-  opacity: 1;
-  box-shadow: 0 4px 12px rgba(0, 108, 255, 0.5);
-}
-.softphone-fab.fab-collapsed.fab-connected {
-  opacity: 0.7;
-}
-.softphone-fab.fab-collapsed .material-icons {
-  font-size: 20px;
-}
-.softphone-fab .material-icons {
-  font-size: 28px;
-  color: #fff;
-  transition: font-size 0.3s ease;
-}
-.softphone-fab .fab-badge {
-  position: absolute;
-  top: 8px;
-  right: 8px;
-  width: 12px;
-  height: 12px;
-  border-radius: 50%;
-  background: #f44336;
-  border: 2px solid #fff;
-  animation: pulse 1.2s infinite;
-}
-/* 振铃时FAB脉冲动画 */
-.softphone-fab.fab-ringing {
-  animation: fab-ringing-pulse 1s infinite;
-  opacity: 1 !important;
-  width: 64px !important;
-  height: 64px !important;
-  background: #f44336 !important;
-  box-shadow: 0 6px 20px rgba(244, 67, 54, 0.55) !important;
-}
-@keyframes fab-ringing-pulse {
-  0% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.7); }
-  70% { box-shadow: 0 0 0 20px rgba(244, 67, 54, 0); }
-  100% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0); }
-}
-.softphone-fab.fab-ringing .material-icons {
-  font-size: 32px;
-}
-.softphone-fab.fab-ringing .fab-badge {
-  width: 14px;
-  height: 14px;
-  top: 6px;
-  right: 6px;
-}
-
-/* ===== 来电醒目提示卡片 ===== */
-.incoming-call-alert {
-  position: fixed;
-  right: 24px;
-  bottom: 100px;
-  z-index: 10002;
-  min-width: 300px;
-  max-width: calc(100vw - 48px);
-  box-sizing: border-box;
-  background: linear-gradient(135deg, #ff5252 0%, #d32f2f 100%);
-  color: #fff;
-  border-radius: 16px;
-  padding: 14px 20px 14px 16px;
-  box-shadow: 0 8px 32px rgba(211, 47, 47, 0.45), 0 0 0 2px rgba(255, 255, 255, 0.25);
-  cursor: pointer;
-  overflow: visible;
-}
-.incoming-call-alert__ripple {
-  position: absolute;
-  inset: 0;
-  border-radius: inherit;
-  box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.35);
-  animation: incoming-alert-ripple 1.5s ease-out infinite;
-  pointer-events: none;
-  overflow: hidden;
-}
-.incoming-call-alert__content {
-  position: relative;
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  animation: incoming-alert-shake 2s ease-in-out infinite;
-}
-.incoming-call-alert__icon-wrap {
-  width: 48px;
-  height: 48px;
-  border-radius: 50%;
-  background: rgba(255, 255, 255, 0.2);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-shrink: 0;
-}
-.incoming-call-alert__icon-wrap .material-icons {
-  font-size: 28px;
-  animation: incoming-alert-icon-ring 0.6s ease-in-out infinite alternate;
-}
-@keyframes incoming-alert-icon-ring {
-  from { transform: rotate(-12deg); }
-  to { transform: rotate(12deg); }
-}
-.incoming-call-alert__info {
-  flex: 1;
-  min-width: 0;
-  display: flex;
-  flex-direction: column;
-  gap: 2px;
-}
-.incoming-call-alert__title {
-  font-size: 16px;
-  font-weight: 700;
-  letter-spacing: 0.5px;
-}
-.incoming-call-alert__number {
-  font-size: 20px;
-  font-weight: 700;
-  letter-spacing: 1px;
-  line-height: 1.2;
-}
-.incoming-call-alert__hint {
-  font-size: 11px;
-  opacity: 0.85;
-  margin-top: 2px;
-}
-.incoming-call-alert__actions {
-  display: flex;
-  flex-direction: column;
-  gap: 8px;
-  flex-shrink: 0;
-  padding-right: 2px;
-}
-.incoming-call-alert__btn {
-  width: 44px;
-  height: 44px;
-  border: none;
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  cursor: pointer;
-  flex-shrink: 0;
-  transition: transform 0.15s ease, box-shadow 0.15s ease;
-}
-.incoming-call-alert__btn .material-icons {
-  font-size: 22px;
-  color: #fff;
-}
-.incoming-call-alert__btn.answer {
-  background: #4caf50;
-  box-shadow: 0 4px 12px rgba(76, 175, 80, 0.5);
-}
-.incoming-call-alert__btn.reject {
-  background: rgba(0, 0, 0, 0.25);
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
-}
-.incoming-call-alert__btn:hover {
-  transform: scale(1.08);
-}
-.incoming-call-alert__btn:active {
-  transform: scale(0.95);
-}
-@keyframes incoming-alert-ripple {
-  0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.45); }
-  70% { box-shadow: 0 0 0 14px rgba(255, 255, 255, 0); }
-  100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
-}
-@keyframes incoming-alert-shake {
-  0%, 100% { transform: translateY(0); }
-  50% { transform: translateY(-2px); }
-}
-/* 气泡过渡 */
-.call-bubble-fade-enter-active { transition: opacity 0.3s ease, transform 0.3s ease; }
-.call-bubble-fade-leave-active { transition: opacity 0.2s ease, transform 0.2s ease; }
-.call-bubble-fade-enter, .call-bubble-fade-leave-to { opacity: 0; transform: translateY(10px) scale(0.9); }
-
-/* ===== 面板过渡 ===== */
-.softphone-fade-enter-active, .softphone-fade-leave-active {
-  transition: opacity 0.25s ease, transform 0.25s ease;
-}
-.softphone-fade-enter, .softphone-fade-leave-to {
-  opacity: 0;
-  transform: scale(0.9) translateY(10px);
-}
-
-/* ===== 面板容器 ===== */
-.softphone-panel {
-  position: fixed;
-  z-index: 9999;
-}
-
-/* ===== 拖拽手柄 ===== */
-.drag-handle {
-  cursor: move;
-}
-.drag-handle .minimize-btn {
-  cursor: pointer;
-  color: #999;
-  font-size: 20px;
-  margin-left: 4px;
-  transition: color 0.2s;
-}
-.drag-handle .minimize-btn:hover {
-  color: #f44336;
-}
-
-/* ===== 拨号器主体(复用原始样式) ===== */
-.dialer {
-  width: 280px;
-  min-height: 480px;
-  background-color: #fafafa;
-  border-radius: 16px;
-  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
-  position: relative;
-  display: flex;
-  flex-direction: column;
-  transition: box-shadow 0.3s ease;
-  padding-bottom: 8px;
-}
-
-.status-bar {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  height: 28px;
-  background-color: transparent;
-  padding: 8px 12px;
-  gap: 4px;
-  box-sizing: border-box;
-  border-bottom: 1px solid #f0f0f0;
-  border-radius: 16px 16px 0 0;
-}
-.status-left { flex: 0 0 auto; display: flex; align-items: center; gap: 2px; }
-.status-center { flex: 1; text-align: center; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; padding: 0 4px; }
-.status-right { flex: 0 0 auto; display: flex; align-items: center; gap: 4px; }
-
-.material-icons { font-size: 22px; transition: color 0.2s ease; }
-.network-icon { cursor: default; width: 22px; }
-.no-network { color: #ff5252; }
-.network-available { color: #4caf50; }
-.network-connecting { color: #ffa726; animation: pulse 1.5s infinite; }
-@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
-
-.microphone-icon, .speaker-icon { cursor: pointer; color: #555; z-index: 1; transition: all 0.2s ease; }
-.microphone-icon:hover, .speaker-icon:hover { color: #2196f3; transform: scale(1.1); }
-.microphone-icon.muted, .speaker-icon.muted { color: #bbb; }
-.microphone-icon.connection-success, .speaker-icon.connection-success { color: #4caf50; }
-.microphone-icon.connection-failed, .speaker-icon.connection-failed { color: #999; }
-
-.call-status-icon { cursor: default; color: #4caf50; width: 22px; }
-.call-status-icon.inprogress { color: #2196f3; }
-.ringing-icon { animation: ringing 0.8s infinite; }
-@keyframes ringing { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(8deg); } }
-
-.container { display: flex; justify-content: center; margin: 4px 0; }
-.call-timer { cursor: default; font-size: 13px; color: #666; font-weight: 500; height: 16px; }
-.province { cursor: default; font-size: 13px; color: #666; font-weight: 500; height: 16px; }
-
-.display-user { cursor: default; font-size: 14px; font-weight: 500; display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #333; }
-.user-avatar-dropdown { position: relative; display: inline-block; cursor: pointer; }
-.user-avatar-icon { color: #555; transition: all 0.2s ease; }
-.user-avatar-icon:hover { color: #2196f3; transform: scale(1.1); }
-
-.dropdown-menu { position: absolute; top: 100%; left: 40%; min-width: 140px; background-color: #fff; border: 1px solid #e0e0e0; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); padding: 4px; z-index: 10000; animation: fadeIn 0.2s ease; }
-@keyframes fadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } }
-.dropdown-group { margin-top: 4px; }
-.dropdown-item { display: flex; align-items: center; gap: 8px; text-decoration: none; color: #555; width: 100%; white-space: nowrap; padding: 6px 10px; border-radius: 4px; transition: all 0.2s ease; font-size: 13px; }
-.dropdown-item i.material-icons { font-size: 18px; width: 18px; text-align: center; }
-.dropdown-item:hover { background-color: #f5f5f5; color: #2196f3; }
-
-.volume-control-group { position: relative; display: inline-flex; align-items: center; }
-.volume-slider-container { position: absolute; bottom: 32px; left: 50%; transform: translateX(-50%); width: 160px; background: white; padding: 10px; border-radius: 8px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); z-index: 10000; animation: slideUp 0.2s ease; }
-@keyframes slideUp { from { opacity: 0; transform: translateX(-50%) translateY(5px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
-.volume-slider { width: 100%; cursor: pointer; }
-
-.display-wrapper { display: flex; align-items: center; justify-content: center; margin: 28px 16px 0 16px; position: relative; gap: 6px; width: calc(100% - 32px); box-sizing: border-box; }
-
-.dialer-display { flex: 1; min-width: 0; height: 42px; font-size: 22px; font-weight: 500; border: none; outline: none; background-color: transparent; color: #333; border-bottom: 2px solid #e0e0e0; padding: 0 6px; box-sizing: border-box; overflow-x: auto; white-space: nowrap; transition: border-color 0.2s ease; line-height: 42px; text-align: center; }
-.dialer-display:focus { border-bottom-color: #2196f3; }
-.dialer-display.center-align { text-align: center; }
-.dialer-display.right-align { text-align: right; }
-.dialer-display::-webkit-scrollbar { height: 2px; }
-.dialer-display::-webkit-scrollbar-thumb { background: #ccc; border-radius: 2px; }
-
-.delete-icon { cursor: pointer; color: #999; font-size: 26px; transition: all 0.2s ease; user-select: none; flex-shrink: 0; }
-.delete-icon:hover { color: #f44336; transform: scale(1.1); }
-
-.dialer-keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; padding: 8px 20px; margin: 4px 0; }
-.dialer-button { width: 100%; aspect-ratio: 1 / 1; max-width: 56px; margin: 0 auto; display: flex; justify-content: center; align-items: center; border-radius: 50%; background-color: #f5f5f5; border: 2px solid #d0d0d0; font-size: 26px; font-weight: 500; cursor: pointer; color: #333; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); }
-.dialer-button:hover { background-color: #e8e8e8; transform: scale(1.05); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); border-color: #bbb; }
-.dialer-button:active { background-color: #ddd; transform: scale(0.95); }
-
-.call-buttons { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; padding: 4px 24px 8px 24px; margin-bottom: 8px; }
-.call-button { width: 56px; height: 56px; border: none; border-radius: 50%; background-color: #4caf50; color: #fff; cursor: pointer; display: flex; justify-content: center; align-items: center; box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); transition: all 0.3s ease; font-size: 26px; outline: none; margin: 0 auto; }
-.call-button:hover:not(.disabled) { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(76, 175, 80, 0.4); }
-.call-button:active:not(.disabled) { transform: translateY(0); box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3); }
-.call-button.hangup { background-color: #f44336; color: white; box-shadow: 0 4px 12px rgba(244, 67, 54, 0.3); }
-.call-button.hangup:hover { box-shadow: 0 6px 16px rgba(244, 67, 54, 0.4); }
-.call-button.normal { background-color: #f5f5f5; color: #333; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); }
-.call-button.normal:hover { background-color: #e8e8e8; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }
-.call-button.call-ready { background-color: #4caf50; color: white; box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); }
-.call-button.call-ready:hover { box-shadow: 0 6px 16px rgba(76, 175, 80, 0.4); }
-.call-button.disabled { background-color: #e0e0e0; color: #999; cursor: not-allowed; box-shadow: none; }
-.hidden { visibility: hidden; }
-
-.status-footer { display: flex; justify-content: space-between; align-items: center; padding: 6px 12px; margin-top: 4px; background: transparent; }
-.status-footer-left { display: flex; flex: 1; align-items: center; gap: 8px; overflow: hidden; }
-.status-bar-message { font-size: 11px; max-width: 160px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: rgba(0, 0, 0, 0.75); padding: 4px 10px; border-radius: 16px; color: white; font-weight: 500; box-shadow: 0 2px 8px rgba(0,0,0,0.2); animation: slideInLeft 0.3s ease; display: inline-block; }
-@keyframes slideInLeft { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } }
-.status-bar-message.error { background: rgba(244, 67, 54, 0.9); }
-.status-bar-message.success { background: rgba(76, 175, 80, 0.9); }
-.status-bar-message.warn { background: rgba(255, 152, 0, 0.9); }
-.status-bar-message.info { background: rgba(33, 150, 243, 0.9); }
-.reconnect-failed { font-size: 11px; background: rgba(244, 67, 54, 0.95); padding: 4px 10px; border-radius: 16px; color: white; animation: shake 0.5s ease; display: inline-block; }
-@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
-.version-ribbon { font-size: 11px; color: #bbb; background: rgba(0,0,0,0.03); padding: 3px 8px; border-radius: 12px; pointer-events: none; font-family: monospace; font-weight: 500; flex-shrink: 0; }
-
-/* ===== 模态框 ===== */
-.modal { display: block; position: fixed; z-index: 10001; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 340px; max-height: 80vh; overflow-y: auto; background-color: #fff; border-radius: 12px; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); animation: modalFadeIn 0.3s ease; }
-@keyframes modalFadeIn { from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } }
-.modal-header { display: flex; align-items: center; justify-content: center; padding: 14px; border-radius: 12px 12px 0 0; background-color: #f8f8f8; gap: 8px; border-bottom: 1px solid #e8e8e8; }
-.modal-header i.material-icons { font-size: 24px; color: #2196f3; }
-.modal-header span { font-size: 16px; font-weight: 600; color: #333; }
-.modal-content { background-color: #fff; padding: 16px; }
-.form-group { margin-bottom: 16px; position: relative; }
-.form-label { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: #555; margin-bottom: 6px; }
-.form-group input[type="number"], .form-group input[type="text"], .form-group input[type="password"], .form-group select { width: 100%; padding: 10px 12px; box-sizing: border-box; border: 1px solid #e0e0e0; border-radius: 6px; color: #333; font-size: 14px; transition: all 0.2s ease; background-color: #fafafa; }
-.form-group input:focus, .form-group select:focus { outline: none; border-color: #2196f3; background-color: #fff; box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1); }
-.form-group .password-toggle { position: absolute; right: 10px; top: 36px; cursor: pointer; color: #bbb; transition: all 0.2s ease; }
-.form-group .password-toggle:hover { color: #2196f3; }
-.form-buttons { display: flex; justify-content: center; gap: 60px; margin-top: 20px; }
-.form-buttons button { width: 35%; padding: 10px; background-color: transparent; color: #fff; border: none; cursor: pointer; border-radius: 6px; font-size: 15px; font-weight: 500; transition: all 0.2s ease-in-out; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); }
-.form-buttons .add-button { background-color: #4caf50; }
-.form-buttons .add-button:hover { background-color: #43a047; box-shadow: 0 4px 8px rgba(76, 175, 80, 0.3); transform: translateY(-1px); }
-.form-buttons .cancel-button { background-color: #9e9e9e; }
-.form-buttons .cancel-button:hover { background-color: #757575; box-shadow: 0 4px 8px rgba(158, 158, 158, 0.3); transform: translateY(-1px); }
-</style>

+ 0 - 103
src/components/ManualCallDialog/index.vue

@@ -1,103 +0,0 @@
-<template>
-  <el-dialog
-    :title="title"
-    :visible.sync="dialogVisible"
-    width="1500px"
-    append-to-body
-    destroy-on-close
-    class="manual-call-dialog"
-    @close="handleClose"
-  >
-    <call-center-phone-bar
-      :key="dialogKey"
-      ref="callCenterPhoneBar"
-      :init-phone-number="phone"
-      :robotic-id="roboticId"
-      :company-id="companyId"
-      :company-user-id="companyUserId"
-      :workflow-instance-id="workflowInstanceId"
-      :customer-id="customerId"
-      :hide-phone-bar="useFloatingPhone"
-      @close="dialogVisible = false"
-    />
-  </el-dialog>
-</template>
-
-<script>
-import CallCenterPhoneBar from '@/views/aiSipCall/aiSipCallManualOutbound.vue';
-
-export default {
-  name: 'ManualCallDialog',
-  components: { CallCenterPhoneBar },
-  props: {
-    /** 是否使用浮动软电话模式(隐藏弹窗内电话栏,用浮动电话拨号) */
-    useFloatingPhone: {
-      type: Boolean,
-      default: true
-    }
-  },
-  data() {
-    return {
-      dialogVisible: false,
-      dialogKey: 0,
-      title: '人工外呼',
-      phone: '',
-      customerId: null,
-      companyId: null,
-      companyUserId: null,
-      roboticId: null,
-      workflowInstanceId: null
-    };
-  },
-  beforeDestroy() {
-    window.__floatingPhoneCallDelegateActive = false;
-  },
-  methods: {
-    /**
-     * 打开外呼弹窗
-     * @param {Object} options
-     * @param {string} options.phone - 电话号码(加密或明文)
-     * @param {string} [options.customerName] - 客户名称(用于标题显示)
-     * @param {number|string} [options.customerId] - 客户ID
-     * @param {number|string} [options.companyId] - 公司ID
-     * @param {number|string} [options.companyUserId] - 员工ID
-     * @param {number|string} [options.roboticId] - 机器人任务ID
-     * @param {number|string} [options.workflowInstanceId] - 工作流实例ID
-     */
-    open(options = {}) {
-      this.phone = options.phone || '';
-      this.customerId = options.customerId || null;
-      this.companyId = options.companyId || null;
-      this.companyUserId = options.companyUserId || null;
-      this.roboticId = options.roboticId || null;
-      this.workflowInstanceId = options.workflowInstanceId || null;
-      this.title = `人工外呼 - ${options.customerName || '未知客户'}`;
-      this.dialogKey += 1;
-      this.dialogVisible = true;
-      window.__floatingPhoneCallDelegateActive = !!this.useFloatingPhone;
-
-      // 浮动软电话模式:同步弹出浮动电话并填充号码
-      if (this.useFloatingPhone && this.phone) {
-        this.$root.$emit('floating-softphone-dial', {
-          phone: this.phone,
-          customerName: options.customerName || '未知客户'
-        });
-      }
-    },
-    handleClose() {
-      window.__floatingPhoneCallDelegateActive = false;
-      this.phone = '';
-      this.customerId = null;
-      this.companyId = null;
-      this.companyUserId = null;
-      this.roboticId = null;
-      this.workflowInstanceId = null;
-      this.$emit('close');
-    }
-  }
-};
-</script>
-
-<style scoped>
-/* 弹窗样式由 el-dialog 全局样式和 aiSipCallManualOutbound 内部样式控制 */
-</style>

+ 0 - 3
src/layout/index.vue

@@ -12,12 +12,10 @@
         <settings />
       </right-panel>
     </div>
-    <floating-soft-phone />
   </div>
 </template>
 
 <script>
-import FloatingSoftPhone from '@/components/FloatingSoftPhone'
 import RightPanel from '@/components/RightPanel'
 import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
 import ResizeMixin from './mixin/ResizeHandler'
@@ -28,7 +26,6 @@ export default {
   name: 'Layout',
   components: {
     AppMain,
-    FloatingSoftPhone,
     Navbar,
     RightPanel,
     Settings,

+ 0 - 6
src/router/index.js

@@ -327,12 +327,6 @@ export const constantRoutes = [
       component: () => import('@/views/company/companyWorkflow/design'),
       name: 'AiWorkflowEdit',
       meta: { title: '编辑AI外呼工作流', activeMenu: '/companyWx/companyWorkflow' }
-    },
-    {
-      path: 'manual',
-      component: () => import('@/views/company/companyVoiceRobotic/handleManualAnswered'),
-      name: 'HandleManualAnswered',
-      meta: { title: '转人工通话列表', activeMenu: '/companyWx/companyWorkflow' }
     }
   ]
   },

+ 0 - 71
src/utils/ccPhoneBarShared.js

@@ -1,71 +0,0 @@
-/**
- * ccPhoneBar 全局单例协调(避免同一分机重复登录 status:201)
- */
-
-export function getConnectedSharedCCPhoneBar() {
-  const shared = window.__sharedCCPhoneBar;
-  if (shared && typeof shared.getIsConnected === 'function' && shared.getIsConnected()) {
-    return shared;
-  }
-  return null;
-}
-
-export function beginCCPhoneBarInit() {
-  if (!window.__ccPhoneBarInitPromise) {
-    window.__ccPhoneBarInitPromise = new Promise((resolve, reject) => {
-      window.__ccPhoneBarInitResolve = resolve;
-      window.__ccPhoneBarInitReject = reject;
-    });
-  }
-  return window.__ccPhoneBarInitPromise;
-}
-
-export function finishCCPhoneBarInitSuccess(phoneBar) {
-  window.__sharedCCPhoneBar = phoneBar;
-  if (typeof window.__ccPhoneBarInitResolve === 'function') {
-    window.__ccPhoneBarInitResolve(phoneBar);
-  }
-  clearCCPhoneBarInitPromise();
-}
-
-export function finishCCPhoneBarInitFailed(error) {
-  if (typeof window.__ccPhoneBarInitReject === 'function') {
-    window.__ccPhoneBarInitReject(error);
-  }
-  clearCCPhoneBarInitPromise();
-}
-
-export function clearCCPhoneBarInitPromise() {
-  window.__ccPhoneBarInitPromise = null;
-  window.__ccPhoneBarInitResolve = null;
-  window.__ccPhoneBarInitReject = null;
-}
-
-/**
- * 等待 FloatingSoftPhone / softPhone 完成 ccPhoneBar 连接
- */
-export function waitForSharedCCPhoneBar(timeoutMs = 25000) {
-  const connected = getConnectedSharedCCPhoneBar();
-  if (connected) {
-    return Promise.resolve(connected);
-  }
-  if (!window.__ccPhoneBarInitPromise) {
-    return Promise.resolve(null);
-  }
-  return Promise.race([
-    window.__ccPhoneBarInitPromise,
-    new Promise((_, reject) => {
-      setTimeout(() => reject(new Error('等待软电话连接超时')), timeoutMs);
-    })
-  ]);
-}
-
-export function incrementSharedCCPhoneBarRef() {
-  window.__sharedCCPhoneBarRefCount = (window.__sharedCCPhoneBarRefCount || 0) + 1;
-}
-
-export function decrementSharedCCPhoneBarRef() {
-  if (window.__sharedCCPhoneBarRefCount > 0) {
-    window.__sharedCCPhoneBarRefCount--;
-  }
-}

+ 10 - 0
src/utils/common.js

@@ -316,6 +316,16 @@ export function getDateRange(days) {
 	];
 }
 
+/** 列表展示手机号:11位明文脱敏,加密串原样展示 */
+export function maskMobileForDisplay(phone) {
+	if (!phone) return phone;
+	const text = String(phone);
+	if (/^1\d{10}$/.test(text)) {
+		return text.substring(0, 3) + '****' + text.substring(text.length - 4);
+	}
+	return text;
+}
+
 
 // export function callNumber(mobile){
 // 	var that=this;

+ 0 - 229
src/utils/incomingCallAttention.js

@@ -1,229 +0,0 @@
-/**
- * 来电注意力提示:标签页标题闪烁、Favicon 闪烁、桌面通知
- * 用于浏览器切到后台或其他标签页时提醒坐席有来电
- */
-
-const FLASH_INTERVAL_MS = 700;
-const ALERT_TITLE_PREFIX = '\u3010\u6765\u7535\u3011';
-
-let originalTitle = '';
-let originalFaviconHref = '';
-let alertFaviconHref = '';
-let faviconLinkEl = null;
-let titleFlashTimer = null;
-let showAlertState = true;
-let desktopNotification = null;
-let active = false;
-let visibilityHandler = null;
-
-function getFaviconLink() {
-  if (faviconLinkEl) {
-    return faviconLinkEl;
-  }
-  faviconLinkEl = document.querySelector("link[rel~='icon']")
-    || document.querySelector("link[rel='shortcut icon']");
-  if (!faviconLinkEl) {
-    faviconLinkEl = document.createElement('link');
-    faviconLinkEl.rel = 'icon';
-    document.head.appendChild(faviconLinkEl);
-  }
-  return faviconLinkEl;
-}
-
-/** 生成红色告警 Favicon(Canvas 绘制,无需额外资源) */
-function buildAlertFavicon() {
-  const size = 32;
-  const canvas = document.createElement('canvas');
-  canvas.width = size;
-  canvas.height = size;
-  const ctx = canvas.getContext('2d');
-
-  ctx.fillStyle = '#f44336';
-  ctx.beginPath();
-  ctx.arc(size / 2, size / 2, size / 2 - 1, 0, Math.PI * 2);
-  ctx.fill();
-
-  ctx.fillStyle = '#ffffff';
-  ctx.font = 'bold 20px sans-serif';
-  ctx.textAlign = 'center';
-  ctx.textBaseline = 'middle';
-  ctx.fillText('!', size / 2, size / 2 + 1);
-
-  return canvas.toDataURL('image/png');
-}
-
-function ensureAlertFavicon() {
-  if (!alertFaviconHref) {
-    alertFaviconHref = buildAlertFavicon();
-  }
-  return alertFaviconHref;
-}
-
-function applyFlashFrame(caller) {
-  const link = getFaviconLink();
-  if (!originalFaviconHref && link.href) {
-    originalFaviconHref = link.href;
-  }
-
-  if (showAlertState) {
-    document.title = `${ALERT_TITLE_PREFIX}${caller || '\u672a\u77e5\u53f7\u7801'}`;
-    link.href = ensureAlertFavicon();
-    link.type = 'image/png';
-  } else if (originalTitle) {
-    document.title = originalTitle;
-    if (originalFaviconHref) {
-      link.href = originalFaviconHref;
-    }
-  }
-  showAlertState = !showAlertState;
-}
-
-function closeDesktopNotification() {
-  if (desktopNotification) {
-    try {
-      desktopNotification.close();
-    } catch (e) {
-      // ignore
-    }
-    desktopNotification = null;
-  }
-}
-
-function showDesktopNotification({ caller, body }) {
-  if (typeof window === 'undefined' || !('Notification' in window)) {
-    return;
-  }
-  if (Notification.permission !== 'granted') {
-    return;
-  }
-
-  closeDesktopNotification();
-
-  const text = caller || '\u672a\u77e5\u53f7\u7801';
-  desktopNotification = new Notification('\u6765\u7535\u54cd\u94c3\u4e2d', {
-    body: body || `\u6765\u7535\u53f7\u7801\uff1a${text}`,
-    icon: ensureAlertFavicon(),
-    tag: 'softphone-incoming-call',
-    requireInteraction: true,
-    silent: false
-  });
-
-  desktopNotification.onclick = () => {
-    window.focus();
-    closeDesktopNotification();
-  };
-}
-
-function onVisibilityChange(caller, body) {
-  if (!active) {
-    return;
-  }
-  if (document.hidden) {
-    showDesktopNotification({ caller, body });
-  }
-}
-
-function bindVisibilityListener(caller, body) {
-  unbindVisibilityListener();
-  visibilityHandler = () => onVisibilityChange(caller, body);
-  document.addEventListener('visibilitychange', visibilityHandler);
-}
-
-function unbindVisibilityListener() {
-  if (visibilityHandler) {
-    document.removeEventListener('visibilitychange', visibilityHandler);
-    visibilityHandler = null;
-  }
-}
-
-/**
- * 预请求桌面通知权限(建议在用户交互后调用)
- */
-export async function requestIncomingCallNotificationPermission() {
-  if (typeof window === 'undefined' || !('Notification' in window)) {
-    return 'unsupported';
-  }
-  if (Notification.permission === 'granted' || Notification.permission === 'denied') {
-    return Notification.permission;
-  }
-  try {
-    return await Notification.requestPermission();
-  } catch (e) {
-    return 'denied';
-  }
-}
-
-/**
- * 开始来电注意力提示
- * @param {{ caller?: string, body?: string }} options
- */
-export function startIncomingCallAttention(options = {}) {
-  if (typeof document === 'undefined') {
-    return;
-  }
-
-  const caller = options.caller || '\u672a\u77e5\u53f7\u7801';
-  const body = options.body || `\u6765\u7535\u53f7\u7801\uff1a${caller}`;
-
-  stopIncomingCallAttention();
-
-  active = true;
-  originalTitle = document.title;
-  showAlertState = true;
-
-  const link = getFaviconLink();
-  if (link.href) {
-    originalFaviconHref = link.href;
-  }
-
-  applyFlashFrame(caller);
-  titleFlashTimer = setInterval(() => {
-    if (active) {
-      applyFlashFrame(caller);
-    }
-  }, FLASH_INTERVAL_MS);
-
-  bindVisibilityListener(caller, body);
-  if (document.hidden) {
-    showDesktopNotification({ caller, body });
-  }
-}
-
-/**
- * 停止来电注意力提示并恢复标签页标题 / Favicon
- */
-export function stopIncomingCallAttention() {
-  active = false;
-
-  if (titleFlashTimer) {
-    clearInterval(titleFlashTimer);
-    titleFlashTimer = null;
-  }
-
-  unbindVisibilityListener();
-  closeDesktopNotification();
-
-  if (originalTitle) {
-    document.title = originalTitle;
-  }
-
-  const link = getFaviconLink();
-  if (originalFaviconHref) {
-    link.href = originalFaviconHref;
-  }
-
-  originalTitle = '';
-  originalFaviconHref = '';
-  showAlertState = true;
-}
-
-if (typeof window !== 'undefined') {
-  window.__testIncomingCallAttention = function(caller = '13800138000') {
-    startIncomingCallAttention({ caller, body: `\u6d4b\u8bd5\u6765\u7535\uff1a${caller}` });
-    console.log('[\u6d4b\u8bd5] \u6807\u7b7e\u9875\u95ea\u70c1\u5df2\u542f\u52a8\uff0c\u6267\u884c __testStopIncomingCallAttention() \u505c\u6b62');
-  };
-  window.__testStopIncomingCallAttention = function() {
-    stopIncomingCallAttention();
-    console.log('[\u6d4b\u8bd5] \u6807\u7b7e\u9875\u95ea\u70c1\u5df2\u505c\u6b62');
-  };
-}

+ 125 - 411
src/views/aiSipCall/aiSipCallManualOutbound.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="call-center-phone-bar">
-        <form v-show="!hidePhoneBar">
+        <form>
             <table width="1224">
                 <tr>
                     <td width="70%" colspan="2" height="35" style="text-indent: 20px;">
@@ -371,13 +371,9 @@
         </form>
 
         <!-- 聊天和表单并排布局 -->
-        <div class="call-content-layout">
+        <div style="display: flex; gap: 20px; margin-top: 20px;">
             <!-- ASR 实时对话文本框 -->
-            <div id="chat-container" v-show="showChatContainer" class="chat-panel">
-                <div class="panel-header">
-                    <i class="el-icon-chat-dot-round"></i>
-                    <span>实时对话</span>
-                </div>
+            <div id="chat-container" v-show="showChatContainer" style="flex: 1; min-width: 0;">
                 <div id="chat-messages" class="message-container">
                     <div
                         v-for="(msg, index) in chatMessages"
@@ -394,49 +390,43 @@
             </div>
 
             <!-- 客户信息登记表单(外呼时自动显示) -->
-            <div v-if="showCustomerForm" class="customer-form-panel">
-                <div class="panel-header">
-                    <i class="el-icon-user"></i>
-                    <span>客户信息登记</span>
-                </div>
-                <div class="customer-form-body">
-                    <el-form :model="customerForm" label-width="80px" class="customer-form" size="small">
-                        <el-form-item label="姓名">
-                            <el-input v-model="customerForm.customerName" placeholder="请输入客户姓名"></el-input>
-                        </el-form-item>
-                        <el-form-item label="性别">
-                            <el-select v-model="customerForm.sex" placeholder="请选择性别">
-                                <el-option label="男" value="0"></el-option>
-                                <el-option label="女" value="1"></el-option>
-                                <el-option label="未知" value="2"></el-option>
-                            </el-select>
-                        </el-form-item>
-                        <el-form-item label="所在地区">
-                            <el-cascader
-                                ref="citySelect"
-                                v-model="cityIds"
-                                :options="citys"
-                                @change="handleCityChange"
-                                placeholder="请选择地区"
-                                clearable
-                                style="width: 100%;">
-                            </el-cascader>
-                        </el-form-item>
-                        <el-form-item label="详细地址">
-                            <el-input v-model="customerForm.detailAddress" type="textarea" :rows="2" placeholder="请输入详细地址"></el-input>
-                        </el-form-item>
-                        <el-form-item label="沟通内容">
-                            <el-input v-model="customerForm.communicationContent" type="textarea" :rows="4" placeholder="请输入本次沟通内容" maxlength="100" show-word-limit></el-input>
-                        </el-form-item>
-                        <el-form-item label="历史记录">
-                            <el-input v-model="customerForm.historicalCommunication" type="textarea" :rows="6" readonly></el-input>
-                        </el-form-item>
-                        <el-form-item>
-                            <el-button type="primary" size="small" @click="saveCustomerForm">保存</el-button>
-                            <el-button size="small" @click="closeCustomerForm">关闭</el-button>
-                        </el-form-item>
-                    </el-form>
-                </div>
+            <div v-if="showCustomerForm" class="customer-form" style="flex: 1; min-width: 0; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.1);">
+                <h3 style="margin-bottom: 20px; color: #409EFF; border-left: 4px solid #409EFF; padding-left: 12px;">客户信息登记表</h3>
+                <el-form :model="customerForm" label-width="100px" class="customer-form">
+                    <el-form-item label="姓名">
+                        <el-input v-model="customerForm.customerName" placeholder="请输入客户姓名"></el-input>
+                    </el-form-item>
+                    <el-form-item label="性别">
+                        <el-select v-model="customerForm.sex" placeholder="请选择性别">
+                            <el-option label="男" value="0"></el-option>
+                            <el-option label="女" value="1"></el-option>
+                            <el-option label="未知" value="2"></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="所在地区">
+                        <el-cascader
+                            ref="citySelect"
+                            v-model="cityIds"
+                            :options="citys"
+                            @change="handleCityChange"
+                            placeholder="请选择地区"
+                            clearable>
+                        </el-cascader>
+                    </el-form-item>
+                    <el-form-item label="详细地址">
+                        <el-input v-model="customerForm.detailAddress" type="textarea" :rows="2" placeholder="请输入详细地址"></el-input>
+                    </el-form-item>
+                    <el-form-item label="沟通内容">
+                        <el-input v-model="customerForm.communicationContent" type="textarea" :rows="4" placeholder="请输入本次沟通内容" maxlength="100" show-word-limit></el-input>
+                    </el-form-item>
+                    <el-form-item label="历史沟通记录">
+                        <el-input v-model="customerForm.historicalCommunication" type="textarea" :rows="6" readonly></el-input>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" @click="saveCustomerForm">保存</el-button>
+                        <el-button @click="closeCustomerForm">关闭</el-button>
+                    </el-form-item>
+                </el-form>
             </div>
         </div>
 
@@ -489,11 +479,6 @@ import {
     DefaultConfig
 } from '@/assets/callCenterPhoneBarSdk/constants.js';
 import ccPhoneBarSocket from '@/assets/callCenterPhoneBarSdk/ccPhoneBarSocket.js';
-import {
-  getConnectedSharedCCPhoneBar,
-  incrementSharedCCPhoneBarRef,
-  waitForSharedCCPhoneBar
-} from '@/utils/ccPhoneBarShared';
 import {myCallUser,getToolbarBasicParam} from "../../api/aiSipCall/aiSipCallUser";
 import {addCustcallrecord, getCustCommunicationInfo, syncByUuid} from "../../api/aiSipCall/aiSipCallOutboundCdr";
 import { getCustomerDetails, updateCustomer } from "@/api/crm/customer";
@@ -526,19 +511,11 @@ export default {
         type: [Number, String],
         default: null
       },
-        hidePhoneBar: {
-            type: Boolean,
-            default: false
-        },
     },
     data() {
         return {
             // 工具条对象
             phoneBar: null,
-            // 标记当前 phoneBar 是否为共享复用实例(非本组件创建)
-            _isSharedPhoneBar: false,
-            // 共享 phoneBar 时跟踪注册的事件回调,用于销毁时清理
-            _phoneBarCallbackMap: null,
 
             // 配置参数
             scriptServer: DefaultConfig.scriptServer,
@@ -692,41 +669,13 @@ export default {
                 transferToGroupId.addEventListener('change', this.handleGroupIdChange);
             }
         });
-        // 监听浮动软电话的外呼触发事件
-        this.$root.$on('floating-softphone-call-triggered', this.onFloatingPhoneCallTriggered);
-        // 监听浮动软电话的挂机/保持委托事件
-        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) {
-            // 递减引用计数
-            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;
+            console.log('[beforeDestroy] 断开 WebSocket 连接');
+            this.phoneBar.disconnect();
         }
         // 移除键盘事件监听
         document.removeEventListener('keyup', this._escKeyHandler);
@@ -736,12 +685,6 @@ export default {
         if (transferToGroupId) {
             transferToGroupId.removeEventListener('change', this.handleGroupIdChange);
         }
-        // 移除浮动软电话事件监听
-        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;
@@ -749,87 +692,15 @@ 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 重连,更新引用并重新注册事件');
-            this._cleanupPhoneBarListeners();
-            this.phoneBar = newPhoneBar;
-            this._isSharedPhoneBar = true;
-            incrementSharedCCPhoneBarRef();
-            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 = '签入';
-        },
-
         /**
          * 初始化电话工具条
-         * 优先等待 FloatingSoftPhone 的共享 ccPhoneBar,避免重复登录 status:201
          */
-        async initPhoneBar() {
-            let shared = getConnectedSharedCCPhoneBar();
-            if (!shared) {
-                try {
-                    shared = await waitForSharedCCPhoneBar(25000);
-                } catch (err) {
-                    console.warn('[电话工具条] 等待共享 ccPhoneBar 超时', err);
-                }
-            }
-            if (shared && typeof shared.getIsConnected === 'function' && shared.getIsConnected()) {
-                console.log('[电话工具条] 复用共享 ccPhoneBar,避免 status:201 冲突');
-                this.phoneBar = shared;
-                this._isSharedPhoneBar = true;
-                incrementSharedCCPhoneBarRef();
-                this.onlineButtonText = '签出';
-                this.callStatus = '已签入';
-                this.agentStatus = '忙碌';
-                this.loginTime = new Date().toLocaleTimeString();
-                return;
-            }
+        initPhoneBar() {
+            // 创建工具条对象
+            this.phoneBar = new ccPhoneBarSocket();
 
-            this.$message.warning('软电话正在连接,请稍候;若长时间未就绪,请刷新页面');
+            // 先调用 myCallUser 获取 extNum
+            this.loadExtNumAndInit();
         },
 
         loadExtNumAndInit() {
@@ -864,10 +735,7 @@ export default {
                         ipccServer: this.ipccServerUrl,
                         gatewayList: response.data.gatewayList,
                         gatewayEncrypted: false,
-                        extPassword: response.data.encryptPsw,
-                        enableWss: true,
-                        enableHeartBeat: true,
-                        heartBeatIntervalSecs: 16
+                        extPassword: response.data.encryptPsw
                     };
 
                     console.log('初始化工具条基础配置参数:', callConfig);
@@ -927,7 +795,7 @@ export default {
          */
         setupEventListeners() {
             // websocket 通信对象断开事件
-            this._phoneBarOn(EventList.WS_DISCONNECTED, (msg) => {
+            this.phoneBar.on(EventList.WS_DISCONNECTED, (msg) => {
                 console.log("websocket 通信对象断开事件:" + msg);
                 this.updatePhoneBar(msg, EventList.WS_DISCONNECTED);
                 this.showTransferArea = false;
@@ -947,7 +815,7 @@ export default {
                 }
             });
 
-            this._phoneBarOn(EventList.OUTBOUND_START, (msg) => {
+            this.phoneBar.on(EventList.OUTBOUND_START, (msg) => {
                 console.log('outbound_start 事件:' + msg);
                 this.showChatContainer = true;
                 this.showCustomerForm = true;
@@ -955,7 +823,7 @@ export default {
                 this.markExecuteSuccess();
             });
 
-            this._phoneBarOn(EventList.REQUEST_ARGS_ERROR, (msg) => {
+            this.phoneBar.on(EventList.REQUEST_ARGS_ERROR, (msg) => {
                 console.log('request_args_error事件:' + msg);
                 this.updatePhoneBar(msg, EventList.REQUEST_ARGS_ERROR);
                 this.callExecuteFailed = true;
@@ -964,7 +832,7 @@ export default {
             });
 
             if (EventList.SERVER_ERROR) {
-                this._phoneBarOn(EventList.SERVER_ERROR, (msg) => {
+                this.phoneBar.on(EventList.SERVER_ERROR, (msg) => {
                     this.updatePhoneBar(msg, EventList.SERVER_ERROR);
                     this.callExecuteFailed = true;
                     this.callFailReason = '服务器内部错误';
@@ -973,7 +841,7 @@ export default {
             }
 
             if (EventList.CALLER_BUSY) {
-                this._phoneBarOn(EventList.CALLER_BUSY, (msg) => {
+                this.phoneBar.on(EventList.CALLER_BUSY, (msg) => {
                     this.updatePhoneBar(msg, EventList.CALLER_BUSY);
                     this.callExecuteFailed = true;
                     this.callFailReason = '分机忙';
@@ -982,7 +850,7 @@ export default {
             }
 
             if (EventList.CALLER_NOT_LOGIN) {
-                this._phoneBarOn(EventList.CALLER_NOT_LOGIN, (msg) => {
+                this.phoneBar.on(EventList.CALLER_NOT_LOGIN, (msg) => {
                     this.updatePhoneBar(msg, EventList.CALLER_NOT_LOGIN);
                     this.callExecuteFailed = true;
                     this.callFailReason = '分机未登录';
@@ -991,7 +859,7 @@ export default {
             }
 
             if (EventList.CALLER_RESPOND_TIMEOUT) {
-                this._phoneBarOn(EventList.CALLER_RESPOND_TIMEOUT, (msg) => {
+                this.phoneBar.on(EventList.CALLER_RESPOND_TIMEOUT, (msg) => {
                     this.updatePhoneBar(msg, EventList.CALLER_RESPOND_TIMEOUT);
                     this.callExecuteFailed = true;
                     this.callFailReason = '分机应答超时';
@@ -1000,16 +868,13 @@ export default {
             }
 
             // 用户已在其他设备登录
-            this._phoneBarOn(EventList.USER_LOGIN_ON_OTHER_DEVICE, (msg) => {
+            this.phoneBar.on(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._phoneBarOn(EventList.WS_CONNECTED, (msg) => {
+            this.phoneBar.on(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 = '忙碌';
@@ -1020,15 +885,13 @@ export default {
                 }
             });
 
-            this._phoneBarOn(EventList.CALLEE_RINGING, (msg) => {
+            this.phoneBar.on(EventList.CALLEE_RINGING, (msg) => {
                 console.log('被叫振铃事件:' + msg.content);
                 this.updatePhoneBar(msg, EventList.CALLEE_RINGING);
                 this.markExecuteSuccess();
-                // 同步状态到浮动软电话(外呼被叫振铃,非来电)
-                this.$root.$emit('dialog-call-ringing', { incoming: false });
             });
 
-            this._phoneBarOn(EventList.CALLER_ANSWERED, (msg) => {
+            this.phoneBar.on(EventList.CALLER_ANSWERED, (msg) => {
                 console.log('主叫接通事件:' + msg);
                 this.agentStatus = '通话中';
                 if (msg && msg.object && msg.object.uuid) {
@@ -1036,11 +899,9 @@ export default {
                     console.log('[主叫接通] 获取到通话 UUID:', this.currentCallUuid);
                 }
                 this.updatePhoneBar(msg, EventList.CALLER_ANSWERED);
-                // 同步状态到浮动软电话
-                this.$root.$emit('dialog-call-talking');
             });
 
-            this._phoneBarOn(EventList.CALLER_HANGUP, (msg) => {
+            this.phoneBar.on(EventList.CALLER_HANGUP, (msg) => {
                 console.log('主叫挂断事件:' + msg);
                 this.agentStatus = '通话结束';
                 this.canSendVideoReInvite = false;
@@ -1051,26 +912,22 @@ export default {
                     this.currentCallUuid = msg.object.uuid;
                 }
                 this.updatePhoneBar(msg, EventList.CALLER_HANGUP);
-                // 同步状态到浮动软电话
-                this.$root.$emit('dialog-call-ended');
 
                 if (this.callExecuted && !this.callExecuteFailed) {
                     this.openIntentDialog('CALLER_HANGUP', msg);
                 }
             });
 
-            this._phoneBarOn(EventList.CALLEE_ANSWERED, (msg) => {
+            this.phoneBar.on(EventList.CALLEE_ANSWERED, (msg) => {
                 console.log('被叫接通事件:' + msg);
                 if (msg && msg.object && msg.object.uuid) {
                     this.currentCallUuid = msg.object.uuid;
                     console.log('[被叫接通] 获取到通话 UUID:', this.currentCallUuid);
                 }
                 this.updatePhoneBar(msg, EventList.CALLEE_ANSWERED);
-                // 同步状态到浮动软电话
-                this.$root.$emit('dialog-call-talking');
             });
 
-            this._phoneBarOn(EventList.CALLEE_HANGUP, (msg) => {
+            this.phoneBar.on(EventList.CALLEE_HANGUP, (msg) => {
                 console.log('被叫挂断事件:' + msg);
                 this.showTransferArea = false;
                 this.showConferenceArea = false;
@@ -1078,15 +935,13 @@ export default {
                     this.currentCallUuid = msg.object.uuid;
                 }
                 this.updatePhoneBar(msg, EventList.CALLEE_HANGUP);
-                // 同步状态到浮动软电话
-                this.$root.$emit('dialog-call-ended');
 
                 if (this.callExecuted && !this.callExecuteFailed) {
                     this.openIntentDialog('CALLEE_HANGUP', msg);
                 }
             });
 
-            this._phoneBarOn(EventList.STATUS_CHANGED, (msg) => {
+            this.phoneBar.on(EventList.STATUS_CHANGED, (msg) => {
                 console.log('座席状态改变事件:' + msg);
 
                 // 确保 msg.object 存在才设置
@@ -1103,57 +958,53 @@ export default {
                 }
             });
 
-            this._phoneBarOn(EventList.ACD_GROUP_QUEUE_NUMBER, (msg) => {
+            this.phoneBar.on(EventList.ACD_GROUP_QUEUE_NUMBER, (msg) => {
                 console.log('当前排队人数消息事件:' + msg);
                 this.queueNumber = msg.object.queue_number;
             });
 
-            this._phoneBarOn(EventList.ON_AUDIO_CALL_CONNECTED, (msg) => {
+            this.phoneBar.on(EventList.ON_AUDIO_CALL_CONNECTED, (msg) => {
                 console.log('音频通话已建立事件:' + msg);
                 this.canSendVideoReInvite = true;
             });
 
-            this._phoneBarOn(EventList.CUSTOMER_CHANNEL_HOLD, (msg) => {
+            this.phoneBar.on(EventList.CUSTOMER_CHANNEL_HOLD, (msg) => {
                 console.log('客户通话已保持事件:' + msg);
                 this.callStatus = '通话已保持';
                 this.showHoldBtn = false;
                 this.showUnHoldBtn = true;
-                // 同步状态到浮动软电话
-                this.$root.$emit('dialog-call-hold');
             });
 
-            this._phoneBarOn(EventList.CUSTOMER_CHANNEL_UNHOLD, (msg) => {
+            this.phoneBar.on(EventList.CUSTOMER_CHANNEL_UNHOLD, (msg) => {
                 console.log('客户通话已接回事件:' + msg);
                 this.callStatus = '客户通话已接回';
                 this.showHoldBtn = true;
                 this.showUnHoldBtn = false;
-                // 同步状态到浮动软电话
-                this.$root.$emit('dialog-call-unhold');
             });
 
-            this._phoneBarOn(EventList.CUSTOMER_ON_HOLD_HANGUP, (msg) => {
+            this.phoneBar.on(EventList.CUSTOMER_ON_HOLD_HANGUP, (msg) => {
                 console.log('保持的通话已挂机事件:' + msg);
                 this.showHoldBtn = true;
                 this.showUnHoldBtn = false;
                 this.callStatus = '保持的通话已挂机.';
             });
 
-            this._phoneBarOn(EventList.ON_VIDEO_CALL_CONNECTED, (msg) => {
+            this.phoneBar.on(EventList.ON_VIDEO_CALL_CONNECTED, (msg) => {
                 console.log('视频通话已建立事件:' + msg);
                 this.canSendVideoFile = true;
             });
 
-            this._phoneBarOn(EventList.INNER_CONSULTATION_START, (msg) => {
+            this.phoneBar.on(EventList.INNER_CONSULTATION_START, (msg) => {
                 console.log('咨询开始事件:' + msg);
                 this.callStatus = '咨询开始.';
             });
 
-            this._phoneBarOn(EventList.INNER_CONSULTATION_STOP, (msg) => {
+            this.phoneBar.on(EventList.INNER_CONSULTATION_STOP, (msg) => {
                 console.log('咨询结束事件:' + msg);
                 this.callStatus = '咨询结束.';
             });
 
-            this._phoneBarOn(EventList.TRANSFER_CALL_SUCCESS, (msg) => {
+            this.phoneBar.on(EventList.TRANSFER_CALL_SUCCESS, (msg) => {
                 console.log('电话转接成功事件:' + msg);
                 // 转接成功后隐藏转接/咨询区域(与 source 一致)
                 this.showTransferArea = false;
@@ -1161,7 +1012,7 @@ export default {
                 this.externalPhoneNumber = '';
             });
 
-            this._phoneBarOn(EventList.AGENT_STATUS_DATA_CHANGED, (msg) => {
+            this.phoneBar.on(EventList.AGENT_STATUS_DATA_CHANGED, (msg) => {
                 console.log('agent_status_data_changed 事件:' + msg);
 
                 // 解析并更新坐席列表数据
@@ -1186,7 +1037,7 @@ export default {
             });
 
             // 客户通话等待相关事件
-            this._phoneBarOn(EventList.CUSTOMER_CHANNEL_CALL_WAIT, (msg) => {
+            this.phoneBar.on(EventList.CUSTOMER_CHANNEL_CALL_WAIT, (msg) => {
                 console.log('客户电话等待中事件:' + msg);
                 // 隐藏会议区域和客户信息表单(与 source 一致)
                 this.showConferenceArea = false;
@@ -1198,7 +1049,7 @@ export default {
                 this.showTransferArea = true;
             });
 
-            this._phoneBarOn(EventList.CUSTOMER_CHANNEL_OFF_CALL_WAIT, (msg) => {
+            this.phoneBar.on(EventList.CUSTOMER_CHANNEL_OFF_CALL_WAIT, (msg) => {
                 console.log('等待的电话已接回事件:' + msg);
                 // 隐藏转接/咨询区域
                 this.showStopCallWait = false;
@@ -1207,7 +1058,7 @@ export default {
                 this.callStatus = '等待的电话已接回.';
             });
 
-            this._phoneBarOn(EventList.INNER_CONSULTATION_START, (msg) => {
+            this.phoneBar.on(EventList.INNER_CONSULTATION_START, (msg) => {
                 console.log('咨询已开始事件:' + msg);
                 // 隐藏会议区域和客户信息表单(与 source 一致)
                 this.showConferenceArea = false;
@@ -1218,7 +1069,7 @@ export default {
                 this.showDoTransferBtn = false;
             });
 
-            this._phoneBarOn(EventList.INNER_CONSULTATION_STOP, (msg) => {
+            this.phoneBar.on(EventList.INNER_CONSULTATION_STOP, (msg) => {
                 console.log('咨询已结束事件:' + msg);
                 // 隐藏转接/咨询区域
                 this.showTransferCallWait = false;
@@ -1226,7 +1077,7 @@ export default {
                 this.callStatus = '咨询结束.';
             });
 
-            this._phoneBarOn(EventList.CUSTOMER_ON_CALL_WAIT_HANGUP, (msg) => {
+            this.phoneBar.on(EventList.CUSTOMER_ON_CALL_WAIT_HANGUP, (msg) => {
                 console.log('等待的客户已挂机事件:' + msg);
                 // 隐藏转接/咨询区域
                 this.showStopCallWait = false;
@@ -1236,17 +1087,17 @@ export default {
             });
 
             // 会议相关事件
-            this._phoneBarOn(EventList.CONFERENCE_MEMBER_ANSWERED, (msg) => {
+            this.phoneBar.on(EventList.CONFERENCE_MEMBER_ANSWERED, (msg) => {
                 console.log('会议成员已经接通事件:' + msg);
                 this.updateConferenceMemberStatus(msg.object.phone, '通话中', 'green');
             });
 
-            this._phoneBarOn(EventList.CONFERENCE_MEMBER_HANGUP, (msg) => {
+            this.phoneBar.on(EventList.CONFERENCE_MEMBER_HANGUP, (msg) => {
                 console.log('会议成员已经挂机事件:' + msg);
                 this.updateConferenceMemberHangup(msg.object.phone, msg.object);
             });
 
-            this._phoneBarOn(EventList.CONFERENCE_MODERATOR_ANSWERED, (msg) => {
+            this.phoneBar.on(EventList.CONFERENCE_MODERATOR_ANSWERED, (msg) => {
                 console.log('电话会议开始,主持人已接通事件:' + msg);
                 // 隐藏转接/咨询区域和客户信息表单(与 source 一致)
                 this.showTransferArea = false;
@@ -1254,13 +1105,13 @@ export default {
                 this.onConferenceStart();
             });
 
-            this._phoneBarOn(EventList.CONFERENCE_MODERATOR_HANGUP, (msg) => {
+            this.phoneBar.on(EventList.CONFERENCE_MODERATOR_HANGUP, (msg) => {
                 console.log('电话会议结束,主持人已挂机事件:' + msg);
                 // 会议结束后隐藏会议区域
                 this.onConferenceEnd();
             });
 
-            this._phoneBarOn(EventList.CONFERENCE_MEMBER_MUTED_SUCCESS, (msg) => {
+            this.phoneBar.on(EventList.CONFERENCE_MEMBER_MUTED_SUCCESS, (msg) => {
                 console.log('会议成员已被禁言事件:' + msg);
                 const memberPhone = msg.object.phone;
                 const member = this.conferenceMembers.find(m => m.phone === memberPhone);
@@ -1269,7 +1120,7 @@ export default {
                 }
             });
 
-            this._phoneBarOn(EventList.CONFERENCE_MEMBER_UNMUTED_SUCCESS, (msg) => {
+            this.phoneBar.on(EventList.CONFERENCE_MEMBER_UNMUTED_SUCCESS, (msg) => {
                 console.log('会议成员解除禁言成功事件:' + msg);
                 const memberPhone = msg.object.phone;
                 const member = this.conferenceMembers.find(m => m.phone === memberPhone);
@@ -1278,7 +1129,7 @@ export default {
                 }
             });
 
-            this._phoneBarOn(EventList.CONFERENCE_MEMBER_VMUTED_SUCCESS, (msg) => {
+            this.phoneBar.on(EventList.CONFERENCE_MEMBER_VMUTED_SUCCESS, (msg) => {
                 console.log('会议成员已被禁用视频事件:' + msg);
                 const memberPhone = msg.object.phone;
                 const member = this.conferenceMembers.find(m => m.phone === memberPhone);
@@ -1287,7 +1138,7 @@ export default {
                 }
             });
 
-            this._phoneBarOn(EventList.CONFERENCE_MEMBER_UNVMUTED_SUCCESS, (msg) => {
+            this.phoneBar.on(EventList.CONFERENCE_MEMBER_UNVMUTED_SUCCESS, (msg) => {
                 console.log('会议成员启用视频成功事件:' + msg);
                 const memberPhone = msg.object.phone;
                 const member = this.conferenceMembers.find(m => m.phone === memberPhone);
@@ -1297,19 +1148,19 @@ export default {
             });
 
             // ASR 相关事件
-            this._phoneBarOn(EventList.ASR_PROCESS_STARTED, (msg) => {
+            this.phoneBar.on(EventList.ASR_PROCESS_STARTED, (msg) => {
                 this.chatMessages = [];
             });
 
-            this._phoneBarOn(EventList.ASR_RESULT_GENERATE, (msg) => {
+            this.phoneBar.on(EventList.ASR_RESULT_GENERATE, (msg) => {
                 this.handleAsrMessage(msg);
             });
 
-            this._phoneBarOn(EventList.ASR_PROCESS_END_CUSTOMER, (msg) => {
+            this.phoneBar.on(EventList.ASR_PROCESS_END_CUSTOMER, (msg) => {
                 this.handleAsrMessage(msg);
             });
 
-            this._phoneBarOn(EventList.ASR_PROCESS_END_AGENT, (msg) => {
+            this.phoneBar.on(EventList.ASR_PROCESS_END_AGENT, (msg) => {
                 this.handleAsrMessage(msg);
             });
         },
@@ -1489,96 +1340,6 @@ export default {
 
             this.phoneBar.call(this.phoneNumber.trim(), this.callType, this.videoLevel);
         },
-        /**
-         * 浮动软电话触发外呼:接收号码,通过弹窗phoneBar发起外呼(走完整的通话记录、客户信息流程)
-         * @param {string} phoneNumber - 浮动软电话传来的号码(已解密或明文)
-         */
-        onFloatingPhoneCallTriggered(phoneNumber) {
-            console.log('[浮动电话触发] 外呼号码:', phoneNumber, ',通过弹窗phoneBar执行外呼');
-            if (!phoneNumber || phoneNumber.trim().length < 3) {
-                this.$message.warning('号码格式不正确');
-                return;
-            }
-            if (!this.phoneBar || !this.phoneBar.getIsConnected()) {
-                // 检查共享实例是否已更新(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;
-                }
-            }
-            // 工具条可见时依赖 DOM 按钮状态;浮动软电话模式下 UI 隐藏,改由连接状态判断
-            if (!this.hidePhoneBar) {
-                const callBtn = document.getElementById('callBtn');
-                if (!callBtn || !callBtn.classList.contains('on')) {
-                    this.$message.warning('当前无法外呼,请先置忙');
-                    return;
-                }
-            }
-            // 检查当前是否有通话
-            if (this.phoneBar.getCallConnected()) {
-                this.$message.warning('当前有通话正在进行,请先挂机');
-                return;
-            }
-            // 设置phoneNumber并重置通话记录状态(和handleCall一致)
-            this.phoneNumber = phoneNumber.trim();
-            this.currentCallUuid = '';
-            this.callFinishedAt = '';
-            this.callExecuted = false;
-            this.callExecuteFailed = false;
-            this.callResultRecorded = false;
-            this.callFailReason = '';
-            if (this.recordTimer) {
-                clearTimeout(this.recordTimer);
-                this.recordTimer = null;
-            }
-            // 通过弹窗phoneBar发起外呼
-            this.phoneBar.call(this.phoneNumber, this.callType, this.videoLevel);
-        },
-        /**
-         * 浮动软电话触发挂机:通过弹窗phoneBar执行挂机
-         */
-        onFloatingPhoneHangupTriggered() {
-            console.log('[浮动电话触发] 通过弹窗phoneBar执行挂机');
-            if (this.phoneBar && this.phoneBar.getIsConnected()) {
-                this.phoneBar.hangup();
-            }
-        },
-        /**
-         * 浮动软电话触发保持/恢复:通过弹窗phoneBar执行
-         */
-        onFloatingPhoneHoldTriggered() {
-            console.log('[浮动电话触发] 通过弹窗phoneBar执行保持/恢复');
-            if (!this.phoneBar || !this.phoneBar.getIsConnected()) {
-                this.$message.warning('请先上线');
-                return;
-            }
-            if (this.showUnHoldBtn) {
-                // 当前是保持状态,执行取消保持
-                const btn = document.getElementById('unHoldBtn');
-                if (btn && btn.classList.contains('on')) {
-                    this.phoneBar.unHoldCall();
-                }
-            } else {
-                // 当前是通话状态,执行保持
-                const btn = document.getElementById('holdBtn');
-                if (btn && btn.classList.contains('on')) {
-                    this.phoneBar.holdCall();
-                }
-            }
-        },
 
         formatDateTime(date = new Date()) {
             const pad = (n) => String(n).padStart(2, '0');
@@ -2907,75 +2668,28 @@ export default {
 }
 
 /* ASR 聊天容器 */
-.call-center-phone-bar .call-content-layout {
-    display: flex;
-    gap: 16px;
-    margin-top: 16px;
-    min-height: 0;
-}
-
-.call-center-phone-bar .chat-panel,
-.call-center-phone-bar .customer-form-panel {
-    flex: 1;
-    min-width: 0;
-    background: #fff;
-    border: 1px solid #e4e7ed;
-    border-radius: 8px;
-    overflow: hidden;
-    display: flex;
-    flex-direction: column;
-}
-
-.call-center-phone-bar .panel-header {
-    display: flex;
-    align-items: center;
-    gap: 6px;
-    padding: 10px 16px;
-    background: #f5f7fa;
-    border-bottom: 1px solid #e4e7ed;
-    font-size: 14px;
-    font-weight: 600;
-    color: #303133;
-    flex-shrink: 0;
-}
-
-.call-center-phone-bar .panel-header i {
-    font-size: 16px;
-    color: #409EFF;
-}
-
-.call-center-phone-bar .customer-form-body {
-    padding: 16px;
-    overflow-y: auto;
-    flex: 1;
-}
-
 .call-center-phone-bar #chat-container {
-    margin: 0;
-    max-width: none;
-    width: auto;
-    background: transparent;
-    border: none;
-    border-radius: 0;
-    box-shadow: none;
-    padding: 0;
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    min-height: 0;
+    width: 90%;
+    max-width: 600px;
+    margin: 20px auto;
+    background: rgba(255, 255, 255, 0.98);
+    border: 1px solid #e0e6ed;
+    border-radius: 12px;
+    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
+    padding: 15px;
 }
 
 .call-center-phone-bar .message {
-    padding: 10px 14px;
-    margin: 6px 12px;
-    border-radius: 8px;
+    padding: 12px 16px;
+    margin: 10px 0;
+    border-radius: 12px;
     animation: slideIn 0.3s ease;
 }
 
 @keyframes slideIn {
     from {
         opacity: 0;
-        transform: translateY(8px);
+        transform: translateY(10px);
     }
     to {
         opacity: 1;
@@ -2984,69 +2698,69 @@ export default {
 }
 
 .call-center-phone-bar .customer {
-    background: #f0f0f0;
-    border-left: 3px solid #909399;
+    background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+    border-left: 4px solid #909399;
 }
 
 .call-center-phone-bar .agent {
-    background: #ecf5ff;
-    border-left: 3px solid #409EFF;
+    background: linear-gradient(135deg, #f0f9ff 0%, #c6efff 100%);
+    border-right: 4px solid #409EFF;
 }
 
 .call-center-phone-bar .system-message {
     text-align: center;
     color: #909399;
-    font-size: 12px;
-    padding: 6px 12px;
-    background: #fafafa;
-    border-radius: 4px;
-    margin: 6px 12px;
+    font-style: italic;
+    padding: 10px;
+    background: rgba(245, 247, 250, 0.5);
+    border-radius: 8px;
+    margin: 10px 0;
 }
 
 .call-center-phone-bar .message-container {
     display: flex;
     flex-direction: column;
-    max-height: 500px;
+    max-height: 400px;
     overflow-y: auto;
-    padding: 8px 0;
 }
 
 .call-center-phone-bar .message-container::-webkit-scrollbar {
-    width: 5px;
+    width: 6px;
 }
 
 .call-center-phone-bar .message-container::-webkit-scrollbar-track {
-    background: transparent;
+    background: #f1f1f1;
+    border-radius: 3px;
 }
 
 .call-center-phone-bar .message-container::-webkit-scrollbar-thumb {
-    background: #dcdfe6;
+    background: #c0c4cc;
     border-radius: 3px;
 }
 
 .call-center-phone-bar .message-container::-webkit-scrollbar-thumb:hover {
-    background: #c0c4cc;
+    background: #909399;
 }
 
 .call-center-phone-bar .message-header {
     font-weight: 600;
-    margin-bottom: 4px;
+    margin-bottom: 6px;
     color: #606266;
-    font-size: 12px;
+    font-size: 13px;
 }
 
 .call-center-phone-bar .message-content {
     color: #303133;
-    font-size: 13px;
-    line-height: 1.5;
+    font-size: 14px;
+    line-height: 1.6;
 }
 
 /* 客户信息表单 */
 .call-center-phone-bar .customer-form {
-    background: transparent;
-    border-radius: 0;
-    box-shadow: none;
-    border: none;
+    background: rgba(255, 255, 255, 0.98);
+    border-radius: 12px;
+    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
+    border: 1px solid #e0e6ed;
 }
 
 .call-center-phone-bar .customer-form h3 {

+ 49 - 95
src/views/aiSipCall/softPhone.vue

@@ -166,7 +166,7 @@
             <input type="text" id="note" v-model="accountForm.note" placeholder="备注">
           </div>
           <div class="form-group">
-            <input type="text" id="server" v-model="accountForm.server" placeholder="服务(wss://sip.ylrzcloud.com:8443)" required>
+            <input type="text" id="server" v-model="accountForm.server" placeholder="服务(ws://129.28.164.235:5066)" required>
           </div>
           <div class="form-group">
             <input type="text" id="username" v-model="accountForm.username" placeholder="用户名">
@@ -279,20 +279,38 @@
 </template>
 
 <script>
-import { WebPhone, ProfileManager, checkMicrophonePermission, IPCC_DEFAULTS, JS_SIP_DEFAULTS } from '@/api/aiSipCall/softPhone.js';
+import { WebPhone, ProfileManager, checkMicrophonePermission } from '@/api/aiSipCall/softPhone.js';
 import ccPhoneBarSocket from '@/assets/callCenterPhoneBarSdk/ccPhoneBarSocket.js';
 import { EventList, VideoLevels, AgentStatusEnum } from '@/assets/callCenterPhoneBarSdk/constants.js';
 import { myCallUser, getToolbarBasicParam } from '@/api/aiSipCall/aiSipCallUser.js';
 import { syncByUuid, encryptMobile } from '@/api/aiSipCall/aiSipCallOutboundCdr.js';
 
 // ==================== 全局配置常量 ====================
-// IPCC 和 JsSIP 默认配置已从 softPhone.js 统一导入,此处仅作别名引用
-
-/** IPCC(呼叫中心)服务器配置(来自统一常量) */
-const IPCC_CONFIG = IPCC_DEFAULTS;
+/**
+ * IPCC服务器配置
+ */
+const IPCC_CONFIG = {
+  /** 生产环境IPCC服务器地址 */
+  SERVER_PROD: 'sip.ylrzcloud.com',
+  /** 本地调试IPCC服务器地址 */
+  SERVER_LOCAL: '129.28.164.235',
+  /** 本地调试端口 */
+  PORT_LOCAL: 1081,
+  /** WebSocket连接超时时间(毫秒) */
+  CONNECT_TIMEOUT: 15000
+};
 
-/** JsSIP(软电话)SIP 默认账号配置(来自统一常量) */
-const JS_SIP_CONFIG = JS_SIP_DEFAULTS;
+/**
+ * SIP默认账号配置
+ */
+const SIP_DEFAULT_CONFIG = {
+  /** 默认服务器地址 */
+  SERVER: 'ws://129.28.164.235:5066',
+  /** 默认域名 */
+  DOMAIN: '129.28.164.235',
+  /** 默认传输协议 */
+  TRANSPORT: 'ws'
+};
 
 /**
  * 音量控制配置
@@ -375,12 +393,12 @@ export default {
       editingUserId: null,
       accountForm: {
         note: '',
-        server: JS_SIP_CONFIG.SERVER,
+        server: SIP_DEFAULT_CONFIG.SERVER,
         username: '',
-        domain: JS_SIP_CONFIG.DOMAIN,
+        domain: SIP_DEFAULT_CONFIG.DOMAIN,
         loginName: '',
         password: '',
-        transport: JS_SIP_CONFIG.TRANSPORT
+        transport: SIP_DEFAULT_CONFIG.TRANSPORT
       },
       showPassword: false,
 
@@ -413,8 +431,6 @@ export default {
 
       // 呼叫中心集成
       ccPhoneBar: null,
-      // 标记当前 ccPhoneBar 是否为共享复用实例(非本组件创建)
-      _isSharedCCPhoneBar: false,
       ccSocketConnected: false,
       ccSocketFailed: false,
       ccConnectingPromise: null,
@@ -637,23 +653,6 @@ 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) {
@@ -670,12 +669,6 @@ export default {
         const configData = basicRes.data;
         if (!configData.loginToken) throw new Error('登录令牌无效');
 
-        // 将 loginToken 拼接到 SIP 服务器地址上(去掉旧的 loginToken 再拼接新的)
-        if (this.currentUserId && this.userList[this.currentUserId]) {
-          const baseUrl = this.userList[this.currentUserId].server.split('?')[0];
-          this.userList[this.currentUserId].server = `${baseUrl}?loginToken=${configData.loginToken}`;
-        }
-
         // 构建呼叫配置
         const callConfig = {
           useDefaultUi: false,
@@ -692,7 +685,6 @@ export default {
         };
 
         // 初始化并连接
-        this._isSharedCCPhoneBar = false;
         this.ccPhoneBar = new ccPhoneBarSocket();
         this.ccPhoneBar.initConfig(callConfig);
 
@@ -727,9 +719,6 @@ 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();
@@ -918,17 +907,17 @@ export default {
       }
 
       if (existingUserId) {
-        // 更新现有账号(始终重置 server/domain/transport 为线上配置,避免残留旧的 IP 地址配置)
-        console.log(`[jsSip] 更新已有账号: ${existingUserId}`);
+        // 更新现有账号
+        console.log(`[账号] 更新已有账号: ${existingUserId}`);
         const updatedProfile = {
           ...this.userList[existingUserId],
           note: extNum,
           display_name: extNum,
           password: extPass,
           user: extNum,
-          domain: JS_SIP_CONFIG.DOMAIN,
-          server: JS_SIP_CONFIG.SERVER,
-          transport: JS_SIP_CONFIG.TRANSPORT
+          domain: this.userList[existingUserId].domain || IPCC_CONFIG.SERVER_PROD,
+          server: this.userList[existingUserId].server || `wss://${IPCC_CONFIG.SERVER_PROD}`,
+          transport: this.userList[existingUserId].transport || 'wss'
         };
         this.profileManager.updateUser(existingUserId, updatedProfile);
         if (this.currentUserId !== existingUserId) {
@@ -937,15 +926,15 @@ export default {
         }
       } else {
         // 创建新账号
-        console.log(`[jsSip] 创建新账号: ${extNum}`);
+        console.log(`[账号] 创建新账号: ${extNum}`);
         const newProfile = {
           note: extNum,
           user: extNum,
-          domain: JS_SIP_CONFIG.DOMAIN,
+          domain: SIP_DEFAULT_CONFIG.DOMAIN,
           password: extPass,
           display_name: extNum,
-          server: JS_SIP_CONFIG.SERVER,
-          transport: JS_SIP_CONFIG.TRANSPORT
+          server: SIP_DEFAULT_CONFIG.SERVER,
+          transport: SIP_DEFAULT_CONFIG.TRANSPORT
         };
         this.profileManager.addUser(newProfile);
         const updatedProfile = this.profileManager.getProfile();
@@ -1100,13 +1089,7 @@ export default {
         // 注意:不清空 currentCallUuid,因为后续还需要用它来保存通话记录
       }
     },
-    onSessionClosed(event) {
-      // 控制台测试拒接不触发额外逻辑
-      if (event && event.reason === 'test_rejected') {
-        this._resetCallState();
-        this.showStatus('测试来电已结束', 'info');
-        return;
-      }
+    onSessionClosed() {
       // 显示已挂机状态
       this.showStatus('已挂机', 'info');
       this._resetCallState();
@@ -1336,7 +1319,7 @@ export default {
       this.isEditMode = false;
       this.editingUserId = null;
       this.accountDialogTitle = '添加账号';
-      this.accountForm = { note: '', server: JS_SIP_CONFIG.SERVER, username: '', domain: JS_SIP_CONFIG.DOMAIN, loginName: '', password: '', transport: JS_SIP_CONFIG.TRANSPORT };
+      this.accountForm = { note: '', server: 'ws://129.28.164.235:5066', username: '', domain: '129.28.164.235', loginName: '', password: '', transport: 'ws' };
       this.showPassword = false;
       this.accountDialogVisible = true;
     },
@@ -1466,23 +1449,10 @@ export default {
 
       // 断开 IPCC 连接
       if (this.ccPhoneBar) {
-        // 递减引用计数
-        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;
-          }
-        }
+        console.log('[重置] 断开IPCC连接');
+        try {
+          this.ccPhoneBar.disconnect();
+        } catch(e) {}
         this.ccPhoneBar = null;
       }
 
@@ -1550,27 +1520,11 @@ export default {
 
       // 断开 IPCC 连接
       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 {
-          // 创建者:无其他组件引用,安全断开
-          console.log('[销毁] 断开IPCC连接');
-          try {
-            this.ccPhoneBar.disconnect();
-          } catch(e) {
-            console.error('[销毁] IPCC连接断开失败:', e);
-          }
-          if (window.__sharedCCPhoneBar === this.ccPhoneBar) {
-            window.__sharedCCPhoneBar = null;
-          }
+        console.log('[销毁] 断开IPCC连接');
+        try {
+          this.ccPhoneBar.disconnect();
+        } catch(e) {
+          console.error('[销毁] IPCC连接断开失败:', e);
         }
         this.ccPhoneBar = null;
       }

+ 21 - 215
src/views/company/companyVoiceRobotic/handleManualAnswered.vue

@@ -127,92 +127,22 @@
     </el-dialog>
 
     <!-- 查看弹窗(已处理时使用,只读展示) -->
-    <el-dialog title="查看处理结果" :visible.sync="viewDialogVisible" width="900px" append-to-body class="manual-view-dialog">
-      <div v-loading="viewLoading">
-        <el-form :model="viewForm" label-width="110px" class="view-info-form">
-          <el-row>
-            <el-col :span="12">
-              <el-form-item label="任务名称">
-                <span>{{ viewForm.roboticName || '-' }}</span>
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="客户号码">
-                <span>{{ desensitizePhone(viewForm.callerNum || viewForm.mobile) }}</span>
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row>
-            <el-col :span="12">
-              <el-form-item label="客户名称">
-                <span>{{ viewForm.customerName || '-' }}</span>
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="有效客户">
-                <el-tag v-if="viewForm.effectiveCustomer === 1" type="success">有效</el-tag>
-                <el-tag v-else-if="viewForm.effectiveCustomer === 0" type="info">无效</el-tag>
-                <span v-else>-</span>
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row>
-            <el-col :span="12">
-              <el-form-item label="AI通话时长">
-                <span>{{ viewForm.callTime || 0 }} 秒</span>
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="人工接听时长">
-                <span>{{ msToSeconds(viewForm.manualAnsweredTimeLen) }} 秒</span>
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-form-item label="AI外呼备注">
-            <span>{{ viewForm.aiCallRemark || '无' }}</span>
-          </el-form-item>
-        </el-form>
-
-        <!-- 录音播放 -->
-        <div class="view-section">
-          <div class="view-section-title">
-            <i class="el-icon-headset"></i>
-            <span>通话录音</span>
-          </div>
-          <div class="view-section-body">
-            <audio
-              v-if="viewForm.recordPath"
-              controls
-              :src="handleRecordPath(viewForm.recordPath)"
-              class="audio-player"
-            >您的浏览器不支持音频播放</audio>
-            <div v-else class="empty-tip">暂无录音</div>
-          </div>
-        </div>
-
-        <!-- 聊天记录 -->
-        <div class="view-section">
-          <div class="view-section-title">
-            <i class="el-icon-chat-line-square"></i>
-            <span>对话内容</span>
-          </div>
-          <div class="view-section-body">
-            <div v-if="!viewForm.contentList || parseContentList(viewForm.contentList).length === 0" class="empty-tip">暂无对话内容</div>
-            <div v-else class="chat-container">
-              <div
-                v-for="(msg, index) in parseContentList(viewForm.contentList)"
-                :key="index"
-                :class="['chat-item', msg.role === 'user' ? 'chat-right' : 'chat-left']"
-              >
-                <div class="chat-bubble-wrapper">
-                  <div class="chat-role">{{ msg.role === 'user' ? '客户' : 'AI客服' }}</div>
-                  <div class="chat-bubble">{{ msg.content }}</div>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
+    <el-dialog title="查看处理结果" :visible.sync="viewDialogVisible" width="600px" append-to-body>
+      <el-form :model="viewForm" label-width="110px" v-loading="viewLoading">
+        <el-form-item label="客户名称">
+          <span>{{ viewForm.customerName }}</span>
+        </el-form-item>
+        <el-form-item label="客户号码">
+          <span>{{ desensitizePhone(viewForm.mobile) }}</span>
+        </el-form-item>
+        <el-form-item label="有效客户">
+          <el-tag v-if="viewForm.effectiveCustomer === 1" type="success">有效</el-tag>
+          <el-tag v-else type="info">无效</el-tag>
+        </el-form-item>
+        <el-form-item label="AI外呼备注">
+          <span>{{ viewForm.aiCallRemark || '无' }}</span>
+        </el-form-item>
+      </el-form>
       <div slot="footer" class="dialog-footer">
         <el-button @click="viewDialogVisible = false">关 闭</el-button>
       </div>
@@ -308,23 +238,18 @@ export default {
     },
     /** 查看处理结果(已处理数据,只读展示) */
     handleView(row) {
-      this.viewForm = {
-        recordPath: row.recordPath || '',
-        contentList: row.contentList || '',
-        callerNum: row.callerNum || '',
-        roboticName: row.roboticName || '',
-        callTime: row.callTime || 0,
-        manualAnsweredTimeLen: row.manualAnsweredTimeLen || 0
-      };
+      this.viewForm = {};
       this.viewDialogVisible = true;
       this.viewLoading = true;
       getCrmCustomerByLogId(row.logId).then(response => {
         if (response.code === 200 && response.data) {
-          // 合并CRM客户信息(保留通话相关字段)
-          this.viewForm = { ...this.viewForm, ...response.data };
+          this.viewForm = response.data;
+        } else {
+          this.viewForm = {};
         }
         this.viewLoading = false;
       }).catch(() => {
+        this.viewForm = {};
         this.viewLoading = false;
       });
     },
@@ -376,126 +301,7 @@ export default {
     msToSeconds(ms) {
       if (!ms || ms <= 0) return 0;
       return Math.ceil(ms / 1000);
-    },
-    /** 录音URL处理(统一走代理转发,避免跨域) */
-    handleRecordPath(url) {
-      if (!url) return '';
-      let fullUrl = '';
-      if (url.startsWith('http')) {
-        fullUrl = url;
-      } else {
-        fullUrl = 'http://129.28.164.235:8899/recordings/files?filename=' + url;
-      }
-      return process.env.VUE_APP_BASE_API + '/common/proxy/recording?url=' + encodeURIComponent(fullUrl);
-    },
-    /** 解析聊天记录(contentList JSON字符串) */
-    parseContentList(content) {
-      if (!content) return [];
-      try {
-        const parsed = typeof content === 'string' ? JSON.parse(content) : content;
-        if (!Array.isArray(parsed)) return [];
-        return parsed.filter(item => {
-          if (!item) return false;
-          if (item.role === 'system') return false;
-          const text = item.content || '';
-          if (!String(text).trim()) return false;
-          return true;
-        });
-      } catch (e) {
-        return [];
-      }
     }
   }
 };
 </script>
-
-<style scoped>
-.manual-view-dialog .view-info-form {
-  background: #fafafa;
-  padding: 12px 16px 0 16px;
-  border-radius: 6px;
-  margin-bottom: 16px;
-}
-.view-section {
-  margin-top: 16px;
-  border: 1px solid #e8e8e8;
-  border-radius: 6px;
-  overflow: hidden;
-}
-.view-section-title {
-  padding: 10px 16px;
-  background: #f5f7fa;
-  font-size: 14px;
-  font-weight: 600;
-  color: #303133;
-  border-bottom: 1px solid #e8e8e8;
-  display: flex;
-  align-items: center;
-  gap: 6px;
-}
-.view-section-title i {
-  font-size: 16px;
-  color: #409EFF;
-}
-.view-section-body {
-  padding: 16px;
-  background: #fff;
-}
-.audio-player {
-  width: 100%;
-  outline: none;
-}
-.empty-tip {
-  text-align: center;
-  color: #909399;
-  font-size: 13px;
-  padding: 20px 0;
-}
-.chat-container {
-  max-height: 360px;
-  overflow-y: auto;
-  padding: 8px 0;
-}
-.chat-item {
-  display: flex;
-  margin-bottom: 14px;
-}
-.chat-item.chat-left {
-  justify-content: flex-start;
-}
-.chat-item.chat-right {
-  justify-content: flex-end;
-}
-.chat-bubble-wrapper {
-  max-width: 70%;
-  display: flex;
-  flex-direction: column;
-}
-.chat-item.chat-right .chat-bubble-wrapper {
-  align-items: flex-end;
-}
-.chat-role {
-  font-size: 12px;
-  color: #909399;
-  margin-bottom: 4px;
-  padding: 0 4px;
-}
-.chat-bubble {
-  padding: 10px 14px;
-  border-radius: 8px;
-  font-size: 14px;
-  line-height: 1.6;
-  word-break: break-word;
-  white-space: pre-wrap;
-}
-.chat-item.chat-left .chat-bubble {
-  background: #f0f2f5;
-  color: #303133;
-  border-top-left-radius: 2px;
-}
-.chat-item.chat-right .chat-bubble {
-  background: #95ec69;
-  color: #303133;
-  border-top-right-radius: 2px;
-}
-</style>

+ 68 - 33
src/views/company/companyVoiceRobotic/index.vue

@@ -855,11 +855,26 @@
     <el-drawer size="75%" title="客户详情" :visible.sync="customerDetailShow" append-to-body>
       <customer-details ref="customerDetails" />
     </el-drawer>
-    <manual-call-dialog
-      ref="manualCallDialog"
-      :use-floating-phone="true"
+    <el-dialog
+      :title="manualCallDialog.title"
+      :visible.sync="manualCallDialog.visible"
+      width="1500px"
+      append-to-body
+      destroy-on-close
+      class="manual-call-dialog"
       @close="handleManualCallDialogClose"
-    />
+    >
+      <call-center-phone-bar
+          :key="manualCallDialog.key"
+          ref="callCenterPhoneBar"
+          :init-phone-number="manualCallDialog.phone"
+          :robotic-id="manualCallDialog.roboticId"
+          :company-id="manualCallDialog.companyId"
+          :company-user-id="manualCallDialog.companyUserId"
+          :workflow-instance-id="manualCallDialog.workflowInstanceId"
+          :customer-id="manualCallDialog.customerId"
+      />
+    </el-dialog>
 
     <el-dialog
       title="对话内容"
@@ -940,12 +955,12 @@ import { queryPhone } from "@/api/crm/customer";
 import {getDicts} from "@/api/system/dict/data";
 import { optionList, getWorkflowNodeTypeCodes } from '@/api/company/companyWorkflow'
 import {wxListQw} from "../../../api/company/companyVoiceRobotic";
-import ManualCallDialog from '@/components/ManualCallDialog/index.vue'
+import CallCenterPhoneBar from '../../aiSipCall/aiSipCallManualOutbound.vue'
 import AiTagPanel from "../../crm/components/AiTagPanel.vue";
 
 export default {
   name: "Robotic",
-  components: {AiTagPanel, draggable, customerDetails, customerSelect, appendCustomerSelect, qwUserSelect,qwUserSelectTwo,ManualCallDialog},
+  components: {AiTagPanel, draggable, customerDetails, customerSelect, appendCustomerSelect, qwUserSelect,qwUserSelectTwo,CallCenterPhoneBar},
   data() {
     return {
       submitFormLoading:false,
@@ -1079,6 +1094,18 @@ export default {
         encryptPhone: '',
         onlyCallNode: false
       },
+      manualCallDialog: {
+        visible: false,
+        phone: '',
+        title: "人工外呼",
+        record: null,
+        key: 0,
+        roboticId: null,
+        companyId: null,
+        companyUserId: null,
+        workflowInstanceId: null,
+        customerId: null
+      },
       contentDialog: {
         visible: false,
         content: '',
@@ -1787,11 +1814,18 @@ export default {
           contentList: item.contentList || ''
         }))
         this.execLogs.total = res.total || 0
-        this.execLogs.stats = res.stats || {
-          callDone: 0,
-          addWxDone: 0,
-          sendMsgDone: 0
+        // this.execLogs.stats = res.stats || {
+        //   callDone: 0,
+        //   addWxDone: 0,
+        //   sendMsgDone: 0
+        // }
+        // 前端计算统计数据
+        this.execLogs.stats = {
+          callDone: this.execLogs.list.reduce((sum, r) => sum + (Number(r.callPhoneDone) || 0), 0),
+          addWxDone: this.execLogs.list.reduce((sum, r) => sum + (Number(r.addWxDone) || 0), 0),
+          sendMsgDone: this.execLogs.list.reduce((sum, r) => sum + (Number(r.sendMsgDone) || 0), 0)
         }
+
       } catch (e) {
         this.$message.error("获取执行日志失败")
       } finally {
@@ -1824,36 +1858,37 @@ export default {
       this.getExecLogs()
     },
     handleManualCall(record) {
-      const customerId = record.customerId;
-      if (!customerId) {
-        this.$message.warning('客户ID不存在,无法获取手机号');
-        return;
+      const phone = record.customerPhone || '';
+
+      if (!phone) {
+          this.$message.warning('当前客户没有可外呼的手机号');
+          return;
       }
 
+      this.manualCallDialog.record = record;
+      this.manualCallDialog.phone = phone;
+      this.manualCallDialog.title = `人工外呼 - ${record.customerName || '未知客户'}`;
+
       const currentTask = this.roboticList.find(
         item => item.id === this.execLogs.currentTaskId
       );
-
-      queryPhone(customerId).then(response => {
-        const phone = response.mobile || '';
-        if (!phone) {
-          this.$message.warning('当前客户没有可外呼的手机号');
-          return;
-        }
-        this.$refs.manualCallDialog.open({
-          phone,
-          customerName: record.customerName || '未知客户',
-          roboticId: record.roboticId || null,
-          companyId: (currentTask && currentTask.companyId) || null,
-          companyUserId: (currentTask && currentTask.companyUserId) || null,
-          workflowInstanceId: record.workflowInstanceId || null,
-          customerId: customerId
-        });
-      }).catch(() => {
-        this.$message.error('获取手机号失败');
-      });
+      // 带过去的额外参数
+      this.manualCallDialog.roboticId = record.roboticId || null;
+      this.manualCallDialog.companyId = (currentTask && currentTask.companyId) || null;
+      this.manualCallDialog.companyUserId = (currentTask && currentTask.companyUserId) || null;
+      this.manualCallDialog.workflowInstanceId = record.workflowInstanceId || null;
+      this.manualCallDialog.customerId = record.customerId || null;
+      this.manualCallDialog.key += 1;
+      this.manualCallDialog.visible = true;
     },
     handleManualCallDialogClose() {
+      this.manualCallDialog.phone = '';
+      this.manualCallDialog.record = null;
+      this.manualCallDialog.roboticId = null;
+      this.manualCallDialog.companyId = null;
+      this.manualCallDialog.companyUserId = null;
+      this.manualCallDialog.workflowInstanceId = null;
+      this.manualCallDialog.customerId = null;
     },
     handleShowContent(record,log) {
       this.contentDialog.customerName = record.customerName || '';

+ 67 - 33
src/views/company/companyVoiceRobotic/myIndex.vue

@@ -838,11 +838,26 @@
     <el-drawer size="75%" title="客户详情" :visible.sync="customerDetailShow" append-to-body>
       <customer-details ref="customerDetails" />
     </el-drawer>
-    <manual-call-dialog
-      ref="manualCallDialog"
-      :use-floating-phone="true"
+    <el-dialog
+      :title="manualCallDialog.title"
+      :visible.sync="manualCallDialog.visible"
+      width="1500px"
+      append-to-body
+      destroy-on-close
+      class="manual-call-dialog"
       @close="handleManualCallDialogClose"
-    />
+    >
+      <call-center-phone-bar
+          :key="manualCallDialog.key"
+          ref="callCenterPhoneBar"
+          :init-phone-number="manualCallDialog.phone"
+          :robotic-id="manualCallDialog.roboticId"
+          :company-id="manualCallDialog.companyId"
+          :company-user-id="manualCallDialog.companyUserId"
+          :workflow-instance-id="manualCallDialog.workflowInstanceId"
+          :customer-id="manualCallDialog.customerId"
+      />
+    </el-dialog>
 
     <el-dialog
       title="对话内容"
@@ -918,12 +933,12 @@ import { queryPhone } from "@/api/crm/customer";
 import {getDicts} from "@/api/system/dict/data";
 import { optionList, getWorkflowNodeTypeCodes } from '@/api/company/companyWorkflow'
 import {wxListQw} from "../../../api/company/companyVoiceRobotic";
-import ManualCallDialog from '@/components/ManualCallDialog/index.vue'
+import CallCenterPhoneBar from '../../aiSipCall/aiSipCallManualOutbound.vue'
 import AiTagPanel from "../../crm/components/AiTagPanel.vue";
 
 export default {
   name: "MyRobotic",
-  components: {AiTagPanel, draggable, customerDetails, customerSelect, qwUserSelect,qwUserSelectTwo,ManualCallDialog},
+  components: {AiTagPanel, draggable, customerDetails, customerSelect, qwUserSelect,qwUserSelectTwo,CallCenterPhoneBar},
   data() {
     return {
       submitFormLoading:false,
@@ -1055,6 +1070,18 @@ export default {
         customerPhone: '',
         onlyCallNode: false
       },
+      manualCallDialog: {
+        visible: false,
+        phone: '',
+        title: "人工外呼",
+        record: null,
+        key: 0,
+        roboticId: null,
+        companyId: null,
+        companyUserId: null,
+        workflowInstanceId: null,
+        customerId: null
+      },
       contentDialog: {
         visible: false,
         content: '',
@@ -1679,10 +1706,16 @@ export default {
           contentList: item.contentList || ''
         }))
         this.execLogs.total = res.total || 0
-        this.execLogs.stats = res.stats || {
-          callDone: 0,
-          addWxDone: 0,
-          sendMsgDone: 0
+        // this.execLogs.stats = res.stats || {
+        //   callDone: 0,
+        //   addWxDone: 0,
+        //   sendMsgDone: 0
+        // }
+        // 前端计算统计数据
+        this.execLogs.stats = {
+          callDone: this.execLogs.list.reduce((sum, r) => sum + (Number(r.callPhoneDone) || 0), 0),
+          addWxDone: this.execLogs.list.reduce((sum, r) => sum + (Number(r.addWxDone) || 0), 0),
+          sendMsgDone: this.execLogs.list.reduce((sum, r) => sum + (Number(r.sendMsgDone) || 0), 0)
         }
 
       } catch (e) {
@@ -1716,36 +1749,37 @@ export default {
       this.getExecLogs()
     },
     handleManualCall(record) {
-      const customerId = record.customerId;
-      if (!customerId) {
-        this.$message.warning('客户ID不存在,无法获取手机号');
-        return;
+      const phone = record.customerPhone || '';
+
+      if (!phone) {
+          this.$message.warning('当前客户没有可外呼的手机号');
+          return;
       }
 
+      this.manualCallDialog.record = record;
+      this.manualCallDialog.phone = phone;
+      this.manualCallDialog.title = `人工外呼 - ${record.customerName || '未知客户'}`;
+
       const currentTask = this.roboticList.find(
         item => item.id === this.execLogs.currentTaskId
       );
-
-      queryPhone(customerId).then(response => {
-        const phone = response.mobile || '';
-        if (!phone) {
-          this.$message.warning('当前客户没有可外呼的手机号');
-          return;
-        }
-        this.$refs.manualCallDialog.open({
-          phone,
-          customerName: record.customerName || '未知客户',
-          roboticId: record.roboticId || null,
-          companyId: (currentTask && currentTask.companyId) || null,
-          companyUserId: (currentTask && currentTask.companyUserId) || null,
-          workflowInstanceId: record.workflowInstanceId || null,
-          customerId: customerId
-        });
-      }).catch(() => {
-        this.$message.error('获取手机号失败');
-      });
+      // 带过去的额外参数
+      this.manualCallDialog.roboticId = record.roboticId || null;
+      this.manualCallDialog.companyId = (currentTask && currentTask.companyId) || null;
+      this.manualCallDialog.companyUserId = (currentTask && currentTask.companyUserId) || null;
+      this.manualCallDialog.workflowInstanceId = record.workflowInstanceId || null;
+      this.manualCallDialog.customerId = record.customerId || null;
+      this.manualCallDialog.key += 1;
+      this.manualCallDialog.visible = true;
     },
     handleManualCallDialogClose() {
+      this.manualCallDialog.phone = '';
+      this.manualCallDialog.record = null;
+      this.manualCallDialog.roboticId = null;
+      this.manualCallDialog.companyId = null;
+      this.manualCallDialog.companyUserId = null;
+      this.manualCallDialog.workflowInstanceId = null;
+      this.manualCallDialog.customerId = null;
     },
     handleShowContent(record,log) {
       this.contentDialog.customerName = record.customerName || '';

+ 1 - 32
src/views/company/wxAccount/index.vue

@@ -87,12 +87,7 @@
       <el-table-column label="id" align="center" prop="id" />
       <el-table-column label="微信昵称" align="center" prop="wxNickName" />
       <el-table-column label="微信号" align="center" prop="wxNo" />
-      <el-table-column label="手机号" align="center" prop="phone">
-        <template slot-scope="scope">
-          {{ scope.row.phone }}
-          <el-button v-if="scope.row.phone" type="text" size="mini" @click="handleManualCall(scope.row)">拨打</el-button>
-        </template>
-      </el-table-column>
+      <el-table-column label="手机号" align="center" prop="phone" />
       <el-table-column label="员工" align="center" prop="companyUserName" />
       <el-table-column label="微信备注前缀" align="center" prop="wxRemark" />
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
@@ -153,24 +148,16 @@
         <el-button @click="cancel">取 消</el-button>
       </div>
     </el-dialog>
-    <!--人工外呼弹窗-->
-    <manual-call-dialog
-      ref="manualCallDialog"
-      :use-floating-phone="true"
-      @close="handleManualCallDialogClose"
-    />
   </div>
 </template>
 
 <script>
 import { listCompanyAccount, getCompanyAccount, delCompanyAccount, addCompanyAccount, updateCompanyAccount, exportCompanyAccount, companyListAll, syncWx } from "@/api/company/companyAccount";
 import {getAllUserlist} from "@/api/company/companyUser";
-import ManualCallDialog from '@/components/ManualCallDialog/index.vue';
 
 
 export default {
   name: "CompanyAccount",
-  components: { ManualCallDialog },
   data() {
     return {
       // 遮罩层
@@ -356,24 +343,6 @@ export default {
           }
         }).catch(function() {});
     },
-    /** 手动外呼 */
-    handleManualCall(row) {
-      const phone = row.phone || '';
-      if (!phone) {
-        this.$message.warning('当前账号没有可外呼的手机号');
-        return;
-      }
-      this.$refs.manualCallDialog.open({
-        phone,
-        customerName: row.wxNickName || '未知',
-        customerId: null,
-        companyId: null,
-        companyUserId: row.companyUserId || null
-      });
-    },
-    handleManualCallDialogClose() {
-      this.getList();
-    },
   }
 };
 </script>

+ 22 - 2
src/views/crm/components/AppendCustomerSelect.vue

@@ -122,7 +122,19 @@
         <el-table-column label="所属公司" align="center" prop="companyName" width="220"/>
         <!-- <el-table-column label="客户编码" align="center" prop="customerCode" width="150"/> -->
         <el-table-column label="客户名称" align="center" prop="customerName" width="110"/>
-        <el-table-column label="手机" align="center" prop="mobile" width="110"/>
+        <el-table-column label="手机" align="center" prop="mobile" width="160">
+          <template slot-scope="scope">
+            <span>{{ scope.row.mobile }}</span>
+            <el-button
+              v-if="scope.row.customerId"
+              type="text"
+              v-hasPermi="['crm:customer:queryPhone']"
+              size="mini"
+              icon="el-icon-view"
+              @click="handleQueryPhone(scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
         <el-table-column label="性别" align="center" prop="sex" width="55">
           <template slot-scope="scope">
             <el-tag prop="sex" v-for="(item, index) in sexOptions" :key="'sex'+index" v-if="scope.row.sex==item.dictValue">{{item.dictLabel}}</el-tag>
@@ -220,7 +232,7 @@
 </template>
 
 <script>
-import { listCustomerAll,listNoPage } from "@/api/crm/customer";
+import { listCustomerAll,listNoPage, queryPhone } from "@/api/crm/customer";
 import { getCompanyList } from "@/api/company/company";
 import customerDetails from '@/views/crm/components/customerDetails.vue';
 import {getCitys} from "@/api/store/city";
@@ -347,6 +359,14 @@ export default {
     this.getList();
   },
   methods: {
+    handleQueryPhone(row) {
+      queryPhone(row.customerId).then(response => {
+        this.$alert(response.mobile, '手机号', {
+          confirmButtonText: '确定',
+          callback: action => {}
+        });
+      });
+    },
     isExcluded(customerId) {
       return this._excludeCustomerIds.includes(customerId);
     },

+ 22 - 2
src/views/crm/components/CustomerSelect.vue

@@ -123,7 +123,19 @@
         <el-table-column label="所属公司" align="center" prop="companyName" width="220"/>
         <!-- <el-table-column label="客户编码" align="center" prop="customerCode" width="150"/> -->
         <el-table-column label="客户名称" align="center" prop="customerName" width="110"/>
-        <el-table-column label="手机" align="center" prop="mobile" width="110"/>
+        <el-table-column label="手机" align="center" prop="mobile" width="160">
+          <template slot-scope="scope">
+            <span>{{ scope.row.mobile }}</span>
+            <el-button
+              v-if="scope.row.customerId"
+              type="text"
+              v-hasPermi="['crm:customer:queryPhone']"
+              size="mini"
+              icon="el-icon-view"
+              @click="handleQueryPhone(scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
         <el-table-column label="性别" align="center" prop="sex" width="55">
           <template slot-scope="scope">
             <el-tag prop="sex" v-for="(item, index) in sexOptions" :key="'sex'+index" v-if="scope.row.sex==item.dictValue">{{item.dictLabel}}</el-tag>
@@ -221,7 +233,7 @@
 </template>
 
 <script>
-import { listCustomerAll,listNoPage } from "@/api/crm/customer";
+import { listCustomerAll,listNoPage, queryPhone } from "@/api/crm/customer";
 import { getCompanyList } from "@/api/company/company";
 import customerDetails from '@/views/crm/components/customerDetails.vue';
 import editCustomerSource from '@/views/crm/components/editSource.vue';
@@ -365,6 +377,14 @@ export default {
     this.getList();
   },
   methods: {
+    handleQueryPhone(row) {
+      queryPhone(row.customerId).then(response => {
+        this.$alert(response.mobile, '手机号', {
+          confirmButtonText: '确定',
+          callback: action => {}
+        });
+      });
+    },
     setRows(rows){
       this.shows = true;
       this.rows = rows || [];

+ 6 - 2
src/views/crm/components/addOrEditCustomer.vue

@@ -352,8 +352,12 @@
                 });
             },
             desensitizePhone(phone) {
-                if (!phone || phone.length < 7) return phone;
-                return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4);
+                if (!phone) return phone;
+                const text = String(phone);
+                if (/^1\d{10}$/.test(text)) {
+                    return text.substring(0, 3) + '****' + text.substring(text.length - 4);
+                }
+                return text;
             },
             /** 提交按钮 */
             submitForm() {

+ 5 - 5
src/views/crm/components/addVisitStatus.vue

@@ -17,7 +17,7 @@
             </div>
     </div>
 </template>
-  
+
 <script>
     import { updateCustomer, getTradeDicts  } from "@/api/crm/customer";
     export default {
@@ -31,12 +31,12 @@
                 },
                 // 表单校验
                 rules: {
-                  
+
                     visitStatus: [
                         { required: true, message: "跟进阶段不能为空", trigger: "change" }
                     ],
                 }
-                 
+
             };
         },
         created() {
@@ -54,6 +54,7 @@
             submitForm() {
                 this.$refs["form"].validate(valid => {
                 if (valid) {
+                  this.form.visitTime = new Date().toLocaleString().replaceAll("/", "-");
                     updateCustomer(this.form).then(response => {
                         if (response.code === 200) {
                             this.msgSuccess("提交成功");
@@ -71,7 +72,7 @@
     height: 100%;
     background-color: #fff;
     padding: 20px;
-        
+
 }
 .footer{
     display: flex;
@@ -97,4 +98,3 @@
 
 
 
- 

+ 21 - 2
src/views/crm/components/customerAssignList.vue

@@ -67,7 +67,19 @@
       <el-dialog :title="customer.title" :visible.sync="customer.open" width="1000px" append-to-body>
           <el-table border  :data="customers" >
               <el-table-column label="客户名称" align="center" prop="customerName" />
-              <el-table-column label="客户手机号" align="center" prop="mobile" />
+              <el-table-column label="客户手机号" align="center" prop="mobile" width="180">
+                <template slot-scope="scope">
+                  <span>{{ scope.row.mobile }}</span>
+                  <el-button
+                    v-if="scope.row.customerId"
+                    type="text"
+                    v-hasPermi="['crm:customer:queryPhone']"
+                    size="mini"
+                    icon="el-icon-view"
+                    @click="handleQueryPhone(scope.row)"
+                  >查看</el-button>
+                </template>
+              </el-table-column>
           </el-table>
       </el-dialog>
     </div>
@@ -76,7 +88,7 @@
   <script>
 
   import { listCustomerAssign,   cancelCustomerAssign } from "@/api/crm/customerAssign";
-  import { getCustomerListByIds } from "@/api/crm/customer";
+  import { getCustomerListByIds, queryPhone } from "@/api/crm/customer";
   export default {
     name: "CustomerAssign",
     data() {
@@ -121,6 +133,13 @@
     created() {
     },
     methods: {
+        handleQueryPhone(row) {
+            queryPhone(row.customerId).then(response => {
+                this.$alert(response.mobile, '手机号', {
+                    confirmButtonText: '确定'
+                });
+            });
+        },
         handleShow(item){
           this.getCustomerListByIds(item.customerIds)
         },

+ 20 - 7
src/views/crm/components/customerCallLogList.vue

@@ -1,9 +1,17 @@
 <template>
     <div>
         <el-table border v-loading="loading" :data="list">
-            <el-table-column label="客户号码" align="center" prop="callerNum" width="140">
+            <el-table-column label="客户号码" align="center" prop="callerNum" width="180">
                 <template slot-scope="scope">
-                    <span>{{ desensitizePhone(scope.row.callerNum) }}</span>
+                    <span>{{ maskMobileForDisplay(scope.row.callerNum) }}</span>
+                    <el-button
+                      v-if="scope.row.customerId"
+                      type="text"
+                      v-hasPermi="['crm:customer:queryPhone']"
+                      size="mini"
+                      icon="el-icon-view"
+                      @click="handleQueryPhone(scope.row)"
+                    >查看</el-button>
                 </template>
             </el-table-column>
             <el-table-column label="坐席号码" align="center" prop="calleeNum" width="140" />
@@ -95,7 +103,8 @@
 
 <script>
 import { listCustomerCallLog } from "@/api/crm/customerCallLog";
-import { parseTime } from "@/utils/common";
+import { queryPhone } from "@/api/crm/customer";
+import { parseTime, maskMobileForDisplay } from "@/utils/common";
 import { checkPermi } from "@/utils/permission";
 
 export default {
@@ -129,11 +138,8 @@ export default {
     },
     methods: {
         parseTime,
+        maskMobileForDisplay,
         checkPermi,
-        desensitizePhone(phone) {
-            if (!phone || phone.length < 7) return phone;
-            return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4);
-        },
         formatAnswerTime(answerTime, callCreateTime) {
             if (!answerTime || answerTime === 0 || answerTime === '0' || answerTime === '') return '';
             // 如果接通时间早于呼叫时间,说明是无效数据
@@ -162,6 +168,13 @@ export default {
             this.contentDialog.content = row.contentList || '';
             this.contentDialog.visible = true;
         },
+        handleQueryPhone(row) {
+            queryPhone(row.customerId).then(response => {
+                this.$alert(response.mobile, '手机号', {
+                    confirmButtonText: '确定'
+                });
+            });
+        },
         parseContentList(content) {
             if (!content) return [];
             try {

+ 21 - 1
src/views/crm/components/customerContacts.vue

@@ -36,7 +36,19 @@
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="ID" align="center" prop="contactsId" />
         <el-table-column label="姓名" align="center" prop="name" />
-        <el-table-column label="手机" align="center" prop="mobile" />
+        <el-table-column label="手机" align="center" prop="mobile" width="180">
+          <template slot-scope="scope">
+            <span>{{ scope.row.mobile }}</span>
+            <el-button
+              v-if="scope.row.customerId"
+              type="text"
+              v-hasPermi="['crm:customer:queryPhone']"
+              size="mini"
+              icon="el-icon-view"
+              @click="handleQueryPhone(scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
         <el-table-column label="备注" align="center" prop="remark" />
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template slot-scope="scope">
@@ -95,6 +107,7 @@
   <script>
   import addSms from './addSms.vue';
   import { listCustomerContacts, getCustomerContacts, delCustomerContacts, addCustomerContacts, updateCustomerContacts, exportCustomerContacts } from "@/api/crm/customerContacts";
+  import { queryPhone } from "@/api/crm/customer";
   export default {
     components: {addSms },
     name: "CustomerContacts",
@@ -157,6 +170,13 @@
      
     },
     methods: {
+      handleQueryPhone(row) {
+        queryPhone(row.customerId).then(response => {
+          this.$alert(response.mobile, '手机号', {
+            confirmButtonText: '确定'
+          });
+        });
+      },
         closeSms(){
             this.addSms.open=false;
             this.getDetails(this.customerId)

+ 6 - 2
src/views/crm/components/customerDetails.vue

@@ -675,8 +675,12 @@ export default {
             });
         },
         desensitizePhone(phone) {
-            if (!phone || phone.length < 7) return phone;
-            return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4);
+            if (!phone) return phone;
+            const text = String(phone);
+            if (/^1\d{10}$/.test(text)) {
+                return text.substring(0, 3) + '****' + text.substring(text.length - 4);
+            }
+            return text;
         },
         // 打开历史沟通弹窗(数据复用 item.historicalCommunication,无需额外请求)
         handleViewHistory() {

+ 21 - 1
src/views/crm/components/customerSmsLogsList.vue

@@ -3,7 +3,19 @@
         <el-table border v-loading="loading" :data="list" >
             <el-table-column label="ID" align="center" prop="logsId" />
             <el-table-column label="模板CODE" align="center" prop="tempCode" />
-            <el-table-column label="手机号" align="center" prop="phone" />
+            <el-table-column label="手机号" align="center" prop="phone" width="180">
+              <template slot-scope="scope">
+                <span>{{ scope.row.phone }}</span>
+                <el-button
+                  v-if="queryParams.customerId"
+                  type="text"
+                  v-hasPermi="['crm:customer:queryPhone']"
+                  size="mini"
+                  icon="el-icon-view"
+                  @click="handleQueryPhone"
+                >查看</el-button>
+              </template>
+            </el-table-column>
             <el-table-column label="短信内容" show-overflow-tooltip align="center" prop="content" />
             <el-table-column label="提交时间" align="center" prop="createTime" width="180">
             </el-table-column>
@@ -32,6 +44,7 @@
   
 <script>
 import { listCompanySmsLogs, getCompanySmsLogs, delCompanySmsLogs, addCompanySmsLogs, updateCompanySmsLogs, exportCompanySmsLogs } from "@/api/company/companySmsLogs";
+import { queryPhone } from "@/api/crm/customer";
 
     export default {
         name: "customerVisit",
@@ -57,6 +70,13 @@ import { listCompanySmsLogs, getCompanySmsLogs, delCompanySmsLogs, addCompanySms
             });
         },
         methods: {
+            handleQueryPhone() {
+                queryPhone(this.queryParams.customerId).then(response => {
+                    this.$alert(response.mobile, '手机号', {
+                        confirmButtonText: '确定'
+                    });
+                });
+            },
             getData(customerId){
                 this.queryParams.customerId=customerId;
                 this.queryParams.pageNum=1;

+ 9 - 3
src/views/crm/customer/assist.vue

@@ -181,8 +181,14 @@
       <el-table-column  label="手机" width="180px"  align="center" prop="mobile"   >
         <template slot-scope="scope">
           {{scope.row.mobile}}
-          <el-button type="text" v-hasPermi="['crm:customer:queryPhone']" size="mini"
-                     @click="handleQueryPhone(scope.row)" icon="el-icon-view">查看</el-button>
+          <el-button
+            v-if="scope.row.customerId"
+            type="text"
+            v-hasPermi="['crm:customer:queryPhone']"
+            size="mini"
+            @click="handleQueryPhone(scope.row)"
+            icon="el-icon-view"
+          >查看</el-button>
           <el-button type="text"    size="mini" @click="callNumber(scope.row.customerId,null)">拨号</el-button>
           <el-button v-hasPermi="['crm:customer:addVisit']"  type="text" size="mini" @click="handleAddVisit(scope.row)">写跟进</el-button>
           <el-button type="text"    size="mini" @click="addPackageOrder(scope.row)" style="margin-right: 15px;">创建订单</el-button>
@@ -226,7 +232,7 @@
         </template>
       </el-table-column>
       <el-table-column label="进线客户详情" align="center" :show-overflow-tooltip="true" prop="registerDesc" />
-      <el-table-column label="最新跟进时间" align="center" prop="lastTime" />
+      <el-table-column label="最新跟进时间" align="center" prop="visitTime" />
       <el-table-column label="领取时间" align="center" prop="receiveTime" />
       <el-table-column label="进线客户提交日期" align="center" prop="registerSubmitTime" />
       <el-table-column label="创建时间" align="center" prop="customerCreateTime" width="180">

+ 25 - 14
src/views/crm/customer/customerAll.vue

@@ -136,7 +136,19 @@
             <el-link @click="handleShow(scope.row)" :underline="false" type="primary">{{ scope.row.isDuplicate==1?getCustomerName(scope.row.customerName)+"[从]":getCustomerName(scope.row.customerName) }}</el-link>
           </template>
         </el-table-column>
-        <el-table-column label="手机" align="center" prop="mobile" width="120" />
+        <el-table-column label="手机" align="center" prop="mobile" width="180">
+          <template slot-scope="scope">
+            <span>{{ scope.row.mobile }}</span>
+            <el-button
+              v-if="scope.row.customerId"
+              type="text"
+              v-hasPermi="['crm:customer:queryPhone']"
+              size="mini"
+              icon="el-icon-view"
+              @click="handleQueryPhone(scope.row)"
+            >查看</el-button>
+          </template>
+        </el-table-column>
         <el-table-column label="客户来源" align="center" prop="source" width="90">
           <template slot-scope="scope">
               <el-tag prop="status" v-for="(item, index) in sourceOptions" v-if="scope.row.source==item.dictValue">{{item.dictLabel}}</el-tag>
@@ -157,6 +169,11 @@
         <el-table-column label="进线客户详情" align="center" :show-overflow-tooltip="true" prop="registerDesc" min-width="120" />
         <el-table-column label="分配销售" align="center" prop="companyUserNickName" :show-overflow-tooltip="true" width="90" />
         <el-table-column label="领取时间" align="center" prop="startTime" width="135" />
+        <el-table-column label="最新外呼时间" align="center" prop="latestManualCallTime" width="165">
+          <template slot-scope="scope">
+            <span>{{ scope.row.latestManualCallTime || '-' }}</span>
+          </template>
+        </el-table-column>
         <el-table-column label="提交日期" align="center" prop="registerSubmitTime" width="110" />
         <el-table-column label="创建时间" align="center" prop="createTime" width="135" />
         <el-table-column label="最后跟进时间" align="center" prop="receiveTime" width="135" />
@@ -164,7 +181,6 @@
         <el-table-column label="操作" align="center" fixed="right" width="220">
           <template slot-scope="scope">
             <el-button size="mini" type="text" icon="el-icon-phone-outline" @click="handleViewCallLog(scope.row)">外呼记录</el-button>
-            <el-button size="mini" type="text" icon="el-icon-view" @click="handleViewPhone(scope.row)">查看手机号</el-button>
             <el-button
               v-if="scope.row.customerUserId"
               size="mini"
@@ -198,7 +214,7 @@
 
 <script>
 import { getCustomerAllList, recoverAll, batchRecoverAll } from "@/api/crm/customerAll";
-import { getMyCustomerPhone,getTradeDicts } from "@/api/crm/customer";
+import { queryPhone, getTradeDicts } from "@/api/crm/customer";
 import customerDetails from '../components/customerDetails.vue';
 import customerCallLogList from '../components/customerCallLogList.vue';
 
@@ -288,17 +304,12 @@ export default {
         that.$refs.customerCallLogList.getData(row.customerId);
       }, 200);
     },
-    handleViewPhone(row) {
-      getMyCustomerPhone(row.customerId).then(res => {
-        if (res.msg) {
-          this.$alert(res.msg, '手机号码', {
-            confirmButtonText: '确定'
-          });
-        } else {
-          this.$message.warning('获取手机号失败');
-        }
-      }).catch(() => {
-        this.$message.error('获取手机号失败');
+    handleQueryPhone(row) {
+      queryPhone(row.customerId).then(response => {
+        this.$alert(response.mobile, '手机号', {
+          confirmButtonText: '确定',
+          callback: action => {}
+        });
       });
     },
     getList() {

+ 18 - 2
src/views/crm/customer/full.vue

@@ -166,9 +166,17 @@
               <el-link @click="handleShow(scope.row)" :underline="false" type="primary" >{{scope.row.customerName}}</el-link>
             </template>
           </el-table-column>
-          <el-table-column  label="手机" width="120px"  align="center" prop="mobile"   >
+          <el-table-column  label="手机" width="180px"  align="center" prop="mobile"   >
             <template slot-scope="scope">
               {{scope.row.mobile}}
+              <el-button
+                v-if="scope.row.customerId"
+                type="text"
+                v-hasPermi="['crm:customer:queryPhone']"
+                size="mini"
+                icon="el-icon-view"
+                @click="handleQueryPhone(scope.row)"
+              >查看</el-button>
             </template>
           </el-table-column>
           <el-table-column  label="客户来源" align="center" prop="source">
@@ -254,7 +262,7 @@
 
 <script>
 
-import { assignToUser,receive,getFullCustomerList,addCustomer,updateCustomer,getCustomerDetails,exportCustomer  } from "@/api/crm/customer";
+import { assignToUser,receive,getFullCustomerList,addCustomer,updateCustomer,getCustomerDetails,exportCustomer,queryPhone  } from "@/api/crm/customer";
 import customerDetails from '../components/customerDetails.vue';
 import {getCitys} from "@/api/store/city";
 import { treeselect } from "@/api/company/companyDept";
@@ -425,6 +433,14 @@ export default {
     this.getList();
   },
   methods: {
+    handleQueryPhone(row) {
+      queryPhone(row.customerId).then(response => {
+        this.$alert(response.mobile, '手机号', {
+          confirmButtonText: '确定',
+          callback: action => {}
+        });
+      });
+    },
     handleShow(row){
       this.show.open=true;
       var that=this;

+ 8 - 2
src/views/crm/customer/index.vue

@@ -249,8 +249,14 @@
           <el-table-column  label="手机" width="150px"  align="center" prop="mobile"   >
             <template slot-scope="scope">
               {{scope.row.mobile}}
-              <el-button type="text" v-hasPermi="['crm:customer:queryPhone']" size="mini"
-                         @click="handleQueryPhone(scope.row)" icon="el-icon-view">查看</el-button>
+              <el-button
+                v-if="scope.row.customerId"
+                type="text"
+                v-hasPermi="['crm:customer:queryPhone']"
+                size="mini"
+                @click="handleQueryPhone(scope.row)"
+                icon="el-icon-view"
+              >查看</el-button>
             </template>
           </el-table-column>
           <el-table-column  label="客户来源" align="center" prop="source">

+ 3 - 2
src/views/crm/customer/line.vue

@@ -179,14 +179,15 @@
         <el-table-column  label="手机" width="150px"  align="center" prop="mobile"   >
           <template slot-scope="scope">
             <span>{{scope.row.mobile}}</span>
-           <!-- <el-button
+            <el-button
+              v-if="scope.row.customerId"
               type="text"
               icon="el-icon-view"
               size="mini"
               v-hasPermi="['crm:customer:queryPhone']"
               @click="handleQueryPhone(scope.row)"
               style="margin-left: 5px;"
-            >查看</el-button> -->
+            >查看</el-button>
           </template>
         </el-table-column>
         <el-table-column  label="客户来源" align="center" prop="source">

+ 29 - 7
src/views/crm/customer/manualOutboundCallLog.vue

@@ -30,6 +30,16 @@
             <el-option label="失败" :value="3" />
           </el-select>
         </el-form-item>
+        <el-form-item label="客户跟进状态" prop="visitStatus">
+          <el-select v-model="queryParams.visitStatus" placeholder="请选择跟进状态" clearable filterable size="small" style="width: 160px">
+            <el-option
+              v-for="item in visitStatusOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="parseInt(item.dictValue)"
+            />
+          </el-select>
+        </el-form-item>
         <el-form-item label="呼叫时间" prop="callDateRange">
           <el-date-picker
             v-model="callDateRange"
@@ -67,7 +77,7 @@
       <el-table border v-loading="loading" :data="list">
         <el-table-column label="客户号码" align="center" prop="callerNum" width="180">
           <template slot-scope="scope">
-            <span>{{ desensitizePhone(scope.row.callerNum) }}</span>
+            <span>{{ maskMobileForDisplay(scope.row.callerNum) }}</span>
             <el-button
               v-if="scope.row.customerId"
               type="text"
@@ -79,6 +89,16 @@
           </template>
         </el-table-column>
         <el-table-column label="坐席号码" align="center" prop="calleeNum" width="140" />
+        <el-table-column label="客户跟进状态" align="center" prop="visitStatus" width="120">
+          <template slot-scope="scope">
+            <el-tag
+              v-for="item in visitStatusOptions"
+              :key="item.dictValue"
+              v-if="scope.row.visitStatus != null && scope.row.visitStatus == item.dictValue"
+            >{{ item.dictLabel }}</el-tag>
+            <span v-if="scope.row.visitStatus == null">-</span>
+          </template>
+        </el-table-column>
         <el-table-column label="呼叫时间" align="center" prop="callCreateTime" width="165">
           <template slot-scope="scope">
             <span>{{ scope.row.callCreateTime ? parseTime(scope.row.callCreateTime, '{y}-{m}-{d} {h}:{i}') : '-' }}</span>
@@ -159,8 +179,8 @@
 
 <script>
 import { listManualOutboundCallLog, sumBillingMinute } from "@/api/crm/manualOutboundCallLog";
-import { queryPhone } from "@/api/crm/customer";
-import { parseTime } from "@/utils/common";
+import { queryPhone, getTradeDicts } from "@/api/crm/customer";
+import { parseTime, maskMobileForDisplay } from "@/utils/common";
 import { checkPermi } from "@/utils/permission";
 
 export default {
@@ -178,6 +198,7 @@ export default {
       callDateRange: [],
       // 计费分钟合计
       sumBillingMinute: null,
+      visitStatusOptions: [],
       // 对话记录弹窗
       contentDialog: {
         visible: false,
@@ -190,21 +211,22 @@ export default {
         callerNum: null,
         encryptedCallerNum: null,
         status: null,
+        visitStatus: null,
         minCallTime: null,
         maxCallTime: null
       }
     };
   },
   created() {
+    getTradeDicts().then((response) => {
+      this.visitStatusOptions = (response.data && response.data["crm_customer_user_status"]) || [];
+    });
     this.getList();
   },
   methods: {
     parseTime,
+    maskMobileForDisplay,
     checkPermi,
-    desensitizePhone(phone) {
-      if (!phone || phone.length < 7) return phone;
-      return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4);
-    },
     formatAnswerTime(answerTime, callCreateTime) {
       if (!answerTime || answerTime === 0 || answerTime === '0' || answerTime === '') return '';
       if (callCreateTime && new Date(answerTime).getTime() < new Date(callCreateTime).getTime()) return '';

+ 55 - 33
src/views/crm/customer/my.vue

@@ -141,15 +141,6 @@
           v-hasPermi="['crm:customer:assignToUser']"
         >客户分配</el-button>
       </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="success"
-          icon="el-icon-s-operation"
-          size="mini"
-          @click="handleConditionAssign"
-          v-hasPermi="['crm:customer:assignToUser']"
-        >条件分配</el-button>
-      </el-col>
       <el-col :span="1.5">
         <el-button
           type="success"
@@ -188,11 +179,17 @@
           <el-link @click="handleShow(scope.row)" :underline="false" type="primary" >{{scope.row.customerName}}</el-link>
         </template>
       </el-table-column>
-      <el-table-column  label="手机" width="170px"  align="center" prop="mobile"   >
+      <el-table-column  label="手机" width="200px"  align="center" prop="mobile"   >
         <template slot-scope="scope">
           {{scope.row.mobile}}
-          <!-- <el-button type="text"    size="mini" @click="callNumber(scope.row.customerId,null,null,null)">拨号</el-button>
-          <el-button v-hasPermi="['crm:customer:addVisit']"  type="text" size="mini" @click="handleAddVisit(scope.row)">写跟进</el-button> -->
+          <el-button
+            v-if="scope.row.customerId"
+            type="text"
+            v-hasPermi="['crm:customer:queryPhone']"
+            size="mini"
+            icon="el-icon-view"
+            @click="handleQueryPhone(scope.row)"
+          >查看</el-button>
           <el-button type="text" size="mini" @click="handleManualCall(scope.row)">
             手动外呼
           </el-button>
@@ -233,7 +230,7 @@
         </template>
       </el-table-column>
       <el-table-column label="进线客户详情" align="center" :show-overflow-tooltip="true" prop="registerDesc" />
-      <el-table-column label="最新跟进时间" align="center" prop="lastTime" />
+      <el-table-column label="最新跟进时间" align="center" prop="visitTime" />
       <el-table-column label="领取时间" align="center" prop="receiveTime" />
       <el-table-column label="进线客户提交日期" align="center" prop="registerSubmitTime" />
       <el-table-column label="创建时间" align="center" prop="customerCreateTime" width="180">
@@ -307,11 +304,25 @@
         <add-visit-status ref="visitStatus" @close="closeVisitStatus()"></add-visit-status>
     </el-dialog>
     <!--人工外呼弹窗-->
-    <manual-call-dialog
-      ref="manualCallDialog"
-      :use-floating-phone="true"
+    <el-dialog
+      :title="manualCallDialog.title"
+      :visible.sync="manualCallDialog.visible"
+      width="1500px"
+      append-to-body
+      destroy-on-close
+      class="manual-call-dialog"
       @close="handleManualCallDialogClose"
-    />
+    >
+      <call-center-phone-bar
+        :key="manualCallDialog.key"
+        ref="callCenterPhoneBar"
+        :init-phone-number="manualCallDialog.phone"
+        :company-id="manualCallDialog.companyId"
+        :company-user-id="manualCallDialog.companyUserId"
+        :customer-id="manualCallDialog.customerId"
+        @close="manualCallDialog.visible = false"
+      />
+    </el-dialog>
   </div>
 </template>
 
@@ -328,10 +339,10 @@ import addTag from '../components/addTag.vue';
 import addRemark from '../components/addRemark.vue';
 import addCustomerType from '../components/addCustomerType.vue';
 import addVisitStatus from '../components/addVisitStatus.vue';
-import ManualCallDialog from '@/components/ManualCallDialog/index.vue';
+import CallCenterPhoneBar from '../../aiSipCall/aiSipCallManualOutbound.vue';
 export default {
   name: "Customer",
-  components: {addVisitStatus,addCustomerType,addRemark,addTag,assignUser,addOrEditCustomer,editSource, addBatchSms,customerDetails,addVisit,ManualCallDialog },
+  components: {addVisitStatus,addCustomerType,addRemark,addTag,assignUser,addOrEditCustomer,editSource, addBatchSms,customerDetails,addVisit,CallCenterPhoneBar },
   data() {
     return {
       addVisitStatus:{
@@ -472,6 +483,16 @@ export default {
         ],
       },
       loading:null,
+      manualCallDialog: {
+        visible: false,
+        customerId: null,
+        phone: '',
+        title: '人工外呼',
+        record: null,
+        key: 0,
+        companyId: null,
+        companyUserId: null
+      },
     };
   },
   created() {
@@ -584,13 +605,6 @@ export default {
           that.$refs.assignUser.init(ids,3);
       }, 200);
     },
-    handleConditionAssign(){
-      var that=this;
-      that.assign.open=true;
-      setTimeout(() => {
-          that.$refs.assignUser.initByCondition('my', that.queryParams, 3);
-      }, 200);
-    },
     closeAssign(){
       this.assign.open=false;
       this.getList();
@@ -758,13 +772,15 @@ export default {
           this.$message.warning('当前客户没有可外呼的手机号');
           return;
         }
-        this.$refs.manualCallDialog.open({
-          phone,
-          customerName: row.customerName || '未知客户',
-          customerId: row.customerId || null,
-          companyId: row.companyId || null,
-          companyUserId: row.companyUserId || null
-        });
+        this.manualCallDialog.record = row;
+        this.manualCallDialog.phone = phone;
+        this.manualCallDialog.customerId = row.customerId || null;
+        this.manualCallDialog.title = `人工外呼 - ${row.customerName || '未知客户'}`;
+        this.manualCallDialog.companyId = row.companyId || null;
+        this.manualCallDialog.companyUserId = row.companyUserId ||  null;
+        this.manualCallDialog.key += 1;
+        this.manualCallDialog.visible = true;
+
       } catch (e) {
         console.error(e);
         this.$message.error('获取手机号失败');
@@ -772,6 +788,12 @@ export default {
     },
 
     handleManualCallDialogClose() {
+      this.manualCallDialog.phone = '';
+      this.manualCallDialog.record = null;
+      this.manualCallDialog.roboticId = null;
+      this.manualCallDialog.companyId = null;
+      this.manualCallDialog.companyUserId = null;
+      this.manualCallDialog.workflowInstanceId = null;
       // 完成手动外呼关闭弹窗后刷新列表,保证“是否已外呼”筛选及外呼次数列数据同步
       this.getList();
     }

+ 45 - 7
src/views/crm/customer/myManualOutboundCallLog.vue

@@ -30,6 +30,16 @@
             <el-option label="失败" :value="3" />
           </el-select>
         </el-form-item>
+        <el-form-item label="客户跟进状态" prop="visitStatus">
+          <el-select v-model="queryParams.visitStatus" placeholder="请选择跟进状态" clearable filterable size="small" style="width: 160px">
+            <el-option
+              v-for="item in visitStatusOptions"
+              :key="item.dictValue"
+              :label="item.dictLabel"
+              :value="parseInt(item.dictValue)"
+            />
+          </el-select>
+        </el-form-item>
         <el-form-item label="呼叫时间" prop="callDateRange">
           <el-date-picker
             v-model="callDateRange"
@@ -65,12 +75,30 @@
 
       <!-- 表格区域 -->
       <el-table border v-loading="loading" :data="list">
-        <el-table-column label="客户号码" align="center" prop="callerNum" width="140">
+        <el-table-column label="客户号码" align="center" prop="callerNum" width="180">
           <template slot-scope="scope">
-            <span>{{ desensitizePhone(scope.row.callerNum) }}</span>
+            <span>{{ maskMobileForDisplay(scope.row.callerNum) }}</span>
+            <el-button
+              v-if="scope.row.customerId"
+              type="text"
+              v-hasPermi="['crm:customer:queryPhone']"
+              size="mini"
+              icon="el-icon-view"
+              @click="handleQueryPhone(scope.row)"
+            >查看</el-button>
           </template>
         </el-table-column>
         <el-table-column label="坐席号码" align="center" prop="calleeNum" width="140" />
+        <el-table-column label="客户跟进状态" align="center" prop="visitStatus" width="120">
+          <template slot-scope="scope">
+            <el-tag
+              v-for="item in visitStatusOptions"
+              :key="item.dictValue"
+              v-if="scope.row.visitStatus != null && scope.row.visitStatus == item.dictValue"
+            >{{ item.dictLabel }}</el-tag>
+            <span v-if="scope.row.visitStatus == null">-</span>
+          </template>
+        </el-table-column>
         <el-table-column label="呼叫时间" align="center" prop="callCreateTime" width="165">
           <template slot-scope="scope">
             <span>{{ scope.row.callCreateTime ? parseTime(scope.row.callCreateTime, '{y}-{m}-{d} {h}:{i}') : '-' }}</span>
@@ -151,7 +179,8 @@
 
 <script>
 import { listMyManualOutboundCallLog, mySumBillingMinute } from "@/api/crm/manualOutboundCallLog";
-import { parseTime } from "@/utils/common";
+import { queryPhone, getTradeDicts } from "@/api/crm/customer";
+import { parseTime, maskMobileForDisplay } from "@/utils/common";
 import { checkPermi } from "@/utils/permission";
 
 export default {
@@ -169,6 +198,7 @@ export default {
       callDateRange: [],
       // 计费分钟合计
       sumBillingMinute: null,
+      visitStatusOptions: [],
       // 对话记录弹窗
       contentDialog: {
         visible: false,
@@ -181,21 +211,22 @@ export default {
         callerNum: null,
         encryptedCallerNum: null,
         status: null,
+        visitStatus: null,
         minCallTime: null,
         maxCallTime: null
       }
     };
   },
   created() {
+    getTradeDicts().then((response) => {
+      this.visitStatusOptions = (response.data && response.data["crm_customer_user_status"]) || [];
+    });
     this.getList();
   },
   methods: {
     parseTime,
+    maskMobileForDisplay,
     checkPermi,
-    desensitizePhone(phone) {
-      if (!phone || phone.length < 7) return phone;
-      return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4);
-    },
     formatAnswerTime(answerTime, callCreateTime) {
       if (!answerTime || answerTime === 0 || answerTime === '0' || answerTime === '') return '';
       if (callCreateTime && new Date(answerTime).getTime() < new Date(callCreateTime).getTime()) return '';
@@ -223,6 +254,13 @@ export default {
       this.contentDialog.content = row.contentList || '';
       this.contentDialog.visible = true;
     },
+    handleQueryPhone(row) {
+      queryPhone(row.customerId).then(response => {
+        this.$alert(response.mobile, '手机号', {
+          confirmButtonText: '确定'
+        });
+      });
+    },
     parseContentList(content) {
       if (!content) return [];
       try {

+ 21 - 1
src/views/crm/customerContacts/index.vue

@@ -126,7 +126,19 @@
       <el-table-column label="是否删除" align="center" prop="contactsId" />
       <el-table-column label="客户ID" align="center" prop="customerId" />
       <el-table-column label="联系人名称" align="center" prop="name" />
-      <el-table-column label="手机" align="center" prop="mobile" />
+      <el-table-column label="手机" align="center" prop="mobile" width="180">
+        <template slot-scope="scope">
+          <span>{{ scope.row.mobile }}</span>
+          <el-button
+            v-if="scope.row.customerId"
+            type="text"
+            v-hasPermi="['crm:customer:queryPhone']"
+            size="mini"
+            icon="el-icon-view"
+            @click="handleQueryPhone(scope.row)"
+          >查看</el-button>
+        </template>
+      </el-table-column>
       <el-table-column label="电子邮箱" align="center" prop="email" />
       <el-table-column label="部门" align="center" prop="weixin" />
       <el-table-column label="地址" align="center" prop="address" />
@@ -206,6 +218,7 @@
 
 <script>
 import { listCustomerContacts, getCustomerContacts, delCustomerContacts, addCustomerContacts, updateCustomerContacts, exportCustomerContacts } from "@/api/crm/customerContacts";
+import { queryPhone } from "@/api/crm/customer";
 
 export default {
   name: "CustomerContacts",
@@ -257,6 +270,13 @@ export default {
     this.getList();
   },
   methods: {
+    handleQueryPhone(row) {
+      queryPhone(row.customerId).then(response => {
+        this.$alert(response.mobile, '手机号', {
+          confirmButtonText: '确定'
+        });
+      });
+    },
     /** 查询客户联系人列表 */
     getList() {
       this.loading = true;

+ 1 - 1
src/views/qw/externalContact/mycustomer.vue

@@ -63,7 +63,7 @@
       <el-table-column  label="备注" width="150px"  align="center" prop="remark"   >
       </el-table-column>
       <el-table-column label="进线客户详情" align="center" :show-overflow-tooltip="true" prop="registerDesc" />
-      <el-table-column label="最新跟进时间" align="center" prop="lastTime" />
+      <el-table-column label="最新跟进时间" align="center" prop="visitTime" />
       <el-table-column label="领取时间" align="center" prop="receiveTime" />
       <el-table-column label="进线客户提交日期" align="center" prop="registerSubmitTime" />
       <el-table-column label="创建时间" align="center" prop="customerCreateTime" width="180">