<template>
|
<div class="call-container">
|
<div class="sip-status" :class="sipStatusClass">
|
SIP状态: {{ sipStatus }}
|
<span v-if="reconnectCount > 0" class="reconnect-info">
|
(重连: {{ reconnectCount }}次)
|
</span>
|
</div>
|
|
<!-- 状态显示 -->
|
<div class="call-status" :class="callStatusClass">
|
{{ callStatusText }}
|
</div>
|
|
<!-- 呼叫按钮 -->
|
<button
|
:class="[
|
'call-btn',
|
{
|
calling: isCalling,
|
registering: isRegistering,
|
reconnecting: isReconnecting,
|
},
|
]"
|
@click="startCall"
|
: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>
|
|
<!-- 挂断按钮 -->
|
<button v-if="isCalling" class="end-call-btn" @click="endCall">挂断</button>
|
|
<!-- 音频元素(隐藏) -->
|
<audio id="remoteAudio" autoplay></audio>
|
</div>
|
</template>
|
|
<script>
|
import sipService from "@/utils/sipService";
|
import { CallsetState, CallgetList } from "@/api/AiCentre/index";
|
|
export default {
|
props: {
|
phoneNumber: {
|
type: String,
|
default: "",
|
},
|
},
|
data() {
|
const randomNum = Math.floor(Math.random() * 20) + 1000;
|
return {
|
isCalling: false,
|
isRegistering: true,
|
isReconnecting: false, // 添加重连中状态
|
randomNum: randomNum,
|
randomID: null,
|
orgname: localStorage.getItem("orgname"),
|
callStatus: "idle",
|
sipStatus: "未连接",
|
sipStatusClass: "status-disconnected",
|
reconnectCount: 0, // 重连次数
|
lastActivityTime: null, // 最后活动时间
|
heartbeatTimer: null, // 心跳定时器
|
reconnectTimer: null, // 重连定时器
|
maxReconnectAttempts: 5, // 最大重连尝试次数
|
reconnectDelay: 5000, // 重连延迟(ms)
|
sipConfig: {
|
wsUrl: "",
|
sipUri: "",
|
password: "Smartor@2023",
|
displayName: "Web 小龙",
|
},
|
};
|
},
|
computed: {
|
callStatusText() {
|
const statusMap = {
|
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}`;
|
},
|
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";
|
}
|
},
|
|
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.warning("请输入电话号码");
|
return;
|
}
|
|
try {
|
// 先检查是否可以呼叫
|
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("呼叫失败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.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;
|
gap: 10px;
|
max-width: 300px;
|
margin: 0 auto;
|
padding: 20px;
|
border: 1px solid #eee;
|
border-radius: 8px;
|
}
|
|
.reconnect-info {
|
font-size: 12px;
|
color: #666;
|
margin-left: 5px;
|
}
|
|
.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: #f44336;
|
color: white;
|
border: none;
|
border-radius: 4px;
|
cursor: pointer;
|
}
|
|
.end-call-btn:hover {
|
background-color: #d32f2f;
|
}
|
|
/* 状态样式保持不变 */
|
.sip-status,
|
.call-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;
|
}
|
|
.status-idle {
|
background-color: #f5f5f5;
|
color: #666;
|
}
|
|
.status-calling {
|
background-color: #fff8e1;
|
color: #ff8f00;
|
}
|
|
.status-connected {
|
background-color: #e8f5e9;
|
color: #2e7d32;
|
}
|
|
.status-ended {
|
background-color: #ffebee;
|
color: #c62828;
|
}
|
</style>
|