diff --git a/src/utils/ws.js b/src/utils/ws.js new file mode 100644 index 0000000..4278df6 --- /dev/null +++ b/src/utils/ws.js @@ -0,0 +1,199 @@ +import { getToken } from '@/utils/auth' + +/** + * 当前 WebSocket 实例 + */ +let ws = null + +/** + * 重连定时器 + */ +let reconnectTimer = null + +/** + * 是否人为关闭(用于区分主动关闭 vs 异常断开) + */ +let manualClose = false + +/** + * 当前连接的部门ID(用于断线重连) + */ +let currentDeptId = null + +/** + * 当前消息回调函数 + */ +let currentOnMessage = null + +/** + * WebSocket路径 + */ +const WS_PATH = '/ws' + +/** + * 重连间隔(毫秒) + */ +const RECONNECT_INTERVAL = 5000 + +/** + * 获取 WebSocket 基础地址 + * 适配: + * 1. VITE_APP_BASE_API 是完整URL(http://xxx) + * 2. VITE_APP_BASE_API 是相对路径(/dev-api) + */ +function getWsBaseUrl() { + const baseApi = import.meta.env.VITE_APP_BASE_API || '' + + // 如果是完整URL(http/https) + if (/^https?:\/\//.test(baseApi)) { + // 转换为 ws/wss + return baseApi.replace(/^http/, 'ws').replace(/\/$/, '') + } + + // 根据当前页面协议自动判断 ws / wss + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + return `${protocol}//${window.location.host}${baseApi}`.replace(/\/$/, '') +} + +/** + * 构建 WebSocket 连接地址 + * @param deptId 部门ID(用于数据隔离) + */ +function buildWsUrl(deptId) { + const url = new URL(`${getWsBaseUrl()}${WS_PATH}`) + const token = getToken() + + // 当前后端仍依赖 deptId 做数据隔离 + url.searchParams.set('deptId', deptId) + + // token 用于后续鉴权升级(预留) + if (token) { + url.searchParams.set('token', token) + } + + return url.toString() +} + +/** + * 建立 WebSocket 连接 + * @param deptId 当前用户部门ID + * @param onMessage 消息回调函数 + */ +export function connectWs(deptId, onMessage) { + // 必须传 deptId(当前后端依赖) + if (!deptId) { + console.warn('[WebSocket] 缺少 deptId') + return + } + + currentDeptId = deptId + currentOnMessage = onMessage + manualClose = false + + // 清除旧的重连任务 + clearReconnectTimer() + + // 如果已经连接或正在连接,直接返回(防止重复连接) + if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { + return + } + + // 创建 WebSocket 连接 + ws = new WebSocket(buildWsUrl(deptId)) + + /** + * 连接成功 + */ + ws.onopen = () => { + console.log('[WebSocket] 连接成功 deptId=', deptId) + } + + /** + * 收到消息 + */ + ws.onmessage = (event) => { + let data = event.data + + // 尝试解析 JSON + try { + data = JSON.parse(event.data) + } catch (e) { + // 非JSON消息保持原样 + } + + // 回调给业务层 + currentOnMessage && currentOnMessage(data, event) + } + + /** + * 连接关闭 + */ + ws.onclose = () => { + console.log('[WebSocket] 连接关闭 deptId=', currentDeptId) + ws = null + + // 非人为关闭 → 自动重连 + if (!manualClose) { + reconnectTimer = setTimeout(() => { + console.log('[WebSocket] 开始重连 deptId=', currentDeptId) + connectWs(currentDeptId, currentOnMessage) + }, RECONNECT_INTERVAL) + } + } + + /** + * 连接异常 + */ + ws.onerror = (error) => { + console.error('[WebSocket] 连接异常', error) + } +} + +/** + * 主动关闭 WebSocket + */ +export function closeWs() { + manualClose = true + + // 清除重连任务 + clearReconnectTimer() + + if (ws) { + ws.close() + ws = null + } +} + +/** + * 发送消息 + * @param data 支持字符串或对象 + * @returns 是否发送成功 + */ +export function sendWs(data) { + // 连接未就绪直接返回 + if (!ws || ws.readyState !== WebSocket.OPEN) { + console.warn('[WebSocket] 发送失败,连接未建立') + return false + } + + // 自动转JSON + ws.send(typeof data === 'string' ? data : JSON.stringify(data)) + return true +} + +/** + * 获取当前连接状态 + */ +export function getWsState() { + return ws ? ws.readyState : WebSocket.CLOSED +} + +/** + * 清除重连定时器 + */ +function clearReconnectTimer() { + if (reconnectTimer) { + clearTimeout(reconnectTimer) + reconnectTimer = null + } +} \ No newline at end of file