diff --git a/backend/app/mqtt.py b/backend/app/mqtt.py index b485c76..78201f5 100644 --- a/backend/app/mqtt.py +++ b/backend/app/mqtt.py @@ -6,6 +6,7 @@ import json import logging +import os import threading import time import uuid @@ -22,6 +23,11 @@ def new_cmd_id(): return uuid.uuid4().hex[:12] +def _unique_client_id(base): + """每个客户端进程使用唯一 client_id,避免与其它实例竞争被 broker 踢下线""" + return f"{base}-{os.getpid()}-{uuid.uuid4().hex[:6]}" + + class MqttHub: def __init__(self): self.client = None @@ -37,7 +43,7 @@ class MqttHub: return try: self.client = mqtt.Client( - client_id=settings.MQTT_CLIENT_ID, + client_id=_unique_client_id(settings.MQTT_CLIENT_ID), clean_session=True, protocol=mqtt.MQTTv311, ) diff --git a/src/pages/MediaScreen.jsx b/src/pages/MediaScreen.jsx index a561a2e..fc98401 100644 --- a/src/pages/MediaScreen.jsx +++ b/src/pages/MediaScreen.jsx @@ -16,6 +16,19 @@ import '../styles/mediascreen.css'; 页脚(园区概览条)按需保留,中间为全屏媒体播放器 ========================================================= */ +/** 展示名清洗:URL 只取文件名,不显示完整播放地址 */ +function cleanName(name) { + if (!name) return ''; + const text = String(name); + try { + const u = new URL(text); + const last = decodeURIComponent(u.pathname.split('/').filter(Boolean).pop() || ''); + return last ? `${last}${u.search ? ' …' : ''}` : text; + } catch { + return text; + } +} + export default function MediaScreen() { const d = useParkSim(); const navigate = useNavigate(); @@ -496,33 +509,33 @@ export default function MediaScreen() { )} - {/* 中央播放控制(悬浮层) */} + {/* 右下角轻量播放控制(弱化) */}
{/* 播放/暂停 */} {/* 上一段 / 下一段 */}
{currentItem ? `${index + 1}` : '0'} / {list.length}
- {/* 当前播放名 + LIVE(点击进入后台登录) */} + {/* 当前播放(仅文件名,不显示完整地址;点击进入后台) */}
- {currentItem ? currentItem.name : '等待播放…'} + {currentItem ? cleanName(currentItem.name) : '等待播放…'}
{/* 声音 */}
- + 点击开启声音
diff --git a/src/styles/mediascreen.css b/src/styles/mediascreen.css index 6e70913..6758dc3 100644 --- a/src/styles/mediascreen.css +++ b/src/styles/mediascreen.css @@ -109,31 +109,32 @@ border-bottom: 1px solid rgba(47, 107, 255, 0.3); } -/* 中央播放控制层(悬浮) */ +/* 右下角轻量播放控制(弱化悬浮) */ .ms-controls { position: absolute; - left: 50%; - bottom: 22px; - transform: translateX(-50%); + right: 16px; + bottom: 14px; + left: auto; + transform: none; z-index: 10; display: flex; align-items: center; - gap: 16px; - padding: 10px 18px; - background: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(120, 160, 220, 0.2); - border-radius: 40px; - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - box-shadow: 0 12px 40px rgba(56, 100, 170, 0.18); + gap: 8px; + padding: 6px 12px; + background: rgba(255, 255, 255, 0.55); + border: 1px solid rgba(120, 160, 220, 0.16); + border-radius: 30px; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + box-shadow: 0 6px 22px rgba(56, 100, 170, 0.12); opacity: 0; transition: opacity 0.5s ease; } .ms-controls.visible { opacity: 1; } .ms-play { - width: 42px; - height: 42px; + width: 30px; + height: 30px; border-radius: 50%; border: none; display: flex; @@ -142,7 +143,7 @@ color: #fff; background: linear-gradient(140deg, var(--bd-blue, #2f6bff), var(--bd-cyan, #4cc2ff)); cursor: pointer; - box-shadow: 0 6px 20px rgba(47, 107, 255, 0.4); + box-shadow: 0 4px 12px rgba(47, 107, 255, 0.3); transition: transform 0.2s; } .ms-play:hover { transform: scale(1.08); } @@ -150,14 +151,14 @@ .ms-nav { display: flex; align-items: center; - gap: 10px; + gap: 6px; } .ms-nav button { - width: 34px; - height: 34px; + width: 26px; + height: 26px; border-radius: 50%; - border: 1px solid rgba(47, 107, 255, 0.2); - background: rgba(255, 255, 255, 0.7); + border: 1px solid rgba(47, 107, 255, 0.18); + background: rgba(255, 255, 255, 0.6); color: var(--bd-blue, #2f6bff); display: flex; align-items: center; @@ -172,32 +173,32 @@ .ms-nav-num { display: inline-flex; align-items: baseline; - gap: 4px; + gap: 3px; font-family: "JetBrains Mono", "Oswald", monospace; } .ms-nav-num b { - font-size: 16px; + font-size: 13px; color: var(--bd-ink, #1d2c44); } .ms-nav-num em { font-style: normal; - font-size: 12px; + font-size: 10.5px; color: var(--bd-sub, #7d8fa9); } .ms-now { display: flex; align-items: center; - gap: 8px; - padding-left: 16px; - border-left: 1px solid rgba(47, 107, 255, 0.14); + gap: 6px; + padding-left: 10px; + border-left: 1px solid rgba(47, 107, 255, 0.12); } .ms-now-dot { - width: 8px; - height: 8px; + width: 6px; + height: 6px; border-radius: 50%; background: var(--bd-green, #17b26a); - box-shadow: 0 0 10px rgba(23, 178, 106, 0.8); + box-shadow: 0 0 8px rgba(23, 178, 106, 0.8); animation: msPulse 1.6s ease-in-out infinite; } @keyframes msPulse { @@ -205,10 +206,10 @@ 50% { opacity: 0.4; transform: scale(0.75); } } .ms-now-name { - font-size: 13px; - font-weight: 600; - color: var(--bd-ink, #1d2c44); - max-width: 280px; + font-size: 11px; + font-weight: 500; + color: var(--bd-sub, #7d8fa9); + max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -218,13 +219,13 @@ .ms-sound { display: inline-flex; align-items: center; - gap: 7px; - padding: 8px 16px; - font-size: 12.5px; + gap: 6px; + padding: 5px 11px; + font-size: 11px; font-weight: 500; color: #4a5b76; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(47, 107, 255, 0.16); + background: rgba(255, 255, 255, 0.75); + border: 1px solid rgba(47, 107, 255, 0.14); border-radius: 30px; cursor: pointer; } diff --git a/src/utils/mqtt.js b/src/utils/mqtt.js index 5fdb2a5..d5576b1 100644 --- a/src/utils/mqtt.js +++ b/src/utils/mqtt.js @@ -20,7 +20,8 @@ function notifyStatus() { export function connectMqtt() { if (client) return; try { - clientId = `dpm-screen-${Math.random().toString(16).slice(2, 8)}`; + // 每个页面会话生成唯一 client_id(时间戳 + 高熵随机),避免多屏/多实例竞争 + clientId = `dpm-screen-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 12)}`; client = mqtt.connect(MQTT_URL, { reconnectPeriod: 3000, connectTimeout: 8000,