feat(web): 新增前端预警列表页面与服务层。实现前端预警列表展示页、WebSocket 订阅服务和全局预警状态管理。
This commit is contained in:
158
apps/web/src/services/mqtt.client.js
Normal file
158
apps/web/src/services/mqtt.client.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 预警 WebSocket 客户端 (MVP-2 / D23)
|
||||
*
|
||||
* 连接后端 /ws/alerts,接收实时预警事件并推送到 Pinia Store。
|
||||
*
|
||||
* 提供:
|
||||
* - 自动重连 (指数退避,最多重试 10 次)
|
||||
* - 心跳 (每 30 秒 ping)
|
||||
* - 过滤订阅 (按事件类型 / source_id)
|
||||
* - 连接状态同步到 alertStore
|
||||
*
|
||||
* 注意: 当前架构中前端不直连 MQTT Broker,而是通过后端 WebSocket
|
||||
* 代理接收预警,MQTT 仅用于后端与其他系统(如移动端)的互通。
|
||||
*/
|
||||
|
||||
import { useAlertStore } from '@/stores/alertStore'
|
||||
|
||||
// ---- 配置 ----
|
||||
const WS_BASE = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/api/ws/alerts`
|
||||
const PING_INTERVAL = 30000 // 心跳间隔 (ms)
|
||||
const RECONNECT_BASE = 2000 // 首次重连间隔 (ms)
|
||||
const RECONNECT_MAX = 30000 // 最大重连间隔 (ms)
|
||||
const MAX_RECONNECT_RETRIES = 10 // 最大重连次数
|
||||
|
||||
// ---- 内部状态 ----
|
||||
let socket = null
|
||||
let pingTimer = null
|
||||
let reconnectTimer = null
|
||||
let reconnectAttempts = 0
|
||||
let filterConfig = null
|
||||
|
||||
/**
|
||||
* 连接预警 WebSocket
|
||||
* @param {Object} [filter] - 订阅过滤 { event_types?: string[], source_ids?: string[] }
|
||||
*/
|
||||
export function connectAlertSocket(filter) {
|
||||
const store = useAlertStore()
|
||||
|
||||
// 避免重复连接
|
||||
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
||||
if (filter) {
|
||||
sendSubscribe(filter)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
filterConfig = filter || null
|
||||
socket = new WebSocket(WS_BASE)
|
||||
|
||||
socket.onopen = () => {
|
||||
store.setConnectionStatus('connected')
|
||||
reconnectAttempts = 0
|
||||
startPing()
|
||||
|
||||
// 发送订阅
|
||||
if (filterConfig) {
|
||||
sendSubscribe(filterConfig)
|
||||
}
|
||||
}
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data)
|
||||
|
||||
if (message.type === 'alert') {
|
||||
store.addAlert(message.data)
|
||||
} else if (message.type === 'welcome') {
|
||||
// 连接成功确认, 暂不处理
|
||||
} else if (message.type === 'subscribed') {
|
||||
// 订阅成功确认, 暂不处理
|
||||
} else if (message.type === 'pong') {
|
||||
// 心跳响应, 暂不处理
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[AlertWS] 消息解析失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
socket.onclose = (event) => {
|
||||
store.setConnectionStatus('disconnected')
|
||||
stopPing()
|
||||
scheduleReconnect()
|
||||
}
|
||||
|
||||
socket.onerror = () => {
|
||||
// onclose 会自动触发, 不重复处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开预警 WebSocket
|
||||
*/
|
||||
export function disconnectAlertSocket() {
|
||||
stopPing()
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectAttempts = 0
|
||||
|
||||
if (socket) {
|
||||
socket.onclose = null
|
||||
socket.onerror = null
|
||||
socket.close()
|
||||
socket = null
|
||||
}
|
||||
|
||||
useAlertStore().setConnectionStatus('disconnected')
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订阅过滤
|
||||
*/
|
||||
export function subscribeAlert(filter) {
|
||||
filterConfig = filter || null
|
||||
sendSubscribe(filter)
|
||||
}
|
||||
|
||||
function sendSubscribe(filter) {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) return
|
||||
socket.send(JSON.stringify({
|
||||
action: 'subscribe',
|
||||
filter: filter || {}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 心跳: 每 30s 发送 ping
|
||||
*/
|
||||
function startPing() {
|
||||
stopPing()
|
||||
pingTimer = setInterval(() => {
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({ action: 'ping' }))
|
||||
}
|
||||
}, PING_INTERVAL)
|
||||
}
|
||||
|
||||
function stopPing() {
|
||||
if (pingTimer) {
|
||||
clearInterval(pingTimer)
|
||||
pingTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (reconnectAttempts >= MAX_RECONNECT_RETRIES) return
|
||||
|
||||
const delay = Math.min(
|
||||
RECONNECT_BASE * Math.pow(2, reconnectAttempts),
|
||||
RECONNECT_MAX
|
||||
)
|
||||
|
||||
useAlertStore().setConnectionStatus('connecting')
|
||||
reconnectAttempts++
|
||||
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectTimer = setTimeout(() => {
|
||||
connectAlertSocket(filterConfig)
|
||||
}, delay)
|
||||
}
|
||||
Reference in New Issue
Block a user