From 9d53d7deb0d7c7cd5ec4d7b7dc9674a8fdfbc538 Mon Sep 17 00:00:00 2001 From: GiveMePast <1248298477@qq.com> Date: Sat, 8 Aug 2026 15:06:28 +0800 Subject: [PATCH] wip --- src/utils/socket.js | 131 +++++++++++++++++++++++++++++++++++++++++ src/views/UserView.vue | 24 ++++++-- 2 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 src/utils/socket.js diff --git a/src/utils/socket.js b/src/utils/socket.js new file mode 100644 index 0000000..b42f0e5 --- /dev/null +++ b/src/utils/socket.js @@ -0,0 +1,131 @@ +// ============================================================ +// WebSocket 广播客户端封装(Workerman 2346 端口) +// 特性:单例 + 引用计数 + 断线指数退避重连 + 心跳 + 重连后通知 +// 用法: +// import { openSocket, closeSocket, onTicketChanged, onReconnect } from '@/utils/socket' +// onMounted(() => { +// openSocket() +// const off1 = onTicketChanged(() => fetchTickets(true)) +// const off2 = onReconnect(() => fetchTickets(true)) +// onBeforeUnmount(() => { off1(); off2(); closeSocket() }) +// }) +// ============================================================ + +const WS_URL = 'ws://localhost:2346' +const MAX_RETRY_DELAY = 30000 // 重连封顶 30s +const HEARTBEAT_INTERVAL = 25000 + +let socket = null +let retryDelay = 1000 +let reconnectTimer = null +let heartbeatTimer = null +let manualClosed = false // 手动关闭后不再重连 +let refCount = 0 + +const ticketListeners = new Set() // 收到 ticket_changed 回调 +const reconnectListeners = new Set() // 重连成功回调 + +function connect() { + manualClosed = false + try { + socket = new WebSocket(WS_URL) + } catch (e) { + scheduleReconnect() + return + } + + socket.onopen = () => { + retryDelay = 1000 + console.log('[socket] 已连接到工单推送服务 ws://localhost:2346') + startHeartbeat() + reconnectListeners.forEach((fn) => fn()) + } + + socket.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + if (data.type === 'ticket_changed') { + ticketListeners.forEach((fn) => fn(data)) + } + } catch (e) { + console.warn('[socket] 消息解析失败:', event.data) + } + } + + socket.onclose = () => { + stopHeartbeat() + if (!manualClosed) scheduleReconnect() + } + + socket.onerror = () => { + // 连接错误后必然触发 onclose,无需额外处理 + } +} + +function scheduleReconnect() { + if (reconnectTimer || manualClosed) return + console.log(`[socket] 连接断开,${Math.round(retryDelay / 1000)}s 后重试...`) + reconnectTimer = setTimeout(() => { + reconnectTimer = null + connect() + }, retryDelay) + retryDelay = Math.min(retryDelay * 2, MAX_RETRY_DELAY) +} + +function startHeartbeat() { + stopHeartbeat() + heartbeatTimer = setInterval(() => { + if (socket && socket.readyState === WebSocket.OPEN) { + socket.send('ping') + } + }, HEARTBEAT_INTERVAL) +} + +function stopHeartbeat() { + if (heartbeatTimer) { + clearInterval(heartbeatTimer) + heartbeatTimer = null + } +} + +/** 注册工单变更监听,返回注销函数 */ +export function onTicketChanged(callback) { + ticketListeners.add(callback) + return () => ticketListeners.delete(callback) +} + +/** 注册重连成功监听(适合在此主动拉一次数据补齐断线期间变化),返回注销函数 */ +export function onReconnect(callback) { + reconnectListeners.add(callback) + return () => reconnectListeners.delete(callback) +} + +/** 打开连接(引用计数 +1;多处页面可共享同一连接) */ +export function openSocket() { + refCount += 1 + if (!socket || socket.readyState === WebSocket.CLOSED) { + connect() + } +} + +/** 关闭连接(引用计数 -1;归零才真正断开) */ +export function closeSocket() { + refCount = Math.max(0, refCount - 1) + if (refCount === 0) { + manualClosed = true + stopHeartbeat() + if (reconnectTimer) { + clearTimeout(reconnectTimer) + reconnectTimer = null + } + if (socket) { + socket.onclose = null + socket.close() + socket = null + } + } +} + +export function isSocketOpen() { + return socket !== null && socket.readyState === WebSocket.OPEN +} diff --git a/src/views/UserView.vue b/src/views/UserView.vue index 69bda68..0244707 100644 --- a/src/views/UserView.vue +++ b/src/views/UserView.vue @@ -209,7 +209,7 @@