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:
@@ -57,6 +57,8 @@ class Settings:
|
||||
TOPIC_COMMAND = _env("DPM_TOPIC_COMMAND", "opc/display/command")
|
||||
TOPIC_TICK = _env("DPM_TOPIC_TICK", "opc/dashboard/tick")
|
||||
TOPIC_ACK = _env("DPM_TOPIC_ACK", "opc/display/ack")
|
||||
TOPIC_HEARTBEAT = _env("DPM_TOPIC_HEARTBEAT", "opc/display/heartbeat")
|
||||
SCREEN_TTL = float(_env("DPM_SCREEN_TTL", "30")) # 大屏心跳过期秒数
|
||||
|
||||
# ---- 阿里云 DashScope(LLM + 语音识别) ----
|
||||
DASHSCOPE_API_KEY = _env("DASHSCOPE_API_KEY", "")
|
||||
|
||||
@@ -69,6 +69,17 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def no_cache_admin_static(request: Request, call_next):
|
||||
"""管理后台页面与静态资源禁用缓存,避免旧 JS/CSS 残留导致按钮无响应"""
|
||||
response = await call_next(request)
|
||||
if request.url.path.startswith(("/static/", "/admin")):
|
||||
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
||||
response.headers["Pragma"] = "no-cache"
|
||||
response.headers["Expires"] = "0"
|
||||
return response
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
# 媒体资源静态服务(上传/播放的文件)
|
||||
|
||||
+42
-3
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""MQTT 发布中心 —— 后端 → 前端控制通道
|
||||
页面控制 / 媒体控制 / 卡片展示指令统一走 opc/display/command
|
||||
数据快照按 DPM_MQTT_TICK_INTERVAL 推送 opc/dashboard/tick
|
||||
"""MQTT 发布/订阅中心 —— 后端 ↔ 前端控制通道
|
||||
发布:opc/display/command(页面/媒体/卡片控制)、opc/dashboard/tick(数据快照)
|
||||
订阅:opc/display/heartbeat(大屏在线心跳,统计在线大屏数)
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -27,6 +27,8 @@ class MqttHub:
|
||||
self.client = None
|
||||
self.connected = False
|
||||
self._lock = threading.RLock()
|
||||
self._screens = {} # client_id -> last_seen_ts
|
||||
self.last_command = None # {action, params, ts, published}
|
||||
|
||||
# ---------- 生命周期 ----------
|
||||
def start(self):
|
||||
@@ -43,6 +45,7 @@ class MqttHub:
|
||||
self.client.username_pw_set(settings.MQTT_USERNAME, settings.MQTT_PASSWORD)
|
||||
self.client.on_connect = self._on_connect
|
||||
self.client.on_disconnect = self._on_disconnect
|
||||
self.client.on_message = self._on_message
|
||||
self.client.connect_async(settings.MQTT_HOST, settings.MQTT_PORT, keepalive=30)
|
||||
self.client.loop_start()
|
||||
log.info("MQTT 连接中 %s:%s ...", settings.MQTT_HOST, settings.MQTT_PORT)
|
||||
@@ -62,6 +65,7 @@ class MqttHub:
|
||||
if rc == 0:
|
||||
self.connected = True
|
||||
log.info("MQTT 已连接 %s:%s", settings.MQTT_HOST, settings.MQTT_PORT)
|
||||
client.subscribe(settings.TOPIC_HEARTBEAT, qos=0)
|
||||
else:
|
||||
log.warning("MQTT 连接失败 rc=%s", rc)
|
||||
|
||||
@@ -70,6 +74,34 @@ class MqttHub:
|
||||
if rc != 0:
|
||||
log.warning("MQTT 断开(rc=%s),自动重连中...", rc)
|
||||
|
||||
def _on_message(self, client, userdata, msg):
|
||||
"""接收大屏心跳:记录 client_id 与时间"""
|
||||
if msg.topic == settings.TOPIC_HEARTBEAT:
|
||||
try:
|
||||
payload = json.loads(msg.payload.decode("utf-8"))
|
||||
cid = payload.get("client_id") or msg.topic
|
||||
with self._lock:
|
||||
self._screens[cid] = time.time()
|
||||
# 清理过期
|
||||
cutoff = time.time() - settings.SCREEN_TTL
|
||||
self._screens = {k: v for k, v in self._screens.items() if v > cutoff}
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
# ---------- 状态查询 ----------
|
||||
def screens_online(self):
|
||||
with self._lock:
|
||||
cutoff = time.time() - settings.SCREEN_TTL
|
||||
return sum(1 for v in self._screens.values() if v > cutoff)
|
||||
|
||||
def status(self):
|
||||
return {
|
||||
"mqtt_connected": self.connected,
|
||||
"mqtt_host": f"{settings.MQTT_HOST}:{settings.MQTT_PORT}",
|
||||
"screens_online": self.screens_online(),
|
||||
"last_command": self.last_command,
|
||||
}
|
||||
|
||||
# ---------- 发布 ----------
|
||||
def publish(self, topic, payload, qos=1, retain=False):
|
||||
if not self.client or not self.connected:
|
||||
@@ -93,6 +125,13 @@ class MqttHub:
|
||||
}
|
||||
ok = self.publish(settings.TOPIC_COMMAND, payload)
|
||||
payload["published"] = ok
|
||||
with self._lock:
|
||||
self.last_command = {
|
||||
"action": action,
|
||||
"params": params or {},
|
||||
"ts": payload["ts"],
|
||||
"published": ok,
|
||||
}
|
||||
if ok:
|
||||
bus.emit(payload) # SSE 兼容通道(仅发布成功时推送)
|
||||
return payload
|
||||
|
||||
@@ -364,11 +364,7 @@ async def display_command_publish(body: DisplayCommandBody):
|
||||
|
||||
@router.get("/api/display/state")
|
||||
async def display_state():
|
||||
return {
|
||||
"mqtt_connected": hub.connected,
|
||||
"mqtt_host": f"{settings.MQTT_HOST}:{settings.MQTT_PORT}",
|
||||
"page": None, # 前端可通过 MQTT 回执上报当前页(后续扩展)
|
||||
}
|
||||
return hub.status()
|
||||
|
||||
|
||||
# ==================== SSE 兼容通道(MQTT 不可用时前端回退) ====================
|
||||
|
||||
Reference in New Issue
Block a user