diff --git a/package.json b/package.json index 1d69479..b32b9ec 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@ant-design/icons-vue": "^7.0.1", "@fortawesome/fontawesome-free": "^6.7.2", "@tailwindcss/vite": "^4.1.4", + "@vueuse/core": "^13.5.0", "@vueuse/head": "^2.0.0", "ant-design-vue": "^4.1.1", "axios": "^1.6.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87b22cc..b5ab189 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@tailwindcss/vite': specifier: ^4.1.4 version: 4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0)) + '@vueuse/core': + specifier: ^13.5.0 + version: 13.5.0(vue@3.5.13) '@vueuse/head': specifier: ^2.0.0 version: 2.0.0(vue@3.5.13) @@ -551,6 +554,9 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@unhead/dom@1.11.20': resolution: {integrity: sha512-jgfGYdOH+xHJF/j8gudjsYu3oIjFyXhCWcgKaw3vQnT616gSqyqnGQGOItL+BQtQZACKNISwIfx5PuOtztMKLA==} @@ -607,11 +613,24 @@ packages: '@vue/shared@3.5.13': resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + '@vueuse/core@13.5.0': + resolution: {integrity: sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g==} + peerDependencies: + vue: ^3.5.0 + '@vueuse/head@2.0.0': resolution: {integrity: sha512-ykdOxTGs95xjD4WXE4na/umxZea2Itl0GWBILas+O4oqS7eXIods38INvk3XkJKjqMdWPcpCyLX/DioLQxU1KA==} peerDependencies: vue: '>=2.7 || >=3' + '@vueuse/metadata@13.5.0': + resolution: {integrity: sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw==} + + '@vueuse/shared@13.5.0': + resolution: {integrity: sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g==} + peerDependencies: + vue: ^3.5.0 + ant-design-vue@4.2.6: resolution: {integrity: sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==} engines: {node: '>=12.22.0'} @@ -1402,6 +1421,8 @@ snapshots: '@types/estree@1.0.7': {} + '@types/web-bluetooth@0.0.21': {} + '@unhead/dom@1.11.20': dependencies: '@unhead/schema': 1.11.20 @@ -1491,6 +1512,13 @@ snapshots: '@vue/shared@3.5.13': {} + '@vueuse/core@13.5.0(vue@3.5.13)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 13.5.0 + '@vueuse/shared': 13.5.0(vue@3.5.13) + vue: 3.5.13 + '@vueuse/head@2.0.0(vue@3.5.13)': dependencies: '@unhead/dom': 1.11.20 @@ -1499,6 +1527,12 @@ snapshots: '@unhead/vue': 1.11.20(vue@3.5.13) vue: 3.5.13 + '@vueuse/metadata@13.5.0': {} + + '@vueuse/shared@13.5.0(vue@3.5.13)': + dependencies: + vue: 3.5.13 + ant-design-vue@4.2.6(vue@3.5.13): dependencies: '@ant-design/colors': 6.0.0 diff --git a/src/components/CallInviteDialog.vue b/src/components/CallInviteDialog.vue new file mode 100644 index 0000000..1342805 --- /dev/null +++ b/src/components/CallInviteDialog.vue @@ -0,0 +1,487 @@ + + + + + + + {{ callerInfo.name.charAt(0) }} + + + {{ callerInfo.name }} + {{ callType === 'video' ? '视频通话' : '语音通话' }} + + + + + + {{ statusText }} + + + + + + + + + + + + 接听 + + + + + {{ isVisible && isIncoming ? '拒绝' : '取消' }} + + + + + + + {{ processingText }} + + + + + + + + diff --git a/src/components/MediaPreview.vue b/src/components/MediaPreview.vue index 1062eaf..3c0ce98 100644 --- a/src/components/MediaPreview.vue +++ b/src/components/MediaPreview.vue @@ -1,19 +1,19 @@ - - + - + --> + - + import { ref, computed, watch, onMounted, onUnmounted } from 'vue'; import { useMediaPreview } from "@/composables/useMediaPreview.js"; +import VueEasyLightbox from 'vue-easy-lightbox'; import { useThemeStore } from '@/stores/theme'; const themeStore = useThemeStore(); -const { visible, mediaData, closePreview } = useMediaPreview(); +const { visible, mediaData, hidePreview, previewType } = useMediaPreview(); // 视频相关状态 const videoPlayer = ref(null); @@ -134,8 +140,8 @@ const isPortrait = ref(false); const controlsTimeout = ref(null); // 计算属性 -const showImagePreview = computed(() => visible.value && mediaData.value?.type === 'image'); -const showVideoPreview = computed(() => visible.value && mediaData.value?.type === 'video'); +const showImagePreview = computed(() => visible.value && previewType.value === 'image'); +const showVideoPreview = computed(() => visible.value && previewType.value === 'video'); const progressPercent = computed(() => { if (duration.value === 0) return 0; @@ -296,7 +302,7 @@ const handleKeydown = (event) => { switch (event.code) { case 'Escape': - closePreview(); + hidePreview(); break; case 'Space': if (showVideoPreview.value) { @@ -321,7 +327,7 @@ const handleKeydown = (event) => { // 监听媒体数据变化 watch(mediaData, () => { - if (mediaData.value?.type === 'video') { + if (previewType.value=== 'video') { isPlaying.value = false; currentTime.value = 0; duration.value = 0; diff --git a/src/components/MessageBubble.vue b/src/components/MessageBubble.vue index 106800b..3d4a204 100644 --- a/src/components/MessageBubble.vue +++ b/src/components/MessageBubble.vue @@ -2,8 +2,9 @@ {{ senderInfo.avatar }} @@ -24,7 +25,7 @@ + + + + + {{ getCallStatusText(message.callStatus) }} + + + 通话时长: {{ formatDuration(message.duration) }} + + + @@ -103,11 +115,10 @@ diff --git a/src/components/SideNavigation.vue b/src/components/SideNavigation.vue index 6f6b498..48bb489 100644 --- a/src/components/SideNavigation.vue +++ b/src/components/SideNavigation.vue @@ -1,45 +1,46 @@ - - + + + + - - {{ userStore.currentUser?.name?.charAt(0) || 'U' }} + + {{ userStore.currentUser.name.charAt(0) }} - + - - + - + {{ item.label }} + + {{ item.badge }} + - - + - - + - - + diff --git a/src/components/UserProfileModal.vue b/src/components/UserProfileModal.vue index 8798965..bb8bb3b 100644 --- a/src/components/UserProfileModal.vue +++ b/src/components/UserProfileModal.vue @@ -1,162 +1,207 @@ - - - {{ userStore.currentUser?.name?.charAt(0) || 'U' }} - - - - + + {{ userInfo.name?.charAt(0) }} - - - {{ userStore.currentUser?.name || '未知用户' }} - ID: {{ userStore.currentUser?.id || '000000' }} + + {{ userInfo.name }} + ID: {{ userInfo.id }} - - 在线 + + {{ isOnline ? '在线' : '离线' }} - - - - {{ friendsCount }} - 好友 + + + + 个人信息 + + + 昵称 + {{ userInfo.name || '未设置' }} + + + 用户ID + {{ userInfo.id }} + + + 注册时间 + {{ formatDate(userInfo.createdAt) }} + + + 最后在线 + {{ formatDate(userInfo.lastSeen) }} + + - - {{ groupsCount }} - 群聊 - - - {{ messagesCount }} - 消息 + + + + 聊天统计 + + + {{ chatStats.messageCount || 0 }} + 消息数量 + + + {{ chatStats.callCount || 0 }} + 通话次数 + + + {{ formatDuration(chatStats.totalCallTime || 0) }} + 通话时长 + + - - - 个人设置 - - - - - - 昵称 - {{ userStore.currentUser?.name || '未设置' }} - - - - - - - - - - - - 主题颜色 - 个性化你的聊天界面 - - - - - - - - - - - - 消息通知 - 管理通知设置 - - - - - - - - - - - - 隐私设置 - 控制谁可以联系你 - - - - - - + + + + + 发送消息 + + + + 语音通话 + + + + 视频通话 + - - - - 关闭 - - - 保存更改 - - diff --git a/src/components/VideoCallComponent.vue b/src/components/VideoCallComponent.vue index a239094..20fb50a 100644 --- a/src/components/VideoCallComponent.vue +++ b/src/components/VideoCallComponent.vue @@ -1,44 +1,74 @@ - + {{ callerInfo.name.charAt(0) }} {{ callerInfo.name }} - {{ callStatus }} + {{ callStatus }} {{ formattedDuration }} - + - + - - - - {{ callerInfo.name.charAt(0) }} + + + + 摄像头未工作 + + 麦克风未工作 + + + + + + + + + + {{ callerInfo.name.charAt(0) }} + + + + {{ callerInfo.name }} - {{ callStatus }} - - - + {{ formattedDuration }} @@ -63,7 +93,10 @@ - + + + + {{ callerInfo.name.charAt(0) }} + + @@ -116,7 +157,7 @@ @@ -315,9 +378,14 @@ onUnmounted(() => { box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); overflow: hidden; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - user-select: none; - width: 320px; - height: 240px; + cursor: default; +} + +.video-call-container.dragging { + opacity: 0.9; + cursor: grabbing; + box-shadow: 0 25px 70px rgba(0, 0, 0, 0.5); + transition: none; } .video-call-container.dark { @@ -326,9 +394,7 @@ onUnmounted(() => { } .video-call-container.minimized { - width: 200px; - height: 60px; - cursor: pointer; + cursor: move; } .call-header { @@ -338,6 +404,7 @@ onUnmounted(() => { display: flex; align-items: center; justify-content: space-between; + height: 56px; cursor: move; } @@ -401,36 +468,84 @@ onUnmounted(() => { background: #ff6b6b; } +.device-status-container { + position: absolute; + top: 65px; + left: 0; + right: 0; + display: flex; + justify-content: center; + gap: 15px; + z-index: 10; + padding: 5px; +} + +.status-alert { + background: rgba(255, 87, 87, 0.8); + color: white; + padding: 4px 10px; + border-radius: 20px; + font-size: 12px; + animation: blink 1.5s infinite; +} + +@keyframes blink { + 0% { opacity: 1; } + 50% { opacity: 0.6; } + 100% { opacity: 1; } +} + .minimized-content { display: flex; align-items: center; - padding: 12px 16px; - gap: 12px; height: 100%; background: linear-gradient(135deg, #4361ee, #3f37c9); color: white; + padding: 0 10px; + cursor: move; } -.mini-avatar { - width: 36px; - height: 36px; +.remote-video-preview { + width: 40px; + height: 40px; + border-radius: 8px; + overflow: hidden; +} + +.remote-video { + width: 100%; + height: 100%; + object-fit: cover; +} + +.video-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.3); +} + +.placeholder-avatar { + width: 30px; + height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; - color: white; font-weight: bold; - font-size: 14px; - flex-shrink: 0; + color: white; } .mini-info { flex: 1; min-width: 0; + padding: 0 10px; } .mini-name { - font-size: 14px; + font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; @@ -438,36 +553,15 @@ onUnmounted(() => { } .mini-status { - font-size: 12px; + font-size: 11px; opacity: 0.8; } -.call-indicator { - display: flex; - align-items: center; -} - -.pulse-dot { - width: 8px; - height: 8px; - background: #4caf50; - border-radius: 50%; - animation: pulse 2s infinite; -} - -@keyframes pulse { - 0% { transform: scale(1); opacity: 1; } - 50% { transform: scale(1.2); opacity: 0.7; } - 100% { transform: scale(1); opacity: 1; } -} - .video-area { position: relative; - flex: 1; + height: calc(100% - 116px); background: #000; overflow: hidden; - display: flex; - flex-direction: column; } .remote-video-container { @@ -476,12 +570,6 @@ onUnmounted(() => { position: relative; } -.remote-video { - width: 100%; - height: 100%; - object-fit: cover; -} - .video-placeholder { width: 100%; height: 100%; @@ -495,19 +583,19 @@ onUnmounted(() => { } .placeholder-avatar { - width: 60px; - height: 60px; + width: 80px; + height: 80px; border-radius: 50%; display: flex; align-items: center; justify-content: center; - font-size: 24px; + font-size: 32px; font-weight: bold; - margin-bottom: 12px; + margin-bottom: 15px; } .placeholder-text { - font-size: 16px; + font-size: 18px; font-weight: 500; margin-bottom: 8px; } @@ -519,20 +607,41 @@ onUnmounted(() => { .local-video-container { position: absolute; - bottom: 12px; - right: 12px; - width: 80px; - height: 60px; + bottom: 16px; + right: 16px; + width: 120px; + height: 90px; border-radius: 8px; overflow: hidden; border: 2px solid white; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } -.local-video { - width: 100%; - height: 100%; - object-fit: cover; +.local-placeholder-container { + position: absolute; + bottom: 16px; + right: 16px; + width: 120px; + height: 90px; + border-radius: 8px; + overflow: hidden; + border: 2px solid white; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; +} + +.local-avatar { + width: 50px; + height: 50px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + font-weight: bold; + color: white; } .call-controls { @@ -542,6 +651,7 @@ onUnmounted(() => { display: flex; justify-content: center; gap: 16px; + height: 60px; } .control-btn { @@ -581,28 +691,35 @@ onUnmounted(() => { } /* 响应式设计 */ -@media (max-width: 768px) { - .video-call-container { - width: 280px; - height: 200px; +@media (max-width: 600px) { + .video-call-container:not(.minimized) { + width: 95vw !important; + height: 75vh !important; + max-width: 95vw; + max-height: 75vh; } - .video-call-container.minimized { - width: 180px; - height: 50px; - } - - .local-video-container { - width: 60px; - height: 45px; - bottom: 8px; - right: 8px; + .call-header, + .call-controls { + padding: 8px; } .control-btn { - width: 40px; - height: 40px; + width: 36px; + height: 36px; font-size: 14px; } + + .local-video-container, + .local-placeholder-container { + width: 80px; + height: 60px; + } } - + +@media (max-height: 500px) { + .video-call-container:not(.minimized) { + height: 95vh !important; + } +} + \ No newline at end of file diff --git a/src/composables/useMediaPreview.js b/src/composables/useMediaPreview.js index 6edd95f..b9e93b9 100644 --- a/src/composables/useMediaPreview.js +++ b/src/composables/useMediaPreview.js @@ -2,22 +2,33 @@ import { ref } from "vue" const visible = ref(false) const mediaData = ref(null) +const previewType = ref("image") export function useMediaPreview() { - const previewMedia = (url, type) => { - mediaData.value = { url, type } - visible.value = true - } + const showPreview = (content, type = "image") => { + console.log("显示媒体预览:", content, type) + mediaData.value = content + previewType.value = type + visible.value = true + } - const closePreview = () => { - visible.value = false - mediaData.value = null - } + const hidePreview = () => { + visible.value = false + mediaData.value = null + previewType.value = "image" + } - return { - visible, - mediaData, - previewMedia, - closePreview, - } + return { + visible, + mediaData, + previewType, + showPreview, + hidePreview, + } +} + +// 导出一个函数供组件直接使用 +export default function previewMedia(content, type = "image") { + const { showPreview } = useMediaPreview() + showPreview(content, type) } diff --git a/src/composables/useWebRTC.js b/src/composables/useWebRTC.js new file mode 100644 index 0000000..c71b5ae --- /dev/null +++ b/src/composables/useWebRTC.js @@ -0,0 +1,132 @@ +import { ref } from "vue" + +export function useWebRTC() { + const localStream = ref(null) + const remoteStream = ref(null) + const peerConnection = ref(null) + const isConnected = ref(false) + const isConnecting = ref(false) + + const configuration = { + iceServers: [{ urls: "stun:stun.l.google.com:19302" }, { urls: "stun:stun1.l.google.com:19302" }], + } + + const createPeerConnection = () => { + peerConnection.value = new RTCPeerConnection(configuration) + + peerConnection.value.onicecandidate = (event) => { + if (event.candidate) { + // 发送ICE候选到对端 + console.log("ICE候选:", event.candidate) + } + } + + peerConnection.value.ontrack = (event) => { + remoteStream.value = event.streams[0] + console.log("接收到远程流:", event.streams[0]) + } + + peerConnection.value.onconnectionstatechange = () => { + const state = peerConnection.value.connectionState + console.log("连接状态变化:", state) + + if (state === "connected") { + isConnected.value = true + isConnecting.value = false + } else if (state === "disconnected" || state === "failed") { + isConnected.value = false + isConnecting.value = false + } + } + + return peerConnection.value + } + + const getLocalMedia = async (constraints = { audio: true, video: true }) => { + try { + localStream.value = await navigator.mediaDevices.getUserMedia(constraints) + return localStream.value + } catch (error) { + console.error("获取本地媒体失败:", error) + throw error + } + } + + const createOffer = async () => { + if (!peerConnection.value) { + createPeerConnection() + } + + // 添加本地流到连接 + if (localStream.value) { + localStream.value.getTracks().forEach((track) => { + peerConnection.value.addTrack(track, localStream.value) + }) + } + + const offer = await peerConnection.value.createOffer() + await peerConnection.value.setLocalDescription(offer) + + return offer + } + + const createAnswer = async (offer) => { + if (!peerConnection.value) { + createPeerConnection() + } + + // 添加本地流到连接 + if (localStream.value) { + localStream.value.getTracks().forEach((track) => { + peerConnection.value.addTrack(track, localStream.value) + }) + } + + await peerConnection.value.setRemoteDescription(offer) + const answer = await peerConnection.value.createAnswer() + await peerConnection.value.setLocalDescription(answer) + + return answer + } + + const setRemoteAnswer = async (answer) => { + await peerConnection.value.setRemoteDescription(answer) + } + + const addIceCandidate = async (candidate) => { + if (peerConnection.value && peerConnection.value.remoteDescription) { + await peerConnection.value.addIceCandidate(candidate) + } + } + + const closeConnection = () => { + if (localStream.value) { + localStream.value.getTracks().forEach((track) => track.stop()) + localStream.value = null + } + + if (peerConnection.value) { + peerConnection.value.close() + peerConnection.value = null + } + + remoteStream.value = null + isConnected.value = false + isConnecting.value = false + } + + return { + localStream, + remoteStream, + peerConnection, + isConnected, + isConnecting, + createPeerConnection, + getLocalMedia, + createOffer, + createAnswer, + setRemoteAnswer, + addIceCandidate, + closeConnection, + } +} diff --git a/src/composables/useWebSocket.js b/src/composables/useWebSocket.js index 468cbca..8d2bd8b 100644 --- a/src/composables/useWebSocket.js +++ b/src/composables/useWebSocket.js @@ -1,3 +1,4 @@ +import { ref, onMounted, onUnmounted } from "vue" import { useUserStore } from "@/stores/user" import { useChatStore } from "@/stores/chat" @@ -5,76 +6,243 @@ export function useWebSocket() { const userStore = useUserStore() const chatStore = useChatStore() + const socket = ref(null) + const isConnected = ref(false) + const isConnecting = ref(false) + const reconnectAttempts = ref(0) + const maxReconnectAttempts = 5 + const reconnectDelay = ref(1000) + const connectWebSocket = () => { + if (isConnecting.value || isConnected.value) return + + isConnecting.value = true + chatStore.connectionStatus = "connecting" + try { - chatStore.connectionStatus = "connecting" + const wsUrl = import.meta.env.VITE_WS_URL || "ws://localhost:12080/ws" + socket.value = new WebSocket(wsUrl) - // const wsUrl = "ws://g-ws.nailaoyun.cn/ws" - const wsUrl = "ws://127.0.0.1:12080/ws" - console.log("连接WebSocket:", wsUrl) - - chatStore.socket = new WebSocket(wsUrl) - - chatStore.socket.onopen = () => { - console.log("WebSocket连接已建立") - chatStore.connectionStatus = "connected" - - // 发送绑定请求 - if (userStore.currentUser) { - const bindMsg = { - request_type: "bind", - sender_user_id: userStore.currentUser.id.toString(), - } - chatStore.socket.send(JSON.stringify(bindMsg)) - } - } - - chatStore.socket.onmessage = (event) => { - try { - const message = JSON.parse(event.data) - console.log("收到消息:", message) - - // 检查是否是系统消息 - if (message.clientId) return - - chatStore.handleIncomingMessage(message, userStore.currentUser.id) - } catch (e) { - console.error("解析消息失败:", e) - } - } - - chatStore.socket.onclose = (event) => { - console.log("WebSocket连接已关闭, 代码:", event.code, "原因:", event.reason) - chatStore.connectionStatus = "disconnected" - - // 智能重连策略 - if (event.code !== 1000) { - setTimeout(() => { - console.log("尝试重新连接WebSocket...") - connectWebSocket() - }, 3000) - } - } - - chatStore.socket.onerror = (error) => { - console.error("WebSocket错误:", error) - chatStore.connectionStatus = "disconnected" - } + socket.value.onopen = handleOpen + socket.value.onmessage = handleMessage + socket.value.onclose = disconnectWebSocket + socket.value.onerror = handleError } catch (error) { - console.error("连接WebSocket失败:", error) + console.error("WebSocket连接失败:", error) + isConnecting.value = false chatStore.connectionStatus = "disconnected" } } - const disconnectWebSocket = () => { - if (chatStore.socket) { - chatStore.socket.close() - chatStore.socket = null + const handleOpen = (event) => { + console.log("WebSocket连接已建立") + isConnected.value = true + isConnecting.value = false + reconnectAttempts.value = 0 + reconnectDelay.value = 1000 + chatStore.connectionStatus = "connected" + chatStore.socket = socket.value + } + + const handleMessage = (event) => { + try { + const data = JSON.parse(event.data) + + console.log("收到消息:", data) + // 处理客户端ID + if (data?.clientId) { + // console.log("收到客户端ID:", data.clientId) + // userStore.setClientId(data.clientId) + // + // // 绑定用户 + // if (userStore.currentUser?.id) { + bindUser(userStore.currentUser.id) + // } + return + } + + // 处理通话信令 + if (data.call_id && data.call_status) { + handleCallSignal(data) + return + } + + // 处理普通消息 + if (data.sender_user_id && data.receiver_user_id) { + chatStore.handleIncomingMessage(data, userStore.currentUser?.id) + } + } catch (error) { + console.error("解析WebSocket消息失败:", error) } } + const disconnectWebSocket = (event) => { + console.log("WebSocket连接已关闭:", event.code, event.reason) + isConnected.value = false + isConnecting.value = false + chatStore.connectionStatus = "disconnected" + chatStore.socket = null + + // 自动重连 + if (reconnectAttempts.value < maxReconnectAttempts) { + setTimeout(() => { + reconnectAttempts.value++ + reconnectDelay.value = Math.min(reconnectDelay.value * 2, 30000) + console.log(`尝试重连 (${reconnectAttempts.value}/${maxReconnectAttempts})`) + connect() + }, reconnectDelay.value) + } + } + + const handleError = (error) => { + console.error("WebSocket错误:", error) + isConnecting.value = false + chatStore.connectionStatus = "disconnected" + } + + const handleCallSignal = (data) => { + console.log("收到通话信令:", data) + + // 根据通话状态处理 + switch (data.call_status) { + case "invite": + // 显示来电通知 + showIncomingCallDialog(data) + break + case "accepted": + // 通话被接受 + handleCallAccepted(data) + break + case "rejected": + // 通话被拒绝 + handleCallRejected(data) + break + case "ended": + // 通话结束 + handleCallEnded(data) + break + case "candidate": + // ICE候选 + handleIceCandidate(data) + break + } + } + + const bindUser = (userId) => { + console.log(userId, 'ssssssssss') + if (!isConnected.value || !userId) return + + const bindMessage = { + request_type: "bind", + sender_user_id: userId, + client_id: userStore.clientId, + } + + send(bindMessage) + } + + const send = (message) => { + if (!isConnected.value || !socket.value) { + console.error("WebSocket未连接,无法发送消息") + return false + } + + try { + socket.value.send(JSON.stringify(message)) + return true + } catch (error) { + console.error("发送消息失败:", error) + return false + } + } + + const sendMessage = (receiverUserId, messageType, content) => { + const message = { + request_type: "send_message", + sender_user_id: userStore.currentUser?.id, + receiver_user_id: receiverUserId, + message_type: messageType, + message_content: content, + } + + return send(message) + } + + const sendCallSignal = (signal) => { + return send(signal) + } + + const disconnect = () => { + if (socket.value) { + socket.value.close() + socket.value = null + } + isConnected.value = false + isConnecting.value = false + chatStore.connectionStatus = "disconnected" + chatStore.socket = null + } + + // 生成唯一ID + const generateCallId = () => { + return Date.now().toString() + Math.random().toString(36).substr(2, 9) + } + + onMounted(() => { + connectWebSocket() + }) + + onUnmounted(() => { + disconnectWebSocket() + }) + return { + socket, + isConnected, + isConnecting, connectWebSocket, disconnectWebSocket, + send, + sendMessage, + sendCallSignal, + bindUser, + generateCallId, } } + +// 全局通话处理函数 +const incomingCallDialog = null +const currentCall = null + +function showIncomingCallDialog(callData) { + // 这里应该显示来电弹窗 + console.log("显示来电弹窗:", callData) + + // 创建来电通知 + if ("Notification" in window && Notification.permission === "granted") { + new Notification(`${callData.sender_user_id} 邀请您进行${callData.message_type === 6 ? "视频" : "语音"}通话`, { + icon: "/favicon.ico", + tag: "incoming-call", + }) + } +} + +function handleCallAccepted(data) { + console.log("通话被接受:", data) + // 开始WebRTC连接 +} + +function handleCallRejected(data) { + console.log("通话被拒绝:", data) + // 显示拒绝消息 +} + +function handleCallEnded(data) { + console.log("通话结束:", data) + // 清理通话状态 +} + +function handleIceCandidate(data) { + console.log("收到ICE候选:", data) + // 处理ICE候选 +} diff --git a/src/views/Chat.vue b/src/views/Chat.vue index bc4976e..2f7f2d4 100644 --- a/src/views/Chat.vue +++ b/src/views/Chat.vue @@ -16,7 +16,7 @@ - +
{{ callType === 'video' ? '视频通话' : '语音通话' }}
ID: {{ userStore.currentUser?.id || '000000' }}
ID: {{ userInfo.id }}