diff --git a/src/api/worn/socket.js b/src/api/worn/socket.js index 4dd043f..1a1135e 100644 --- a/src/api/worn/socket.js +++ b/src/api/worn/socket.js @@ -15,3 +15,19 @@ export function controlSocket(data) { data: payload }) } + +// 智慧照明开关远程控制 +export function controlLightSwitch(data) { + const payload = { + devEui: data.devEui, + channel: data.channel, + status: data.status + } + + return request({ + url: '/worn/socket/switch', + method: 'post', + params: payload, + data: payload + }) +} diff --git a/src/utils/ws.js b/src/utils/ws.js index 310c26f..a09c6cf 100644 --- a/src/utils/ws.js +++ b/src/utils/ws.js @@ -3,8 +3,8 @@ import { getToken } from '@/utils/auth' let ws = null let reconnectTimer = null let manualClose = false -let currentOnMessage = null let retryCount = 0 +const subscribers = new Set() const WS_PATH = '/ws' const MAX_RECONNECT_DELAY = 30000 @@ -37,7 +37,10 @@ export function connectWs(onMessage) { return } - currentOnMessage = onMessage + if (typeof onMessage === 'function') { + subscribers.add(onMessage) + } + manualClose = false clearReconnectTimer() @@ -61,17 +64,23 @@ export function connectWs(onMessage) { // Keep plain text messages as-is. } - currentOnMessage && currentOnMessage(data, event) + subscribers.forEach((handler) => { + try { + handler(data, event) + } catch (error) { + console.error('[WebSocket] subscriber error', error) + } + }) } ws.onclose = () => { console.log('[WebSocket] closed') ws = null - if (!manualClose) { + if (!manualClose && subscribers.size > 0) { reconnectTimer = setTimeout(() => { console.log('[WebSocket] reconnecting') - connectWs(currentOnMessage) + connectWs() }, getReconnectDelay()) } } @@ -81,21 +90,42 @@ export function connectWs(onMessage) { } } -export function closeWs() { +export function closeWs(onMessage) { + if (typeof onMessage === 'function') { + subscribers.delete(onMessage) + } else { + subscribers.clear() + } + + if (subscribers.size > 0) { + return + } + manualClose = true retryCount = 0 clearReconnectTimer() + if (ws) { + ws.close() + ws = null + } +} + +export function reconnectWs(onMessage) { + if (typeof onMessage === 'function') { + subscribers.add(onMessage) + } + + manualClose = false + retryCount = 0 + clearReconnectTimer() if (ws) { ws.close() ws = null } -} -export function reconnectWs(onMessage = currentOnMessage) { - closeWs() manualClose = false - connectWs(onMessage) + connectWs() } export function sendWs(data) { diff --git a/src/views/index.vue b/src/views/index.vue index 4fba2bb..14f7573 100644 --- a/src/views/index.vue +++ b/src/views/index.vue @@ -391,7 +391,7 @@ function normalizeEvent(data) { const now = new Date() const type = data?.type || 'unknown' const level = getEventLevel(data) - const deviceName = data?.deviceName || `设备 ${data?.deviceId || ''}`.trim() + const deviceName = getRealtimeDeviceLabel(data) const deptName = data?.deptName || `部门 ${data?.deptId || ''}`.trim() const desc = translateEventValue(data?.eventDesc || data?.statusDesc || data?.event || getTypeName(type)) @@ -405,6 +405,57 @@ function normalizeEvent(data) { } } +function getRealtimeDeviceLabel(data) { + const rawDeviceName = String(data?.deviceName || '').trim() + const remark = String(data?.remark || data?.deviceRemark || data?.deviceAlias || '').trim() + const typeLabel = getRealtimeDeviceTypeName(data) + + if (remark) { + if (rawDeviceName && rawDeviceName !== remark) { + return `${remark}(${rawDeviceName})` + } + return remark + } + + if (typeLabel) { + if (rawDeviceName) { + return `${typeLabel}(${rawDeviceName})` + } + return typeLabel + } + + return rawDeviceName || `设备 ${data?.deviceId || ''}`.trim() +} + +function getRealtimeDeviceTypeName(data) { + const rawType = String(data?.deviceType || data?.type || '').trim().toLowerCase() + const rawName = String(data?.deviceName || '').trim().toLowerCase() + + if (rawType === 'switch' || rawName.includes('light-switch') || rawName.includes('light switch')) { + return '智慧照明开关' + } + if (rawName.includes('ac-socket') || rawName.includes('ac socket') || rawName.includes('空调')) { + return '智能空调插座' + } + if (rawType === 'socket' || rawName.includes('socket')) { + return '智能排风' + } + if (rawType === 'smoke' || rawName.includes('smoke')) { + return '烟雾传感器' + } + if (rawType === 'water' || rawName.includes('water')) { + return '水浸传感器' + } + if (rawType === 'door' || rawName.includes('door')) { + return '门禁传感器' + } + if (rawType === 'env' || rawName.includes('env')) { + return '环境传感器' + } + + return '' +} + function isDuplicateRealtimeEvent(event) { const now = Date.now() const lastTime = recentEventKeys.get(event.fingerprint) @@ -436,7 +487,10 @@ function buildEventFingerprint(data) { concentration: data?.concentration, water: data?.water, doorStatus: data?.doorStatus, - tamperStatus: data?.tamperStatus + tamperStatus: data?.tamperStatus, + switch1: data?.switch1 ?? data?.switch_1, + switch2: data?.switch2 ?? data?.switch_2, + switch3: data?.switch3 ?? data?.switch_3 }) } @@ -540,7 +594,13 @@ function buildExtraMetrics(data, knownKeys) { 'deptName', 'eventDesc', 'statusDesc', - 'time' + 'time', + 'switch_1_change', + 'switch1_change', + 'switch_2_change', + 'switch2_change', + 'switch_3_change', + 'switch3_change' ]) return Object.keys(data || {}) @@ -559,6 +619,9 @@ function formatExtraMetricValue(key, value) { if (key === 'level') { return translateLevelValue(value) } + if (['switch_1', 'switch_2', 'switch_3', 'switch1', 'switch2', 'switch3'].includes(key)) { + return translateSwitchShort(value) + } return String(value) } @@ -607,11 +670,28 @@ function getExtraMetricLabel(key) { const labelMap = { event: '事件', eventType: '事件类型', - level: '等级' + level: '等级', + switch_1: '开关1', + switch_2: '开关2', + switch_3: '开关3', + switch1: '开关1', + switch2: '开关2', + switch3: '开关3' } return labelMap[key] || key } +function translateSwitchShort(value) { + const normalized = String(value ?? '').trim().toLowerCase() + if (normalized === '1' || normalized === 'on' || normalized === 'true' || normalized === 'open' || normalized === 'opened') { + return '开' + } + if (normalized === '0' || normalized === 'off' || normalized === 'false' || normalized === 'close' || normalized === 'closed') { + return '关' + } + return String(value ?? '--') +} + function getEventLevel(data) { if (data?.type === 'alarm' || data?.eventType === 'alarm' || data?.event === 'alarm') { return 'alarm' @@ -680,7 +760,7 @@ onBeforeUnmount(() => { clearTimeout(tickerTimer) tickerTimer = null } - closeWs() + closeWs(handleWsMessage) }) diff --git a/src/views/worn/mqttDevice/index.vue b/src/views/worn/mqttDevice/index.vue index 7723da2..738d003 100644 --- a/src/views/worn/mqttDevice/index.vue +++ b/src/views/worn/mqttDevice/index.vue @@ -57,6 +57,7 @@ +