Files
workspace/code/link56/link56-frontend-new/src/store/websocket.ts
T
2025-12-29 17:31:10 +08:00

288 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<typeof createWebSocket> | null = null
function createWebSocket(options?: Partial<UseWebSocketOptions>) {
const userStore = useUserStore()
/* ================== 配置 ================== */
const config: Required<UseWebSocketOptions> = {
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<UseWebSocketOptions>) {
if (!singleton) {
singleton = createWebSocket(options)
}
return singleton
}