修复:

1. 接听电话以实现
2. 后端接口适配实现

现有问题:
1. rtc通话问题待修复
2. 点击接听会有挂断的报错,需要检查逻辑
3. 修复lastMessage的更新问题
This commit is contained in:
2025-07-09 09:34:26 +08:00
parent 3637c8b7c6
commit 56d6a51754
4 changed files with 513 additions and 211 deletions

View File

@@ -124,11 +124,19 @@
<i class="fas fa-phone-slash"></i>
<span>对方忙线中</span>
</div>
<template v-else>
<button class="accept-btn" @click="acceptCall">
<i class="fas fa-phone"></i> 接听
<template v-else-if="callStatus === 'ringing'">
<button
class="accept-btn"
@click="acceptCall"
:disabled="isAccepting"
>
<i class="fas fa-phone"></i> {{ isAccepting ? '接听中...' : '接听' }}
</button>
<button class="reject-btn" @click="rejectCall">
<button
class="reject-btn"
@click="rejectCall"
:disabled="isAccepting"
>
<i class="fas fa-phone-slash"></i> 拒绝
</button>
</template>
@@ -209,13 +217,13 @@ const isMinimized = ref(false);
const isMicOn = ref(true);
const isCameraOn = ref(props.callType === 'video');
const isSpeakerOn = ref(true);
const hasLocalVideo = ref(false);
const hasRemoteVideo = ref(false);
const callDuration = ref(0);
const callTimer = ref(null);
const isCameraWorking = ref(true);
const isMicWorking = ref(true);
const isDragging = ref(false);
const isAccepting = ref(false); // 新增:防止重复接听
// 拖拽相关状态
const position = reactive({ x: 0, y: 0 });
@@ -281,14 +289,26 @@ const containerStyle = computed(() => {
};
});
// 初始化媒体设备
// 初始化媒体设备 - 修复:只在需要时获取媒体
const initializeMedia = async () => {
try {
// 只有在接听来电或通话已接受时才获取媒体
if (props.isIncoming && callStatus.value === "ringing") {
console.log("来电状态,暂不获取媒体流");
return;
}
if (callStatus.value === "calling") {
console.log("呼出状态,暂不获取媒体流");
return;
}
const constraints = {
audio: true,
video: props.callType === 'video'
};
console.log("初始化媒体设备,约束:", constraints);
// 获取本地媒体流
await webrtc.getLocalMedia(constraints);
@@ -361,10 +381,18 @@ const endCall = () => {
// 接听通话
const acceptCall = () => {
// 确保只接听一次
if (chatStore.callStatus === "ongoing") return;
// 防止重复点击
if (isAccepting.value || chatStore.callStatus === "connecting" || chatStore.callStatus === "ongoing") {
return;
}
chatStore.acceptCall();
isAccepting.value = true;
chatStore.acceptCall().finally(() => {
// 延迟重置状态,避免快速重复点击
setTimeout(() => {
isAccepting.value = false;
}, 2000);
});
emit('accept-call');
};
@@ -436,10 +464,8 @@ const handleWindowResize = () => {
watch(() => webrtc.localStream.value, (newStream) => {
if (newStream && localVideo.value) {
localVideo.value.srcObject = newStream;
hasLocalVideo.value = true;
} else if (localVideo.value) {
localVideo.value.srcObject = null;
hasLocalVideo.value = false;
}
});
@@ -464,15 +490,24 @@ watch(() => webrtc.remoteStream.value, (newStream) => {
}
});
// 监听通话状态
// 监听通话状态 - 修复:避免过早关闭连接
watch(() => chatStore.callStatus, (status) => {
console.log("VideoCallComponent 监听到通话状态变化:", status);
if (status === "ongoing") {
// 开始计时
if (!callTimer.value) {
startCallTimer();
}
} else if (status === "ended" || status === "idle") {
endCall();
// 只有在真正结束时才关闭
console.log("通话结束,准备关闭组件");
setTimeout(() => {
endCall();
}, 100); // 延迟一点确保状态同步
} else if (status === "connecting") {
// 连接中时初始化媒体
initializeMedia();
}
});
@@ -483,13 +518,18 @@ onMounted(() => {
window.addEventListener('resize', handleWindowResize);
// 如果是呼出方或已接听,初始化媒体
if (!props.isIncoming || callStatus.value === 'ongoing') {
// 修复:根据通话状态决定是否初始化媒体
console.log("VideoCallComponent mounted, callStatus:", callStatus.value, "isIncoming:", props.isIncoming);
// 只有在特定状态下才初始化媒体
if (callStatus.value === 'ongoing' || callStatus.value === 'connecting') {
initializeMedia();
}
});
onUnmounted(() => {
console.log("VideoCallComponent unmounted");
if (callTimer.value) clearInterval(callTimer.value);
document.removeEventListener('mousemove', handleDrag);
document.removeEventListener('mouseup', stopDrag);
@@ -506,6 +546,7 @@ onUnmounted(() => {
remoteVideoPreview.value.srcObject = null;
}
// 关闭WebRTC连接
webrtc.closeConnection();
});
</script>

View File

@@ -1,6 +1,7 @@
import { ref } from "vue"
import { useChatStore } from "@/stores/chat"
import { useUserStore } from "@/stores/user"
import { callAPI } from "@/utils/request.js"
export function useWebRTC() {
const localStream = ref(null)
@@ -9,8 +10,8 @@ export function useWebRTC() {
const isConnected = ref(false)
const isConnecting = ref(false)
const chatStore = useChatStore()
const userStore = useUserStore()
const chatStore = useChatStore() // Moved to top-level
const configuration = {
iceServers: [
@@ -24,22 +25,33 @@ export function useWebRTC() {
const createPeerConnection = (callId, peerId) => {
if (peerConnection.value) {
console.log("关闭现有的WebRTC连接")
closeConnection()
}
console.log("创建新的WebRTC连接")
peerConnection.value = new RTCPeerConnection(configuration)
peerConnection.value.onicecandidate = (event) => {
peerConnection.value.onicecandidate = async (event) => {
if (event.candidate) {
console.log("发送ICE候选:", event.candidate)
// 发送ICE候选给对方
chatStore.sendCallSignal({
call_id: callId,
call_status: "candidate",
candidate: event.candidate,
receiver_user_id: peerId,
})
// 只有在连接建立过程中才发送 ICE 候选
if (chatStore.callStatus === "connecting" || chatStore.callStatus === "ongoing") {
try {
await callAPI.sendCandidate(
userStore.currentUser?.id,
peerId,
callId,
event.candidate,
6, // 默认视频通话类型
)
} catch (error) {
console.error("发送ICE候选失败:", error)
}
} else {
console.log("通话状态不正确跳过发送ICE候选:", chatStore.callStatus)
}
}
}
@@ -57,11 +69,16 @@ export function useWebRTC() {
if (state === "connected") {
isConnected.value = true
isConnecting.value = false
chatStore.callStatus = "ongoing"
chatStore.callConnectionStatus = "通话中"
// 只有在 connecting 状态时才切换到 ongoing
if (chatStore.callStatus === "connecting") {
chatStore.callStatus = "ongoing"
chatStore.callConnectionStatus = "通话中"
}
} else if (state === "connecting") {
isConnecting.value = true
chatStore.callConnectionStatus = "正在连接..."
if (chatStore.callStatus === "connecting") {
chatStore.callConnectionStatus = "正在连接..."
}
} else if (state === "disconnected" || state === "failed") {
isConnected.value = false
isConnecting.value = false
@@ -89,10 +106,12 @@ export function useWebRTC() {
const getLocalMedia = async (constraints = { audio: true, video: true }) => {
try {
// 如果已有本地流,先停止
if (localStream.value) {
localStream.value.getTracks().forEach((track) => track.stop())
}
console.log("获取本地媒体,约束:", constraints)
localStream.value = await navigator.mediaDevices.getUserMedia(constraints)
console.log("获取本地媒体成功:", localStream.value)
return localStream.value
@@ -123,14 +142,6 @@ export function useWebRTC() {
await peerConnection.value.setLocalDescription(offer)
console.log("创建Offer成功:", offer)
// 发送offer给对方
chatStore.sendCallSignal({
call_id: callId,
call_status: "offer",
offer: offer,
receiver_user_id: peerId,
})
return offer
} catch (error) {
console.error("创建Offer失败:", error)
@@ -156,15 +167,6 @@ export function useWebRTC() {
await peerConnection.value.setLocalDescription(answer)
console.log("创建Answer成功:", answer)
// 发送answer给对方
chatStore.sendCallSignal({
call_id: callId,
call_status: "answer",
answer: answer,
receiver_user_id: peerId,
})
return answer
} catch (error) {
console.error("创建Answer失败:", error)

View File

@@ -3,6 +3,7 @@ import { ref, computed, watch } from "vue"
import { chatDB } from "@/utils/db"
import { useWebRTC } from "@/composables/useWebRTC"
import { useUserStore } from "@/stores/user"
import { sendCallSignal, callAPI } from "@/utils/request.js"
export const useChatStore = defineStore("chat", () => {
const friends = ref([])
@@ -56,7 +57,7 @@ export const useChatStore = defineStore("chat", () => {
callConnectionStatus.value = ""
currentCall.value = null
incomingCall.value = null
webrtc.closeConnection()
// 注意这里不要立即关闭WebRTC连接让组件自己处理
}
// 清除通话超时
@@ -76,21 +77,46 @@ export const useChatStore = defineStore("chat", () => {
}, duration)
}
// 发送通话信令
const sendCallSignal = (signal) => {
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
const message = {
call_status: signal.call_status,
call_type: signal.call_type || (signal.receiver_user_id ? 6 : 7), // 默认视频通话
call_id: signal.call_id,
caller_id: userStore.currentUser.id,
callee_id: signal.receiver_user_id,
data: JSON.stringify(signal.offer || signal.answer || signal.candidate || {}),
// 发送通话信令 - 使用新的专用方法
const sendCallSignalInternal = async (signal) => {
try {
console.log("发送通话信令:", signal)
// 确保必要参数存在
if (!userStore.currentUser?.id) {
throw new Error("用户未登录")
}
socket.value.send(JSON.stringify(message))
console.log("发送通话信令:", message)
} else {
console.error("WebSocket未连接无法发送通话信令")
if (!signal.receiver_user_id) {
throw new Error("缺少接收方用户ID")
}
if (!signal.call_id) {
throw new Error("缺少通话ID")
}
if (!signal.call_status) {
throw new Error("缺少通话状态")
}
// 构建请求数据
const requestData = {
sender_user_id: userStore.currentUser.id,
receiver_user_id: signal.receiver_user_id,
message_type: signal.call_type || (signal.call_status === "invite" ? 6 : 7), // 6=视频, 7=语音
message_content: JSON.stringify(signal.data || {}),
call_id: signal.call_id,
call_status: signal.call_status,
}
console.log("发送通话信令请求数据:", requestData)
const response = await sendCallSignal(requestData)
console.log("发送通话信令成功:", signal)
return response
} catch (error) {
console.error("发送通话信令失败:", error)
throw error
}
}
@@ -330,15 +356,24 @@ export const useChatStore = defineStore("chat", () => {
// 处理Offer信令
const handleOffer = async (data) => {
if (incomingCall.value && incomingCall.value.callId === data.call_id) {
// 确保只处理一次 Offer
if (callStatus.value === "ongoing") {
console.log("通话已建立,忽略重复的 Offer")
return
}
try {
const offer = JSON.parse(data.data)
const offer = JSON.parse(data.content || data.message_content || "{}")
console.log("收到Offer:", offer)
// 设置远程Offer并创建Answer
await webrtc.setRemoteOffer(offer)
const answer = await webrtc.createAnswer(offer, data.call_id, data.caller_id)
const answer = await webrtc.createAnswer(offer, data.call_id, data.sender_user_id)
console.log("发送Answer:", answer)
// 发送Answer - 使用便捷方法
await callAPI.sendAnswer(userStore.currentUser.id, data.sender_user_id, data.call_id, answer, data.message_type)
console.log("发送Answer成功")
} catch (error) {
console.error("处理Offer失败:", error)
rejectCall("failed")
@@ -349,8 +384,14 @@ export const useChatStore = defineStore("chat", () => {
// 处理Answer信令
const handleAnswer = async (data) => {
if (currentCall.value && currentCall.value.callId === data.call_id) {
// 确保只处理一次 Answer
if (callStatus.value === "ongoing") {
console.log("通话已建立,忽略重复的 Answer")
return
}
try {
const answer = JSON.parse(data.data)
const answer = JSON.parse(data.content || data.message_content || "{}")
console.log("收到Answer:", answer)
// 设置远程Answer
@@ -417,24 +458,20 @@ export const useChatStore = defineStore("chat", () => {
// 只有在当前没有通话时才处理来电
if (isInCall.value) {
console.log("当前正在通话中,发送忙线状态")
sendCallSignal({
call_status: "busy",
call_id: data.call_id,
receiver_user_id: data.caller_id,
})
callAPI.sendBusy(userStore.currentUser.id, data.sender_user_id, data.call_id, data.message_type)
// 添加未接来电消息
const friend = friends.value.find((f) => f.id === data.caller_id)
const friend = friends.value.find((f) => f.id === data.sender_user_id)
if (friend) {
friend.lastMessage = "[未接来电]"
chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, "[未接来电]", friend.unreadCount)
// 添加通话记录
const callMessage = {
type: data.call_type === 6 ? "video-call" : "audio-call",
type: data.message_type === 6 ? "video-call" : "audio-call",
content: "[未接来电]",
time: new Date().toLocaleTimeString().slice(0, 5),
senderId: data.caller_id,
senderId: data.sender_user_id,
read: true,
timestamp: Date.now(),
duration: 0,
@@ -450,9 +487,9 @@ export const useChatStore = defineStore("chat", () => {
incomingCall.value = {
callId: data.call_id,
callerId: data.caller_id,
callType: data.call_type === 6 ? "video" : "audio",
content: data.data,
callerId: data.sender_user_id,
callType: data.message_type === 6 ? "video" : "audio",
content: data.content,
}
callStatus.value = "ringing"
callError.value = null
@@ -462,26 +499,37 @@ export const useChatStore = defineStore("chat", () => {
}
// 处理通话接受
const handleCallAccepted = (data) => {
const handleCallAccepted = async (data) => {
if (currentCall.value && currentCall.value.callId === data.call_id) {
// 防止重复处理 accepted 状态
if (callStatus.value === "connecting" || callStatus.value === "ongoing") {
console.log("通话已在连接中,忽略重复的 accepted 信令")
return
}
callStatus.value = "connecting"
callError.value = null
callConnectionStatus.value = "对方已接听,正在连接..."
console.log("通话已被接受,开始交换信令")
// 开始创建Offer
setTimeout(async () => {
try {
await webrtc.getLocalMedia({
audio: true,
video: currentCall.value.callType === "video",
})
await webrtc.createOffer(currentCall.value.callId, currentCall.value.peerId)
} catch (error) {
console.error("创建Offer失败:", error)
endCall("failed")
}
}, 100)
// 接受通话后开始创建Offer
try {
const offer = await webrtc.createOffer(currentCall.value.callId, currentCall.value.peerId)
// 发送Offer给对方 - 使用便捷方法
await callAPI.sendOffer(
userStore.currentUser.id,
currentCall.value.peerId,
currentCall.value.callId,
offer,
currentCall.value.callType === "video" ? 6 : 7,
)
console.log("发送Offer成功")
} catch (error) {
console.error("创建Offer失败:", error)
endCall("failed")
}
}
}
@@ -573,7 +621,7 @@ export const useChatStore = defineStore("chat", () => {
(incomingCall.value && incomingCall.value.callId === data.call_id)
) {
try {
const candidate = JSON.parse(data.data)
const candidate = JSON.parse(data.content || data.message_content || "{}")
console.log("收到ICE候选:", candidate)
// 添加到WebRTC连接
@@ -653,7 +701,7 @@ export const useChatStore = defineStore("chat", () => {
// 处理对方挂断
const handleCallHangup = (data) => {
const callId = data?.call_id || currentCall.value?.callId || incomingCall.value?.callId
const peerId = data?.caller_id || data?.callee_id || currentCall.value?.peerId || incomingCall.value?.callerId
const peerId = data?.sender_user_id || currentCall.value?.peerId || incomingCall.value?.callerId
if (callId && peerId) {
// 添加通话记录
@@ -694,7 +742,7 @@ export const useChatStore = defineStore("chat", () => {
// 处理对方掉线
const handleCallDisconnected = (data) => {
const callId = data?.call_id || currentCall.value?.callId || incomingCall.value?.callId
const peerId = data?.caller_id || data?.callee_id || currentCall.value?.peerId || incomingCall.value?.callerId
const peerId = data?.sender_user_id || currentCall.value?.peerId || incomingCall.value?.callerId
if (callId && peerId) {
// 添加通话记录
@@ -735,7 +783,7 @@ export const useChatStore = defineStore("chat", () => {
// 处理对方终止呼叫
const handleCallTerminated = (data) => {
const callId = data.call_id
const peerId = data.caller_id || data.callee_id
const peerId = data.sender_user_id
if (
(currentCall.value && currentCall.value.callId === callId) ||
@@ -784,6 +832,12 @@ export const useChatStore = defineStore("chat", () => {
return
}
// 验证用户登录状态
if (!userStore.currentUser?.id) {
alert("用户未登录,无法发起通话")
return
}
resetCallState()
const callId = Date.now().toString() + Math.random().toString(36).substr(2, 9)
@@ -799,18 +853,8 @@ export const useChatStore = defineStore("chat", () => {
callStatus.value = "calling"
callConnectionStatus.value = "等待对方接听..."
// 获取本地媒体
try {
await webrtc.getLocalMedia({
audio: true,
video: type === "video",
})
} catch (error) {
console.error("获取本地媒体失败:", error)
resetCallState()
alert("无法访问摄像头或麦克风")
return
}
// 先不获取本地媒体,等对方接听后再获取
console.log("发起通话,等待对方接听...")
// 设置30秒超时无人接听
setCallTimeout(() => {
@@ -819,12 +863,8 @@ export const useChatStore = defineStore("chat", () => {
callConnectionStatus.value = "对方无人接听"
console.log("呼叫超时,对方无人接听")
// 发送无人接听信号
sendCallSignal({
call_status: "no-answer",
call_id: callId,
receiver_user_id: peerId,
})
// 发送无人接听信号 - 使用便捷方法
callAPI.sendNoAnswer(userStore.currentUser.id, peerId, callId, type === "video" ? 6 : 7)
// 更新好友最后消息为未接来电
friend.lastMessage = "[未接来电]"
@@ -849,21 +889,46 @@ export const useChatStore = defineStore("chat", () => {
}
}, 30000)
// 发送通话请求
sendCallSignal({
call_status: "invite",
call_id: callId,
call_type: type === "video" ? 6 : 7,
receiver_user_id: peerId,
})
// 发送通话请求 - 使用便捷方法
try {
console.log("准备发送通话邀请:", {
senderUserId: userStore.currentUser.id,
receiverUserId: peerId,
callId: callId,
callType: type === "video" ? 6 : 7,
})
console.log("发起通话:", currentCall.value)
await callAPI.invite(userStore.currentUser.id, peerId, callId, type === "video" ? 6 : 7)
console.log("发起通话成功:", currentCall.value)
} catch (error) {
console.error("发起通话失败:", error)
resetCallState()
// 提供更友好的错误提示
let errorMessage = "发起通话失败"
if (error.response?.status === 400) {
errorMessage = "请求参数错误,请检查网络连接"
} else if (error.response?.status === 500) {
errorMessage = "服务器错误,请稍后重试"
} else if (error.message) {
errorMessage = error.message
}
alert(errorMessage)
}
}
// 接听来电
const acceptCall = async () => {
if (!incomingCall.value) return
// 防止重复接听
if (callStatus.value === "connecting" || callStatus.value === "ongoing") {
console.log("通话已在进行中,忽略重复接听")
return
}
callStatus.value = "connecting"
callConnectionStatus.value = "正在连接..."
@@ -874,12 +939,13 @@ export const useChatStore = defineStore("chat", () => {
video: incomingCall.value.callType === "video",
})
// 发送接受信号
sendCallSignal({
call_status: "accepted",
call_id: incomingCall.value.callId,
receiver_user_id: incomingCall.value.callerId,
})
// 发送接受信号 - 使用便捷方法
await callAPI.accept(
userStore.currentUser.id,
incomingCall.value.callerId,
incomingCall.value.callId,
incomingCall.value.callType === "video" ? 6 : 7,
)
currentCall.value = {
callId: incomingCall.value.callId,
@@ -889,7 +955,7 @@ export const useChatStore = defineStore("chat", () => {
status: "connecting",
}
console.log("接听通话,等待Offer...")
console.log("接听通话成功,等待信令交换...")
} catch (error) {
console.error("接听通话失败:", error)
rejectCall("failed")
@@ -897,50 +963,64 @@ export const useChatStore = defineStore("chat", () => {
}
// 拒绝来电
const rejectCall = (reason = "rejected") => {
const rejectCall = async (reason = "rejected") => {
if (!incomingCall.value) return
// 发送拒绝信号
sendCallSignal({
call_status: reason,
call_id: incomingCall.value.callId,
receiver_user_id: incomingCall.value.callerId,
})
// 添加通话记录
const friend = friends.value.find((f) => f.id === incomingCall.value.callerId)
if (friend) {
const content = reason === "busy" ? "[对方忙线]" : "[已拒绝]"
const callMessage = {
type: incomingCall.value.callType === "video" ? "video-call" : "audio-call",
content: content,
time: new Date().toLocaleTimeString().slice(0, 5),
senderId: userStore.currentUser?.id,
read: true,
timestamp: Date.now(),
duration: 0,
try {
// 发送拒绝信号 - 使用便捷方法
if (reason === "rejected") {
await callAPI.reject(
userStore.currentUser.id,
incomingCall.value.callerId,
incomingCall.value.callId,
incomingCall.value.callType === "video" ? 6 : 7,
)
} else {
await callAPI.sendFailed(
userStore.currentUser.id,
incomingCall.value.callerId,
incomingCall.value.callId,
incomingCall.value.callType === "video" ? 6 : 7,
)
}
chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id)
// 更新最后消息
friend.lastMessage = content
chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, content, friend.unreadCount)
// 添加通话记录
const friend = friends.value.find((f) => f.id === incomingCall.value.callerId)
if (friend) {
const content = reason === "busy" ? "[对方忙线]" : "[已拒绝]"
const callMessage = {
type: incomingCall.value.callType === "video" ? "video-call" : "audio-call",
content: content,
time: new Date().toLocaleTimeString().slice(0, 5),
senderId: userStore.currentUser?.id,
read: true,
timestamp: Date.now(),
duration: 0,
}
chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id)
// 更新最后消息
friend.lastMessage = content
chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, content, friend.unreadCount)
}
resetCallState()
callConnectionStatus.value = reason === "busy" ? "对方忙线" : "已拒绝"
// 5秒后清除状态
setTimeout(() => {
callConnectionStatus.value = ""
}, 5000)
console.log("拒绝通话成功")
} catch (error) {
console.error("拒绝通话失败:", error)
}
resetCallState()
callConnectionStatus.value = reason === "busy" ? "对方忙线" : "已拒绝"
// 5秒后清除状态
setTimeout(() => {
callConnectionStatus.value = ""
}, 5000)
console.log("拒绝通话")
}
// 结束通话
const endCall = (reason = "ended") => {
const endCall = async (reason = "ended") => {
const callId = currentCall.value?.callId || incomingCall.value?.callId
const peerId = currentCall.value?.peerId || incomingCall.value?.callerId
const callTypeVal = currentCall.value?.callType || incomingCall.value?.callType
@@ -950,55 +1030,56 @@ export const useChatStore = defineStore("chat", () => {
return
}
// 发送结束信号
sendCallSignal({
call_status: reason,
call_id: callId,
receiver_user_id: peerId,
})
try {
// 发送结束信号 - 使用便捷方法
await callAPI.end(userStore.currentUser.id, peerId, callId, callTypeVal === "video" ? 6 : 7)
// 添加通话记录
const friend = friends.value.find((f) => f.id === peerId)
if (friend) {
const duration =
callStatus.value === "ongoing"
? Math.floor((Date.now() - (currentCall.value?.startTime || incomingCall.value?.startTime)) / 1000)
: 0
// 添加通话记录
const friend = friends.value.find((f) => f.id === peerId)
if (friend) {
const duration =
callStatus.value === "ongoing"
? Math.floor((Date.now() - (currentCall.value?.startTime || incomingCall.value?.startTime)) / 1000)
: 0
let content = ""
if (reason === "no-answer") {
content = "[未接来电]"
} else if (duration > 0) {
content = `[${callTypeVal === "video" ? "视频" : "语音"}通话] ${Math.floor(duration / 60)}${duration % 60}`
} else {
content = "[通话结束]"
let content = ""
if (reason === "no-answer") {
content = "[未接来电]"
} else if (duration > 0) {
content = `[${callTypeVal === "video" ? "视频" : "语音"}通话] ${Math.floor(duration / 60)}${duration % 60}`
} else {
content = "[通话结束]"
}
const callMessage = {
type: callTypeVal === "video" ? "video-call" : "audio-call",
content: content,
time: new Date().toLocaleTimeString().slice(0, 5),
senderId: userStore.currentUser?.id,
read: true,
timestamp: Date.now(),
duration: duration,
}
chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id)
// 更新最后消息
friend.lastMessage = content
chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, content, friend.unreadCount)
}
const callMessage = {
type: callTypeVal === "video" ? "video-call" : "audio-call",
content: content,
time: new Date().toLocaleTimeString().slice(0, 5),
senderId: userStore.currentUser?.id,
read: true,
timestamp: Date.now(),
duration: duration,
}
chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id)
resetCallState()
callConnectionStatus.value = reason === "no-answer" ? "对方无人接听" : "通话结束"
// 更新最后消息
friend.lastMessage = content
chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, content, friend.unreadCount)
// 5秒后清除状态
setTimeout(() => {
callConnectionStatus.value = ""
}, 5000)
console.log("结束通话成功")
} catch (error) {
console.error("结束通话失败:", error)
resetCallState()
}
resetCallState()
callConnectionStatus.value = reason === "no-answer" ? "对方无人接听" : "通话结束"
// 5秒后清除状态
setTimeout(() => {
callConnectionStatus.value = ""
}, 5000)
console.log("结束通话")
}
return {
@@ -1032,7 +1113,7 @@ export const useChatStore = defineStore("chat", () => {
rejectCall,
endCall,
resetCallState,
sendCallSignal,
sendCallSignal: sendCallSignalInternal,
clearCallTimeout,
}
})

View File

@@ -19,7 +19,7 @@ service.interceptors.request.use(
(error) => {
console.error("Request error:", error)
return Promise.reject(error)
}
},
)
// 响应拦截器
@@ -59,7 +59,7 @@ service.interceptors.response.use(
message.error("网络请求失败")
return Promise.reject(error)
}
},
)
// CORS错误的备用处理方案
@@ -107,7 +107,7 @@ export const upload = (url, file) => {
})
}
// 发送消息API(支持普通消息和通话信令)
// 发送普通消息API
export const sendMessage = (data) => {
const apiUrl = "/api/send-to-user"
@@ -121,7 +121,7 @@ export const sendMessage = (data) => {
"medical-record": 5,
"video-call": 6,
"audio-call": 7,
file: 8
file: 8,
}
const requestData = {
@@ -130,7 +130,7 @@ export const sendMessage = (data) => {
message_type: messageTypeMap[data.type] || 0,
message_content: data.content || "",
// 添加来源标记
isFromHttp: true
isFromHttp: true,
}
// 如果包含通话信令添加call_id和call_status
@@ -161,4 +161,182 @@ export const sendMessage = (data) => {
})
}
export default service
// 专门的通话信令发送方法
export const sendCallSignal = (signalData) => {
const apiUrl = "/api/send-to-user"
// 验证必要参数
if (!signalData.sender_user_id) {
throw new Error("缺少发送方用户ID")
}
if (!signalData.receiver_user_id) {
throw new Error("缺少接收方用户ID")
}
if (!signalData.call_id) {
throw new Error("缺少通话ID")
}
if (!signalData.call_status) {
throw new Error("缺少通话状态")
}
// 构建通话信令请求数据
const requestData = {
sender_user_id: signalData.sender_user_id,
receiver_user_id: signalData.receiver_user_id,
message_type: signalData.message_type || 6, // 默认视频通话
message_content: signalData.message_content || JSON.stringify(signalData.data || {}),
call_id: signalData.call_id,
call_status: signalData.call_status,
// 添加来源标记
isFromHttp: true,
}
console.log("发送通话信令到API:", apiUrl, requestData)
return axios
.post(apiUrl, requestData, {
headers: {
"Content-Type": "application/json",
},
timeout: 15000,
withCredentials: false,
})
.then((response) => {
console.log("通话信令发送成功:", response.data)
return response.data
})
.catch((error) => {
console.error("通话信令发送失败:", error)
// 提供更详细的错误信息
if (error.response) {
console.error("响应错误:", error.response.data)
console.error("响应状态:", error.response.status)
console.error("响应头:", error.response.headers)
} else if (error.request) {
console.error("请求错误:", error.request)
} else {
console.error("配置错误:", error.message)
}
throw error
})
}
// 通话相关的便捷方法
export const callAPI = {
// 发起通话邀请
invite: (senderUserId, receiverUserId, callId, callType = 6) => {
return sendCallSignal({
sender_user_id: senderUserId,
receiver_user_id: receiverUserId,
call_id: callId,
call_status: "invite",
message_type: callType, // 6=视频, 7=语音
})
},
// 接受通话
accept: (senderUserId, receiverUserId, callId, callType = 6) => {
return sendCallSignal({
sender_user_id: senderUserId,
receiver_user_id: receiverUserId,
call_id: callId,
call_status: "accepted",
message_type: callType,
})
},
// 拒绝通话
reject: (senderUserId, receiverUserId, callId, callType = 6) => {
return sendCallSignal({
sender_user_id: senderUserId,
receiver_user_id: receiverUserId,
call_id: callId,
call_status: "rejected",
message_type: callType,
})
},
// 结束通话
end: (senderUserId, receiverUserId, callId, callType = 6) => {
return sendCallSignal({
sender_user_id: senderUserId,
receiver_user_id: receiverUserId,
call_id: callId,
call_status: "ended",
message_type: callType,
})
},
// 发送WebRTC Offer
sendOffer: (senderUserId, receiverUserId, callId, offerData, callType = 6) => {
return sendCallSignal({
sender_user_id: senderUserId,
receiver_user_id: receiverUserId,
call_id: callId,
call_status: "offer",
message_type: callType,
data: offerData,
})
},
// 发送WebRTC Answer
sendAnswer: (senderUserId, receiverUserId, callId, answerData, callType = 6) => {
return sendCallSignal({
sender_user_id: senderUserId,
receiver_user_id: receiverUserId,
call_id: callId,
call_status: "answer",
message_type: callType,
data: answerData,
})
},
// 发送ICE候选
sendCandidate: (senderUserId, receiverUserId, callId, candidateData, callType = 6) => {
return sendCallSignal({
sender_user_id: senderUserId,
receiver_user_id: receiverUserId,
call_id: callId,
call_status: "candidate",
message_type: callType,
data: candidateData,
})
},
// 发送忙线状态
sendBusy: (senderUserId, receiverUserId, callId, callType = 6) => {
return sendCallSignal({
sender_user_id: senderUserId,
receiver_user_id: receiverUserId,
call_id: callId,
call_status: "busy",
message_type: callType,
})
},
// 发送无人接听
sendNoAnswer: (senderUserId, receiverUserId, callId, callType = 6) => {
return sendCallSignal({
sender_user_id: senderUserId,
receiver_user_id: receiverUserId,
call_id: callId,
call_status: "no-answer",
message_type: callType,
})
},
// 发送通话失败
sendFailed: (senderUserId, receiverUserId, callId, callType = 6) => {
return sendCallSignal({
sender_user_id: senderUserId,
receiver_user_id: receiverUserId,
call_id: callId,
call_status: "failed",
message_type: callType,
})
},
}
export default service