修复:
1. 接听电话以实现 2. 后端接口适配实现 现有问题: 1. rtc通话问题待修复 2. 点击接听会有挂断的报错,需要检查逻辑 3. 修复lastMessage的更新问题
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user