import type { MessageRequest, MessageResponse } from '@/types/im' import { readonly, ref } from 'vue' import { MessageType } from '@/types/im' import { useUserStore } from './user' interface UseWebSocketOptions { url: string heartbeatInterval?: number pongTimeout?: number reconnectInterval?: number maxReconnectTimes?: number } /* ================== 单例 ================== */ let singleton: ReturnType | null = null function createWebSocket(options?: Partial) { const userStore = useUserStore() /* ================== 配置 ================== */ const config: Required = { url: options?.url || import.meta.env.VITE_WEBSOCKET_BASEURL || 'ws://127.0.0.1:8090/ws', heartbeatInterval: options?.heartbeatInterval ?? 15_000, pongTimeout: options?.pongTimeout ?? 5_000, reconnectInterval: options?.reconnectInterval ?? 3_000, maxReconnectTimes: options?.maxReconnectTimes ?? 5, } /* ================== 状态 ================== */ let ws: WebSocket | null = null const connected = ref(false) const authenticated = ref(false) let cachedToken: string | null = userStore.token let heartbeatTimer: number | null = null let pongTimeoutTimer: number | null = null let reconnectTimer: number | null = null let reconnectTimes = 0 let manualClose = false /* ================== 外部消息回调 ================== */ const messageHandlers: Array<(msg: MessageResponse) => void> = [] /* ================== 连接管理 ================== */ function connect() { if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { console.log('[WebSocket] 已存在连接或正在连接,跳过') return } console.log('[WebSocket] 开始建立连接') manualClose = false ws = new WebSocket(config.url) ws.onopen = () => { connected.value = true reconnectTimes = 0 console.log('[WebSocket] 连接成功') startHeartbeat() // 自动重新登录 if (cachedToken) { console.log('[WebSocket] 自动重新发送 LOGIN') login(cachedToken) } } ws.onmessage = (evt) => { const msg: MessageResponse = JSON.parse(evt.data) console.log('===收到消息===') console.log(msg) handleMessage(msg) } ws.onclose = () => { console.warn('[WebSocket] 连接关闭') connected.value = false authenticated.value = false stopHeartbeat() stopPongTimeout() ws = null if (!manualClose) { tryReconnect() } } ws.onerror = (err) => { console.error('[WebSocket] 连接错误', err) } } function close() { console.log('[WebSocket] 主动关闭连接') manualClose = true stopHeartbeat() stopPongTimeout() clearReconnectTimer() ws?.close() ws = null } /* ================== 重连 ================== */ function tryReconnect() { if (reconnectTimer || reconnectTimes >= config.maxReconnectTimes) return reconnectTimes++ console.warn(`[WebSocket] 第 ${reconnectTimes} 次重连中...`) reconnectTimer = window.setTimeout(() => { reconnectTimer = null connect() }, config.reconnectInterval) } function clearReconnectTimer() { if (reconnectTimer) { clearTimeout(reconnectTimer) reconnectTimer = null } } /* ================== 心跳 ================== */ function startHeartbeat() { stopHeartbeat() heartbeatTimer = window.setTimeout(() => { sendRaw(MessageType.PING) startPongTimeout() startHeartbeat() }, config.heartbeatInterval) } function stopHeartbeat() { if (heartbeatTimer) { clearTimeout(heartbeatTimer) heartbeatTimer = null } } function startPongTimeout() { stopPongTimeout() pongTimeoutTimer = window.setTimeout(() => { console.error('[WebSocket] 心跳超时,断开连接') ws?.close() }, config.pongTimeout) } function stopPongTimeout() { if (pongTimeoutTimer) { clearTimeout(pongTimeoutTimer) pongTimeoutTimer = null } } /* ================== 发送 ================== */ function sendRaw(type: MessageType, data?: any, to?: any) { if (!ws || ws.readyState !== WebSocket.OPEN) { console.warn('[WebSocket] 发送失败,未连接') return } const msg: MessageRequest = { type, from: userStore.user.id, to: to ?? null, timestamp: Date.now(), data: data ?? null, } ws.send(JSON.stringify(msg)) } function send(type: MessageType, data?: any, to?: any) { if (!authenticated.value && type !== MessageType.LOGIN) { console.warn('[WebSocket] 未 LOGIN,禁止发送业务消息') return } sendRaw(type, data, to) } /* ================== 登录相关 ================== */ function login(token: string) { console.log('触发login:', authenticated.value) if (!connected.value || authenticated.value) return cachedToken = token sendRaw(MessageType.LOGIN, { token }) } function logout() { cachedToken = null authenticated.value = false } /* ================== 消息处理(核心修改点) ================== */ function handleMessage(msg: MessageResponse) { switch (msg.type) { case MessageType.PONG: stopPongTimeout() return case MessageType.LOGIN: authenticated.value = true console.log('[WebSocket] LOGIN 成功') return case MessageType.LOGOUT: authenticated.value = false console.log('[WebSocket] 已 LOGOUT') return default: // 所有业务消息交给外部 messageHandlers.forEach(cb => cb(msg)) } } /* ================== 对外订阅 ================== */ function onMessage(handler: (msg: MessageResponse) => void) { messageHandlers.push(handler) } /* ================== 业务 API ================== */ function sendPrivate(toUserId: number, content: string) { send(MessageType.PRIVATE, { content }, toUserId) } function sendGroup(groupId: number, content: string) { send(MessageType.GROUP, { groupId, content }) } /* ================== 初始化 ================== */ connect() return { connect, close, login, logout, sendPrivate, sendGroup, onMessage, connected: readonly(connected), authenticated: readonly(authenticated), } } /* ================== 对外 ================== */ export function useWebSocket(options?: Partial) { if (!singleton) { singleton = createWebSocket(options) } return singleton }