fix: 管理后台连接状态 + 修复按钮无响应

连接状态(正式):
- /api/display/state 扩展:screens_online(大屏心跳统计,opc/display/heartbeat)+ last_command(最近指令与发布结果)
- 前端大屏每 10s 上报心跳(稳定 client_id),后端 30s TTL 统计在线大屏数
- 管理台新增「连接状态」卡片:后端/MQTT/在线大屏/最近指令,3s 轮询刷新

按钮无响应根治:
- admin.js 改为 document 事件委托(data-nav/data-control/data-action/复制/预览),不再依赖逐个绑定
- /static 与 /admin 响应加 no-cache 头,杜绝旧 JS/CSS 缓存导致的按钮失效
- SSE 不再自动重载页面(避免与操作回调双重重载清掉反馈);立即播放改为 toast 不重载
This commit is contained in:
Pine
2026-08-17 21:54:23 +08:00
parent 1a95ecbacb
commit c55835fb7d
10 changed files with 297 additions and 86 deletions
+19 -1
View File
@@ -10,6 +10,7 @@ import { MQTT_URL, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK, MQTT_USERNAME, MQTT_PASS
let client = null;
let status = 'connecting'; // connecting | connected | offline
let clientId = null;
const listeners = new Set();
function notifyStatus() {
@@ -19,10 +20,11 @@ function notifyStatus() {
export function connectMqtt() {
if (client) return;
try {
clientId = `dpm-screen-${Math.random().toString(16).slice(2, 8)}`;
client = mqtt.connect(MQTT_URL, {
reconnectPeriod: 3000,
connectTimeout: 8000,
clientId: `dpm-screen-${Math.random().toString(16).slice(2, 8)}`,
clientId,
clean: true,
username: MQTT_USERNAME || undefined,
password: MQTT_PASSWORD || undefined,
@@ -63,6 +65,22 @@ 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);
+22 -1
View File
@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { onMqttMessage, connectMqtt } from './mqtt';
import { onMqttMessage, connectMqtt, publishMqtt, getClientId, useMqttStatus } from './mqtt';
import { MQTT_TOPIC_HEARTBEAT } from '../config';
import { PAGE_ORDER } from './pageNav';
/* =========================================================
@@ -9,6 +10,7 @@ import { PAGE_ORDER } from './pageNav';
· play / pause / next / prev / set_mode:媒体控制
· alert:全局通知
· show_card:展示信息卡片(AI 工具调用 / 管理端指令)
· 心跳:定时上报 opc/display/heartbeat(后端统计在线大屏)
========================================================= */
const PAGE_ALIAS = {
@@ -28,7 +30,26 @@ function resolvePage(page) {
export function useMqttControl() {
const navigate = useNavigate();
const location = useLocation();
const mqttStatus = useMqttStatus();
// 大屏在线心跳(后端据此统计在线大屏数)
useEffect(() => {
connectMqtt();
const sendHeartbeat = () => {
if (mqttStatus === 'connected') {
publishMqtt(MQTT_TOPIC_HEARTBEAT, {
client_id: getClientId(),
page: location.pathname,
ts: Date.now(),
});
}
};
sendHeartbeat();
const hb = setInterval(sendHeartbeat, 10000);
return () => clearInterval(hb);
}, [mqttStatus, location.pathname]);
// 控制指令订阅
useEffect(() => {
connectMqtt();
const off = onMqttMessage((topic, cmd) => {