diff --git a/src/components/MessageInput.vue b/src/components/MessageInput.vue index 5e2be85..adb05a2 100644 --- a/src/components/MessageInput.vue +++ b/src/components/MessageInput.vue @@ -140,6 +140,7 @@ const sendUploadedFile = () => { content: uploadPreview.value.url, time: new Date().toLocaleTimeString().slice(0, 5), senderId: userStore.currentUser.id, + receiverId: chatStore.currentFriend.id, read: true, duration: 0 }; @@ -190,6 +191,7 @@ const sendTextMessage = async () => { duration: 0 }; + // return console.log('发送消息:' + userStore.currentUser.id, messageObj); // 添加到本地消息列表 chatStore.addMessage(messageObj, userStore.currentUser.id); diff --git a/src/components/VideoCallComponent.vue b/src/components/VideoCallComponent.vue index bac42dc..17742eb 100644 --- a/src/components/VideoCallComponent.vue +++ b/src/components/VideoCallComponent.vue @@ -119,7 +119,7 @@
-
+
对方忙线中 @@ -216,7 +216,6 @@ const callTimer = ref(null); const isCameraWorking = ref(true); const isMicWorking = ref(true); const isDragging = ref(false); -const isOngoing = ref(false); // 拖拽相关状态 const position = reactive({ x: 0, y: 0 }); @@ -230,7 +229,7 @@ const remoteVideoPreview = ref(null); // 获取当前通话状态 const callStatus = computed(() => { - return chatStore.callError; + return chatStore.callStatus; }); // 连接状态文本 @@ -241,16 +240,16 @@ const connectionText = computed(() => { } // 处理忙线状态 - if (chatStore.callError === 'busy') { + if (callStatus.value === "busy") { return '对方忙线中'; } // 默认状态显示 - if (props.isIncoming && !isOngoing.value) { + if (props.isIncoming && callStatus.value !== "ongoing") { return '来电中...'; } - if (isOngoing.value) { + if (callStatus.value === "ongoing") { return `通话中 ${formattedDuration.value}`; } @@ -354,19 +353,27 @@ const endCall = () => { if (remoteVideoPreview.value) remoteVideoPreview.value.srcObject = null; isVisible.value = false; + chatStore.endCall(); emit('end-call'); }; // 接听通话 const acceptCall = () => { - isOngoing.value = true; + // 确保只接听一次 + if (chatStore.callStatus === "ongoing") return; + initializeMedia(); + chatStore.acceptCall(); emit('accept-call'); }; // 拒绝通话 const rejectCall = () => { + // 确保只拒绝一次 + if (chatStore.callStatus === "idle") return; + isVisible.value = false; + chatStore.rejectCall(); emit('reject-call'); }; @@ -458,9 +465,12 @@ watch(() => webrtc.remoteStream.value, (newStream) => { // 监听通话状态 watch(() => chatStore.callStatus, (status) => { - if (status === 'ongoing') { - isOngoing.value = true; - } else if (status === 'ended') { + if (status === "ongoing") { + // 只在状态变化时初始化媒体 + if (!hasLocalVideo.value) { + initializeMedia(); + } + } else if (status === "ended" || status === "idle") { endCall(); } }); @@ -473,7 +483,7 @@ onMounted(() => { window.addEventListener('resize', handleWindowResize); // 如果是呼出方或已接听,初始化媒体 - if (!props.isIncoming || isOngoing.value) { + if (!props.isIncoming || callStatus.value === 'ongoing') { initializeMedia(); } }); diff --git a/src/composables/useWebRTC.js b/src/composables/useWebRTC.js index c2a580a..3ec5bb3 100644 --- a/src/composables/useWebRTC.js +++ b/src/composables/useWebRTC.js @@ -1,4 +1,6 @@ import { ref } from "vue" +import { useChatStore } from "@/stores/chat" +import { useUserStore } from "@/stores/user" export function useWebRTC() { const localStream = ref(null) @@ -7,19 +9,52 @@ export function useWebRTC() { const isConnected = ref(false) const isConnecting = ref(false) + const chatStore = useChatStore() + const userStore = useUserStore() + const configuration = { iceServers: [ { urls: "stun:stun.l.google.com:19302" }, - { urls: "stun:stun1.l.google.com:19302" } + { urls: "stun:stun1.l.google.com:19302" }, + { urls: "stun:stun2.l.google.com:19302" }, + { urls: "stun:stun3.l.google.com:19302" }, + { urls: "stun:stun4.l.google.com:19302" }, + // {urls:'stun:stun01.sipphone.com'}, + // {urls:'stun:stun.ekiga.net'}, + // {urls:'stun:stun.fwdnet.net'}, + // {urls:'stun:stun.ideasip.com'}, + // {urls:'stun:stun.iptel.org'}, + // {urls:'stun:stun.rixtelecom.se'}, + // {urls:'stun:stun.schlund.de'}, + // {urls:'stun:stun.l.google.com:19302'}, + // {urls:'stun:stun1.l.google.com:19302'}, + // {urls:'stun:stun2.l.google.com:19302'}, + // {urls:'stun:stun3.l.google.com:19302'}, + // {urls:'stun:stun4.l.google.com:19302'}, + // {urls:'stun:stunserver.org'}, + // {urls:'stun:stun.softjoys.com'}, + // {urls:'stun:stun.voiparound.com'}, + // {urls:'stun:stun.voipbuster.com'}, + // {urls:'stun:stun.voipstunt.com'}, + // {urls:'stun:stun.voxgratia.org'}, + // {urls:'stun:stun.xten.com'}, ] } - const createPeerConnection = () => { + const createPeerConnection = (callId, peerId) => { peerConnection.value = new RTCPeerConnection(configuration) peerConnection.value.onicecandidate = (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 + }) } } @@ -40,6 +75,7 @@ export function useWebRTC() { } else if (state === "disconnected" || state === "failed") { isConnected.value = false isConnecting.value = false + closeConnection(); } } @@ -56,9 +92,9 @@ export function useWebRTC() { } } - const createOffer = async () => { + const createOffer = async (callId, peerId) => { if (!peerConnection.value) { - createPeerConnection() + createPeerConnection(callId, peerId) } // 添加本地流到连接 @@ -78,9 +114,9 @@ export function useWebRTC() { return offer } - const createAnswer = async (offer) => { + const createAnswer = async (offer, callId, peerId) => { if (!peerConnection.value) { - createPeerConnection() + createPeerConnection(callId, peerId) } // 添加本地流到连接 diff --git a/src/stores/chat.js b/src/stores/chat.js index 10607c1..315596a 100644 --- a/src/stores/chat.js +++ b/src/stores/chat.js @@ -1,5 +1,5 @@ import { defineStore } from "pinia" -import { ref, computed } from "vue" +import { ref, computed, watch } from "vue" import { chatDB } from "@/utils/db" import { useWebRTC } from "@/composables/useWebRTC" import { useUserStore } from "@/stores/user" @@ -12,22 +12,82 @@ export const useChatStore = defineStore("chat", () => { const connectionStatus = ref("disconnected") const socket = ref(null) - // 通话相关状态 + // 通话相关状态增强 const currentCall = ref(null) const incomingCall = ref(null) - const callStatus = ref("idle") // idle, calling, ringing, ongoing, ended - const callError = ref(null) // 错误状态:'rejected', 'no-answer', 'busy' - const callConnectionStatus = ref("") // 连接状态:等待接听、正在连接等 + const callStatus = ref("idle") // idle, calling, ringing, connecting, ongoing, ended, disconnected, hangup + const callError = ref(null) // 错误状态:'rejected', 'no-answer', 'busy', 'failed' + const callConnectionStatus = ref("") // 连接状态描述 + const callTimeout = ref(null) // 通话超时计时器 const webrtc = useWebRTC() const userStore = useUserStore() // 计算属性 const isConnected = computed(() => connectionStatus.value === "connected") - const isInCall = computed(() => callStatus.value !== "idle") + const isInCall = computed(() => + ["calling", "ringing", "connecting", "ongoing"].includes(callStatus.value) + ) const isCalling = computed(() => callStatus.value === "calling") const isRinging = computed(() => callStatus.value === "ringing") + const isConnecting = computed(() => callStatus.value === "connecting") const isOngoingCall = computed(() => callStatus.value === "ongoing") + const isDisconnected = computed(() => callStatus.value === "disconnected") + const isHangup = computed(() => callStatus.value === "hangup") + + // 监听通话状态变化 + watch(() => callStatus.value, (newStatus) => { + console.log("通话状态变化:", newStatus) + switch(newStatus) { + case "disconnected": + handleCallDisconnected() + break + case "hangup": + handleCallHangup() + break + } + }) + + // 重置所有通话状态 + const resetCallState = () => { + clearCallTimeout() + callStatus.value = "idle" + callError.value = null + callConnectionStatus.value = "" + currentCall.value = null + incomingCall.value = null + } + + // 清除通话超时 + const clearCallTimeout = () => { + if (callTimeout.value) { + clearTimeout(callTimeout.value) + callTimeout.value = null + } + } + + // 设置通话超时 + const setCallTimeout = (callback, duration = 30000) => { + clearCallTimeout() + callTimeout.value = setTimeout(() => { + callback() + clearCallTimeout() + }, duration) + } + + // 发送通话信令 + const sendCallSignal = (signal) => { + if (socket.value && socket.value.readyState === WebSocket.OPEN) { + const message = { + request_type: "call_signal", + ...signal, + sender_user_id: userStore.currentUser?.id + } + socket.value.send(JSON.stringify(message)) + } else { + console.error("WebSocket未连接,无法发送通话信令") + } + } // 初始化数据库 const initDB = async () => { @@ -106,24 +166,25 @@ export const useChatStore = defineStore("chat", () => { const addMessage = async (message, currentUserId) => { messages.value.push(message) - if (currentFriend.value) { + // 更新好友列表中的最后消息 + const friend = friends.value.find((f) => (f.id === message.senderId || f.id === currentFriend.value?.id)) + if (friend) { + let lastMessage = message.content + if (message.type === "image") lastMessage = "[图片]" + if (message.type === "video") lastMessage = "[视频]" + if (message.type === "audio") lastMessage = "[语音]" + if (message.type === "video-call" || message.type === "audio-call") lastMessage = "[通话]" + + friend.lastMessage = lastMessage.length > 20 ? lastMessage.substring(0, 20) + "..." : lastMessage + + // 更新数据库中的好友信息 + await chatDB.updateFriendLastMessage(friend.id, currentUserId, friend.lastMessage, 0) + } + + if (currentFriend.value && friend && friend.id === currentFriend.value.id) { try { // 保存到数据库 await chatDB.saveMessage(message, currentUserId, currentFriend.value.id) - - // 更新好友列表中的最后消息 - const friend = friends.value.find((f) => f.id === currentFriend.value.id) - if (friend) { - let lastMessage = message.content - if (message.type === "image") lastMessage = "[图片]" - if (message.type === "video") lastMessage = "[视频]" - if (message.type === "audio") lastMessage = "[语音]" - - friend.lastMessage = lastMessage.length > 20 ? lastMessage.substring(0, 20) + "..." : lastMessage - - // 更新数据库中的好友信息 - await chatDB.updateFriendLastMessage(friend.id, currentUserId, friend.lastMessage, 0) - } } catch (error) { console.error("保存消息失败:", error) } @@ -172,18 +233,22 @@ export const useChatStore = defineStore("chat", () => { const friend = friends.value.find((f) => f.id == senderId) if (friend) { friend.unreadCount = (friend.unreadCount || 0) + 1 - - // 更新最后消息 - let lastMessage = newMessage.content - if (newMessage.type === "image") lastMessage = "[图片]" - if (newMessage.type === "video") lastMessage = "[视频]" - if (newMessage.type === "audio") lastMessage = "[语音]" - friend.lastMessage = lastMessage - - // 更新数据库 - await chatDB.updateFriendLastMessage(friend.id, currentUserId, lastMessage, friend.unreadCount) } } + + // 更新最后消息 + const friend = friends.value.find((f) => f.id == senderId) + if (friend) { + let lastMessage = newMessage.content + if (newMessage.type === "image") lastMessage = "[图片]" + if (newMessage.type === "video") lastMessage = "[视频]" + if (newMessage.type === "audio") lastMessage = "[语音]" + if (newMessage.type === "video-call" || newMessage.type === "audio-call") lastMessage = "[通话]" + friend.lastMessage = lastMessage + + // 更新数据库 + await chatDB.updateFriendLastMessage(friend.id, currentUserId, lastMessage, friend.unreadCount) + } } catch (error) { console.error("处理接收消息失败:", error) } @@ -202,6 +267,7 @@ export const useChatStore = defineStore("chat", () => { // 处理通话信令 const handleCallSignal = (data) => { console.log("处理通话信令:", data) + console.log("通话状态:", data.call_status) // 根据通话状态处理 switch (data.call_status) { @@ -220,22 +286,79 @@ export const useChatStore = defineStore("chat", () => { case "candidate": handleIceCandidate(data) break - case "no-answer": // 添加无人接听状态 + case "no-answer": handleCallNoAnswer(data) break - case "connecting": // 新增连接状态 - callConnectionStatus.value = "正在连接..." + case "connecting": + handleCallConnecting(data) break - case "busy": // 对方忙线中 + case "busy": handleCallBusy(data) break + case "hangup": // 对方主动挂断 + handleCallHangup(data) + break + case "disconnected": // 对方掉线 + handleCallDisconnected(data) + break + case "terminated": // 对方终止呼叫 + handleCallTerminated(data) + break + case "failed": + handleCallFailed(data) + break + } + } + + // 处理连接中状态 + const handleCallConnecting = (data) => { + callConnectionStatus.value = "正在连接..." + if (currentCall.value?.callId === data.call_id) { + callStatus.value = "connecting" + } + } + + // 处理通话失败 + const handleCallFailed = (data) => { + if ((currentCall.value && currentCall.value.callId === data.call_id) || + (incomingCall.value && incomingCall.value.callId === data.call_id)) { + callError.value = "failed" + callConnectionStatus.value = "连接失败" + + // 添加通话记录 + const peerId = currentCall.value?.peerId || incomingCall.value?.callerId + const friend = friends.value.find(f => f.id === peerId) + if (friend) { + const callMessage = { + type: currentCall.value?.callType === "video" || incomingCall.value?.callType === "video" ? + "video-call" : "audio-call", + content: "[连接失败]", + time: new Date().toLocaleTimeString().slice(0, 5), + senderId: peerId, + read: true, + timestamp: Date.now(), + duration: 0 + } + chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id) + + // 更新最后消息 + friend.lastMessage = "[连接失败]" + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, "[连接失败]", friend.unreadCount) + } + + // 5秒后清除状态 + setTimeout(() => { + resetCallState() + }, 5000) } } // 处理来电 const handleIncomingCall = (data) => { - // 如果自己正在通话中,自动拒绝并发送忙线状态 + // 只有在当前没有通话时才处理来电 if (isInCall.value) { + // console.log 输出当前状态 + console.log("当前正在通话中,发送忙线状态") sendMessage({ senderId: userStore.currentUser?.id, receiverId: data.sender_user_id, @@ -249,11 +372,26 @@ export const useChatStore = defineStore("chat", () => { if (friend) { friend.lastMessage = "[未接来电]" chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, "[未接来电]", friend.unreadCount) + + // 添加通话记录 + const callMessage = { + type: data.message_type === 6 ? "video-call" : "audio-call", + content: "[未接来电]", + time: new Date().toLocaleTimeString().slice(0, 5), + senderId: data.sender_user_id, + read: true, + timestamp: Date.now(), + duration: 0 + } + chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id) } return } + // 重置所有通话状态 + resetCallState(); + incomingCall.value = { callId: data.call_id, callerId: data.sender_user_id, @@ -280,16 +418,31 @@ export const useChatStore = defineStore("chat", () => { // 处理通话拒绝 const handleCallRejected = (data) => { if (currentCall.value && currentCall.value.callId === data.call_id) { - callStatus.value = "ended" - callError.value = "rejected" + resetCallState(); callConnectionStatus.value = "对方已拒绝" - currentCall.value = null console.log("通话已被拒绝") + // 添加通话记录 + const friend = friends.value.find(f => f.id === currentCall.value.peerId) + if (friend) { + const callMessage = { + type: currentCall.value.callType === "video" ? "video-call" : "audio-call", + 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 = "[对方已拒绝]" + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, "[对方已拒绝]", friend.unreadCount) + } + // 5秒后清除状态 setTimeout(() => { - callStatus.value = "idle" - callError.value = null callConnectionStatus.value = "" }, 5000) } @@ -299,16 +452,38 @@ export const useChatStore = defineStore("chat", () => { const handleCallEnded = (data) => { if ((currentCall.value && currentCall.value.callId === data.call_id) || (incomingCall.value && incomingCall.value.callId === data.call_id)) { - callStatus.value = "ended" - callError.value = null + // 添加通话记录 + const peerId = currentCall.value?.peerId || incomingCall.value?.callerId + 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 callMessage = { + type: currentCall.value?.callType === "video" || incomingCall.value?.callType === "video" ? + "video-call" : "audio-call", + content: callStatus.value === "ongoing" ? + `[${currentCall.value?.callType === "video" ? "视频" : "语音"}通话] ${Math.floor(duration/60)}分${duration%60}秒` : + "[通话结束]", + time: new Date().toLocaleTimeString().slice(0, 5), + senderId: peerId, + read: true, + timestamp: Date.now(), + duration: duration + } + chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id) + + // 更新最后消息 + friend.lastMessage = callMessage.content + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, callMessage.content, friend.unreadCount) + } + + resetCallState(); callConnectionStatus.value = "通话已结束" - currentCall.value = null - incomingCall.value = null console.log("通话已结束") // 5秒后清除状态 setTimeout(() => { - callStatus.value = "idle" callConnectionStatus.value = "" }, 5000) } @@ -316,25 +491,43 @@ export const useChatStore = defineStore("chat", () => { // 处理ICE候选 const handleIceCandidate = (data) => { - if (currentCall.value && currentCall.value.callId === data.call_id) { - console.log("收到ICE候选:", data.content) - // 这里可以添加WebRTC处理逻辑 + if ((currentCall.value && currentCall.value.callId === data.call_id) || + (incomingCall.value && incomingCall.value.callId === data.call_id)) { + console.log("收到ICE候选:", data.candidate) + + // 添加到WebRTC连接 + webrtc.addIceCandidate(data.candidate) } } // 处理无人接听 const handleCallNoAnswer = (data) => { if (currentCall.value && currentCall.value.callId === data.call_id) { - callStatus.value = "ended" - callError.value = "no-answer" + // 添加未接来电记录 + const friend = friends.value.find(f => f.id === currentCall.value.peerId) + if (friend) { + const callMessage = { + type: currentCall.value.callType === "video" ? "video-call" : "audio-call", + 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 = "[未接来电]" + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, "[未接来电]", friend.unreadCount) + } + + resetCallState(); callConnectionStatus.value = "对方无人接听" - currentCall.value = null console.log("对方无人接听") // 5秒后清除状态 setTimeout(() => { - callStatus.value = "idle" - callError.value = null callConnectionStatus.value = "" }, 5000) } @@ -343,16 +536,152 @@ export const useChatStore = defineStore("chat", () => { // 处理对方忙线 const handleCallBusy = (data) => { if (currentCall.value && currentCall.value.callId === data.call_id) { - callStatus.value = "ended" - callError.value = "busy" + // 添加忙线记录 + const friend = friends.value.find(f => f.id === currentCall.value.peerId) + if (friend) { + const callMessage = { + type: currentCall.value.callType === "video" ? "video-call" : "audio-call", + 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 = "[对方忙线]" + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, "[对方忙线]", friend.unreadCount) + } + + resetCallState(); callConnectionStatus.value = "对方忙线中" - currentCall.value = null console.log("对方忙线中") // 5秒后清除状态 setTimeout(() => { - callStatus.value = "idle" - callError.value = null + callConnectionStatus.value = "" + }, 5000) + } + } + + // 处理对方挂断 + const handleCallHangup = (data) => { + const callId = data.call_id + const peerId = data.sender_user_id + + if ((currentCall.value && currentCall.value.callId === callId) || + (incomingCall.value && incomingCall.value.callId === callId)) { + + // 添加通话记录 + 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 callMessage = { + type: currentCall.value?.callType || incomingCall.value?.callType === "video" ? + "video-call" : "audio-call", + content: `[对方已挂断] ${Math.floor(duration/60)}分${duration%60}秒`, + time: new Date().toLocaleTimeString().slice(0, 5), + senderId: peerId, + read: true, + timestamp: Date.now(), + duration: duration + } + chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id) + + // 更新最后消息 + friend.lastMessage = `[对方已挂断] ${Math.floor(duration/60)}分${duration%60}秒` + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, friend.lastMessage, friend.unreadCount) + } + + resetCallState() + callConnectionStatus.value = "对方已挂断" + console.log("对方已挂断通话") + + // 5秒后清除状态 + setTimeout(() => { + callConnectionStatus.value = "" + }, 5000) + } + } + + // 处理对方掉线 + const handleCallDisconnected = (data) => { + const callId = data?.call_id || currentCall.value?.callId || incomingCall.value?.callId + const peerId = data?.sender_user_id || currentCall.value?.peerId || incomingCall.value?.callerId + + if (callId && peerId) { + // 添加通话记录 + 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 callMessage = { + type: currentCall.value?.callType || incomingCall.value?.callType === "video" ? + "video-call" : "audio-call", + content: `[网络中断] ${Math.floor(duration/60)}分${duration%60}秒`, + time: new Date().toLocaleTimeString().slice(0, 5), + senderId: peerId, + read: true, + timestamp: Date.now(), + duration: duration + } + chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id) + + // 更新最后消息 + friend.lastMessage = `[网络中断] ${Math.floor(duration/60)}分${duration%60}秒` + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, friend.lastMessage, friend.unreadCount) + } + + resetCallState() + callConnectionStatus.value = "网络连接已断开" + console.log("网络连接已断开") + + // 5秒后清除状态 + setTimeout(() => { + callConnectionStatus.value = "" + }, 5000) + } + } + + // 处理对方终止呼叫 + const handleCallTerminated = (data) => { + const callId = data.call_id + const peerId = data.sender_user_id + + if ((currentCall.value && currentCall.value.callId === callId) || + (incomingCall.value && incomingCall.value.callId === callId)) { + + // 添加通话记录 + const friend = friends.value.find(f => f.id === peerId) + if (friend) { + const callMessage = { + type: currentCall.value?.callType || incomingCall.value?.callType === "video" ? + "video-call" : "audio-call", + content: "[对方已终止呼叫]", + time: new Date().toLocaleTimeString().slice(0, 5), + senderId: peerId, + read: true, + timestamp: Date.now(), + duration: 0 + } + chatDB.saveMessage(callMessage, userStore.currentUser.id, friend.id) + + // 更新最后消息 + friend.lastMessage = "[对方已终止呼叫]" + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, "[对方已终止呼叫]", friend.unreadCount) + } + + resetCallState() + callConnectionStatus.value = "对方已终止呼叫" + console.log("对方已终止呼叫") + + // 5秒后清除状态 + setTimeout(() => { callConnectionStatus.value = "" }, 5000) } @@ -371,6 +700,8 @@ export const useChatStore = defineStore("chat", () => { return } + resetCallState() + const callId = Date.now().toString() + Math.random().toString(36).substr(2, 9) currentCall.value = { @@ -382,14 +713,32 @@ export const useChatStore = defineStore("chat", () => { } callStatus.value = "calling" - callError.value = null callConnectionStatus.value = "等待对方接听..." + // 创建WebRTC连接 + webrtc.createPeerConnection(callId, peerId, () => { + // WebRTC连接成功回调 + callStatus.value = "ongoing" + callConnectionStatus.value = "通话中" + clearCallTimeout() + }, (error) => { + // WebRTC连接失败回调 + console.error("WebRTC连接失败:", error) + sendMessage({ + senderId: userStore.currentUser?.id, + receiverId: peerId, + type: type === "video" ? "video-call" : "audio-call", + callId: callId, + callStatus: "failed" + }) + resetCallState() + callConnectionStatus.value = "连接失败" + }) + // 设置30秒超时(无人接听) - const timeoutId = setTimeout(() => { + setCallTimeout(() => { if (callStatus.value === "calling") { - callStatus.value = "ended" - callError.value = "no-answer" + resetCallState() callConnectionStatus.value = "对方无人接听" console.log("呼叫超时,对方无人接听") @@ -406,17 +755,25 @@ export const useChatStore = defineStore("chat", () => { friend.lastMessage = "[未接来电]" chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, "[未接来电]", friend.unreadCount) + // 添加通话记录 + const callMessage = { + type: type === "video" ? "video-call" : "audio-call", + 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) + // 5秒后清除状态 setTimeout(() => { - callStatus.value = "idle" - currentCall.value = null callConnectionStatus.value = "" }, 5000) } }, 30000) - currentCall.value.timeoutId = timeoutId - // 发送通话请求 sendMessage({ senderId: userStore.currentUser?.id, @@ -433,37 +790,67 @@ export const useChatStore = defineStore("chat", () => { const acceptCall = () => { if (!incomingCall.value) return - // 清除可能存在的超时计时器 - if (currentCall.value?.timeoutId) { - clearTimeout(currentCall.value.timeoutId) - } - - // 发送接受信号 + // 发送连接中信号 sendMessage({ senderId: userStore.currentUser?.id, receiverId: incomingCall.value.callerId, type: incomingCall.value.callType === "video" ? "video-call" : "audio-call", callId: incomingCall.value.callId, - callStatus: "accepted" + callStatus: "connecting" }) - currentCall.value = { - callId: incomingCall.value.callId, - peerId: incomingCall.value.callerId, - callType: incomingCall.value.callType, - startTime: Date.now(), - status: 'ongoing' - } + callStatus.value = "connecting" + callConnectionStatus.value = "正在连接..." - callStatus.value = "ongoing" - callConnectionStatus.value = "通话中" - incomingCall.value = null - callError.value = null - console.log("接听通话") + // 创建WebRTC连接 + webrtc.createPeerConnection( + incomingCall.value.callId, + incomingCall.value.callerId, + () => { + // WebRTC连接成功回调 + callStatus.value = "ongoing" + callConnectionStatus.value = "通话中" + + // 发送接受信号 + sendMessage({ + senderId: userStore.currentUser?.id, + receiverId: incomingCall.value.callerId, + type: incomingCall.value.callType === "video" ? "video-call" : "audio-call", + callId: incomingCall.value.callId, + callStatus: "accepted" + }) + + currentCall.value = { + callId: incomingCall.value.callId, + peerId: incomingCall.value.callerId, + callType: incomingCall.value.callType, + startTime: Date.now(), + status: 'ongoing' + } + + incomingCall.value = null + console.log("通话已连接") + }, + (error) => { + // WebRTC连接失败回调 + console.error("WebRTC连接失败:", error) + sendMessage({ + senderId: userStore.currentUser?.id, + receiverId: incomingCall.value.callerId, + type: incomingCall.value.callType === "video" ? "video-call" : "audio-call", + callId: incomingCall.value.callId, + callStatus: "failed" + }) + resetCallState() + callConnectionStatus.value = "连接失败" + } + ) + + console.log("正在连接通话...") } // 拒绝来电 - const rejectCall = () => { + const rejectCall = (reason = "rejected") => { if (!incomingCall.value) return // 发送拒绝信号 @@ -472,26 +859,35 @@ export const useChatStore = defineStore("chat", () => { receiverId: incomingCall.value.callerId, type: incomingCall.value.callType === "video" ? "video-call" : "audio-call", callId: incomingCall.value.callId, - callStatus: "rejected" + callStatus: reason // 可以是'rejected'或'busy' }) - callStatus.value = "ended" - callConnectionStatus.value = "已拒绝" - callError.value = "rejected" - - // 更新好友最后消息为已拒绝 + // 添加通话记录 const friend = friends.value.find(f => f.id === incomingCall.value.callerId) if (friend) { - friend.lastMessage = "[已拒绝]" - chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, "[已拒绝]", friend.unreadCount) + 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) } - incomingCall.value = null + resetCallState() + callConnectionStatus.value = reason === "busy" ? "对方忙线" : "已拒绝" // 5秒后清除状态 setTimeout(() => { - callStatus.value = "idle" - callError.value = null callConnectionStatus.value = "" }, 5000) @@ -499,41 +895,157 @@ export const useChatStore = defineStore("chat", () => { } // 结束通话 - const endCall = () => { + const endCall = (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 if (!callId || !peerId) return - // 清除超时计时器 - if (currentCall.value?.timeoutId) { - clearTimeout(currentCall.value.timeoutId) - } - // 发送结束信号 sendMessage({ senderId: userStore.currentUser?.id, receiverId: peerId, type: callTypeVal === "video" ? "video-call" : "audio-call", callId: callId, - callStatus: "ended" + callStatus: reason // 可以是'ended'或'no-answer' }) - callStatus.value = "ended" - callConnectionStatus.value = "通话结束" + // 添加通话记录 + 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 === "视频" ? "视频" : "语音"}通话] ${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) + } + + resetCallState() + callConnectionStatus.value = reason === "no-answer" ? "对方无人接听" : "通话结束" + // 5秒后清除状态 setTimeout(() => { - callStatus.value = "idle" - currentCall.value = null - incomingCall.value = null - callError.value = null callConnectionStatus.value = "" }, 5000) console.log("结束通话") } + // 主动挂断通话(用户点击挂断按钮) + const hangupCall = () => { + const callId = currentCall.value?.callId || incomingCall.value?.callId + const peerId = currentCall.value?.peerId || incomingCall.value?.callerId + const callTypeVal = currentCall.value?.callType || incomingCall.value?.callType + + if (!callId || !peerId) return + + // 发送挂断信号 + sendMessage({ + senderId: userStore.currentUser?.id, + receiverId: peerId, + type: callTypeVal === "video" ? "video-call" : "audio-call", + callId: callId, + callStatus: "hangup" + }) + + // 添加通话记录 + 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 callMessage = { + type: callTypeVal === "video" ? "video-call" : "audio-call", + content: `[已挂断] ${Math.floor(duration/60)}分${duration%60}秒`, + 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 = `[已挂断] ${Math.floor(duration/60)}分${duration%60}秒` + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, friend.lastMessage, friend.unreadCount) + } + + resetCallState() + callConnectionStatus.value = "已挂断" + + // 5秒后清除状态 + setTimeout(() => { + callConnectionStatus.value = "" + }, 5000) + + console.log("主动挂断通话") + } + + // 终止呼叫(在对方接听前取消) + const terminateCall = () => { + if (!currentCall.value) return + + // 发送终止信号 + sendMessage({ + senderId: userStore.currentUser?.id, + receiverId: currentCall.value.peerId, + type: currentCall.value.callType === "video" ? "video-call" : "audio-call", + callId: currentCall.value.callId, + callStatus: "terminated" + }) + + // 添加通话记录 + const friend = friends.value.find(f => f.id === currentCall.value.peerId) + if (friend) { + const callMessage = { + type: currentCall.value.callType === "video" ? "video-call" : "audio-call", + 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 = "[已取消]" + chatDB.updateFriendLastMessage(friend.id, userStore.currentUser.id, "[已取消]", friend.unreadCount) + } + + resetCallState() + callConnectionStatus.value = "已取消呼叫" + + // 5秒后清除状态 + setTimeout(() => { + callConnectionStatus.value = "" + }, 5000) + + console.log("终止呼叫") + } + return { friends, currentFriend, @@ -549,7 +1061,10 @@ export const useChatStore = defineStore("chat", () => { isInCall, isCalling, isRinging, + isConnecting, isOngoingCall, + isDisconnected, + isHangup, initDB, loadFriends, setCurrentFriend, @@ -560,6 +1075,11 @@ export const useChatStore = defineStore("chat", () => { startCall, acceptCall, rejectCall, - endCall + endCall, + hangupCall, + terminateCall, + resetCallState, + sendCallSignal, + clearCallTimeout } }) \ No newline at end of file diff --git a/src/views/Chat.vue b/src/views/Chat.vue index 2d1f950..0da62f2 100644 --- a/src/views/Chat.vue +++ b/src/views/Chat.vue @@ -93,7 +93,8 @@ const handleNavChange = (nav) => { // 监听通话状态变化 watch(() => chatStore.callStatus, (status) => { - showVideoCall.value = status !== 'idle'; + // 只有当通话状态是活跃状态时才显示视频组件 + showVideoCall.value = status === "connecting" || status === "calling" || status === "ringing" || status === "ongoing"; }); // 监听来电 @@ -113,6 +114,9 @@ watch(() => chatStore.currentCall, (call) => { callerInfo.value = chatStore.friends.find(f => f.id === call.peerId); callType.value = call.callType; isIncoming.value = false; + } else { + // 当通话结束时重置状态 + showVideoCall.value = false; } }); @@ -126,10 +130,11 @@ const handleRejectCall = () => { chatStore.rejectCall(); }; -// 处理结束通话 +// 处理结束通话 - 修复挂断逻辑 const handleEndCall = () => { chatStore.endCall(); }; + // 加载主题 themeStore.loadTheme(); @@ -150,7 +155,6 @@ onUnmounted(() => { disconnectWebSocket(); }); -