feat(admin): 管理后台加入「大屏控制(MQTT 指令)」面板,覆盖全部前端命令

- routers.py:_VALID_DISPLAY_ACTIONS 扩展为全命令白名单
  (navigate/navigate_rel/vision_set/ai_input/ai_preset/ai_company/ai_zone/
   voice_start/voice_stop/voice_refresh/play/pause/next/prev/set_mode/play_target/
   alert/show_card/minimize/settings_changed/playlist_changed)
- admin.html:新增控制面板(页面导航含/voice+左右、视觉开关、AI 提问/预设/企业/分区、
  语音启停/刷新、媒体控制、全局通知)
- admin.js:data-mqtt 按钮(完整指令 JSON)→ /api/display/command;
  AI 提问/通知输入框回车发送
This commit is contained in:
Pine
2026-08-18 02:32:27 +08:00
parent 7e6c1a09c0
commit 66b6a7fbbd
4 changed files with 149 additions and 4 deletions
+15 -3
View File
@@ -347,9 +347,21 @@ async def ai_asr(file: UploadFile = File(...), format: str = "m4a"):
# ==================== 展示控制(管理端 → MQTT ====================
_VALID_DISPLAY_ACTIONS = {"navigate", "navigate_rel", "play", "pause", "next", "prev",
"alert", "show_card", "set_mode", "minimize"}
# 全部 MQTT 前端控制命令白名单(与 docs/mqtt-commands.md 保持一致)
_VALID_DISPLAY_ACTIONS = {
# 页面
"navigate", "navigate_rel",
# 全局视觉
"vision_set",
# AI 助手页
"ai_input", "ai_preset", "ai_company", "ai_zone",
# 语音对话页
"voice_start", "voice_stop", "voice_refresh",
# 媒体
"play", "pause", "next", "prev", "set_mode", "play_target",
# 全局
"alert", "show_card", "minimize", "settings_changed", "playlist_changed",
}
@router.post("/api/display/command")
+17
View File
@@ -1241,3 +1241,20 @@ button { font-family: "Poppins", "PingFang SC", sans-serif; cursor: pointer; tra
overflow: hidden;
text-overflow: ellipsis;
}
/* ============ 大屏控制(MQTT 指令)面板 ============ */
.mqtt-group { margin: 10px 0 4px; }
.mqtt-group-title {
font-size: 12px; font-weight: 600; color: #5a6b8c;
margin-bottom: 6px; letter-spacing: 0.04em;
}
.mqtt-row {
display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
margin-bottom: 6px;
}
.mqtt-input {
flex: 1; min-width: 180px;
padding: 6px 10px; border: 1px solid #d7e1f0; border-radius: 8px;
font-size: 13px; color: #1d2c44; outline: none;
}
.mqtt-input:focus { border-color: #2f6bff; }
+39 -1
View File
@@ -99,8 +99,15 @@
/* =========================================================
事件委托:页面切换 / 播放控制 / 媒体操作
========================================================= */
// 输入框回车提交
document.addEventListener('keydown', function (e) {
if (e.key !== 'Enter') return;
if (e.target && e.target.id === 'mqttAiInput') { document.getElementById('mqttAiSend').click(); }
else if (e.target && e.target.id === 'mqttAlertText') { document.getElementById('mqttAlertSend').click(); }
});
document.addEventListener('click', function (e) {
var btn = e.target.closest ? e.target.closest('[data-nav], [data-control], [data-action], .copy-btn, [data-preview]') : null;
var btn = e.target.closest ? e.target.closest('[data-nav], [data-mqtt], [data-control], [data-action], .copy-btn, [data-preview], #mqttAiSend, #mqttAlertSend') : null;
if (!btn) return;
e.stopPropagation();
@@ -118,6 +125,37 @@
return;
}
// 大屏 MQTT 指令(data-mqtt 携带完整 {action, params} JSON
if (btn.hasAttribute('data-mqtt')) {
var cmd = null;
try { cmd = JSON.parse(btn.getAttribute('data-mqtt')); } catch (err) { toast('指令 JSON 错误', 'error'); return; }
post('/api/display/command', cmd, function (d) {
toast(d && d.ok !== false ? '指令已发送:' + (cmd.action || '') : '发送失败(MQTT 未连接?)',
d && d.ok !== false ? 'ok' : 'error');
});
return;
}
// AI 提问输入框
if (btn.id === 'mqttAiSend') {
var text = (document.getElementById('mqttAiInput').value || '').trim();
if (!text) { toast('请输入问题', 'error'); return; }
post('/api/display/command', { action: 'ai_input', params: { text: text } }, function () {
toast('已发送问题到大屏 AI 页');
});
return;
}
// 通知输入框
if (btn.id === 'mqttAlertSend') {
var alertText = (document.getElementById('mqttAlertText').value || '').trim();
if (!alertText) { toast('请输入通知内容', 'error'); return; }
post('/api/display/command', { action: 'alert', params: { text: alertText } }, function () {
toast('通知已广播');
});
return;
}
// 页面切换
if (btn.hasAttribute('data-nav')) {
var page = btn.getAttribute('data-nav');
+78
View File
@@ -118,6 +118,84 @@
</div>
</div>
<!-- 大屏 MQTT 控制(全部前端命令) -->
<div class="card">
<div class="card-header">
<div class="card-icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#1D5DCE" stroke-width="1.6"><path d="M2 6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z"/><path d="M8 10h8M8 14h4"/></svg></div>
<h3>大屏控制(MQTT 指令)</h3>
</div>
<!-- 页面导航 -->
<div class="mqtt-group">
<div class="mqtt-group-title">页面导航</div>
<div class="mqtt-row">
<button class="page-switch-btn" data-mqtt='{"action":"navigate","params":{"page":"/voice"}}'>语音对话</button>
<button class="page-switch-btn" data-mqtt='{"action":"navigate_rel","params":{"delta":-1}}'>← 向左</button>
<button class="page-switch-btn" data-mqtt='{"action":"navigate_rel","params":{"delta":1}}'>向右 →</button>
</div>
</div>
<!-- 视觉识别 -->
<div class="mqtt-group">
<div class="mqtt-group-title">人物识别(全局)</div>
<div class="mqtt-row">
<button class="page-switch-btn" data-mqtt='{"action":"vision_set","params":{"enabled":true}}'>开启识别</button>
<button class="page-switch-btn" data-mqtt='{"action":"vision_set","params":{"enabled":false}}'>关闭识别</button>
</div>
</div>
<!-- AI 助手页 -->
<div class="mqtt-group">
<div class="mqtt-group-title">AI 助手页</div>
<div class="mqtt-row">
<input class="mqtt-input" id="mqttAiInput" type="text" placeholder="输入问题,回车发送到 AI 页">
<button class="page-switch-btn" id="mqttAiSend">发送问题</button>
</div>
<div class="mqtt-row">
<button class="page-switch-btn" data-mqtt='{"action":"ai_preset","params":{"index":0}}'>预设①入驻</button>
<button class="page-switch-btn" data-mqtt='{"action":"ai_preset","params":{"index":1}}'>预设②政策</button>
<button class="page-switch-btn" data-mqtt='{"action":"ai_preset","params":{"index":2}}'>预设③企业</button>
<button class="page-switch-btn" data-mqtt='{"action":"ai_company","params":{"action":"next"}}'>下一个企业</button>
<button class="page-switch-btn" data-mqtt='{"action":"ai_company","params":{"action":"prev"}}'>上一个企业</button>
</div>
<div class="mqtt-row">
<button class="page-switch-btn" data-mqtt='{"action":"ai_zone","params":{"zone":"加速区"}}'>加速区</button>
<button class="page-switch-btn" data-mqtt='{"action":"ai_zone","params":{"zone":"国际区"}}'>国际区</button>
<button class="page-switch-btn" data-mqtt='{"action":"ai_zone","params":{"zone":"成长区"}}'>成长区</button>
</div>
</div>
<!-- 语音对话页 -->
<div class="mqtt-group">
<div class="mqtt-group-title">语音对话页</div>
<div class="mqtt-row">
<button class="page-switch-btn" data-mqtt='{"action":"voice_start","params":{}}'>启动对话</button>
<button class="page-switch-btn" data-mqtt='{"action":"voice_stop","params":{}}'>关闭对话</button>
<button class="page-switch-btn" data-mqtt='{"action":"voice_refresh","params":{}}'>刷新页面</button>
</div>
</div>
<!-- 媒体控制 -->
<div class="mqtt-group">
<div class="mqtt-group-title">媒体轮播</div>
<div class="mqtt-row">
<button class="page-switch-btn" data-mqtt='{"action":"prev","params":{}}'>上一段</button>
<button class="page-switch-btn" data-mqtt='{"action":"play","params":{}}'>播放</button>
<button class="page-switch-btn" data-mqtt='{"action":"pause","params":{}}'>暂停</button>
<button class="page-switch-btn" data-mqtt='{"action":"next","params":{}}'>下一段</button>
</div>
</div>
<!-- 通知 -->
<div class="mqtt-group">
<div class="mqtt-group-title">全局通知</div>
<div class="mqtt-row">
<input class="mqtt-input" id="mqttAlertText" type="text" placeholder="通知内容,回车广播">
<button class="page-switch-btn" id="mqttAlertSend">广播通知</button>
</div>
</div>
</div>
<!-- 播放设置 -->
<div class="card">
<div class="card-header">