修复:

1. 重复请求
2. 后端接口问题修复

已知bug:
1. 交换信令问题
2. rtc未测试
This commit is contained in:
2025-07-08 16:43:09 +08:00
parent 5fe9c29d6e
commit 36bb210539
5 changed files with 704 additions and 132 deletions

View File

@@ -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);

View File

@@ -119,7 +119,7 @@
<!-- 控制栏 -->
<div v-if="!isMinimized" class="call-controls">
<!-- 来电控制按钮 -->
<div v-if="isIncoming && !isOngoing" class="incoming-call-controls">
<div v-if="isIncoming && callStatus !== 'ongoing'" class="incoming-call-controls">
<div v-if="callStatus === 'busy'" class="busy-status">
<i class="fas fa-phone-slash"></i>
<span>对方忙线中</span>
@@ -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();
}
});

View File

@@ -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)
}
// 添加本地流到连接

View File

@@ -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
}
})

View File

@@ -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();
});
</script>
<style scoped>
@keyframes float {
0% { transform: translateY(0px) rotate(0deg); }