diff --git a/src/App.vue b/src/App.vue index 4121994..7bc6173 100644 --- a/src/App.vue +++ b/src/App.vue @@ -41,6 +41,7 @@ import { useChatStore } from '@/stores/chat' import { useMomentStore } from '@/stores/moment' import { useGroupWebRTC } from '@/composables/useGroupWebRTC' import { wsManager } from '@/api/websocket' +import * as systemApi from '@/api/modules/system' import type { MomentNotifPayload } from '@/types/moment' const webrtcStore = useWebRTCStore() @@ -61,12 +62,24 @@ const handleMomentNotification = (payload: MomentNotifPayload) => { momentStore.handleWsNotification(payload) } +// 后端健康检查 +async function checkBackendHealth() { + try { + const result = await systemApi.healthCheck() + console.log('✅ Backend health check:', result.status) + } catch (error) { + console.warn('⚠️ Backend health check failed:', error) + } +} + onMounted(() => { window.addEventListener('resize', handleResize) // 确保监听启动 groupWebRTC.initListener() // 注册朋友圈通知处理器 wsManager.onMomentNotification(handleMomentNotification) + // 检查后端健康状态 + checkBackendHealth() }) onUnmounted(() => { diff --git a/src/api/modules/room.ts b/src/api/modules/room.ts index 4820845..b14890e 100644 --- a/src/api/modules/room.ts +++ b/src/api/modules/room.ts @@ -52,9 +52,33 @@ export function getUserGroups() { }>>('/groups') } +/** + * 后端返回的群信息响应结构 + * 注意:后端把 ChatRoom 对象嵌套在 room 字段里 + */ +export interface GroupInfoResponse { + room: { + room_id: string + room_type: string + room_name: string + room_avatar: string + owner_id: string + creator_id: string + admin_ids: string + announcement: string + last_message_time?: string + last_message?: string + created_at: string + updated_at: string + } + member_count: number + admin_ids: string[] + owner_id: string +} + // 获取群信息 export function getGroup(roomId: string) { - return request.get(`/groups/${roomId}`) + return request.get(`/groups/${roomId}`) } // 获取群成员列表 diff --git a/src/api/request/index.ts b/src/api/request/index.ts index 80ff2a1..7ed0b52 100644 --- a/src/api/request/index.ts +++ b/src/api/request/index.ts @@ -1,8 +1,20 @@ -import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios' +import axios, { type AxiosInstance, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios' import type { ApiResponse } from './types' +/** + * 扩展 AxiosInstance 类型,修正返回值类型 + * 响应拦截器直接返回 res.result,所以实际返回 T 而非 AxiosResponse + */ +interface CustomAxiosInstance extends Omit { + get(url: string, config?: InternalAxiosRequestConfig): Promise + post(url: string, data?: any, config?: InternalAxiosRequestConfig): Promise + put(url: string, data?: any, config?: InternalAxiosRequestConfig): Promise + delete(url: string, config?: InternalAxiosRequestConfig): Promise + patch(url: string, data?: any, config?: InternalAxiosRequestConfig): Promise +} + // 创建axios实例 -const request: AxiosInstance = axios.create({ +const axiosInstance: AxiosInstance = axios.create({ baseURL: '/api', timeout: 30000, headers: { @@ -11,7 +23,7 @@ const request: AxiosInstance = axios.create({ }) // 请求拦截器 -request.interceptors.request.use( +axiosInstance.interceptors.request.use( (config) => { // 从localStorage获取token const token = localStorage.getItem('token') @@ -33,7 +45,7 @@ request.interceptors.request.use( ) // 响应拦截器 -request.interceptors.response.use( +axiosInstance.interceptors.response.use( (response: AxiosResponse) => { const res = response.data @@ -46,7 +58,7 @@ request.interceptors.response.use( } // 返回result字段的数据 - return res.result + return res.result as any }, (error) => { console.error('Request Error:', error) @@ -54,5 +66,8 @@ request.interceptors.response.use( } ) +// 导出类型正确的实例 +const request = axiosInstance as CustomAxiosInstance + export default request diff --git a/src/api/websocket/index.ts b/src/api/websocket/index.ts index 0cef432..21cb865 100644 --- a/src/api/websocket/index.ts +++ b/src/api/websocket/index.ts @@ -25,6 +25,22 @@ class WebSocketManager { private recentMomentNotifs: Set = new Set() private notifCacheTimeout = 5000 // 5秒内相同通知视为重复 + /** + * 获取 WebSocket URL(支持动态配置) + */ + private getWebSocketUrl(userId: string): string { + // 优先使用环境变量配置 + const envWsUrl = import.meta.env.VITE_WS_URL + if (envWsUrl) { + return `${envWsUrl}?user_id=${userId}` + } + + // 自动根据当前页面协议和 host 构建 WebSocket URL + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const host = window.location.host + return `${protocol}//${host}/ws?user_id=${userId}` + } + /** * 连接WebSocket */ @@ -36,7 +52,7 @@ class WebSocketManager { } this.userId = userId - const wsUrl = `ws://localhost:12080/ws?user_id=${userId}` + const wsUrl = this.getWebSocketUrl(userId) try { this.ws = new WebSocket(wsUrl) @@ -118,6 +134,39 @@ class WebSocketManager { return this.clientId } + /** + * 检查连接状态 + */ + isConnected(): boolean { + return this.ws !== null && this.ws.readyState === WebSocket.OPEN + } + + /** + * 获取连接状态详情 + */ + getConnectionState(): 'connected' | 'connecting' | 'disconnected' { + if (!this.ws) return 'disconnected' + switch (this.ws.readyState) { + case WebSocket.CONNECTING: + return 'connecting' + case WebSocket.OPEN: + return 'connected' + default: + return 'disconnected' + } + } + + /** + * 手动触发重连 + */ + reconnect(): Promise { + if (this.userId) { + this.reconnectAttempts = 0 + return this.connect(this.userId) + } + return Promise.reject('No userId available') + } + /** * 添加普通消息处理器 */ diff --git a/src/components/chat/GroupChatPanel.vue b/src/components/chat/GroupChatPanel.vue index db89e26..18bd836 100644 --- a/src/components/chat/GroupChatPanel.vue +++ b/src/components/chat/GroupChatPanel.vue @@ -447,8 +447,11 @@ async function loadData() { groupApi.getGroupMembers(props.roomId), groupApi.getGroupAnnouncement(props.roomId).catch(() => ({ announcement: '' })) ]) - groupName.value = info.name || '未命名群聊' - groupAvatar.value = info.avatar || '' + // 后端返回结构:{ room: { room_name, room_avatar, ... }, member_count, ... } + // 从嵌套的 room 对象中提取群信息 + const roomInfo = info.room + groupName.value = roomInfo?.room_name || '未命名群聊' + groupAvatar.value = roomInfo?.room_avatar || '' members.value = memberList announcement.value = ann.announcement || '' } catch (e) { diff --git a/src/components/chat/GroupInfoPanel.vue b/src/components/chat/GroupInfoPanel.vue index bd9ab9e..23bd8e8 100644 --- a/src/components/chat/GroupInfoPanel.vue +++ b/src/components/chat/GroupInfoPanel.vue @@ -49,6 +49,30 @@ + +
+
+
+ +

群公告

+
+ +
+
+

{{ announcement }}

+

暂无公告

+
+
+
@@ -75,6 +99,14 @@ 编辑 + + + +
+
+ +
+
+ +
+ + +
+ +
+ +
+ +
+

+ + 设置谁可以邀请新成员加入群聊 +

+
+
+
+ + +
+ + + + +
+
+
+

群公告

+ +
+
+ +
+ {{ announcement || '暂无公告' }} +
+
+
+ + +
+
+
+ @@ -403,6 +519,21 @@ const savingAdmins = ref(false) const editForm = ref({ name: '', avatar: '' }) +// 群设置相关 +const showSettingsModal = ref(false) +const groupSettings = ref({ + description: '', + invite_permission: 0 // 0-所有人 1-仅管理员 2-仅群主 +}) +const loadingSettings = ref(false) +const savingSettings = ref(false) + +// 群公告相关 +const announcement = ref('') +const showAnnouncementModal = ref(false) +const isEditingAnnouncement = ref(false) +const editAnnouncementText = ref('') + // Computed const currentUserRole = computed(() => members.value.find(m => m.user_id === authStore.user?.id)?.role ?? 0) const isOwner = computed(() => currentUserRole.value === 2) @@ -410,6 +541,7 @@ const isAdmin = computed(() => currentUserRole.value === 1) const canInvite = computed(() => isOwner.value || isAdmin.value) const canEdit = computed(() => isOwner.value) const canRemove = computed(() => isOwner.value || isAdmin.value) +const canEditAnnouncement = computed(() => isOwner.value || isAdmin.value) // 判断是否可以移除某个成员 function canRemoveMember(member: GroupMember): boolean { @@ -445,15 +577,29 @@ async function loadGroupInfo() { if (!props.roomId) return loading.value = true try { - const [info, memberList, contacts] = await Promise.all([ + const [info, memberList, contacts, ann] = await Promise.all([ groupApi.getGroup(props.roomId), groupApi.getGroupMembers(props.roomId), - contactApi.getContacts() + contactApi.getContacts(), + groupApi.getGroupAnnouncement(props.roomId).catch(() => ({ announcement: '' })) ]) - groupInfo.value = info + // 后端返回结构:{ room: { room_name, room_avatar, ... }, member_count, owner_id, ... } + // 从嵌套的 room 对象中提取群信息 + const roomInfo = info.room + const normalizedInfo = { + room_id: roomInfo?.room_id || props.roomId, + room_type: 'group' as const, + name: roomInfo?.room_name || '未命名群聊', + avatar: roomInfo?.room_avatar || '', + owner_id: info.owner_id || roomInfo?.owner_id || '', + member_count: info.member_count || 0, + created_at: roomInfo?.created_at || '' + } + groupInfo.value = normalizedInfo as any members.value = memberList + announcement.value = ann.announcement || '' - editForm.value = { name: info.name || '', avatar: info.avatar || '' } + editForm.value = { name: normalizedInfo.name, avatar: normalizedInfo.avatar } // 过滤可邀请的好友 const memberIds = new Set(memberList.map(m => m.user_id)) @@ -709,6 +855,73 @@ async function handleSaveAdmins() { } } +// 群设置相关方法 +async function openSettingsModal() { + showSettingsModal.value = true + loadingSettings.value = true + try { + const settings = await groupApi.getGroupSettings(props.roomId) + groupSettings.value = { + description: settings.description || '', + invite_permission: settings.invite_permission ?? 0 + } + } catch (e) { + // 接口可能不存在,使用默认值 + groupSettings.value = { description: '', invite_permission: 0 } + } finally { + loadingSettings.value = false + } +} + +async function saveGroupSettings() { + savingSettings.value = true + try { + await groupApi.updateGroupSettings(props.roomId, { + description: groupSettings.value.description, + invite_permission: groupSettings.value.invite_permission + }) + toastStore.success('群设置已保存') + showSettingsModal.value = false + emit('updated') + } catch (e) { + toastStore.error('保存失败') + } finally { + savingSettings.value = false + } +} + +function getInvitePermissionText(permission: number): string { + switch (permission) { + case 0: return '所有人' + case 1: return '仅管理员' + case 2: return '仅群主' + default: return '所有人' + } +} + +// 群公告相关方法 +function openAnnouncementModal() { + isEditingAnnouncement.value = false + showAnnouncementModal.value = true +} + +function startEditAnnouncement() { + editAnnouncementText.value = announcement.value + isEditingAnnouncement.value = true +} + +async function saveAnnouncement() { + try { + await groupApi.updateGroupAnnouncement(props.roomId, editAnnouncementText.value) + announcement.value = editAnnouncementText.value + isEditingAnnouncement.value = false + showAnnouncementModal.value = false + toastStore.success('公告已更新') + } catch(e) { + toastStore.error('更新失败') + } +} + watch(() => props.show, (val) => { if (val) loadGroupInfo() }) diff --git a/src/components/common/AttachmentManager.vue b/src/components/common/AttachmentManager.vue new file mode 100644 index 0000000..2b76304 --- /dev/null +++ b/src/components/common/AttachmentManager.vue @@ -0,0 +1,374 @@ + + + + + + diff --git a/src/components/moment/MomentPanel.vue b/src/components/moment/MomentPanel.vue index 02fc44d..80ce3ea 100644 --- a/src/components/moment/MomentPanel.vue +++ b/src/components/moment/MomentPanel.vue @@ -146,7 +146,7 @@
-
+
- + +{{ moment.likes.length - 8 }} + 查看全部
@@ -188,6 +189,16 @@ @click="showUserInfo(comment.user)" >{{ comment.user?.name }} {{ formatCommentTime(comment.created_at) }} + +

{{ comment.content }}

{{ reply.content }}

+ +
+ +
+
+
+ +
+ 暂无点赞 +
+ +
+
+ +
+

{{ like.user?.name }}

+

{{ like.user?.desc || '暂无签名' }}

+
+ {{ formatCommentTime(like.created_at) }} +
+
+
+ + @@ -354,6 +425,7 @@ import UserMomentsModal from './UserMomentsModal.vue' import { parseMediaUrls } from '@/types/moment' import { formatRelativeTime } from '@/utils/format' import { storage } from '@/utils/storage' +import * as momentApi from '@/api/modules/moment' import type { Moment, MomentComment } from '@/types/moment' import type { User } from '@/types/api' @@ -379,12 +451,19 @@ const commentText = ref('') const commenting = ref(false) const replyTo = ref(null) const commentInputRef = ref(null) +const deletingCommentId = ref(null) // 图片预览相关 const showImagePreview = ref(false) const previewImages = ref([]) const previewIndex = ref(0) +// 点赞列表相关 +const showLikeListModal = ref(false) +const likeListMomentId = ref(null) +const likeList = ref([]) +const loadingLikes = ref(false) + // 打开图片预览 function openImagePreview(images: string[], index: number) { previewImages.value = images @@ -392,6 +471,29 @@ function openImagePreview(images: string[], index: number) { showImagePreview.value = true } +// 打开点赞列表弹窗 +async function openLikeListModal(moment: Moment) { + likeListMomentId.value = moment.id + showLikeListModal.value = true + loadingLikes.value = true + try { + const likes = await momentApi.getMomentLikes(moment.id) + likeList.value = likes + } catch (e) { + console.error('获取点赞列表失败', e) + likeList.value = moment.likes || [] + } finally { + loadingLikes.value = false + } +} + +// 关闭点赞列表弹窗 +function closeLikeListModal() { + showLikeListModal.value = false + likeListMomentId.value = null + likeList.value = [] +} + // 初始化 onMounted(() => { momentStore.fetchMoments() @@ -594,6 +696,33 @@ function handleViewMoments() { showUserCard.value = false showUserMomentsModal.value = true } + +// 判断是否可以删除评论(只能删除自己的评论) +function canDeleteComment(comment: MomentComment): boolean { + return authStore.user?.id === comment.user_id +} + +// 删除评论 +async function handleDeleteComment(moment: Moment, comment: MomentComment) { + if (!confirm('确定要删除这条评论吗?')) return + + deletingCommentId.value = comment.id + try { + await momentApi.deleteComment(comment.id) + // 从本地数据中移除评论 + if (moment.comments) { + const index = moment.comments.findIndex(c => c.id === comment.id) + if (index > -1) { + moment.comments.splice(index, 1) + moment.comment_count = Math.max(0, (moment.comment_count || 1) - 1) + } + } + } catch (e) { + console.error('删除评论失败', e) + } finally { + deletingCommentId.value = null + } +} diff --git a/src/composables/useWebRTC.ts b/src/composables/useWebRTC.ts index f707b83..521f597 100644 --- a/src/composables/useWebRTC.ts +++ b/src/composables/useWebRTC.ts @@ -1,9 +1,10 @@ import { reactive, shallowRef, ref } from 'vue' import * as messageApi from '@/api/modules/message' +import * as systemApi from '@/api/modules/system' import { wsManager } from '@/api/websocket' import { useToastStore } from '@/stores/toast' import { useChatStore } from '@/stores/chat' -import type { ChatMessage, Contact } from '@/types/api' +import type { ChatMessage, Contact, ICEServerConfig } from '@/types/api' import type { CallStatus } from '@/types/message' export interface CallState { @@ -167,7 +168,96 @@ export function useWebRTC( } async function sendSummaryMessage(reason: 'connected' | 'cancelled' | 'rejected' | 'busy') { - // 省略具体实现,保持原样 + const roomId = getSafeRoomId(currentReceiverUserId) + if (!roomId) return + + // 根据原因生成系统消息内容 + let content = '' + switch (reason) { + case 'connected': + content = `通话时长 ${formatDuration(call.duration)}` + break + case 'cancelled': + content = '已取消' + break + case 'rejected': + content = '对方未接听' + break + case 'busy': + content = '对方忙' + break + } + + // 构造通话结果的 extra 数据 + const extraData = { + call_type: call.type, + call_reason: reason, + call_duration: call.duration, + call_id: call.id + } + + try { + // 发送系统消息 (message_type = 4) + const payload = { + sender_client_id: wsManager.getClientId() || '', + receiver_user_id: currentReceiverUserId, + room_id: roomId, + message_type: 4, // 系统消息 + content: content, + extra: JSON.stringify(extraData) + } + await messageApi.sendMessage(payload) + + // 同时添加到本地消息列表 + const systemMessage: ChatMessage = { + id: Date.now(), + room_id: roomId, + sender_user_id: userId, + message_type: 4, + content: content, + extra: extraData, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } + chatStore.addMessage(roomId, systemMessage) + } catch (error) { + console.error('发送通话结束消息失败:', error) + } + } + + // ICE 服务器缓存 + let cachedIceServers: RTCIceServer[] | null = null + + /** + * 获取 ICE 服务器配置(优先从后端 API 获取,失败时使用备用配置) + */ + async function getIceServers(): Promise { + // 如果已有缓存,直接使用 + if (cachedIceServers) { + return cachedIceServers + } + + try { + const servers = await systemApi.getIceServers(userId) + if (servers && servers.length > 0) { + // 转换为 RTCIceServer 格式 + cachedIceServers = servers.map((s: ICEServerConfig) => ({ + urls: s.urls, + username: s.username, + credential: s.credential + })) + return cachedIceServers + } + } catch (error) { + console.warn('获取 ICE 服务器配置失败,使用备用配置:', error) + } + + // 备用配置:公共 STUN 服务器 + cachedIceServers = [ + { urls: 'stun:stun.l.google.com:19302' }, + { urls: 'stun:stun1.l.google.com:19302' } + ] + return cachedIceServers } // --- WebRTC --- @@ -189,9 +279,9 @@ export function useWebRTC( } async function createPC(): Promise { - const servers = [{ urls: 'stun:stun.l.google.com:19302' }] + const iceServers = await getIceServers() if (pc) { pc.close(); pc = null } - pc = new RTCPeerConnection({ iceServers: servers }) + pc = new RTCPeerConnection({ iceServers }) pc.oniceconnectionstatechange = () => { if (pc?.iceConnectionState === 'failed') { @@ -312,6 +402,17 @@ export function useWebRTC( function endCall() { stopRingtone() + // 主动挂断时发送通话结束系统消息 + if (isCaller.value) { + if (call.status === 'outgoing') { + sendSummaryMessage('cancelled') // 呼出状态主动取消 + } else if (call.status === 'connected') { + sendSummaryMessage('connected') // 通话中主动挂断 + } + } else if (call.status === 'connected') { + // 被叫方在通话中挂断也发送消息 + sendSummaryMessage('connected') + } sendSignal('hangup') closeCall() } diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 46debad..c2aa317 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -79,6 +79,57 @@ export const useAuthStore = defineStore('auth', () => { } } + // 自动检查定时器 + let tokenCheckInterval: number | null = null + + /** + * 开始定时检查 Token 有效性 + * @param intervalMs 检查间隔(毫秒),默认 5 分钟 + */ + function startTokenAutoCheck(intervalMs = 5 * 60 * 1000) { + stopTokenAutoCheck() // 先清除可能存在的定时器 + + tokenCheckInterval = window.setInterval(async () => { + if (token.value) { + try { + await authApi.checkToken() + } catch (error) { + // Token 失效,触发登出 + console.warn('Token expired during auto check') + logout() + // 触发全局事件通知 + window.dispatchEvent(new CustomEvent('auth-expired')) + } + } + }, intervalMs) + } + + /** + * 停止定时检查 + */ + function stopTokenAutoCheck() { + if (tokenCheckInterval) { + clearInterval(tokenCheckInterval) + tokenCheckInterval = null + } + } + + /** + * 静默检查 Token(不更新用户信息,仅验证有效性) + */ + async function silentCheckToken(): Promise { + if (!token.value) { + return false + } + + try { + await authApi.checkToken() + return true + } catch (error) { + return false + } + } + /** * 更新用户信息 */ @@ -96,6 +147,9 @@ export const useAuthStore = defineStore('auth', () => { logout, checkAuth, updateUserInfo, + startTokenAutoCheck, + stopTokenAutoCheck, + silentCheckToken, } }) diff --git a/src/types/api.ts b/src/types/api.ts index 5e53c3e..22c2d10 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -40,6 +40,8 @@ export interface User { avatar: string desc: string region: string + is_online?: boolean // 用户在线状态 + last_online?: string // 最后在线时间 created_at: string updated_at: string } diff --git a/src/types/conversation.ts b/src/types/conversation.ts index 27f2972..3d75f30 100644 --- a/src/types/conversation.ts +++ b/src/types/conversation.ts @@ -2,7 +2,7 @@ * 聊天会话类型定义 */ -import type { User } from './api' +import type { User, Room } from './api' export interface Conversation { id: number // 数据库主键ID diff --git a/src/views/chat/ChatView.vue b/src/views/chat/ChatView.vue index 087d2a0..885c7dc 100644 --- a/src/views/chat/ChatView.vue +++ b/src/views/chat/ChatView.vue @@ -36,6 +36,49 @@
+ +
+
+ +
+ +
+ + + +
+
+
@@ -669,6 +713,7 @@ import GroupCallBanner from '@/components/chat/GroupCallBanner.vue' import UserInfoCard from '@/components/common/UserInfoCard.vue' import MomentPanel from '@/components/moment/MomentPanel.vue' import * as groupApi from '@/api/modules/room' +import * as systemApi from '@/api/modules/system' const router = useRouter() const authStore = useAuthStore() @@ -684,6 +729,10 @@ const webrtc = webrtcStore.webrtc // 朋友圈未读数 const momentUnreadCount = computed(() => momentStore.unreadCount) +// 连接状态 +const connectionState = ref<'connected' | 'connecting' | 'disconnected'>('connecting') +let connectionCheckInterval: number | null = null + // State const currentTab = ref<'chat' | 'contact' | 'moment'>(storage.getCurrentTab() as 'chat' | 'contact' | 'moment') const searchQuery = ref('') @@ -1419,6 +1468,39 @@ function processMessageAfterConversation(message: ChatMessage, roomId: string) { function backToList() { if (isMobile.value) chatVisible.value = false } function handleLogout() { authStore.logout(); wsManager.disconnect(); router.push('/login') } + +// 连接状态检查 +function checkConnectionState() { + connectionState.value = wsManager.getConnectionState() +} + +// 手动重连 +async function handleReconnect() { + if (connectionState.value === 'disconnected') { + connectionState.value = 'connecting' + try { + await wsManager.reconnect() + toastStore.success('已重新连接') + } catch (e) { + toastStore.error('重连失败') + } + checkConnectionState() + } +} + +// 开始定时检查连接状态 +function startConnectionCheck() { + checkConnectionState() + connectionCheckInterval = window.setInterval(checkConnectionState, 5000) +} + +// 停止定时检查 +function stopConnectionCheck() { + if (connectionCheckInterval) { + clearInterval(connectionCheckInterval) + connectionCheckInterval = null + } +} function getMsgSummary(msg: ChatMessage): string { return getMessageSummary(msg) } // 复制文本 @@ -2100,6 +2182,18 @@ onMounted(async () => { } if (authStore.user) { await wsManager.connect(authStore.user.id) + + // 绑定 ClientID 和 UserID + const clientId = wsManager.getClientId() + if (clientId) { + try { + await systemApi.bindClient({ user_id: authStore.user.id, client_id: clientId }) + console.log('✅ Client bound successfully') + } catch (error) { + console.warn('⚠️ Failed to bind client:', error) + } + } + wsManager.onMessage(handleWebSocketMessage) wsManager.onSignal(webrtc.handleSignaling) // 朋友圈通知已在 App.vue 中统一注册,避免重复 @@ -2108,6 +2202,8 @@ onMounted(async () => { await conversationStore.loadConversations() // 获取朋友圈未读数 momentStore.fetchUnreadCount() + // 开始检查连接状态 + startConnectionCheck() const savedRoomId = storage.getSelectedRoomId() const savedTargetId = storage.getSelectedConversation() @@ -2149,6 +2245,7 @@ onUnmounted(() => { wsManager.offSignal(webrtc.handleSignaling) wsManager.offMomentNotification(momentStore.handleWsNotification) window.removeEventListener('app-tab-change', handleTabChangeEvent) + stopConnectionCheck() }) diff --git a/src/views/contact/ContactCircle.vue b/src/views/contact/ContactCircle.vue index d246192..a38e504 100644 --- a/src/views/contact/ContactCircle.vue +++ b/src/views/contact/ContactCircle.vue @@ -891,7 +891,7 @@ function handleAddFriend() { showActionMenu.value = false contactStore.setLeftPanelMode('friend-manager') // 切换到添加好友视图(ContactView) - router.push('/contacts').catch(() => {}) + router.push('/contact').catch(() => {}) } // 创建群聊处理 diff --git a/src/views/contact/ContactDetail.vue b/src/views/contact/ContactDetail.vue index cab59d3..911f1c5 100644 --- a/src/views/contact/ContactDetail.vue +++ b/src/views/contact/ContactDetail.vue @@ -31,14 +31,26 @@

{{ contact.user?.desc || '暂无签名' }}

- - + + + 检查中... + + + 在线 离线 +
@@ -221,13 +233,14 @@ diff --git a/src/views/contact/ContactView.vue b/src/views/contact/ContactView.vue index 317654c..14ae970 100644 --- a/src/views/contact/ContactView.vue +++ b/src/views/contact/ContactView.vue @@ -86,11 +86,13 @@