UI设计
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTheme } from './composables/useTheme'
|
import { useTheme } from './composables/useTheme'
|
||||||
|
import GlobalCallProvider from './components/call/GlobalCallProvider.vue'
|
||||||
|
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
</script>
|
</script>
|
||||||
@@ -7,6 +8,7 @@ const { theme } = useTheme()
|
|||||||
<template>
|
<template>
|
||||||
<wd-config-provider :theme="theme">
|
<wd-config-provider :theme="theme">
|
||||||
<KuRootView />
|
<KuRootView />
|
||||||
|
<!-- 全局通话组件(单聊 + 群通话) -->
|
||||||
|
<global-call-provider />
|
||||||
</wd-config-provider>
|
</wd-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import { onLaunch, onShow } from '@dcloudio/uni-app'
|
|||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import { useAuthStore, useConversationStore } from '@/stores'
|
import { useAuthStore, useConversationStore } from '@/stores'
|
||||||
import { wsManager } from '@/api/websocket'
|
import { wsManager } from '@/api/websocket'
|
||||||
import { useGroupWebRTC } from '@/composables/useGroupWebRTC'
|
|
||||||
import type { ChatMessage } from '@/types/api'
|
import type { ChatMessage } from '@/types/api'
|
||||||
|
|
||||||
const { initTheme } = useTheme()
|
const { initTheme } = useTheme()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const conversationStore = useConversationStore()
|
const conversationStore = useConversationStore()
|
||||||
const groupWebRTC = useGroupWebRTC()
|
|
||||||
|
|
||||||
// 全局消息处理器
|
// 全局消息处理器
|
||||||
function handleGlobalMessage(message: ChatMessage) {
|
function handleGlobalMessage(message: ChatMessage) {
|
||||||
@@ -30,8 +28,7 @@ async function initWebSocket() {
|
|||||||
await wsManager.connect(userId)
|
await wsManager.connect(userId)
|
||||||
// 注册全局消息处理器
|
// 注册全局消息处理器
|
||||||
wsManager.onMessage(handleGlobalMessage)
|
wsManager.onMessage(handleGlobalMessage)
|
||||||
// 初始化群通话信令监听器
|
// 群通话信令监听器已移至 App.ku.vue 初始化
|
||||||
groupWebRTC.initListener()
|
|
||||||
console.log('✅ WebSocket 全局初始化成功')
|
console.log('✅ WebSocket 全局初始化成功')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('WebSocket 初始化失败:', error)
|
console.error('WebSocket 初始化失败:', error)
|
||||||
|
|||||||
@@ -31,13 +31,15 @@
|
|||||||
|
|
||||||
<!-- 视频区域 -->
|
<!-- 视频区域 -->
|
||||||
<view v-if="call.type === 'video'" class="video-area">
|
<view v-if="call.type === 'video'" class="video-area">
|
||||||
|
<!-- 远端视频 - 使用原生 video 元素以支持 srcObject -->
|
||||||
<video
|
<video
|
||||||
v-if="remoteStream"
|
v-if="remoteStream"
|
||||||
ref="remoteVideoRef"
|
ref="remoteVideoRef"
|
||||||
class="remote-video"
|
class="remote-video"
|
||||||
autoplay
|
autoplay
|
||||||
playsinline
|
playsinline
|
||||||
/>
|
:srcObject="remoteStream"
|
||||||
|
></video>
|
||||||
<view v-else class="video-placeholder">
|
<view v-else class="video-placeholder">
|
||||||
<view class="avatar-section">
|
<view class="avatar-section">
|
||||||
<view class="avatar-ring ring-1"></view>
|
<view class="avatar-ring ring-1"></view>
|
||||||
@@ -47,6 +49,7 @@
|
|||||||
<text class="caller-name">{{ call.callerName || '对方' }}</text>
|
<text class="caller-name">{{ call.callerName || '对方' }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 本地视频 - 使用原生 video 元素以支持 srcObject -->
|
||||||
<video
|
<video
|
||||||
v-if="localStream && !call.camOff"
|
v-if="localStream && !call.camOff"
|
||||||
ref="localVideoRef"
|
ref="localVideoRef"
|
||||||
@@ -54,7 +57,8 @@
|
|||||||
autoplay
|
autoplay
|
||||||
muted
|
muted
|
||||||
playsinline
|
playsinline
|
||||||
/>
|
:srcObject="localStream"
|
||||||
|
></video>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 语音通话 / 视频占位 -->
|
<!-- 语音通话 / 视频占位 -->
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- ========== 单聊通话组件 ========== -->
|
||||||
|
<call-window
|
||||||
|
v-if="callActive"
|
||||||
|
:call="callState"
|
||||||
|
:local-stream="localStreamValue"
|
||||||
|
:remote-stream="remoteStreamValue"
|
||||||
|
:format-duration="webrtc.formatDuration"
|
||||||
|
@accept="webrtc.acceptCall()"
|
||||||
|
@reject="webrtc.rejectCall()"
|
||||||
|
@end="webrtc.endCall()"
|
||||||
|
@toggle-mute="webrtc.toggleMute()"
|
||||||
|
@toggle-camera="webrtc.toggleCamera()"
|
||||||
|
@toggle-minimize="webrtc.toggleMinimize()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- ========== 群通话组件 ========== -->
|
||||||
<!-- 全局群通话来电覆盖层 -->
|
<!-- 全局群通话来电覆盖层 -->
|
||||||
<group-call-incoming />
|
<group-call-incoming />
|
||||||
|
|
||||||
@@ -10,16 +26,30 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { useWebRTC } from '@/composables/useWebRTC'
|
||||||
import { useGroupWebRTC } from '@/composables/useGroupWebRTC'
|
import { useGroupWebRTC } from '@/composables/useGroupWebRTC'
|
||||||
|
import CallWindow from './CallWindow.vue'
|
||||||
import GroupCallIncoming from './GroupCallIncoming.vue'
|
import GroupCallIncoming from './GroupCallIncoming.vue'
|
||||||
import GroupCallWindow from './GroupCallWindow.vue'
|
import GroupCallWindow from './GroupCallWindow.vue'
|
||||||
import GroupCallBanner from './GroupCallBanner.vue'
|
import GroupCallBanner from './GroupCallBanner.vue'
|
||||||
|
|
||||||
const { initListener } = useGroupWebRTC()
|
// 单聊通话
|
||||||
|
const webrtc = useWebRTC()
|
||||||
|
|
||||||
|
// 使用计算属性确保响应式
|
||||||
|
const callActive = computed(() => webrtc.call.active)
|
||||||
|
const callState = computed(() => webrtc.call)
|
||||||
|
const localStreamValue = computed(() => webrtc.localStream.value)
|
||||||
|
const remoteStreamValue = computed(() => webrtc.remoteStream.value)
|
||||||
|
|
||||||
|
// 群通话
|
||||||
|
const { initListener: initGroupListener } = useGroupWebRTC()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化信令监听器
|
// 初始化单聊通话信令监听器
|
||||||
initListener()
|
webrtc.initListener()
|
||||||
|
// 初始化群通话信令监听器
|
||||||
|
initGroupListener()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* WebRTC 通话组合式函数 - UniApp 适配版
|
* WebRTC 通话组合式函数 - 全局单例模式
|
||||||
* 主要支持 H5 平台,APP/小程序需要平台特定实现
|
* 支持在任意页面接听来电
|
||||||
*/
|
*/
|
||||||
import { reactive, shallowRef, ref } from 'vue'
|
import { reactive, shallowRef, ref, computed } from 'vue'
|
||||||
import * as messageApi from '@/api/modules/message'
|
import * as messageApi from '@/api/modules/message'
|
||||||
import { wsManager } from '@/api/websocket'
|
import { wsManager } from '@/api/websocket'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useChatStore } from '@/stores/chat'
|
import { useChatStore } from '@/stores/chat'
|
||||||
|
import { useConversationStore } from '@/stores/conversation'
|
||||||
import type { ChatMessage, Contact } from '@/types/api'
|
import type { ChatMessage, Contact } from '@/types/api'
|
||||||
import type { CallStatus } from '@/types/message'
|
import type { CallStatus } from '@/types/message'
|
||||||
|
|
||||||
@@ -24,46 +26,46 @@ export interface CallState {
|
|||||||
startTime: number | null
|
startTime: number | null
|
||||||
callerName?: string
|
callerName?: string
|
||||||
callerAvatar?: string
|
callerAvatar?: string
|
||||||
|
callerId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWebRTC(
|
// --- 全局单例状态 ---
|
||||||
userId: string,
|
const call = reactive<CallState>({
|
||||||
onIncomingCall?: (senderUserId: string) => void,
|
active: false,
|
||||||
getRoomId?: (receiverUserId?: string) => string
|
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<MediaStream | null>(null)
|
||||||
|
const remoteStream = shallowRef<MediaStream | null>(null)
|
||||||
|
let durationTimer: ReturnType<typeof setInterval> | 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 chatStore = useChatStore()
|
||||||
|
const conversationStore = useConversationStore()
|
||||||
|
|
||||||
const call = reactive<CallState>({
|
const userId = computed(() => authStore.user?.id || '')
|
||||||
active: false,
|
const isActive = computed(() => call.active)
|
||||||
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<MediaStream | null>(null)
|
|
||||||
const remoteStream = shallowRef<MediaStream | null>(null)
|
|
||||||
let durationTimer: ReturnType<typeof setInterval> | null = null
|
|
||||||
let pc: RTCPeerConnection | null = null
|
|
||||||
const pendingCandidates: RTCIceCandidate[] = []
|
|
||||||
let currentReceiverUserId = ''
|
|
||||||
let currentRoomId = ''
|
|
||||||
|
|
||||||
// 音频播放(使用内置音频方法)
|
|
||||||
let audioContext: UniApp.InnerAudioContext | null = null
|
|
||||||
|
|
||||||
function playRingtone() {
|
function playRingtone() {
|
||||||
stopRingtone()
|
stopRingtone()
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
// H5 使用简单的提示音
|
|
||||||
try {
|
try {
|
||||||
const AudioContext = window.AudioContext || (window as any).webkitAudioContext
|
const AudioContext = window.AudioContext || (window as any).webkitAudioContext
|
||||||
const ctx = new AudioContext()
|
const ctx = new AudioContext()
|
||||||
@@ -87,22 +89,38 @@ export function useWebRTC(
|
|||||||
|
|
||||||
function getSafeRoomId(targetUserId?: string): string | null {
|
function getSafeRoomId(targetUserId?: string): string | null {
|
||||||
if (currentRoomId) return currentRoomId
|
if (currentRoomId) return currentRoomId
|
||||||
if (targetUserId && getRoomId) {
|
|
||||||
const rid = getRoomId(targetUserId)
|
// 尝试从联系人获取房间ID
|
||||||
if (rid) return rid
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化信令监听器
|
||||||
|
function initListener() {
|
||||||
|
wsManager.offSignal(handleSignaling)
|
||||||
|
wsManager.onSignal(handleSignaling)
|
||||||
|
}
|
||||||
|
|
||||||
// 信令处理
|
// 信令处理
|
||||||
async function handleSignaling(message: ChatMessage) {
|
async function handleSignaling(message: ChatMessage) {
|
||||||
try {
|
try {
|
||||||
const content = message.content ? JSON.parse(message.content) : {}
|
const content = message.content ? JSON.parse(message.content) : {}
|
||||||
|
|
||||||
// 忽略群聊信令
|
// 忽略群聊信令(由 useGroupWebRTC 处理)
|
||||||
if (content.callRoomId || content.participantIds) {
|
if (content.callRoomId || content.participantIds) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -128,16 +146,28 @@ export function useWebRTC(
|
|||||||
call.minimized = false
|
call.minimized = false
|
||||||
call.status = 'incoming'
|
call.status = 'incoming'
|
||||||
call.statusText = `邀请你${call.type === 'video' ? '视频' : '语音'}通话`
|
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)
|
const contact = chatStore.contacts.find(c => c.contact_user_id === message.sender_user_id)
|
||||||
if (contact?.user) {
|
if (contact?.user) {
|
||||||
call.callerName = contact.remark_name || contact.user.name
|
call.callerName = contact.remark_name || contact.user.name
|
||||||
call.callerAvatar = contact.user.avatar
|
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()
|
playRingtone()
|
||||||
if (onIncomingCall) onIncomingCall(message.sender_user_id)
|
console.log('📞 来电:', call.callerName, call.callerAvatar)
|
||||||
|
|
||||||
} else if (signal === 'accepted') {
|
} else if (signal === 'accepted') {
|
||||||
stopRingtone()
|
stopRingtone()
|
||||||
@@ -180,6 +210,8 @@ export function useWebRTC(
|
|||||||
uni.showToast({ title: '已在其他设备接听', icon: 'none' })
|
uni.showToast({ title: '已在其他设备接听', icon: 'none' })
|
||||||
} else if (signal === 'reject') {
|
} else if (signal === 'reject') {
|
||||||
uni.showToast({ title: '对方已拒绝', icon: 'none' })
|
uni.showToast({ title: '对方已拒绝', icon: 'none' })
|
||||||
|
} else if (signal === 'hangup' || signal === 'ended') {
|
||||||
|
uni.showToast({ title: '通话已结束', icon: 'none' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -249,6 +281,9 @@ export function useWebRTC(
|
|||||||
const targetUserId = receiverUserId || currentReceiverUserId
|
const targetUserId = receiverUserId || currentReceiverUserId
|
||||||
const roomId = getSafeRoomId(targetUserId)
|
const roomId = getSafeRoomId(targetUserId)
|
||||||
if (!roomId) return
|
if (!roomId) return
|
||||||
|
|
||||||
|
// 获取当前用户信息用于传递给对方
|
||||||
|
const currentUser = authStore.user
|
||||||
const payload = {
|
const payload = {
|
||||||
sender_client_id: wsManager.getClientId() || '',
|
sender_client_id: wsManager.getClientId() || '',
|
||||||
receiver_user_id: targetUserId,
|
receiver_user_id: targetUserId,
|
||||||
@@ -257,7 +292,11 @@ export function useWebRTC(
|
|||||||
content: JSON.stringify(data || {}),
|
content: JSON.stringify(data || {}),
|
||||||
call_id: call.id,
|
call_id: call.id,
|
||||||
call_status: status,
|
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)
|
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
|
if (roomId) currentRoomId = roomId
|
||||||
else if (contact?.room_id) currentRoomId = contact.room_id
|
else {
|
||||||
else if (getRoomId) {
|
const foundRoomId = getSafeRoomId(receiverUserId)
|
||||||
const found = getRoomId(receiverUserId)
|
if (foundRoomId) currentRoomId = foundRoomId
|
||||||
if (found) currentRoomId = found
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentRoomId) {
|
if (!currentRoomId) {
|
||||||
@@ -306,6 +344,27 @@ export function useWebRTC(
|
|||||||
call.camOff = false
|
call.camOff = false
|
||||||
call.remoteCamOff = false
|
call.remoteCamOff = false
|
||||||
call.remoteMuted = 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
|
remoteStream.value = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -359,12 +418,18 @@ export function useWebRTC(
|
|||||||
function closeCall() {
|
function closeCall() {
|
||||||
stopRingtone()
|
stopRingtone()
|
||||||
currentRoomId = ''
|
currentRoomId = ''
|
||||||
|
currentReceiverUserId = ''
|
||||||
call.active = false
|
call.active = false
|
||||||
call.status = 'idle'
|
call.status = 'idle'
|
||||||
call.statusText = ''
|
call.statusText = ''
|
||||||
call.id = null
|
call.id = null
|
||||||
call.callerName = undefined
|
call.callerName = undefined
|
||||||
call.callerAvatar = undefined
|
call.callerAvatar = undefined
|
||||||
|
call.callerId = undefined
|
||||||
|
call.muted = false
|
||||||
|
call.camOff = false
|
||||||
|
call.remoteMuted = false
|
||||||
|
call.remoteCamOff = false
|
||||||
stopCallTimer()
|
stopCallTimer()
|
||||||
|
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
@@ -437,6 +502,8 @@ export function useWebRTC(
|
|||||||
call,
|
call,
|
||||||
localStream,
|
localStream,
|
||||||
remoteStream,
|
remoteStream,
|
||||||
|
isActive,
|
||||||
|
initListener,
|
||||||
startCall,
|
startCall,
|
||||||
acceptCall,
|
acceptCall,
|
||||||
rejectCall,
|
rejectCall,
|
||||||
@@ -450,4 +517,3 @@ export function useWebRTC(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default useWebRTC
|
export default useWebRTC
|
||||||
|
|
||||||
|
|||||||
@@ -37,21 +37,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- WebRTC 通话窗口 -->
|
|
||||||
<call-window
|
|
||||||
v-if="webrtc"
|
|
||||||
:call="webrtc.call"
|
|
||||||
:local-stream="webrtc.localStream.value"
|
|
||||||
:remote-stream="webrtc.remoteStream.value"
|
|
||||||
:format-duration="webrtc.formatDuration"
|
|
||||||
@accept="webrtc.acceptCall()"
|
|
||||||
@reject="webrtc.rejectCall()"
|
|
||||||
@end="webrtc.endCall()"
|
|
||||||
@toggle-mute="webrtc.toggleMute()"
|
|
||||||
@toggle-camera="webrtc.toggleCamera()"
|
|
||||||
@toggle-minimize="webrtc.toggleMinimize()"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 消息列表 -->
|
<!-- 消息列表 -->
|
||||||
<scroll-view
|
<scroll-view
|
||||||
class="chat-scroll-area custom-scrollbar"
|
class="chat-scroll-area custom-scrollbar"
|
||||||
@@ -372,7 +357,6 @@ import { useToast } from 'wot-design-uni'
|
|||||||
import { useTheme } from '@/composables/useTheme'
|
import { useTheme } from '@/composables/useTheme'
|
||||||
import { useWebRTC } from '@/composables/useWebRTC'
|
import { useWebRTC } from '@/composables/useWebRTC'
|
||||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||||
import CallWindow from '@/components/call/CallWindow.vue'
|
|
||||||
import MentionPicker from '@/components/chat/MentionPicker.vue'
|
import MentionPicker from '@/components/chat/MentionPicker.vue'
|
||||||
import type { MentionUser } from '@/components/chat/MentionPicker.vue'
|
import type { MentionUser } from '@/components/chat/MentionPicker.vue'
|
||||||
import * as roomApi from '@/api/modules/room'
|
import * as roomApi from '@/api/modules/room'
|
||||||
@@ -384,15 +368,8 @@ const conversationStore = useConversationStore()
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
|
|
||||||
const webrtc = useWebRTC(
|
// 使用全局单例 webrtc
|
||||||
authStore.user?.id || '',
|
const webrtc = useWebRTC()
|
||||||
(senderUserId) => { 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 || ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const roomId = ref(''); const targetId = ref(''); const chatName = ref(''); const targetAvatar = ref('');
|
const roomId = ref(''); const targetId = ref(''); const chatName = ref(''); const targetAvatar = ref('');
|
||||||
const inputText = ref(''); const messages = ref<ChatMessage[]>([]); 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<number | null>(null); const page = ref(1);
|
const inputText = ref(''); const messages = ref<ChatMessage[]>([]); 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<number | null>(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) } });
|
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) });
|
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 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) } }
|
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() } }
|
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 } }
|
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 setupWebSocket() { wsManager.onMessage(handleNewMessage) }
|
||||||
function handleSignal(message: ChatMessage) { webrtc.handleSignaling(message) }
|
function startAudioCall() { if (!targetId.value) { toast.show('无法发起通话'); return } webrtc.startCall('audio', targetId.value, roomId.value, targetUser.value?.name, targetUser.value?.avatar) }
|
||||||
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, targetUser.value?.name, targetUser.value?.avatar) }
|
||||||
function startVideoCall() { if (!targetId.value) { toast.show('无法发起通话'); return } webrtc.startCall('video', targetId.value, roomId.value) }
|
|
||||||
function onMoreAudioCall() { showMore.value = false; startAudioCall() }
|
function onMoreAudioCall() { showMore.value = false; startAudioCall() }
|
||||||
function onMoreVideoCall() { showMore.value = false; startVideoCall() }
|
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) } }
|
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) } }
|
||||||
|
|||||||
@@ -270,9 +270,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</wd-popup>
|
</wd-popup>
|
||||||
|
|
||||||
<!-- 全局群通话组件 -->
|
|
||||||
<global-call-provider />
|
|
||||||
|
|
||||||
<wd-toast />
|
<wd-toast />
|
||||||
<wd-message-box />
|
<wd-message-box />
|
||||||
<app-tab-bar current="contacts" />
|
<app-tab-bar current="contacts" />
|
||||||
@@ -290,7 +287,6 @@ import * as contactApi from '@/api/modules/contact'
|
|||||||
import * as roomApi from '@/api/modules/room'
|
import * as roomApi from '@/api/modules/room'
|
||||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||||
import GlobalCallProvider from '@/components/call/GlobalCallProvider.vue'
|
|
||||||
import type { Contact, ContactGroup } from '@/types/api'
|
import type { Contact, ContactGroup } from '@/types/api'
|
||||||
|
|
||||||
// --- 逻辑完全不变 ---
|
// --- 逻辑完全不变 ---
|
||||||
|
|||||||
@@ -166,9 +166,6 @@
|
|||||||
@logout="logout"
|
@logout="logout"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 全局群通话组件 -->
|
|
||||||
<global-call-provider />
|
|
||||||
|
|
||||||
<wd-toast />
|
<wd-toast />
|
||||||
<wd-message-box :z-index="11000" />
|
<wd-message-box :z-index="11000" />
|
||||||
<app-tab-bar current="messages" />
|
<app-tab-bar current="messages" />
|
||||||
@@ -188,7 +185,6 @@ import * as conversationApi from '@/api/modules/conversation'
|
|||||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||||
import AppDrawer from '@/components/common/AppDrawer.vue'
|
import AppDrawer from '@/components/common/AppDrawer.vue'
|
||||||
import PlusMenu from '@/components/common/PlusMenu.vue'
|
import PlusMenu from '@/components/common/PlusMenu.vue'
|
||||||
import GlobalCallProvider from '@/components/call/GlobalCallProvider.vue'
|
|
||||||
import type { Conversation } from '@/types/conversation'
|
import type { Conversation } from '@/types/conversation'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|||||||
@@ -161,9 +161,6 @@
|
|||||||
cancel-text="取消"
|
cancel-text="取消"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 全局群通话组件 -->
|
|
||||||
<global-call-provider />
|
|
||||||
|
|
||||||
<wd-toast />
|
<wd-toast />
|
||||||
<wd-message-box />
|
<wd-message-box />
|
||||||
|
|
||||||
@@ -183,7 +180,6 @@ import { parseMediaUrls } from '@/types/moment'
|
|||||||
import { useToast, useMessage } from 'wot-design-uni'
|
import { useToast, useMessage } from 'wot-design-uni'
|
||||||
import AppTabBar from '@/components/common/AppTabBar.vue'
|
import AppTabBar from '@/components/common/AppTabBar.vue'
|
||||||
import AppAvatar from '@/components/common/AppAvatar.vue'
|
import AppAvatar from '@/components/common/AppAvatar.vue'
|
||||||
import GlobalCallProvider from '@/components/call/GlobalCallProvider.vue'
|
|
||||||
import type { Moment } from '@/types/moment'
|
import type { Moment } from '@/types/moment'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|||||||
Reference in New Issue
Block a user