18d28be943
- 引入新的`docker-compose.yml`文件,便于后端和MQTT代理(EMQX)的部署。 - 更新多个组件中的API基本检索,使用函数进行动态解析。 - 加强了多个组件中API调用的错误处理和日志记录。 - 优化了AI聊天面板离线场景的回退响应。 - 更新DataScreen和MediaScreen组件中的数据显示和统计,以反映准确的指标。 - 重构MQTT连接逻辑,以支持动态凭证和客户端ID。
115 lines
3.4 KiB
JavaScript
115 lines
3.4 KiB
JavaScript
import { useState, useEffect } from 'react';
|
||
import mqtt from 'mqtt';
|
||
import { getMqttUrl, getMqttCredentials, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK } from '../config';
|
||
|
||
/* =========================================================
|
||
MQTT 客户端(mqtt.js)—— 后端控制通道
|
||
订阅 opc/display/command(页面/媒体/卡片控制)与 opc/dashboard/tick(数据快照)
|
||
断线自动重连;未连接时静默降级(页面仍可本地操作)
|
||
========================================================= */
|
||
|
||
let client = null;
|
||
let status = 'connecting'; // connecting | connected | offline
|
||
let clientId = null;
|
||
const listeners = new Set();
|
||
|
||
function notifyStatus() {
|
||
window.dispatchEvent(new CustomEvent('dpm:mqtt-status', { detail: { status } }));
|
||
}
|
||
|
||
/* 每个连接生成全局唯一 client_id(独立客户端 ID 铁律):
|
||
优先 crypto.randomUUID(WebView2/现代浏览器),回退时间戳+双段高熵随机 */
|
||
function makeClientId() {
|
||
try {
|
||
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
|
||
return `dpm-screen-${crypto.randomUUID().replace(/-/g, '')}`;
|
||
}
|
||
} catch { /* 继续回退 */ }
|
||
return `dpm-screen-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 12)}-${Math.random().toString(16).slice(2, 10)}`;
|
||
}
|
||
|
||
export function connectMqtt() {
|
||
if (client) return;
|
||
try {
|
||
// 独立客户端 ID:避免多屏/多实例/多标签页同 ID 被 broker 互相踢下线
|
||
clientId = makeClientId();
|
||
// 连接时惰性读取 MQTT 地址/账号:打包部署时 main.jsx 引导已从后端 /api/config 覆盖
|
||
const { username, password } = getMqttCredentials();
|
||
client = mqtt.connect(getMqttUrl(), {
|
||
reconnectPeriod: 3000,
|
||
connectTimeout: 8000,
|
||
clientId,
|
||
clean: true,
|
||
username: username || undefined,
|
||
password: password || undefined,
|
||
});
|
||
client.on('connect', () => {
|
||
status = 'connected';
|
||
notifyStatus();
|
||
client.subscribe([MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK], { qos: 1 });
|
||
});
|
||
client.on('reconnect', () => {
|
||
status = 'connecting';
|
||
notifyStatus();
|
||
});
|
||
client.on('close', () => {
|
||
status = 'offline';
|
||
notifyStatus();
|
||
});
|
||
client.on('error', () => {
|
||
status = 'offline';
|
||
notifyStatus();
|
||
});
|
||
client.on('message', (topic, payload) => {
|
||
let data = null;
|
||
try {
|
||
data = JSON.parse(payload.toString());
|
||
} catch {
|
||
return;
|
||
}
|
||
listeners.forEach((fn) => fn(topic, data));
|
||
});
|
||
} catch (e) {
|
||
status = 'offline';
|
||
notifyStatus();
|
||
}
|
||
}
|
||
|
||
export function getMqttStatus() {
|
||
return status;
|
||
}
|
||
|
||
/** 当前连接的稳定 client_id */
|
||
export function getClientId() {
|
||
return clientId;
|
||
}
|
||
|
||
/** 发布消息(大屏心跳等) */
|
||
export function publishMqtt(topic, payload) {
|
||
if (!client || status !== 'connected') return false;
|
||
try {
|
||
client.publish(topic, JSON.stringify(payload), { qos: 1 });
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/** 订阅 MQTT 消息,返回取消订阅函数 */
|
||
export function onMqttMessage(fn) {
|
||
listeners.add(fn);
|
||
return () => listeners.delete(fn);
|
||
}
|
||
|
||
/** React Hook:MQTT 连接状态 */
|
||
export function useMqttStatus() {
|
||
const [s, setS] = useState(status);
|
||
useEffect(() => {
|
||
connectMqtt();
|
||
const on = (e) => setS(e.detail.status);
|
||
window.addEventListener('dpm:mqtt-status', on);
|
||
return () => window.removeEventListener('dpm:mqtt-status', on);
|
||
}, []);
|
||
return s;
|
||
}
|