| | |
| | | <div class="call-container"> |
| | | <div class="sip-status" :class="sipStatusClass"> |
| | | SIP状态: {{ sipStatus }} |
| | | <span v-if="reconnectCount > 0" class="reconnect-info"> |
| | | (重连: {{ reconnectCount }}次) |
| | | </span> |
| | | </div> |
| | | |
| | | <!-- 状态显示 --> |
| | |
| | | |
| | | <!-- 呼叫按钮 --> |
| | | <button |
| | | :class="['call-btn', { calling: isCalling }]" |
| | | :class="[ |
| | | 'call-btn', |
| | | { |
| | | calling: isCalling, |
| | | registering: isRegistering, |
| | | reconnecting: isReconnecting, |
| | | }, |
| | | ]" |
| | | @click="startCall" |
| | | :disabled="isCalling || sipStatus !== '已注册'" |
| | | :disabled="isButtonDisabled" |
| | | > |
| | | <i v-if="isRegistering || isReconnecting" class="el-icon-loading"></i> |
| | | {{ callButtonText }} |
| | | </button> |
| | | |
| | | <!-- 手动重连按钮 --> |
| | | <button |
| | | v-if="showManualReconnect" |
| | | class="reconnect-btn" |
| | | @click="manualReconnect" |
| | | :disabled="isRegistering" |
| | | > |
| | | 手动重连 |
| | | </button> |
| | | |
| | | <!-- 挂断按钮 --> |
| | |
| | | |
| | | <script> |
| | | import sipService from "@/utils/sipService"; |
| | | import { CallsetState, CallgetList } from "@/api/AiCentre/index"; |
| | | |
| | | export default { |
| | | props: { |
| | | phoneNumber: { |
| | | type: String, |
| | | default: '' |
| | | } |
| | | default: "", |
| | | }, |
| | | }, |
| | | data() { |
| | | const randomNum = Math.floor(Math.random() * 20) + 1000; |
| | | return { |
| | | isCalling: false, |
| | | callStatus: 'idle', // idle, calling, connected, ended |
| | | isRegistering: true, |
| | | isReconnecting: false, // 添加重连中状态 |
| | | randomNum: randomNum, |
| | | randomID: null, |
| | | orgname: localStorage.getItem("orgname"), |
| | | callStatus: "idle", |
| | | sipStatus: "未连接", |
| | | sipStatusClass: "status-disconnected", |
| | | randomNum = Math.floor(Math.random() * 21) + 1000, // 生成 1000-1020 的随机整数 |
| | | reconnectCount: 0, // 重连次数 |
| | | lastActivityTime: null, // 最后活动时间 |
| | | heartbeatTimer: null, // 心跳定时器 |
| | | reconnectTimer: null, // 重连定时器 |
| | | maxReconnectAttempts: 5, // 最大重连尝试次数 |
| | | reconnectDelay: 5000, // 重连延迟(ms) |
| | | sipConfig: { |
| | | wsUrl: "wss://192.168.10.124:7443", |
| | | sipUri: `${randomNum}`+"@192.168.10.124", |
| | | wsUrl: "", |
| | | sipUri: "", |
| | | password: "Smartor@2023", |
| | | displayName: "Web 小龙", |
| | | }, |
| | |
| | | computed: { |
| | | callStatusText() { |
| | | const statusMap = { |
| | | idle: '准备就绪', |
| | | calling: '呼叫中...', |
| | | connected: '通话中', |
| | | ended: '通话结束' |
| | | idle: "准备就绪", |
| | | calling: "呼叫中...", |
| | | connected: "通话中", |
| | | ended: "通话结束", |
| | | }; |
| | | return statusMap[this.callStatus]; |
| | | }, |
| | | countdownText() { |
| | | if (this.sipStatus !== "已注册") return ""; |
| | | const { canCall, reason } = sipService.canMakeCall(); |
| | | if (!canCall && reason.includes("等待")) { |
| | | return reason; |
| | | } |
| | | return ""; |
| | | }, |
| | | isButtonDisabled() { |
| | | return ( |
| | | this.isCalling || |
| | | this.sipStatus !== "已注册" || |
| | | this.isRegistering || |
| | | this.isReconnecting |
| | | ); |
| | | }, |
| | | callButtonText() { |
| | | if (this.isRegistering) return "注册中..."; |
| | | if (this.isReconnecting) return "重连中..."; |
| | | return this.isCalling ? "通话中..." : "一键呼叫"; |
| | | }, |
| | | callStatusClass() { |
| | | return `status-${this.callStatus}`; |
| | | }, |
| | | callButtonText() { |
| | | return this.isCalling ? "通话中..." : "一键呼叫"; |
| | | showManualReconnect() { |
| | | return ( |
| | | !this.isCalling && |
| | | !this.isRegistering && |
| | | this.sipStatus !== "已注册" && |
| | | this.sipStatus !== "连接中" |
| | | ); |
| | | }, |
| | | }, |
| | | created() { |
| | | if ( |
| | | this.orgname == "第一人民医院湖滨院区" || |
| | | this.orgname == "第一人民医院吴山院区" |
| | | ) { |
| | | this.sipConfig.password = "heskj@1234"; |
| | | } else { |
| | | this.sipConfig.password = "Smartor@2023"; |
| | | } |
| | | }, |
| | | mounted() { |
| | | sipService.init(this.sipConfig); |
| | | sipService.onStatusChange = (status) => { |
| | | this.sipStatus = status.text; |
| | | this.sipStatusClass = `status-${status.type}`; |
| | | }; |
| | | |
| | | // 监听通话状态变化 |
| | | sipService.onCallStatusChange = (status) => { |
| | | this.callStatus = status.type; |
| | | this.isCalling = status.type === 'calling' || status.type === 'connected'; |
| | | |
| | | // 通知父组件通话状态变化 |
| | | this.$emit('call-status-change', status); |
| | | }; |
| | | async mounted() { |
| | | const orgName = localStorage.getItem("orgname"); |
| | | if (orgName == "景宁畲族自治县人民医院") { |
| | | return; |
| | | } |
| | | await this.CallgetList(); |
| | | this.isRegistering = true; |
| | | this.initSipService(); |
| | | this.setupHeartbeat(); |
| | | }, |
| | | methods: { |
| | | async initSipService() { |
| | | try { |
| | | // 初始化sipService |
| | | sipService.init(this.sipConfig); |
| | | |
| | | // 设置状态回调 |
| | | sipService.onStatusChange = (status) => { |
| | | this.sipStatus = status.text; |
| | | this.sipStatusClass = `status-${status.type}`; |
| | | |
| | | // 处理各种状态 |
| | | if (status.type === "registered") { |
| | | this.handleRegistered(); |
| | | } else if ( |
| | | status.type === "failed" || |
| | | status.type === "disconnected" |
| | | ) { |
| | | this.handleDisconnected(); |
| | | } else if (status.type === "connecting") { |
| | | this.handleConnecting(); |
| | | } |
| | | }; |
| | | |
| | | // 监听通话状态变化 |
| | | sipService.onCallStatusChange = (status) => { |
| | | this.callStatus = status.type; |
| | | this.isCalling = |
| | | status.type === "calling" || status.type === "connected"; |
| | | this.updateLastActivityTime(); // 通话状态变化时更新活动时间 |
| | | |
| | | this.$emit("call-status-change", status); |
| | | }; |
| | | |
| | | // 设置超时处理 |
| | | this.setupRegistrationTimeout(); |
| | | } catch (error) { |
| | | console.error("SIP服务初始化失败:", error); |
| | | this.handleDisconnected(); |
| | | } |
| | | }, |
| | | |
| | | handleRegistered() { |
| | | console.log("SIP注册成功"); |
| | | this.isRegistering = false; |
| | | this.isReconnecting = false; |
| | | this.reconnectCount = 0; // 重置重连计数 |
| | | this.updateLastActivityTime(); |
| | | this.startCallsetState(); |
| | | |
| | | // 清除重连定时器 |
| | | if (this.reconnectTimer) { |
| | | clearTimeout(this.reconnectTimer); |
| | | this.reconnectTimer = null; |
| | | } |
| | | }, |
| | | |
| | | handleDisconnected() { |
| | | console.log("SIP连接断开"); |
| | | this.isRegistering = false; |
| | | this.overCallsetState(); |
| | | |
| | | // 如果不是通话中断开,尝试重连 |
| | | if (!this.isCalling) { |
| | | this.scheduleReconnect(); |
| | | } |
| | | }, |
| | | |
| | | handleConnecting() { |
| | | this.isReconnecting = true; |
| | | }, |
| | | |
| | | setupRegistrationTimeout() { |
| | | setTimeout(() => { |
| | | if (this.isRegistering && this.sipStatus !== "已注册") { |
| | | this.isRegistering = false; |
| | | this.$message.warning("SIP注册超时,正在尝试重连..."); |
| | | this.scheduleReconnect(); |
| | | } |
| | | }, 10000); |
| | | }, |
| | | |
| | | // 设置心跳检测 |
| | | setupHeartbeat() { |
| | | this.heartbeatTimer = setInterval(() => { |
| | | this.checkConnection(); |
| | | }, 60000); // 每30秒检查一次连接 |
| | | }, |
| | | |
| | | // 检查连接状态 |
| | | async checkConnection() { |
| | | // 如果正在注册、重连或通话中,不检查 |
| | | if (this.isRegistering || this.isReconnecting || this.isCalling) { |
| | | return; |
| | | } |
| | | |
| | | // 检查SIP连接状态 |
| | | if (sipService && sipService.ua) { |
| | | const isConnected = sipService.ua.isConnected(); |
| | | const isRegistered = sipService.ua.isRegistered(); |
| | | |
| | | if (!isConnected || !isRegistered) { |
| | | console.log("心跳检测: 连接异常,尝试重连"); |
| | | await this.reconnectSip(); |
| | | } else { |
| | | console.log("心跳检测: 连接正常"); |
| | | this.updateLastActivityTime(); |
| | | } |
| | | } else { |
| | | console.log("心跳检测: UA不存在,尝试重新初始化"); |
| | | await this.reconnectSip(); |
| | | } |
| | | }, |
| | | |
| | | // 计划重连 |
| | | scheduleReconnect() { |
| | | if (this.reconnectTimer || this.isReconnecting) { |
| | | return; |
| | | } |
| | | |
| | | if (this.reconnectCount >= this.maxReconnectAttempts) { |
| | | console.log("达到最大重连次数,停止重连"); |
| | | this.$message.error("SIP连接失败,请刷新页面重试"); |
| | | return; |
| | | } |
| | | |
| | | this.reconnectCount++; |
| | | const delay = Math.min( |
| | | this.reconnectDelay * Math.pow(1.5, this.reconnectCount - 1), |
| | | 30000 |
| | | ); |
| | | |
| | | console.log(`计划在${delay}ms后重连,第${this.reconnectCount}次尝试`); |
| | | |
| | | this.reconnectTimer = setTimeout(() => { |
| | | this.reconnectSip(); |
| | | }, delay); |
| | | }, |
| | | |
| | | // 重新连接SIP |
| | | async reconnectSip() { |
| | | if (this.isReconnecting || this.isRegistering) { |
| | | return; |
| | | } |
| | | |
| | | console.log("开始重连SIP服务..."); |
| | | this.isReconnecting = true; |
| | | |
| | | try { |
| | | // 清理现有连接 |
| | | this.cleanupSipConnection(); |
| | | |
| | | // 等待一段时间 |
| | | await new Promise((resolve) => setTimeout(resolve, 1000)); |
| | | |
| | | // 重新初始化 |
| | | await this.CallgetList(); // 重新获取分机号 |
| | | this.initSipService(); |
| | | } catch (error) { |
| | | console.error("重连失败:", error); |
| | | this.isReconnecting = false; |
| | | this.scheduleReconnect(); // 失败后继续重试 |
| | | } finally { |
| | | if (this.reconnectTimer) { |
| | | clearTimeout(this.reconnectTimer); |
| | | this.reconnectTimer = null; |
| | | } |
| | | } |
| | | }, |
| | | |
| | | // 清理SIP连接 |
| | | cleanupSipConnection() { |
| | | if (sipService && sipService.ua) { |
| | | try { |
| | | sipService.ua.stop(); |
| | | sipService.ua.unregister(); |
| | | } catch (e) { |
| | | console.warn("清理SIP连接时出错:", e); |
| | | } |
| | | } |
| | | }, |
| | | |
| | | // 手动重连 |
| | | async manualReconnect() { |
| | | this.reconnectCount = 0; // 重置重连计数 |
| | | await this.reconnectSip(); |
| | | }, |
| | | |
| | | // 更新最后活动时间 |
| | | updateLastActivityTime() { |
| | | this.lastActivityTime = Date.now(); |
| | | }, |
| | | |
| | | async startCall() { |
| | | if (!this.phoneNumber) { |
| | | this.$message.error("请输入电话号码"); |
| | | this.$message.warning("请输入电话号码"); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | this.callStatus = 'calling'; |
| | | // 先检查是否可以呼叫 |
| | | const { canCall, reason } = sipService.canMakeCall(); |
| | | if (!canCall) { |
| | | // 可选: 可以显示提示 |
| | | } |
| | | |
| | | this.callStatus = "calling"; |
| | | this.isCalling = true; |
| | | console.log("开始呼叫:", sipService); |
| | | |
| | | await sipService.makeCall(this.phoneNumber); |
| | | |
| | | } catch (error) { |
| | | console.error("呼叫失败:", error); |
| | | this.callStatus = 'ended'; |
| | | this.isCalling = false; |
| | | this.$message.error(`呼叫失败: ${error.message}`); |
| | | console.error("呼叫失败1:", error); |
| | | |
| | | if ( |
| | | error.message.includes("Canceled") || |
| | | error.message.includes("未注册") |
| | | ) { |
| | | console.warn("呼叫因页面离开或未注册而取消,不重试"); |
| | | this.callStatus = "ended"; |
| | | this.isCalling = false; |
| | | return; |
| | | } |
| | | try { |
| | | // 尝试加0再次呼叫 |
| | | const { canCall, reason } = sipService.canMakeCall(); |
| | | if (!canCall) { |
| | | // 可选处理 |
| | | } |
| | | this.callStatus = "calling"; |
| | | this.isCalling = true; |
| | | console.log("尝试加0再次呼叫:", sipService); |
| | | |
| | | await sipService.makeCall("0" + this.phoneNumber); |
| | | } catch (error) { |
| | | this.callStatus = "ended"; |
| | | this.isCalling = false; |
| | | this.$message.warning("当前呼叫占线中,请稍后再拨"); |
| | | } |
| | | } |
| | | }, |
| | | |
| | | // 查询可用分机号 |
| | | async CallgetList() { |
| | | try { |
| | | const res = await CallgetList(); |
| | | this.randomNum = res.data[0].tel; |
| | | this.randomID = res.data[0].id; |
| | | |
| | | const orgName = localStorage.getItem("orgname"); |
| | | if (orgName == "丽水市中医院") { |
| | | this.sipConfig.sipUri = `${this.randomNum}@192.168.10.124`; |
| | | } else if (orgName == "龙泉市人民医院") { |
| | | this.sipConfig.sipUri = `${this.randomNum}@10.10.0.220`; |
| | | } else if ( |
| | | orgName == "第一人民医院湖滨院区" || |
| | | orgName == "第一人民医院吴山院区" |
| | | ) { |
| | | this.sipConfig.sipUri = `${this.randomNum}@192.169.129.198`; |
| | | } |
| | | } catch (error) { |
| | | console.error("获取分机号失败:", error); |
| | | throw error; // 抛出错误以便上层处理 |
| | | } |
| | | }, |
| | | |
| | | async startCallsetState() { |
| | | try { |
| | | await CallsetState({ id: this.randomID, state: 1 }); |
| | | console.log("分机号状态更新为使用中"); |
| | | } catch (error) { |
| | | console.error("更新分机号状态失败:", error); |
| | | } |
| | | }, |
| | | |
| | | async overCallsetState() { |
| | | try { |
| | | if (this.randomID) { |
| | | await CallsetState({ id: this.randomID, state: 0 }); |
| | | console.log("分机号状态更新为可用"); |
| | | } |
| | | } catch (error) { |
| | | console.error("释放分机号失败:", error); |
| | | } |
| | | }, |
| | | |
| | | endCall() { |
| | | sipService.endCall(); |
| | | this.callStatus = 'ended'; |
| | | this.callStatus = "ended"; |
| | | this.isCalling = false; |
| | | }, |
| | | |
| | | cleanupResources() { |
| | | // 清除所有定时器 |
| | | if (this.heartbeatTimer) { |
| | | clearInterval(this.heartbeatTimer); |
| | | this.heartbeatTimer = null; |
| | | } |
| | | |
| | | if (this.reconnectTimer) { |
| | | clearTimeout(this.reconnectTimer); |
| | | this.reconnectTimer = null; |
| | | } |
| | | |
| | | // 结束通话 |
| | | if (this.isCalling) { |
| | | sipService.endCall(); |
| | | } |
| | | |
| | | // 释放分机号 |
| | | this.overCallsetState(); |
| | | |
| | | // 断开 SIP 连接 |
| | | this.cleanupSipConnection(); |
| | | }, |
| | | }, |
| | | beforeUnmount() { |
| | | if (this.isCalling) { |
| | | this.endCall(); // 内部设置了 isManualEnd |
| | | } |
| | | } |
| | | // 其他清理(如定时器)... |
| | | this.cleanupResources(); // 但注意不要重复清理定时器,可优化判断 |
| | | }, |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | /* 保持原有样式不变,只添加新样式 */ |
| | | .call-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | input { |
| | | padding: 8px; |
| | | border: 1px solid #ccc; |
| | | border-radius: 4px; |
| | | .reconnect-info { |
| | | font-size: 12px; |
| | | color: #666; |
| | | margin-left: 5px; |
| | | } |
| | | |
| | | .call-btn { |
| | | .reconnect-btn { |
| | | padding: 8px 12px; |
| | | background-color: #ff9800; |
| | | color: white; |
| | | border: none; |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .reconnect-btn:hover:not(:disabled) { |
| | | background-color: #f57c00; |
| | | } |
| | | |
| | | .reconnect-btn:disabled { |
| | | background-color: #cccccc; |
| | | cursor: not-allowed; |
| | | } |
| | | |
| | | .call-btn.reconnecting { |
| | | background-color: #ff9800; |
| | | } |
| | | |
| | | .call-btn:hover:not(:disabled) { |
| | | background-color: #45a049; |
| | | } |
| | | |
| | | .call-btn:disabled { |
| | | background-color: #cccccc; |
| | | cursor: not-allowed; |
| | | } |
| | | |
| | | .call-btn.calling { |
| | | background-color: #2196f3; |
| | | } |
| | | |
| | | .call-btn.registering, |
| | | .call-btn.reconnecting { |
| | | position: relative; |
| | | } |
| | | |
| | | .end-call-btn { |
| | | padding: 10px; |
| | | background-color: #4caf50; |
| | | background-color: #f44336; |
| | | color: white; |
| | | border: none; |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .end-call-btn:hover { |
| | | background-color: #d32f2f; |
| | | } |
| | | |
| | | /* 状态样式保持不变 */ |
| | | .sip-status, |
| | | .call-status { |
| | | padding: 8px; |
| | | margin: 10px 0; |
| | | margin-bottom: 10px; |
| | | border-radius: 4px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .status-disconnected { |
| | | background-color: #ffebee; |
| | | color: #c62828; |
| | | } |
| | | |
| | | .status-connecting { |
| | | background-color: #fff8e1; |
| | | color: #ff8f00; |
| | | } |
| | | |
| | | .status-registered { |
| | | background-color: #e8f5e9; |
| | | color: #2e7d32; |
| | | } |
| | | |
| | | .status-failed { |
| | | background-color: #ffebee; |
| | | color: #c62828; |
| | | } |
| | | |
| | | .status-idle { |
| | |
| | | } |
| | | |
| | | .status-ended { |
| | | background-color: #ffebee; |
| | | color: #c62828; |
| | | } |
| | | |
| | | /* 原有样式保持不变 */ |
| | | .call-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | max-width: 300px; |
| | | margin: 0 auto; |
| | | padding: 20px; |
| | | border: 1px solid #eee; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .call-btn { |
| | | padding: 10px; |
| | | background-color: #4caf50; |
| | | color: white; |
| | | border: none; |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .call-btn:hover:not(:disabled) { |
| | | background-color: #45a049; |
| | | } |
| | | |
| | | .call-btn:disabled { |
| | | background-color: #cccccc; |
| | | cursor: not-allowed; |
| | | } |
| | | |
| | | .call-btn.calling { |
| | | background-color: #2196f3; |
| | | } |
| | | |
| | | .end-call-btn { |
| | | padding: 10px; |
| | | background-color: #f44336; |
| | | color: white; |
| | | border: none; |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .end-call-btn:hover { |
| | | background-color: #d32f2f; |
| | | } |
| | | |
| | | .sip-status { |
| | | padding: 8px; |
| | | margin-bottom: 10px; |
| | | border-radius: 4px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .status-disconnected { |
| | | background-color: #ffebee; |
| | | color: #c62828; |
| | | } |
| | | |
| | | .status-connecting { |
| | | background-color: #fff8e1; |
| | | color: #ff8f00; |
| | | } |
| | | |
| | | .status-registered { |
| | | background-color: #e8f5e9; |
| | | color: #2e7d32; |
| | | } |
| | | |
| | | .status-failed { |
| | | background-color: #ffebee; |
| | | color: #c62828; |
| | | } |
| | | .call-btn:hover { |
| | | background-color: #45a049; |
| | | } |
| | | |
| | | .call-btn.calling { |
| | | background-color: #2196f3; |
| | | } |
| | | |
| | | .end-call-btn { |
| | | padding: 10px; |
| | | background-color: #f44336; |
| | | color: white; |
| | | border: none; |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .end-call-btn:hover { |
| | | background-color: #d32f2f; |
| | | } |
| | | |
| | | .call-status { |
| | | margin-top: 10px; |
| | | font-size: 14px; |
| | | color: #666; |
| | | } |
| | | .sip-status { |
| | | padding: 8px; |
| | | margin-bottom: 10px; |
| | | border-radius: 4px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .status-disconnected { |
| | | background-color: #ffebee; |
| | | color: #c62828; |
| | | } |
| | | |
| | | .status-connecting { |
| | | background-color: #fff8e1; |
| | | color: #ff8f00; |
| | | } |
| | | |
| | | .status-registered { |
| | | background-color: #e8f5e9; |
| | | color: #2e7d32; |
| | | } |
| | | |
| | | .status-failed { |
| | | background-color: #ffebee; |
| | | color: #c62828; |
| | | } |