@@ -1,158 +1,246 @@
<!DOCTYPE html>
< html lang = "zh-CN" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > 管理后台 · 昆明市大学生创业园< / title >
< link rel = "stylesheet" href = "/static/admin.css" >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > 管理后台 · 昆明市大学生创业园< / title >
< link rel = "stylesheet" href = "/static/admin.css" >
< / head >
< body >
{% if not logged_in %}
<!-- ==================== 登录页 ==================== -->
< div class = "admin-page" >
< div class = "login-wrapper" >
< div class = "login-card" >
< div class = "login-brand" >
< div class = "icon" >
< div class = "iso-top" > < / div >
< div class = "iso-left" > < / div >
< div class = "iso-right" > < / div >
< div class = "iso-dot" > < / div >
{% if not logged_in %}
<!-- ==================== 登录页 ==================== -->
< div class = "admin-page" >
< div class = "login-wrapper" >
< div class = "login-card" >
< div class = "login-brand" >
< div class = "icon" >
< div class = "iso-top" > < / div >
< div class = "iso-left" > < / div >
< div class = "iso-right" > < / div >
< div class = "iso-dot" > < / div >
< / div >
< h2 > 昆明市大学生创业园< / h2 >
< p > 大屏幕轮播控制系统< / p >
< / div >
< h2 > 昆明市大学生创业园< / h2 >
< p > 大屏幕轮播控制系统 < / p >
{% if error %}
< div class = "toast error" id = "loginError" style = "position:static;margin-bottom:12px;" > 账号或密码错误,请重试 < / div >
{% endif %}
< form method = "post" action = "/admin/login" >
< div class = "field" >
< label > 账号< / label >
< input name = "username" placeholder = "请输入账号" autocomplete = "username" required >
< / div >
< div class = "field" >
< label > 密码< / label >
< input type = "password" name = "password" placeholder = "请输入密码" autocomplete = "current-password" required >
< / div >
< button type = "submit" class = "btn-login" > 登 录< / button >
< / form >
< / div >
{% if error %}
< div class = "toast error" id = "loginError" style = "position:static;margin-bottom:12px;" > 账号或密码错误,请重试< / div >
{% endif %}
< form method = "post" action = "/admin/login" >
< div class = "field" >
< label > 账号< / label >
< input name = "username" placeholder = "请输入账号" autocomplete = "username" required >
< / div >
< div class = "field" >
< label > 密码< / label >
< input type = "password" name = "password" placeholder = "请输入密码" autocomplete = "current-password" required >
< / div >
< button type = "submit" class = "btn-login" > 登 录< / button >
< / form >
< / div >
< / div >
< / div >
{% else %}
<!-- ==================== 管理台 ==================== -->
< div class = "admin-pag e" >
< div class = "dashboard active" >
{% else %}
<!-- ==================== 管理台 ==================== -->
< div class = "admin-page" >
< div class = "dashboard activ e" >
<!-- 顶栏 -->
< div class = "topbar" >
< div class = "topbar-left" >
< div class = "brand-dot" > < / div >
< h1 > 昆明市大学生创业园< / h1 >
< / div >
< div class = "topbar-center" >
< span class = "status-bar-text" id = "statusText" > 等待大屏连接...< / span >
< / div >
< div class = "topbar-right" >
< button class = "btn-control" data-control = "prev" title = "上一个" >
< svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "currentColor" > < path d = "M11 3L6 8L11 13" stroke = "currentColor" stroke-width = "2" fill = "none" stroke-linecap = "round" stroke-linejoin = "round" / > < / svg >
< / button >
< button class = "btn-control play-btn" data-control = "play" title = "播放" >
< svg width = "18" height = "18" viewBox = "0 0 18 18" fill = "currentColor" > < polygon points = "5,3 15,9 5,15" / > < / svg >
< / button >
< button class = "btn-control play-btn" data-control = "pause" title = "暂停" >
< svg width = "18" height = "18" viewBox = "0 0 18 18" fill = "currentColor" > < rect x = "4" y = "3" width = "3" height = "12" rx = "1" / > < rect x = "11" y = "3" width = "3" height = "12" rx = "1" / > < / svg >
< / button >
< button class = "btn-control" data-control = "next" title = "下一个" >
< svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "currentColor" > < path d = "M5 3L10 8L5 13" stroke = "currentColor" stroke-width = "2" fill = "none" stroke-linecap = "round" stroke-linejoin = "round" / > < / svg >
< / button >
< span class = "topbar-divider" > < / span >
{% if from_screen %}< a class = "btn-return" href = "/screen" > 返回展播< / a > {% endif %}
< a class = "btn-return" href = "/" > 数据大屏< / a >
< a class = "btn-logout" href = "/admin/logout" > 退出< / a >
< / div >
< / div >
< div class = "main-grid" >
<!-- 连接状态 -->
< 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 = "M5 12.5a10 10 0 0 1 14 0" / > < path d = "M8 15.5a6 6 0 0 1 8 0" / > < path d = "M12 19h.01" / > < / svg > < / div >
< h3 > 连接状态< / h3 >
<!-- 顶栏 -->
< div class = "topbar" >
< div class = "topbar-left" >
< div class = "brand-dot" > < / div >
< h1 > 昆明市大学生创业园< / h1 >
< / div >
< div class = "conn-status " >
< div class = "conn-row" id = "connApi" >
< span class = "conn-dot" > < / span >
< span class = "conn-label" > 后端服务 < / span >
< span class = "conn-value" > 检测中... < / span >
< / div >
< div class = "conn-row " id = "c onnMqtt" >
< span class = "conn-dot" > < / span >
< span class = "conn-label" > MQTT Broker < / span >
< span class = "conn-value" > 检测中... < / span >
< / div >
< div class = "conn-row " id = "connScreens " >
< span class = "conn-dot" > < / span >
< span class = "conn-label" > 在线大屏 < / span >
< span class = "conn-value" > 检测中... < / span >
< / div >
< div class = "conn-row" id = "connLastCmd ">
< span class = "conn-dot" > < / span >
< span class = "conn-label" > 最近指令 < / span >
< span class = "conn-value" > — < / span >
< / div >
< div class = "topbar-center " >
< span class = "status-bar-text" id = "statusText" > 等待大屏连接... < / span >
< / div >
< div class = "topbar-right" >
< button class = "btn-control" data-control = "prev" title = "上一个" >
< svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "currentColor" >
< path d = "M11 3L6 8L11 13" stroke = "currentColor" stroke-width = "2 " fill = "n one" stroke-linecap = "round"
stroke-linejoin = "round" / >
< / svg >
< / button >
< button class = "btn-control play-btn" data-control = "play" title = "播放" >
< svg width = "18 " height = "18" viewBox = "0 0 18 18" fill = "currentColor ">
< polygon points = "5,3 15,9 5,15" / >
< / svg >
< / button >
< button class = "btn-control play-btn" data-control = "pause" title = "暂停" >
< svg width = "18" height = "18" viewBox = "0 0 18 18" fill = "currentColor ">
< rect x = "4" y = "3" width = "3" height = "12" rx = "1" / >
< rect x = "11" y = "3" width = "3" height = "12" rx = "1" / >
< / svg >
< / button >
< button class = "btn-control" data-control = "next" title = "下一个" >
< svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "currentColor" >
< path d = "M5 3L10 8L5 13" stroke = "currentColor" stroke-width = "2" fill = "none" stroke-linecap = "round"
stroke-linejoin = "round" / >
< / svg >
< / button >
< span class = "topbar-divider" > < / span >
{% if from_screen %}< a class = "btn-return" href = "/screen" > 返回展播< / a > {% endif %}
< a class = "btn-return" href = "/" > 数据大屏< / a >
< a class = "btn-logout" href = "/admin/logout" > 退出< / a >
< / div >
< / div >
<!-- 页面切换 -->
< 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" > < rect x = "3" y = "3" width = "8" height = "8" rx = "1.5" / > < rect x = "13" y = "3" width = "8" height = "8" rx = "1.5" / > < rect x = "3" y = "13" width = "8" height = "8" rx = "1.5" / > < rect x = "13" y = "13" width = "8" height = "8" rx = "1.5" / > < / svg > < / div >
< h3 > 目标屏幕(按 MQTT 客户端精准 控制) < / h3 >
< / div >
< div class = "page-switch-grid" style = "align-items:center;gap:8px;" >
< span id = "screenCount" class = "mqtt-hint" style = "white-space:nowrap;" > 在线屏幕:加载中…< / span >
< select id = "screenTarget" style = "flex:1;min-width:0;padding:8px 10px;border:1px solid rgba(0,189,125,.2);border-radius:8px;background:#0e1a2b;color:#e8f0fa;font-size:12px;" >
< option value = "" > 全部屏幕< / option >
<!-- 选择屏幕(独立模块) -->
< div class = "control-stack" >
< div class = "remote-target " >
< span id = "screenCount" class = "remote-count" > 在线 0 台 < / span >
< select id = "screenTarget" >
< option value = "" > 全部 控制< / option >
< / select >
< / div >
< div class = "mqtt-hint" > 选择某台屏幕后,下方命令只发给它(按 client_id);「全部」则所有屏幕同步。< / div >
< / div >
< 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" > < rect x = "3" y = "3" width = "8" height = "8" rx = "1.5" / > < rect x = "13" y = "3" width = "8" height = "8" rx = "1.5" / > < rect x = "3" y = "13" width = "8" height = "8" rx = "1.5" / > < rect x = "13" y = "13" width = "8" height = "8" rx = "1.5" / > < / svg > < / div >
< h3 > 页面切换(控制大屏展示)< / h3 >
< / div >
< div class = "page-switch-grid" >
< button class = "page-switch-btn" data-nav = "/" > 数据大屏< / button >
< button class = "page-switch-btn" data-nav = "/twin" > 数字孪生< / button >
< button class = "page-switch-btn" data-nav = "/ai" > AI 助手< / button >
< button class = "page-switch-btn" data-nav = "/screen" > 媒体轮播< / button >
< / 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-g roup " >
< div class = "mqtt-group-title " > 页面导航 < / div >
< div class = "mqtt-row" >
<!-- 页面切换(独立模块 ) -->
< div class = "card control-card " >
< div class = "page-nav-prev " >
< button class = "remote-btn" data-mqtt = '{"action":"navigate_rel","params":{"delta":-1}}' title = "向左切页"
aria-label = "向左" > ‹ < / button >
< button class = "remote-btn" data-mqtt = '{"action":"navigate_rel","params":{"delta":1}}' title = "向右切页"
aria-label = "向右" > › < / button >
< / div >
< div class = "mqtt-row page-nav-links " >
< button class = "page-switch-btn" data-nav = "/ "> 数据大屏 < / button >
< button class = "page-switch-btn" data-nav = "/twin" > 数字孪生 < / button >
< button class = "page-switch-btn" data-nav = "/ai" > AI 助手< / button >
< button class = "page-switch-btn" data-nav = "/screen" > 媒体轮播< / button >
< button class = "page-switch-btn" data-mqtt = '{"action":"navigate","params":{"page":"/voice"}}' > 语音对话< / button >
< button class = "page-switch-btn" data-mqtt = '{"action":"navigate","params":{"page":"/wall"}}' > 企业展示墙< / 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 >
< div class = "main-grid" >
<!-- 连接状态(弱化:沉到底部、紧凑展示) -->
< div class = "card conn-card" style = "order:99" >
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< path d = "M3 12h4l3 8 4-16 3 8h4" / >
< / svg > < / div >
< h3 > 连接状态< / h3 >
< / div >
< div class = "conn-status" >
< div class = "conn-row" id = "connApi" >
< span class = "conn-dot" > < / span >
< span class = "conn-label" > 后端服务< / span >
< span class = "conn-value" > 检测中...< / span >
< / div >
< div class = "conn-row" id = "connMqtt" >
< span class = "conn-dot" > < / span >
< span class = "conn-label" > MQTT Broker< / span >
< span class = "conn-value" > 检测中...< / span >
< / div >
< div class = "conn-row" id = "connScreens" >
< span class = "conn-dot" > < / span >
< span class = "conn-label" > 在线大屏< / span >
< span class = "conn-value" > 检测中...< / span >
< / div >
< div class = "conn-row" id = "connLastCmd" >
< span class = "conn-dot" > < / span >
< span class = "conn-label" > 最近指令< / span >
< span class = "conn-value" > —< / span >
< / div >
< / div >
< / div >
<!-- 企业展示墙 -->
< div class = "mqtt-group " >
< div class = "mqtt-group-title" > 企业展示墙(/wall) < / div >
<!-- 媒体轮播(独立模块) -->
< div class = "card" data-page = "/screen ">
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< path d = "M6 4v16l13-8z" / >
< / svg > < / div >
< h3 > 媒体轮播< / h3 >
< / 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 = "card" data-page = "/voice" >
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< path d = "M9 2a3 3 0 0 1 3 3v5a3 3 0 0 1-6 0V5a3 3 0 0 1 3-3z" / >
< path d = "M5 10a7 7 0 0 0 14 0" / >
< path d = "M12 19v3" / >
< / svg > < / div >
< h3 > 语音对话< / h3 >
< / 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 >
<!-- AI 助手(独立模块) -->
< div class = "card" data-page = "/ai" >
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< path d = "M12 5V3" / >
< path d = "M18 4a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z" / >
< path d = "M9 7h6" / >
< path d = "M10 11h.01M14 11h.01" / >
< path d = "M12 13v3" / >
< path d = "M7 21h10" / >
< / svg > < / div >
< h3 > AI 助手< / h3 >
< / div >
< div class = "mqtt-row" >
< input class = "mqtt-input" id = "mqttAiInput" type = "text" placeholder = "输入问题,回车发送" >
< 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 >
< / div >
< / div >
<!-- 数字孪生(独立模块) -->
< div class = "card" data-page = "/twin" >
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< path d = "M4 8l8-5 8 5-8 5z" / >
< path d = "M4 12l8 5 8-5" / >
< path d = "M4 16l8 5 8-5" / >
< / svg > < / div >
< h3 > 数字孪生< / h3 >
< / div >
< div class = "mqtt-row" >
< 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 = "card" data-page = "/wall" >
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< rect x = "3" y = "21" width = "18" height = "3" / >
< path d = "M5 21V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16" / >
< path d = "M9 21v-4h6v4" / >
< / svg > < / div >
< h3 > 企业展示墙< / h3 >
< / div >
< div class = "mqtt-row" >
< button class = "page-switch-btn" data-mqtt = '{"action":"wall_pause","params":{}}' > 暂停滚动< / button >
< button class = "page-switch-btn" data-mqtt = '{"action":"wall_resume","params":{}}' > 继续滚动< / button >
@@ -164,186 +252,191 @@
< / div >
< / div >
<!-- 视觉识别 -->
< div class = "mqtt-group" >
< div class = "mqtt-group-title" > 人物识别(全局)< / div >
<!-- 人物识别(独立模块) -->
<!-- <div class="card">
<div class="card - header">
<div class="card - icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor"
stroke - width="1.5">
<circle cx="9" cy="7" r="4" />
<path d="M2 21v - 2a4 4 0 0 1 4 - 4h6a4 4 0 0 1 4 4v2" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg></div>
<h3>人物识别</h3>
</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 >
</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 class="card">
<div class="card - header">
<div class="card - icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor"
stroke - width="1.5">
<rect x="2" y="3" width="20" height="14" rx="2" />
<path d="M8 21h8M12 17v4" />
</svg></div>
<h3>双屏控制</h3>
</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":"dual_screen","params":{"on":true}}'>启动双屏</button>
<button class="page - switch - btn" data - mqtt='{"action":"dual_screen","params":{"on":false}}'>关闭双屏</button>
</div>
<div class="mqtt - hint">在电脑另一块屏幕全屏展示(需展播机连接显示器)</div>
< / 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 class="card">
<div class="card - header">
<div class="card - icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor"
stroke - width="1.5">
<path d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3H4a4 4 0 0 0 2 - 3v - 3a7 7 0 0 1 4 - 6z" />
<path d="M9 17v1a3 3 0 0 0 6 0v - 1" />
</svg></div>
<h3>全局通知</h3>
</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> -->
<!-- 播放设置 -->
< 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" > < circle cx = "12" cy = "12" r = "9" / > < path d = "M12 7v5l3 2" / > < / svg > < / div >
< h3 > 播放设置 < / h3 >
< / div >
< div class = "form-inline" >
< div class = "form-group" >
< label > 播放模式 < / label >
< select id = "playMode" >
< option value = "sequential" { % if play_mode = = ' sequential ' % } selected { % endif % } > 顺序播放< / option >
< option value = "random" { % if play_mode = = ' random ' % } selected { % endif % } > 随机播放< / option >
< / select >
<!-- 播放设置 -->
< div class = "card" >
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< path d = "M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10" / >
< circle cx = "18" cy = "18" r = "3" / >
< path d = "M19 19l2 2" / >
< / svg > < / div >
< h3 > 播放设置 < / h3 >
< / div >
< div class = "form-group " >
< label > 图片时长(秒) < / label >
< input type = "number" id = "imageDuration" value = "{{ image_duration }}" min = "1" max = "300" >
< / div >
< div class = "form-group" >
< label > 音量(媒体) < / label >
< input type = "range" id = "volumeRange" min = "0" max = "100" value = "{{ volume }}" >
< div class = "volume-label" id = "volumeLabel" > {{ volume }}% < / div >
< / div >
< div class = "form-group" >
< label > 音量(音效) < / label >
< input type = "range" id = "sfxVolumeRange" min = "0" max = "100" value = "{{ sfx_volume }}" >
< div class = "volume-label" id = "sfxVolumeLabel" > {{ sfx_volume }}% < / div >
< div class = "form-inline " >
< div class = "form-group" >
< label > 播放模式 < / label >
< select id = "playMode" >
< option value = "sequential" { % if play_mode = ='sequential' % } selected { % endif % } > 顺序播放 < / option >
< option value = "random" { % if play_mode = ='random' % } selected { % endif % } > 随机播放 < / option >
< / select >
< / div >
< div class = "form-group" >
< label > 图片时长(秒) < / label >
< input type = "number" id = "imageDuration" value = "{{ image_duration }}" min = "1" max = "300" >
< / div >
< div class = "form-group" >
< label > 音量(媒体)< / label >
< input type = "range" id = "volumeRange" min = "0" max = "100" value = "{{ volume }}" >
< div class = "volume-label" id = "volumeLabel" > {{ volume }}%< / div >
< / div >
< div class = "form-group" >
< label > 音量(音效)< / label >
< input type = "range" id = "sfxVolumeRange" min = "0" max = "100" value = "{{ sfx_volume }}" >
< div class = "volume-label" id = "sfxVolumeLabel" > {{ sfx_volume }}%< / div >
< / div >
< / div >
< / div >
< / div >
<!-- 语音 AI 测试(admin → 前端主动问候) -->
< 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 = "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" / > < path d = "M19 10v2a7 7 0 0 1-14 0v-2M12 19v3" / > < / svg > < / div >
< h3 > 语音 AI 测试 < / h3 >
< / div >
< div class = "form-inline" >
< div class = "form-group" style = "flex:2;" >
< label > 问候语(前端将主动播报,测试语音 AI) < / label >
< input type = "text" id = "voiceGreetText" value = "您好呀,我是园区智能语音助手,有什么可以帮您?" style = "width:100%;" >
<!-- 语音 AI 测试(admin → 前端主动问候) -->
< div class = "card" >
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< path d = "M9 2a3 3 0 0 1 3 3v5a3 3 0 0 1-6 0V5a3 3 0 0 1 3-3z" / >
< path d = "M5 10a7 7 0 0 0 14 0" / >
< path d = "M12 19v3" / >
< / svg > < / div >
< h3 > 语音 AI 测试 < / h3 >
< / div >
< div class = "form-group " >
< label > < / label >
< button class = "btn-primary" id = "btnVoiceGreet" style = "width:100%;" > 播报问候 < / button >
< div class = "form-inline " >
< div class = "form-group" style = "flex:2;" >
< label > 问候语(前端将主动播报,测试语音 AI) < / label >
< input type = "text" id = "voiceGreetText" value = "您好呀,我是园区智能语音助手,有什么可以帮您?" style = "width:100%;" >
< / div >
< div class = "form-group" >
< label > < / label >
< button class = "btn-primary" id = "btnVoiceGreet" style = "width:100%;" > 播报问候< / button >
< / div >
< / div >
< div class = "hint" id = "voiceGreetHint" > 点击后展播端将自动进入语音对话页并主动播报上述问候(LLM 生成 + TTS),用于验证语音链路。< / div >
< / div >
< div class = "hint" id = "voiceGreetHint" > 点击后展播端将自动进入语音对话页并主动播报上述问候(LLM 生成 + TTS),用于验证语音链路。< / div >
< / div >
<!-- 显示控制 -->
< 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" > < rect x = "3" y = "4" width = "18" height = "13" rx = "2" / > < path d = "M8 21h8M12 17v4" / > < / svg > < / div >
< h3 > 显示控制 < / h3 >
< / div >
< div class = "form-inline" >
< div class = "form-group" >
< label > 显示模式 < / label >
< button class = "btn-outline" id = "btnFullscreen" style = "width:100%;" > 切换大屏模式< / button >
<!-- 显示控制 -->
< div class = "card" >
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< rect x = "2" y = "3" width = "20" height = "14" rx = "2" / >
< path d = "M8 21h8M12 17v4" / >
< / svg > < / div >
< h3 > 显示控制 < / h3 >
< / div >
< div class = "form-group " >
< label > 窗口操作 < / label >
< button class = "btn-outline" data-control = "minimize" style = "width:100%;" > 最小化窗口 < / button >
< / div >
< div class = "form-group" >
< label > 开机自启动 < / label >
< button class = "btn-outline" id = "btnAutostart" style = "width:100%;" > 已关闭 < / button >
< div class = "form-inline " >
< div class = "form-group" >
< label > 显示模式 < / label >
< button class = "btn-outline" id = "btnFullscreen" style = "width:100%;" > 切换大屏模式 < / button >
< / div >
< div class = "form-group" >
< label > 窗口操作 < / label >
< button class = "btn-outline" data-control = "minimize" style = "width:100%;" > 最小化窗口< / button >
< / div >
< div class = "form-group" >
< label > 开机自启动< / label >
< button class = "btn-outline" id = "btnAutostart" style = "width:100%;" > 已关闭< / button >
< / div >
< / div >
< / div >
< / div >
<!-- 添加媒体 -->
< 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 = "M12 16V4m0 0L7 9m5-5l5 5" / > < path d = "M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" / > < / svg > < / div >
< h3 > 添加媒体 < / h3 >
< / div >
< div class = "form-group" >
< label > 本地上传 < / label >
< div class = "form-row" >
< input type = "file" id = "fileInput" accept = ".mp4,.mkv,.avi,.jpg,.jpeg,.png" multiple >
< button class = "btn-accent" id = "btnUpload" > 上传< / button >
<!-- 添加媒体 -->
< div class = "card" >
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< path d = "M12 16V4m0 0L7 9m5-5l5 5" / >
< path d = "M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" / >
< / svg > < / div >
< h3 > 添加媒体 < / h3 >
< / div >
< div class = "upload-progress" id = "uploadProgress" style = "display:none; ">
< div class = "progress-bar" > < div class = "progress-fill" id = "progressFill" style = "width:0%" > < / div > < / div >
< span class = "progress-text" id = "progressText" > < / span >
< div class = "form-group " >
< label > 本地上传 < / label >
< div class = "form-row" >
< input type = "file" id = "fileInput" accept = ".mp4,.mkv,.avi,.jpg,.jpeg,.png" multiple >
< button class = "btn-accent" id = "btnUpload" > 上传< / button >
< / div >
< div class = "upload-progress" id = "uploadProgress" style = "display:none;" >
< div class = "progress-bar" >
< div class = "progress-fill" id = "progressFill" style = "width:0%" > < / div >
< / div >
< span class = "progress-text" id = "progressText" > < / span >
< / div >
< / div >
< div class = "form-group" >
< label > 远程 URL / 播放链接< / label >
< div class = "form-row" >
< input type = "text" id = "urlInput" placeholder = "https://example.com/media.mp4" >
< button class = "btn-accent" id = "btnAddUrl" > 添加到媒体库< / button >
< button class = "btn-accent" id = "btnAddUrlPlay" > 加入播放列表< / button >
< / div >
< / div >
< / div >
< div class = "form-group" >
< label > 远程 URL / 播放链接< / label >
< div class = "form-row" >
< input type = "text" id = "urlInput" placeholder = "https://example.com/media.mp4" >
< button class = "btn-accent" id = "btnAddUrl" > 添加到媒体库< / button >
< button class = "btn-accent" id = "btnAddUrlPlay" > 加入播放列表< / button >
< / div >
< / div >
< / div >
<!-- 媒体库 -->
< div class = "card full" >
< 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 = "M4 20V10l6-5 6 5v10" / > < path d = "M10 20v-5h4v5" / > < / svg > < / div >
< h3 > 媒体库({{ files|length }}) < / h3 >
< / div >
< div class = "media-grid" >
{% if files %}
<!-- 媒体库 -->
< div class = "card full" >
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< rect x = "3" y = "3" width = "18" height = "18" rx = "2" / >
< circle cx = "8.5" cy = "8.5" r = "1.5" / >
< path d = "M21 15l-5-5L5 21" / >
< / svg > < / div >
< h3 > 媒体库({{ files|length }}) < / h3 >
< / div >
< div class = "media-grid" >
{% if files %}
{% for item in files %}
< div class = "media-card" data-path = "{{ item.relative_path }}" data-type = "{{ item.type }}" data-source = "{{ item.source }}" data-name = "{{ item.name }}" >
< div class = "media-card" data-path = "{{ item.relative_path }}" data-type = "{{ item.type }}"
data-source = "{{ item.source }}" data-name = "{{ item.name }}" >
{% if item.type == 'image' %}
< img src = "{{ item.url }}" loading = "lazy" class = "thumb" alt = "{{ item.name }}" data-preview >
{% else %}
@@ -355,8 +448,10 @@
< div class = "body" >
< span class = "name" title = "{{ item.name }}" > {{ item.name }}< / span >
< div class = "meta-row" >
< span class = "badge {{ 'badge-url' if item.source == 'url' else 'badge-local' }}" > {{ '远程' if item.source == 'url' else '本地' }} < / span >
< span class = "badge {{ 'badge-video' if item.type == 'video' else 'badge-image' }}" > {{ '视频' if item.typ e == 'video ' else '图片 ' }}< / span >
< span class = "badge {{ 'badge-url' if item.source == 'url' else 'badge-local' }}" > {{ '远程' if
item.sourc e == 'url ' else '本地 ' }}< / span >
< span class = "badge {{ 'badge-video' if item.type == 'video' else 'badge-image' }}" > {{ '视频' if
item.type == 'video' else '图片' }}< / span >
< / div >
< div class = "media-url" data-url = "{{ item.url }}" title = "FastAPI 输出的资源 URL" >
< span > {{ item.url }}< / span >
@@ -370,25 +465,34 @@
< / div >
< / div >
{% endfor %}
{% else %}
{% else %}
< div class = "empty-state" >
< div class = "empty-icon" > < svg viewBox = "0 0 24 24" width = "28" height = "28" fill = "none" stroke = "#9ca3af" stroke-width = "1.4" > < rect x = "3" y = "5" width = "18" height = "14" rx = "2" / > < circle cx = "9" cy = "10" r = "2" / > < path d = "M3 17l5-4 4 3 4-4 5 5" / > < / svg > < / div >
< div class = "empty-icon" > < svg viewBox = "0 0 24 24" width = "28" height = "28" fill = "none" stroke = "#9ca3af"
stroke-width = "1.4" >
< rect x = "3" y = "5" width = "18" height = "14" rx = "2" / >
< circle cx = "9" cy = "10" r = "2" / >
< path d = "M3 17l5-4 4 3 4-4 5 5" / >
< / svg > < / div >
暂无媒体文件< br > 请上传或通过 URL 添加
< / div >
{% endif %}
{% endif %}
< / div >
< / div >
< / div >
<!-- 播放列表 -->
< div class = "card full" >
< 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 = "M4 6h16M4 12h16M4 18h10" / > < / svg > < / div >
< h3 > 播放列表({{ playlist|length }}) < / h3 >
< / div >
< div class = "media-grid" >
{% if playlist %}
<!-- 播放列表 -->
< div class = "card full" >
< div class = "card-header" >
< div class = "card-icon" > < svg viewBox = "0 0 24 24" width = "20" height = "20" fill = "none" stroke = "currentColor"
stroke-width = "1.5" >
< path d = "M4 6h16M4 12h16M4 18h10" / >
< / svg > < / div >
< h3 > 播放列表({{ playlist|length }}) < / h3 >
< / div >
< div class = "media-grid" >
{% if playlist %}
{% for item in playlist %}
< div class = "media-card" data-path = "{{ item.relative_path }}" data-type = "{{ item.type }}" data-source = "{{ item.source }}" data-name = "{{ item.name }}" >
< div class = "media-card" data-path = "{{ item.relative_path }}" data-type = "{{ item.type }}"
data-source = "{{ item.source }}" data-name = "{{ item.name }}" >
{% if item.type == 'image' %}
< img src = "{{ item.url }}" loading = "lazy" class = "thumb" alt = "{{ item.name }}" data-preview >
{% else %}
@@ -400,8 +504,10 @@
< div class = "body" >
< span class = "name" title = "{{ item.name }}" > {{ item.name }}< / span >
< div class = "meta-row" >
< span class = "badge {{ 'badge-url' if item.source == 'url' else 'badge-local' }}" > {{ '远程' if item.source == 'url' else '本地' }} < / span >
< span class = "badge {{ 'badge-video' if item.type == 'video' else 'badge-image' }}" > {{ '视频' if item.typ e == 'video ' else '图片 ' }}< / span >
< span class = "badge {{ 'badge-url' if item.source == 'url' else 'badge-local' }}" > {{ '远程' if
item.sourc e == 'url ' else '本地 ' }}< / span >
< span class = "badge {{ 'badge-video' if item.type == 'video' else 'badge-image' }}" > {{ '视频' if
item.type == 'video' else '图片' }}< / span >
< / div >
< div class = "media-url" data-url = "{{ item.url }}" title = "FastAPI 输出的资源 URL" >
< span > {{ item.url }}< / span >
@@ -414,47 +520,48 @@
< / div >
< / div >
{% endfor %}
{% else %}
{% else %}
< div class = "empty-state" >
< div class = "empty-icon" > ▶< / div >
播放列表为空< br > 从上方媒体库添加内容
< / div >
{% endif %}
{% endif %}
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- 预览弹窗 -->
< div class = "modal-overlay" id = "previewModal" style = "display:none;" >
< div class = "modal-panel" >
< div class = "modal-header" >
< span class = "modal-title" id = "previewTitle" > < / span >
< button class = "modal-close" id = "previewClose" > × < / button >
< / div >
< div class = "modal-body" id = "previewBody" > < / div >
< div class = "modal-footer" >
< span class = "modal-info" id = "previewMeta" > < / span >
< div class = "modal-actions" >
< button class = "btn-accent btn-sm" id = "previewPlay" > 立即播放< / button >
< button class = "btn-accent btn-sm" id = "previewAdd" > 加入播放< / button >
< button class = "btn-danger btn-sm" id = "previewDelete" > 删除< / button >
<!-- 预览弹窗 -->
< div class = "modal-overlay" id = "previewModal" style = "display:none;" >
< div class = "modal-panel" >
< div class = "modal-header" >
< span class = "modal-title" id = "previewTitle" > < / span >
< button class = "modal-close" id = "previewClose" > × < / button >
< / div >
< div class = "modal-body" id = "previewBody" > < / div >
< div class = "modal-footer" >
< span class = "modal-info" id = "previewMeta" > < / span >
< div class = "modal-actions" >
< button class = "btn-accent btn-sm" id = "previewPlay" > 立即播放< / button >
< button class = "btn-accent btn-sm" id = "previewAdd" > 加入播放< / button >
< button class = "btn-danger btn-sm" id = "previewDelete" > 删除< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Toast -->
< div class = "toast-container" id = "toastContainer" > < / div >
< / div >
<!-- Toast -->
< div class = "toast-container" id = "toastContainer" > < / div >
< / div >
< script >
window . _ _DPM _INIT = {
fullscreen : { { 'true' if fullscreen else 'false' } } ,
autostart : { { 'true' if autostart else 'false' } }
} ;
< / script >
< script src = "/static/admin.js" > < / script >
{% endif %}
< script >
window . _ _DPM _INIT = {
fullscreen : { { 'true' if fullscreen else 'false' } } ,
autostart : { { 'true' if autostart else 'false' } } ,
} ;
< / script >
< script src = "/static/admin.js" > < / script >
{% endif %}
< / body >
< / html >
< / html >