diff --git a/src/App.ku.vue b/src/App.ku.vue
index d5c4033..58c4ea7 100644
--- a/src/App.ku.vue
+++ b/src/App.ku.vue
@@ -1,5 +1,6 @@
@@ -7,6 +8,7 @@ const { theme } = useTheme()
+
+
-
diff --git a/src/App.vue b/src/App.vue
index 3417c02..4dd9f70 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -3,13 +3,11 @@ import { onLaunch, onShow } from '@dcloudio/uni-app'
import { useTheme } from '@/composables/useTheme'
import { useAuthStore, useConversationStore } from '@/stores'
import { wsManager } from '@/api/websocket'
-import { useGroupWebRTC } from '@/composables/useGroupWebRTC'
import type { ChatMessage } from '@/types/api'
const { initTheme } = useTheme()
const authStore = useAuthStore()
const conversationStore = useConversationStore()
-const groupWebRTC = useGroupWebRTC()
// 全局消息处理器
function handleGlobalMessage(message: ChatMessage) {
@@ -30,8 +28,7 @@ async function initWebSocket() {
await wsManager.connect(userId)
// 注册全局消息处理器
wsManager.onMessage(handleGlobalMessage)
- // 初始化群通话信令监听器
- groupWebRTC.initListener()
+ // 群通话信令监听器已移至 App.ku.vue 初始化
console.log('✅ WebSocket 全局初始化成功')
} catch (error) {
console.error('WebSocket 初始化失败:', error)
diff --git a/src/components/call/CallWindow.vue b/src/components/call/CallWindow.vue
index 6711f0d..b740e64 100644
--- a/src/components/call/CallWindow.vue
+++ b/src/components/call/CallWindow.vue
@@ -31,13 +31,15 @@
+
+ :srcObject="remoteStream"
+ >
@@ -47,6 +49,7 @@
{{ call.callerName || '对方' }}
+
+ :srcObject="localStream"
+ >
diff --git a/src/components/call/GlobalCallProvider.vue b/src/components/call/GlobalCallProvider.vue
index d26a775..a13ffa7 100644
--- a/src/components/call/GlobalCallProvider.vue
+++ b/src/components/call/GlobalCallProvider.vue
@@ -1,4 +1,20 @@
+
+
+
+
@@ -10,16 +26,30 @@
diff --git a/src/composables/useWebRTC.ts b/src/composables/useWebRTC.ts
index 0bb7ebf..5f250bd 100644
--- a/src/composables/useWebRTC.ts
+++ b/src/composables/useWebRTC.ts
@@ -1,11 +1,13 @@
/**
- * WebRTC 通话组合式函数 - UniApp 适配版
- * 主要支持 H5 平台,APP/小程序需要平台特定实现
+ * WebRTC 通话组合式函数 - 全局单例模式
+ * 支持在任意页面接听来电
*/
-import { reactive, shallowRef, ref } from 'vue'
+import { reactive, shallowRef, ref, computed } from 'vue'
import * as messageApi from '@/api/modules/message'
import { wsManager } from '@/api/websocket'
+import { useAuthStore } from '@/stores/auth'
import { useChatStore } from '@/stores/chat'
+import { useConversationStore } from '@/stores/conversation'
import type { ChatMessage, Contact } from '@/types/api'
import type { CallStatus } from '@/types/message'
@@ -24,46 +26,46 @@ export interface CallState {
startTime: number | null
callerName?: string
callerAvatar?: string
+ callerId?: string
}
-export function useWebRTC(
- userId: string,
- onIncomingCall?: (senderUserId: string) => void,
- getRoomId?: (receiverUserId?: string) => string
-) {
+// --- 全局单例状态 ---
+const call = reactive({
+ active: false,
+ minimized: false,
+ type: 'video',
+ status: 'idle',
+ statusText: '',
+ id: null,
+ muted: false,
+ remoteMuted: false,
+ camOff: false,
+ remoteCamOff: false,
+ duration: 0,
+ startTime: null,
+})
+
+const isCaller = ref(false)
+const localStream = shallowRef(null)
+const remoteStream = shallowRef(null)
+let durationTimer: ReturnType | null = null
+let pc: RTCPeerConnection | null = null
+const pendingCandidates: RTCIceCandidate[] = []
+let currentReceiverUserId = ''
+let currentRoomId = ''
+let audioContext: UniApp.InnerAudioContext | null = null
+
+export function useWebRTC() {
+ const authStore = useAuthStore()
const chatStore = useChatStore()
+ const conversationStore = useConversationStore()
- const call = reactive({
- active: false,
- minimized: false,
- type: 'video',
- status: 'idle',
- statusText: '',
- id: null,
- muted: false,
- remoteMuted: false,
- camOff: false,
- remoteCamOff: false,
- duration: 0,
- startTime: null,
- })
-
- const isCaller = ref(false)
- const localStream = shallowRef(null)
- const remoteStream = shallowRef(null)
- let durationTimer: ReturnType | null = null
- let pc: RTCPeerConnection | null = null
- const pendingCandidates: RTCIceCandidate[] = []
- let currentReceiverUserId = ''
- let currentRoomId = ''
-
- // 音频播放(使用内置音频方法)
- let audioContext: UniApp.InnerAudioContext | null = null
+ const userId = computed(() => authStore.user?.id || '')
+ const isActive = computed(() => call.active)
function playRingtone() {
stopRingtone()
// #ifdef H5
- // H5 使用简单的提示音
try {
const AudioContext = window.AudioContext || (window as any).webkitAudioContext
const ctx = new AudioContext()
@@ -87,22 +89,38 @@ export function useWebRTC(
function getSafeRoomId(targetUserId?: string): string | null {
if (currentRoomId) return currentRoomId
- if (targetUserId && getRoomId) {
- const rid = getRoomId(targetUserId)
- if (rid) return rid
+
+ // 尝试从联系人获取房间ID
+ if (targetUserId) {
+ const contact = chatStore.contacts.find(c => c.contact_user_id === targetUserId)
+ if (contact?.room_id) return contact.room_id
}
- if (targetUserId && userId) {
- return [userId, targetUserId].sort().join('_')
+
+ // 尝试从会话获取房间ID
+ if (targetUserId) {
+ const conv = conversationStore.conversations.find(c => c.target_id === targetUserId)
+ if (conv?.room_id) return conv.room_id
+ }
+
+ // 生成临时房间ID
+ if (targetUserId && userId.value) {
+ return [userId.value, targetUserId].sort().join('_')
}
return null
}
+ // 初始化信令监听器
+ function initListener() {
+ wsManager.offSignal(handleSignaling)
+ wsManager.onSignal(handleSignaling)
+ }
+
// 信令处理
async function handleSignaling(message: ChatMessage) {
try {
const content = message.content ? JSON.parse(message.content) : {}
- // 忽略群聊信令
+ // 忽略群聊信令(由 useGroupWebRTC 处理)
if (content.callRoomId || content.participantIds) {
return
}
@@ -128,16 +146,28 @@ export function useWebRTC(
call.minimized = false
call.status = 'incoming'
call.statusText = `邀请你${call.type === 'video' ? '视频' : '语音'}通话`
+ call.callerId = message.sender_user_id
- // 获取来电者信息
+ // 获取来电者信息 - 从联系人列表
const contact = chatStore.contacts.find(c => c.contact_user_id === message.sender_user_id)
if (contact?.user) {
call.callerName = contact.remark_name || contact.user.name
call.callerAvatar = contact.user.avatar
+ } else {
+ // 尝试从会话列表获取
+ const conv = conversationStore.conversations.find(c => c.target_id === message.sender_user_id)
+ if (conv) {
+ call.callerName = conv.name
+ call.callerAvatar = conv.avatar
+ } else {
+ // 最后尝试从消息中的额外信息获取
+ if (extra.senderName) call.callerName = extra.senderName
+ if (extra.senderAvatar) call.callerAvatar = extra.senderAvatar
+ }
}
playRingtone()
- if (onIncomingCall) onIncomingCall(message.sender_user_id)
+ console.log('📞 来电:', call.callerName, call.callerAvatar)
} else if (signal === 'accepted') {
stopRingtone()
@@ -180,6 +210,8 @@ export function useWebRTC(
uni.showToast({ title: '已在其他设备接听', icon: 'none' })
} else if (signal === 'reject') {
uni.showToast({ title: '对方已拒绝', icon: 'none' })
+ } else if (signal === 'hangup' || signal === 'ended') {
+ uni.showToast({ title: '通话已结束', icon: 'none' })
}
}
} catch (error) {
@@ -249,6 +281,9 @@ export function useWebRTC(
const targetUserId = receiverUserId || currentReceiverUserId
const roomId = getSafeRoomId(targetUserId)
if (!roomId) return
+
+ // 获取当前用户信息用于传递给对方
+ const currentUser = authStore.user
const payload = {
sender_client_id: wsManager.getClientId() || '',
receiver_user_id: targetUserId,
@@ -257,7 +292,11 @@ export function useWebRTC(
content: JSON.stringify(data || {}),
call_id: call.id,
call_status: status,
- extra: JSON.stringify({ type: call.type }),
+ extra: JSON.stringify({
+ type: call.type,
+ senderName: currentUser?.name,
+ senderAvatar: currentUser?.avatar
+ }),
}
messageApi.sendMessage(payload).catch(console.error)
}
@@ -281,12 +320,11 @@ export function useWebRTC(
}
// 发起通话
- async function startCall(type: 'audio' | 'video', receiverUserId: string, roomId?: string, contact?: Contact) {
+ async function startCall(type: 'audio' | 'video', receiverUserId: string, roomId?: string, targetName?: string, targetAvatar?: string) {
if (roomId) currentRoomId = roomId
- else if (contact?.room_id) currentRoomId = contact.room_id
- else if (getRoomId) {
- const found = getRoomId(receiverUserId)
- if (found) currentRoomId = found
+ else {
+ const foundRoomId = getSafeRoomId(receiverUserId)
+ if (foundRoomId) currentRoomId = foundRoomId
}
if (!currentRoomId) {
@@ -306,6 +344,27 @@ export function useWebRTC(
call.camOff = false
call.remoteCamOff = false
call.remoteMuted = false
+ call.callerId = receiverUserId
+
+ // 设置对方信息
+ if (targetName) call.callerName = targetName
+ if (targetAvatar) call.callerAvatar = targetAvatar
+
+ // 尝试从联系人/会话获取对方信息
+ if (!call.callerName || !call.callerAvatar) {
+ const contact = chatStore.contacts.find(c => c.contact_user_id === receiverUserId)
+ if (contact?.user) {
+ call.callerName = call.callerName || contact.remark_name || contact.user.name
+ call.callerAvatar = call.callerAvatar || contact.user.avatar
+ } else {
+ const conv = conversationStore.conversations.find(c => c.target_id === receiverUserId)
+ if (conv) {
+ call.callerName = call.callerName || conv.name
+ call.callerAvatar = call.callerAvatar || conv.avatar
+ }
+ }
+ }
+
remoteStream.value = null
try {
@@ -359,12 +418,18 @@ export function useWebRTC(
function closeCall() {
stopRingtone()
currentRoomId = ''
+ currentReceiverUserId = ''
call.active = false
call.status = 'idle'
call.statusText = ''
call.id = null
call.callerName = undefined
call.callerAvatar = undefined
+ call.callerId = undefined
+ call.muted = false
+ call.camOff = false
+ call.remoteMuted = false
+ call.remoteCamOff = false
stopCallTimer()
// #ifdef H5
@@ -437,6 +502,8 @@ export function useWebRTC(
call,
localStream,
remoteStream,
+ isActive,
+ initListener,
startCall,
acceptCall,
rejectCall,
@@ -450,4 +517,3 @@ export function useWebRTC(
}
export default useWebRTC
-
diff --git a/src/pages/chat/index.vue b/src/pages/chat/index.vue
index 0e670a7..1455ed1 100644
--- a/src/pages/chat/index.vue
+++ b/src/pages/chat/index.vue
@@ -37,21 +37,6 @@
-
-
-
{ console.log('来电:', senderUserId) },
- (receiverUserId) => {
- if (roomId.value) return roomId.value
- const contact = chatStore.contacts.find(c => c.contact_user_id === receiverUserId)
- return contact?.room_id || ''
- }
-)
+// 使用全局单例 webrtc
+const webrtc = useWebRTC()
const roomId = ref(''); const targetId = ref(''); const chatName = ref(''); const targetAvatar = ref('');
const inputText = ref(''); const messages = ref([]); const scrollToId = ref(''); const scrollTop = ref(0); const scrollWithAnimation = ref(false); const loadingMore = ref(false); const hasMore = ref(true); const showMore = ref(false); const showEmoji = ref(false); const playingAudioId = ref(null); const page = ref(1);
@@ -409,16 +386,15 @@ const targetUser = computed(() => { const contact = chatStore.contacts.find(c =>
onLoad((options: any) => { roomId.value = options?.roomId || ''; targetId.value = options?.targetId || ''; chatName.value = decodeURIComponent(options?.name || '聊天'); targetAvatar.value = decodeURIComponent(options?.avatar || ''); isGroupChat.value = !targetId.value && !!roomId.value; loadMessages(); setupWebSocket(); if (isGroupChat.value && roomId.value) { loadGroupMembers() } const callType = options?.callType; if (callType && targetId.value) { setTimeout(() => { if (callType === 'audio') { startAudioCall() } else if (callType === 'video') { startVideoCall() } }, 500) } });
onMounted(() => { if (targetId.value) { conversationStore.clearUnread(targetId.value) } setTimeout(() => { scrollToBottom(false) }, 300) });
-onUnmounted(() => { wsManager.offMessage(handleNewMessage); wsManager.offSignal(handleSignal); stopAudio(); if (webrtc.call.active) { webrtc.endCall() } });
+onUnmounted(() => { wsManager.offMessage(handleNewMessage); stopAudio(); });
async function loadGroupMembers() { if (!roomId.value) return; try { groupMembers.value = await roomApi.getGroupMembers(roomId.value) } catch (e) { console.error('加载群成员失败:', e) } }
async function loadMessages() { if (!roomId.value) return; try { const cached = chatStore.getRoomMessages(roomId.value); if (cached.length > 0) { messages.value = cached.map((m) => ({ ...m, isSelf: m.sender_user_id === currentUser.value?.id, extra: typeof m.extra === 'string' ? JSON.parse(m.extra || '{}') : m.extra })); scrollToBottom(false); return } const res = await messageApi.getMessages(roomId.value, 1, 50); messages.value = (res.data || []).reverse().map((m: ChatMessage) => ({ ...m, isSelf: m.sender_user_id === currentUser.value?.id, extra: typeof m.extra === 'string' ? JSON.parse(m.extra || '{}') : m.extra })); chatStore.setRoomMessages(roomId.value, messages.value); hasMore.value = res.data.length >= 50; scrollToBottom(false) } catch (error) { console.error('加载消息失败:', error) } }
function onScrollToUpper() { if (!loadingMore.value && hasMore.value) { loadMoreMessages() } }
async function loadMoreMessages() { if (loadingMore.value || !hasMore.value) { return } loadingMore.value = true; page.value++; const firstMsgId = messages.value.length > 0 ? messages.value[0].id : null; try { const res = await messageApi.getMessages(roomId.value, page.value, 50); const newMessages = (res.data || []).reverse().map((m: ChatMessage) => ({ ...m, isSelf: m.sender_user_id === currentUser.value?.id, extra: typeof m.extra === 'string' ? JSON.parse(m.extra || '{}') : m.extra })); messages.value = [...newMessages, ...messages.value]; hasMore.value = res.data.length >= 50; if (firstMsgId) { nextTick(() => { scrollWithAnimation.value = false; scrollToId.value = `msg-${firstMsgId}` }) } } catch { page.value-- } finally { loadingMore.value = false } }
-function setupWebSocket() { wsManager.onMessage(handleNewMessage); wsManager.onSignal(handleSignal) }
-function handleSignal(message: ChatMessage) { webrtc.handleSignaling(message) }
-function startAudioCall() { if (!targetId.value) { toast.show('无法发起通话'); return } webrtc.startCall('audio', targetId.value, roomId.value) }
-function startVideoCall() { if (!targetId.value) { toast.show('无法发起通话'); return } webrtc.startCall('video', targetId.value, roomId.value) }
+function setupWebSocket() { wsManager.onMessage(handleNewMessage) }
+function startAudioCall() { if (!targetId.value) { toast.show('无法发起通话'); return } webrtc.startCall('audio', targetId.value, roomId.value, targetUser.value?.name, targetUser.value?.avatar) }
+function startVideoCall() { if (!targetId.value) { toast.show('无法发起通话'); return } webrtc.startCall('video', targetId.value, roomId.value, targetUser.value?.name, targetUser.value?.avatar) }
function onMoreAudioCall() { showMore.value = false; startAudioCall() }
function onMoreVideoCall() { showMore.value = false; startVideoCall() }
function handleNewMessage(msg: ChatMessage) { if (msg.message_type === 6) return; const parsedExtra = typeof msg.extra === 'string' ? JSON.parse(msg.extra || '{}') : msg.extra; const newMsg: ChatMessage = { ...msg, isSelf: msg.sender_user_id === currentUser.value?.id, extra: parsedExtra }; const isCurrentChat = msg.room_id === roomId.value; conversationStore.handleMessageUpdate(newMsg, newMsg.isSelf || false, isCurrentChat); if (!isCurrentChat) return; const exists = messages.value.some( (m) => m.id === newMsg.id || (m.isSelf && m.content === newMsg.content && Math.abs(new Date(m.created_at).getTime() - new Date(newMsg.created_at).getTime()) < 2000) ); if (!exists) { messages.value.push(newMsg); chatStore.addMessage(roomId.value, newMsg); scrollToBottom(true) } }
diff --git a/src/pages/contact/index.vue b/src/pages/contact/index.vue
index d4c54da..570f5c6 100644
--- a/src/pages/contact/index.vue
+++ b/src/pages/contact/index.vue
@@ -270,9 +270,6 @@
-
-
-
@@ -290,7 +287,6 @@ import * as contactApi from '@/api/modules/contact'
import * as roomApi from '@/api/modules/room'
import AppAvatar from '@/components/common/AppAvatar.vue'
import AppTabBar from '@/components/common/AppTabBar.vue'
-import GlobalCallProvider from '@/components/call/GlobalCallProvider.vue'
import type { Contact, ContactGroup } from '@/types/api'
// --- 逻辑完全不变 ---
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
index bfc8c03..f687a2b 100644
--- a/src/pages/index/index.vue
+++ b/src/pages/index/index.vue
@@ -166,9 +166,6 @@
@logout="logout"
/>
-
-
-
@@ -188,7 +185,6 @@ import * as conversationApi from '@/api/modules/conversation'
import AppTabBar from '@/components/common/AppTabBar.vue'
import AppDrawer from '@/components/common/AppDrawer.vue'
import PlusMenu from '@/components/common/PlusMenu.vue'
-import GlobalCallProvider from '@/components/call/GlobalCallProvider.vue'
import type { Conversation } from '@/types/conversation'
const authStore = useAuthStore()
diff --git a/src/pages/moment/index.vue b/src/pages/moment/index.vue
index f0c3dfd..7f0564c 100644
--- a/src/pages/moment/index.vue
+++ b/src/pages/moment/index.vue
@@ -161,9 +161,6 @@
cancel-text="取消"
/>
-
-
-
@@ -183,7 +180,6 @@ import { parseMediaUrls } from '@/types/moment'
import { useToast, useMessage } from 'wot-design-uni'
import AppTabBar from '@/components/common/AppTabBar.vue'
import AppAvatar from '@/components/common/AppAvatar.vue'
-import GlobalCallProvider from '@/components/call/GlobalCallProvider.vue'
import type { Moment } from '@/types/moment'
const authStore = useAuthStore()