优化 mqtt 退出路由 不订阅消息
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Layout, Card, Row, Col, Statistic, Tag, Button, Table,
|
Layout, Card, Row, Col, Statistic, Tag, Button, Table,
|
||||||
Typography, Space, Progress, Badge, Timeline, message,
|
Typography, Space, Progress, Badge, Timeline, message,
|
||||||
@@ -291,7 +291,7 @@ export default function DeviceOverviewPage() {
|
|||||||
}, [selectedDevice, uavState, persistentHostData, persistentDroneData?.wind_speed]);
|
}, [selectedDevice, uavState, persistentHostData, persistentDroneData?.wind_speed]);
|
||||||
|
|
||||||
// MQTT消息处理
|
// MQTT消息处理
|
||||||
const handleMqttMessage = (topic, message) => {
|
const handleMqttMessage = useCallback((topic, message) => {
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -398,7 +398,7 @@ export default function DeviceOverviewPage() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('MQTT解析失败', err);
|
console.error('MQTT解析失败', err);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// MQTT订阅
|
// MQTT订阅
|
||||||
const mqttTopics = useMemo(() => {
|
const mqttTopics = useMemo(() => {
|
||||||
@@ -417,6 +417,7 @@ export default function DeviceOverviewPage() {
|
|||||||
url: envConfig.mqtt_url,
|
url: envConfig.mqtt_url,
|
||||||
topic: mqttTopics,
|
topic: mqttTopics,
|
||||||
onMessage: handleMqttMessage,
|
onMessage: handleMqttMessage,
|
||||||
|
enabled: mqttTopics.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 机器人视频
|
// 机器人视频
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export default function DeviceStatusPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 当MQTT数据更新时,更新持久化数据(只在有新数据时更新)
|
// 当MQTT数据更新时,更新持久化数据(只在有新数据时更新)
|
||||||
const handleMqttMessage = (topic, message) => {
|
const handleMqttMessage = useCallback((topic, message) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedMessage = JSON.parse(message);
|
const parsedMessage = JSON.parse(message);
|
||||||
@@ -355,7 +355,7 @@ export default function DeviceStatusPage() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('DeviceStatusPage MQTT 消息解析失败:', err);
|
console.error('DeviceStatusPage MQTT 消息解析失败:', err);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// 根据选中设备动态构建MQTT主题
|
// 根据选中设备动态构建MQTT主题
|
||||||
const mqttTopics = useMemo(() => {
|
const mqttTopics = useMemo(() => {
|
||||||
@@ -377,6 +377,7 @@ export default function DeviceStatusPage() {
|
|||||||
url: envConfig.mqtt_url,
|
url: envConfig.mqtt_url,
|
||||||
topic: mqttTopics,
|
topic: mqttTopics,
|
||||||
onMessage: handleMqttMessage,
|
onMessage: handleMqttMessage,
|
||||||
|
enabled: mqttTopics.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 合并无人机和机场数据
|
// 合并无人机和机场数据
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Row, Col, Card, Button, Space, Table, Tag, Typography, Progress, Badge,
|
Row, Col, Card, Button, Space, Table, Tag, Typography, Progress, Badge,
|
||||||
Tabs, Input, Switch, Divider, Tooltip, Timeline, Select, message, DatePicker,
|
Tabs, Input, Switch, Divider, Tooltip, Timeline, Select, message, DatePicker,
|
||||||
@@ -93,7 +93,7 @@ export default function WayLinePage({ onRefresh }) {
|
|||||||
|
|
||||||
|
|
||||||
// 当MQTT数据更新时,更新持久化数据(只在有新数据时更新)
|
// 当MQTT数据更新时,更新持久化数据(只在有新数据时更新)
|
||||||
const handleMqttMessage = (topic, message) => {
|
const handleMqttMessage = useCallback((topic, message) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedMessage = JSON.parse(message);
|
const parsedMessage = JSON.parse(message);
|
||||||
@@ -201,7 +201,7 @@ export default function WayLinePage({ onRefresh }) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('WayLinePage MQTT 消息解析失败:', err);
|
console.error('WayLinePage MQTT 消息解析失败:', err);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// 根据选中设备动态构建MQTT主题
|
// 根据选中设备动态构建MQTT主题
|
||||||
const mqttTopics = React.useMemo(() => {
|
const mqttTopics = React.useMemo(() => {
|
||||||
@@ -223,6 +223,7 @@ export default function WayLinePage({ onRefresh }) {
|
|||||||
url: envConfig.mqtt_url,
|
url: envConfig.mqtt_url,
|
||||||
topic: mqttTopics,
|
topic: mqttTopics,
|
||||||
onMessage: handleMqttMessage,
|
onMessage: handleMqttMessage,
|
||||||
|
enabled: mqttTopics.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 优先使用持久化数据,保持显示稳定
|
// 优先使用持久化数据,保持显示稳定
|
||||||
|
|||||||
@@ -1,53 +1,188 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
import mqtt, { MqttClient } from 'mqtt';
|
import mqtt, { MqttClient } from 'mqtt';
|
||||||
|
|
||||||
interface Options {
|
/* ============================================================
|
||||||
url: string;
|
* MQTT 连接池 —— 同一 URL 共享一个连接,避免重复建连
|
||||||
topic: string | string[];
|
* ============================================================ */
|
||||||
onMessage: (topic: string, message: string) => void;
|
const connectionPool = new Map<string, { client: MqttClient; refCount: number }>();
|
||||||
|
|
||||||
|
function getOrCreateClient(url: string, options?: mqtt.IClientOptions): MqttClient {
|
||||||
|
const entry = connectionPool.get(url);
|
||||||
|
if (entry) {
|
||||||
|
entry.refCount++;
|
||||||
|
return entry.client;
|
||||||
|
}
|
||||||
|
const client = mqtt.connect(url, {
|
||||||
|
reconnectPeriod: 3000,
|
||||||
|
clean: true,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
connectionPool.set(url, { client, refCount: 1 });
|
||||||
|
|
||||||
|
// 连接断开后自动清理池条目
|
||||||
|
client.on('close', () => {
|
||||||
|
const cur = connectionPool.get(url);
|
||||||
|
if (cur && cur.client === client) {
|
||||||
|
connectionPool.delete(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMqtt({ url, topic, onMessage }: Options) {
|
function releaseClient(url: string): void {
|
||||||
const clientRef = useRef<MqttClient | null>(null);
|
const entry = connectionPool.get(url);
|
||||||
|
if (!entry) return;
|
||||||
|
entry.refCount--;
|
||||||
|
if (entry.refCount <= 0) {
|
||||||
|
entry.client.end(true);
|
||||||
|
connectionPool.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
* useMqtt Hook
|
||||||
|
* ============================================================ */
|
||||||
|
interface UseMqttOptions {
|
||||||
|
/** broker 地址,如 wss://xxx/mqtt */
|
||||||
|
url: string;
|
||||||
|
/** 要订阅的主题(字符串或数组),传空则不订阅 */
|
||||||
|
topic: string | string[];
|
||||||
|
/** 收到消息的回调 */
|
||||||
|
onMessage: (topic: string, message: string) => void;
|
||||||
|
/** 是否启用订阅,默认 true;设为 false 时不会建立连接 */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** MQTT 连接额外配置(可选) */
|
||||||
|
connectOptions?: mqtt.IClientOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseMqttReturn {
|
||||||
|
/** 当前连接状态 */
|
||||||
|
connected: boolean;
|
||||||
|
/** 手动发布消息 */
|
||||||
|
publish: (topic: string, message: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMqtt({
|
||||||
|
url,
|
||||||
|
topic,
|
||||||
|
onMessage,
|
||||||
|
enabled = true,
|
||||||
|
connectOptions,
|
||||||
|
}: UseMqttOptions): UseMqttReturn {
|
||||||
|
const clientRef = useRef<MqttClient | null>(null);
|
||||||
|
const onMessageRef = useRef(onMessage);
|
||||||
|
const [connected, setConnected] = useState(false);
|
||||||
|
|
||||||
|
// 始终保持 onMessage 回调为最新引用,避免闭包过期
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const client = mqtt.connect(url, {
|
onMessageRef.current = onMessage;
|
||||||
reconnectPeriod: 3000,
|
}, [onMessage]);
|
||||||
|
|
||||||
|
// 将 topic 统一转为数组,方便做 diff
|
||||||
|
const topicList = (Array.isArray(topic) ? topic : [topic]).filter(Boolean);
|
||||||
|
const topicKey = topicList.join(',');
|
||||||
|
|
||||||
|
/* ---------- 核心:建连 / 订阅 / 清理 ---------- */
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !url) return;
|
||||||
|
|
||||||
|
const client = getOrCreateClient(url, {
|
||||||
username: 'maibu',
|
username: 'maibu',
|
||||||
password: 'jsmbzn520',
|
password: 'jsmbzn520',
|
||||||
clean: true,
|
...connectOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
clientRef.current = client;
|
clientRef.current = client;
|
||||||
|
|
||||||
client.on('connect', () => {
|
/* --- 连接成功 → 订阅主题 --- */
|
||||||
console.log('MQTT connected');
|
const handleConnect = () => {
|
||||||
client.subscribe(topic);
|
setConnected(true);
|
||||||
});
|
if (topicList.length > 0) {
|
||||||
|
client.subscribe(topicList, (err) => {
|
||||||
client.on('message', (t, payload) => {
|
if (err) console.error('[useMqtt] subscribe failed:', err);
|
||||||
onMessage(t, payload.toString());
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
client.on('error', err => {
|
|
||||||
console.error('MQTT error', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
client.end(true);
|
|
||||||
};
|
};
|
||||||
}, [url, JSON.stringify(topic)]);
|
|
||||||
|
|
||||||
return clientRef;
|
/* --- 收到消息 → 回调 --- */
|
||||||
|
const handleMessage = (t: string, payload: Buffer) => {
|
||||||
|
console.log(payload.toString(), "订阅无人机数据")
|
||||||
|
onMessageRef.current(t, payload.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --- 连接状态变化 --- */
|
||||||
|
const handleReconnect = () => setConnected(false);
|
||||||
|
const handleOffline = () => setConnected(false);
|
||||||
|
const handleError = (err: Error) => {
|
||||||
|
console.error('[useMqtt] error:', err);
|
||||||
|
};
|
||||||
|
|
||||||
|
client.on('connect', handleConnect);
|
||||||
|
client.on('message', handleMessage);
|
||||||
|
client.on('reconnect', handleReconnect);
|
||||||
|
client.on('offline', handleOffline);
|
||||||
|
client.on('error', handleError);
|
||||||
|
|
||||||
|
// 如果连接已经建立,直接订阅
|
||||||
|
if (client.connected && topicList.length > 0) {
|
||||||
|
client.subscribe(topicList);
|
||||||
|
setConnected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 清理:退出路由时取消订阅 & 释放连接 --- */
|
||||||
|
return () => {
|
||||||
|
// 取消当前 hook 订阅的主题
|
||||||
|
if (topicList.length > 0) {
|
||||||
|
client.unsubscribe(topicList);
|
||||||
|
}
|
||||||
|
// 移除本 hook 注册的事件监听
|
||||||
|
client.removeListener('connect', handleConnect);
|
||||||
|
client.removeListener('message', handleMessage);
|
||||||
|
client.removeListener('reconnect', handleReconnect);
|
||||||
|
client.removeListener('offline', handleOffline);
|
||||||
|
client.removeListener('error', handleError);
|
||||||
|
|
||||||
|
setConnected(false);
|
||||||
|
|
||||||
|
// 释放连接池引用(refCount=0 时自动断开)
|
||||||
|
releaseClient(url);
|
||||||
|
clientRef.current = null;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [enabled, url, topicKey]);
|
||||||
|
|
||||||
|
/* ---------- 主题热更新:topic 变化时 diff 订阅 ---------- */
|
||||||
|
const prevTopicKeyRef = useRef(topicKey);
|
||||||
|
useEffect(() => {
|
||||||
|
const client = clientRef.current;
|
||||||
|
if (!client || !client.connected) return;
|
||||||
|
|
||||||
|
const prevKey = prevTopicKeyRef.current;
|
||||||
|
prevTopicKeyRef.current = topicKey;
|
||||||
|
|
||||||
|
if (prevKey === topicKey) return;
|
||||||
|
|
||||||
|
// 取消旧主题
|
||||||
|
const prevTopics = prevKey ? prevKey.split(',').filter(Boolean) : [];
|
||||||
|
if (prevTopics.length > 0) {
|
||||||
|
client.unsubscribe(prevTopics);
|
||||||
|
}
|
||||||
|
// 订阅新主题
|
||||||
|
if (topicList.length > 0) {
|
||||||
|
client.subscribe(topicList);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [topicKey]);
|
||||||
|
|
||||||
|
/* ---------- 手动发布 ---------- */
|
||||||
|
const publish = useCallback((t: string, message: string) => {
|
||||||
|
const client = clientRef.current;
|
||||||
|
if (client && client.connected) {
|
||||||
|
client.publish(t, message);
|
||||||
|
} else {
|
||||||
|
console.warn('[useMqtt] publish skipped: not connected');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { connected, publish };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* 使用 */
|
|
||||||
// useMqtt({
|
|
||||||
// url: 'wss://broker.emqx.io:8084/mqtt',
|
|
||||||
// topic: 'robot/status',
|
|
||||||
// onMessage: (_, message) => {
|
|
||||||
// setMsg(message);
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|||||||
@@ -489,8 +489,8 @@ const zh = {
|
|||||||
controlLogTitle: '控制日志标题',
|
controlLogTitle: '控制日志标题',
|
||||||
controlRequestContent: '控制请求内容',
|
controlRequestContent: '控制请求内容',
|
||||||
countUnit: '数量单位',
|
countUnit: '数量单位',
|
||||||
coverClosed: '覆盖已关闭',
|
coverClosed: '已关闭',
|
||||||
coverOpen: '覆盖打开',
|
coverOpen: '打开',
|
||||||
coverageEquipment: '覆盖设备',
|
coverageEquipment: '覆盖设备',
|
||||||
coverageValue: '{{types}}/{{units}}',
|
coverageValue: '{{types}}/{{units}}',
|
||||||
createSuccess: '创建成功',
|
createSuccess: '创建成功',
|
||||||
@@ -586,7 +586,7 @@ const zh = {
|
|||||||
headingAngle: '航向角度',
|
headingAngle: '航向角度',
|
||||||
horizontalSpeed: '水平速度',
|
horizontalSpeed: '水平速度',
|
||||||
hotspotSuspect: '热斑疑似',
|
hotspotSuspect: '热斑疑似',
|
||||||
inDock: '内停靠',
|
inDock: '舱内',
|
||||||
inProgressDesc: '进行中',
|
inProgressDesc: '进行中',
|
||||||
indoor: '室内',
|
indoor: '室内',
|
||||||
inputCustomAlias: '输入自定义别名',
|
inputCustomAlias: '输入自定义别名',
|
||||||
@@ -673,7 +673,7 @@ const zh = {
|
|||||||
optimize: '优化',
|
optimize: '优化',
|
||||||
optionalEmpty: '可选空',
|
optionalEmpty: '可选空',
|
||||||
otherWebControlling: '其他网页控制中',
|
otherWebControlling: '其他网页控制中',
|
||||||
outDock: '外停靠',
|
outDock: '舱外',
|
||||||
outdoor: '室外',
|
outdoor: '室外',
|
||||||
panorama: '全景',
|
panorama: '全景',
|
||||||
paramArmChannel: '参数布防渠道',
|
paramArmChannel: '参数布防渠道',
|
||||||
@@ -841,18 +841,18 @@ const zh = {
|
|||||||
stationStatus: '电站状态',
|
stationStatus: '电站状态',
|
||||||
stationTopology: '电站拓扑',
|
stationTopology: '电站拓扑',
|
||||||
stationVideo: '电站视频',
|
stationVideo: '电站视频',
|
||||||
statusExecFailed: '状态执行失败',
|
statusExecFailed: '执行失败',
|
||||||
statusExecSuccess: '状态执行成功',
|
statusExecSuccess: '执行成功',
|
||||||
statusPendingApproval: '状态待处理审批',
|
statusPendingApproval: '状态待处理审批',
|
||||||
statusPreparing: '状态准备中',
|
statusPreparing: '准备中',
|
||||||
statusRestored: '状态已恢复',
|
statusRestored: '已恢复',
|
||||||
statusStartFailed: '状态开始失败',
|
statusStartFailed: '开始失败',
|
||||||
statusSuggestExec: '状态建议执行',
|
statusSuggestExec: '建议执行',
|
||||||
statusTerminated: '状态已终止',
|
statusTerminated: '已终止',
|
||||||
statusTimeout: '状态超时',
|
statusTimeout: '超时',
|
||||||
statusUpdateSuccess: '状态更新成功',
|
statusUpdateSuccess: '更新成功',
|
||||||
statusWaitExec: '状态等待执行',
|
statusWaitExec: '等待执行',
|
||||||
statusWaitingStart: '状态等待中开始',
|
statusWaitingStart: '等待中开始',
|
||||||
stopVideoBtn: '停止视频按钮',
|
stopVideoBtn: '停止视频按钮',
|
||||||
suggestAction: '建议操作',
|
suggestAction: '建议操作',
|
||||||
suggestTime: '建议时间',
|
suggestTime: '建议时间',
|
||||||
|
|||||||
Reference in New Issue
Block a user