From 87e57ea486b15c6a2afbf0e7cfd024f30c464596 Mon Sep 17 00:00:00 2001 From: Pine Date: Wed, 16 Sep 2026 22:34:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(mqtt):=20MQTT=20=E8=BF=9E=E6=8E=A5=E6=88=90?= =?UTF-8?q?=E5=8A=9F=E5=90=8E=E8=87=AA=E5=8A=A8=E5=91=A8=E6=9C=9F=E4=B8=8A?= =?UTF-8?q?=E6=8A=A5=E5=BF=83=E8=B7=B3=EF=BC=88=E7=BB=91=E5=AE=9A=E9=A1=B5?= =?UTF-8?q?/=E4=B8=BB=E5=B1=8F=E9=80=9A=E7=94=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mqtt.js:connect 后立即+每10s 上报 opc/display/heartbeat(client_id/tenant_id/page), 绑定页(BindScreen)不再只连不报——admin 端屏幕管理即可看到在线大屏; 断线 close 时清定时器,重连后自动恢复 - useMqttControl.js:移除重复心跳(避免双发),保留注册/状态上报/订阅逻辑 --- src/utils/mqtt.js | 27 ++++++++++++++++++++++++++- src/utils/useMqttControl.js | 20 ++------------------ 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/utils/mqtt.js b/src/utils/mqtt.js index 4987518..169089f 100644 --- a/src/utils/mqtt.js +++ b/src/utils/mqtt.js @@ -1,23 +1,42 @@ import { useState, useEffect } from 'react'; import mqtt from 'mqtt'; -import { getMqttUrl, getMqttCredentials, getDeviceId, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK, MQTT_TOPIC_BIND } from '../config'; +import { getMqttUrl, getMqttCredentials, getDeviceId, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK, MQTT_TOPIC_BIND, MQTT_TOPIC_HEARTBEAT } from '../config'; import { getTenantId } from './tenant'; /* ========================================================= MQTT 客户端(mqtt.js)—— 后端控制通道 订阅 opc/display/command(页面/媒体/卡片控制)与 opc/dashboard/tick(数据快照) + 连接成功后自动周期上报心跳 opc/display/heartbeat(绑定页/主屏通用, + 后端据此统计在线大屏,admin 端屏幕管理显示在线状态) 断线自动重连;未连接时静默降级(页面仍可本地操作) ========================================================= */ +const HEARTBEAT_MS = 10000; + let client = null; let status = 'connecting'; // connecting | connected | offline let clientId = null; +let heartbeatTimer = null; const listeners = new Set(); function notifyStatus() { window.dispatchEvent(new CustomEvent('dpm:mqtt-status', { detail: { status } })); } +/** 心跳上报(连接成功后自动周期调用;绑定页/主屏均生效) */ +function sendHeartbeat() { + if (status !== 'connected' || !client) return; + try { + const page = (typeof window !== 'undefined' && window.location) ? window.location.pathname : ''; + client.publish(MQTT_TOPIC_HEARTBEAT, JSON.stringify({ + client_id: getClientId() || getDeviceId(), + tenant_id: getTenantId(), + page, + ts: Date.now(), + }), { qos: 1 }); + } catch { /* 忽略单次失败 */ } +} + /* 每个连接生成全局唯一 client_id(独立客户端 ID 铁律): 优先 crypto.randomUUID(WebView2/现代浏览器),回退时间戳+双段高熵随机 */ function makeClientId() { @@ -55,6 +74,11 @@ export function connectMqtt() { tid ? `${MQTT_TOPIC_TICK}/${tid}` : MQTT_TOPIC_TICK, tid ? `${MQTT_TOPIC_COMMAND}/${tid}/${clientId}` : `${MQTT_TOPIC_COMMAND}/${clientId}`, ], { qos: 1 }); + // 连接成功即启动心跳(绑定页/主屏通用;幂等防重复定时器) + if (!heartbeatTimer) { + sendHeartbeat(); + heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_MS); + } }); client.on('reconnect', () => { status = 'connecting'; @@ -63,6 +87,7 @@ export function connectMqtt() { client.on('close', () => { status = 'offline'; notifyStatus(); + if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } }); client.on('error', () => { status = 'offline'; diff --git a/src/utils/useMqttControl.js b/src/utils/useMqttControl.js index f199348..00df0eb 100644 --- a/src/utils/useMqttControl.js +++ b/src/utils/useMqttControl.js @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { onMqttMessage, connectMqtt, publishMqtt, getClientId, useMqttStatus } from './mqtt'; -import { MQTT_TOPIC_HEARTBEAT, MQTT_TOPIC_COMMAND, MQTT_TOPIC_BIND, getApiBase as API_BASE } from '../config'; +import { MQTT_TOPIC_COMMAND, MQTT_TOPIC_BIND, getApiBase as API_BASE } from '../config'; import { getTenantId, clearBinding } from './tenant'; import { Sfx } from './sounds'; import { PAGE_ORDER } from './pageNav'; @@ -75,23 +75,7 @@ export function useMqttControl() { return off; }, []); - // 大屏在线心跳(后端据此统计在线大屏数) - useEffect(() => { - connectMqtt(); - const sendHeartbeat = () => { - if (mqttStatus === 'connected') { - publishMqtt(MQTT_TOPIC_HEARTBEAT, { - client_id: getClientId(), - tenant_id: getTenantId(), - page: location.pathname, - ts: Date.now(), - }); - } - }; - sendHeartbeat(); - const hb = setInterval(sendHeartbeat, 10000); - return () => clearInterval(hb); - }, [mqttStatus, location.pathname]); + // 大屏在线心跳已由 mqtt.js 连接成功后自动周期上报(绑定页/主屏通用),此处不再重复发送 // 页面状态即时上报:路由变化即通知服务端当前所在页面 useEffect(() => {