feat(park-desktop): admin 后台新 UI + vision_yolo 手势接入(merge 自旧仓库 4e73a6b)
This commit is contained in:
@@ -39,6 +39,10 @@ class Settings:
|
|||||||
DATA_FILE = Path(_env("DPM_DATA_FILE", str(BASE_DIR / "data.json")))
|
DATA_FILE = Path(_env("DPM_DATA_FILE", str(BASE_DIR / "data.json")))
|
||||||
DIST_DIR = Path(_env("DPM_DIST_DIR", str(BASE_DIR.parent / "dist"))) # 前端构建产物(可选托管)
|
DIST_DIR = Path(_env("DPM_DIST_DIR", str(BASE_DIR.parent / "dist"))) # 前端构建产物(可选托管)
|
||||||
|
|
||||||
|
# ---- 视频帧保存(摄像头识别帧,仅检测到人脸/手势时落盘,便于排查) ----
|
||||||
|
SAVE_VISION_FRAMES = _env("DPM_SAVE_VISION_FRAMES", "1") == "1"
|
||||||
|
VISION_FRAME_SAVE_DIR = Path(_env("DPM_VISION_SAVE_DIR", str(MEDIA_DIR / "video")))
|
||||||
|
|
||||||
# ---- MQTT(优先使用 .env 中的局域网 Broker 配置) ----
|
# ---- MQTT(优先使用 .env 中的局域网 Broker 配置) ----
|
||||||
MQTT_ENABLED = _env("DPM_MQTT_ENABLED", "1") == "1"
|
MQTT_ENABLED = _env("DPM_MQTT_ENABLED", "1") == "1"
|
||||||
MQTT_HOST = _env("DPM_MQTT_HOST", _env("MQTT_BROKER_HOST", "192.168.1.9"))
|
MQTT_HOST = _env("DPM_MQTT_HOST", _env("MQTT_BROKER_HOST", "192.168.1.9"))
|
||||||
|
|||||||
+8
-18
@@ -14,7 +14,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse, JSONResponse
|
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -108,8 +108,8 @@ app.add_api_route("/admin/logout", admin_logout, methods=["GET"], include_in_sch
|
|||||||
_DOWNLOAD_ROOT = Path(__file__).resolve().parent.parent # backend/
|
_DOWNLOAD_ROOT = Path(__file__).resolve().parent.parent # backend/
|
||||||
|
|
||||||
|
|
||||||
@app.get("/download", include_in_schema=False)
|
@app.get("/download")
|
||||||
@app.get("/11", include_in_schema=False)
|
@app.get("/11")
|
||||||
async def download_installer():
|
async def download_installer():
|
||||||
"""下载 Windows 客户端安装包(backend/ 下 *.setup.exe,如 云超服..._x64-setup.exe)。"""
|
"""下载 Windows 客户端安装包(backend/ 下 *.setup.exe,如 云超服..._x64-setup.exe)。"""
|
||||||
for f in sorted(_DOWNLOAD_ROOT.glob("*setup.exe")):
|
for f in sorted(_DOWNLOAD_ROOT.glob("*setup.exe")):
|
||||||
@@ -146,7 +146,7 @@ class UpdateCheckBody(BaseModel):
|
|||||||
current: str = "" # 前端上报的当前版本,如 0.1.0
|
current: str = "" # 前端上报的当前版本,如 0.1.0
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/update/check", include_in_schema=False)
|
@app.post("/api/update/check")
|
||||||
async def update_check(body: UpdateCheckBody):
|
async def update_check(body: UpdateCheckBody):
|
||||||
"""检测更新:返回最新版本号、安装包下载地址、是否需要更新(仅 Windows)。"""
|
"""检测更新:返回最新版本号、安装包下载地址、是否需要更新(仅 Windows)。"""
|
||||||
inst = _latest_installer()
|
inst = _latest_installer()
|
||||||
@@ -171,17 +171,7 @@ async def update_check(body: UpdateCheckBody):
|
|||||||
# ---------- 前端静态托管 + SPA 回退(浏览器直接访问 :8000 即可) ----------
|
# ---------- 前端静态托管 + SPA 回退(浏览器直接访问 :8000 即可) ----------
|
||||||
|
|
||||||
@app.get("/{path:path}", include_in_schema=False)
|
@app.get("/{path:path}", include_in_schema=False)
|
||||||
async def spa_fallback(path: str):
|
async def spa_fallback(request: Request):
|
||||||
if not DIST.exists():
|
"""默认导向管理后台:屏蔽通过后端地址直接访问大屏前端(含首页 / 及 /twin /ai /screen 等)。
|
||||||
return JSONResponse({"detail": "前端未构建(dist 目录不存在)"}, status_code=404)
|
/api、/admin、/static、/file 等已在更早路由/挂载处理,不会走到这里。"""
|
||||||
dist = DIST.resolve()
|
return RedirectResponse(url="/admin", status_code=307)
|
||||||
if not path:
|
|
||||||
target = dist / "index.html"
|
|
||||||
else:
|
|
||||||
target = (dist / path).resolve()
|
|
||||||
if target.is_file() and target.is_relative_to(dist):
|
|
||||||
return FileResponse(target)
|
|
||||||
index = dist / "index.html"
|
|
||||||
if index.exists():
|
|
||||||
return FileResponse(index)
|
|
||||||
return JSONResponse({"detail": "Not Found"}, status_code=404)
|
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ from pathlib import Path
|
|||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
from .config import settings
|
||||||
|
|
||||||
log = logging.getLogger("dpm.vision")
|
log = logging.getLogger("dpm.vision")
|
||||||
|
|
||||||
|
_frame_seq = 0
|
||||||
|
|
||||||
MODEL_DIR = Path(__file__).resolve().parent.parent / "models"
|
MODEL_DIR = Path(__file__).resolve().parent.parent / "models"
|
||||||
FACE_MODEL_PATH = MODEL_DIR / "yolov8n-face.pt"
|
FACE_MODEL_PATH = MODEL_DIR / "yolov8n-face.pt"
|
||||||
POSE_MODEL_PATH = MODEL_DIR / "yolov8n-pose.pt"
|
POSE_MODEL_PATH = MODEL_DIR / "yolov8n-pose.pt"
|
||||||
@@ -188,6 +192,40 @@ def predict_jpeg(jpeg_bytes: bytes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _should_save(data: dict) -> bool:
|
||||||
|
"""仅当画面检测到人脸/手势时才值得落盘保存。"""
|
||||||
|
return (
|
||||||
|
int(data.get("faces") or 0) > 0
|
||||||
|
or bool(data.get("raised"))
|
||||||
|
or bool(data.get("fists"))
|
||||||
|
or bool(data.get("both_up"))
|
||||||
|
or bool(data.get("pointing"))
|
||||||
|
or bool(data.get("hands_close"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_save_frame(jpeg: bytes, data: dict):
|
||||||
|
"""仅将命中(有人/有动作)的 JPEG 帧落盘到 MEDIA_DIR/video/<小时目录>/,便于排查识别链路。
|
||||||
|
目录按小时分割(YYYYMMDD_HH),不自动清理。
|
||||||
|
可通过 DPM_SAVE_VISION_FRAMES=0 关闭,或 DPM_VISION_SAVE_DIR 改目录。"""
|
||||||
|
global _frame_seq
|
||||||
|
if not settings.SAVE_VISION_FRAMES:
|
||||||
|
return
|
||||||
|
if not _should_save(data):
|
||||||
|
return
|
||||||
|
_frame_seq += 1
|
||||||
|
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||||
|
hour = time.strftime("%Y%m%d_%H")
|
||||||
|
try:
|
||||||
|
save_dir = settings.VISION_FRAME_SAVE_DIR / hour
|
||||||
|
save_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = save_dir / f"{ts}_{_frame_seq:05d}.jpg"
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(jpeg)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
log.warning("vision: 保存帧失败 %s", e)
|
||||||
|
|
||||||
|
|
||||||
def predict_base64(b64: str):
|
def predict_base64(b64: str):
|
||||||
"""入口:base64 JPEG → 检测结果 dict(含 ok 标记,失败时带 error)"""
|
"""入口:base64 JPEG → 检测结果 dict(含 ok 标记,失败时带 error)"""
|
||||||
try:
|
try:
|
||||||
@@ -198,6 +236,7 @@ def predict_base64(b64: str):
|
|||||||
try:
|
try:
|
||||||
data = predict_jpeg(jpeg)
|
data = predict_jpeg(jpeg)
|
||||||
data["ok"] = True
|
data["ok"] = True
|
||||||
|
maybe_save_frame(jpeg, data)
|
||||||
return data
|
return data
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
log.error("vision: 推理失败 %s", e, exc_info=True)
|
log.error("vision: 推理失败 %s", e, exc_info=True)
|
||||||
|
|||||||
@@ -1265,3 +1265,128 @@ button { font-family: "Poppins", "PingFang SC", sans-serif; cursor: pointer; tra
|
|||||||
font-size: 13px; color: #1d2c44; outline: none;
|
font-size: 13px; color: #1d2c44; outline: none;
|
||||||
}
|
}
|
||||||
.mqtt-input:focus { border-color: #2f6bff; }
|
.mqtt-input:focus { border-color: #2f6bff; }
|
||||||
|
|
||||||
|
/* ============ 弱化连接状态(沉底、紧凑、低调) ============ */
|
||||||
|
.conn-card { opacity: 0.88; }
|
||||||
|
.conn-card .card-header { padding-bottom: 6px; }
|
||||||
|
.conn-card .conn-status { display: flex; flex-wrap: wrap; gap: 6px 22px; }
|
||||||
|
.conn-card .conn-row { min-width: 150px; }
|
||||||
|
.conn-card .conn-label { font-size: 11px; }
|
||||||
|
|
||||||
|
/* ============ 大屏控制:mqtt-group 按操作优先级排序 ============ */
|
||||||
|
.mqtt-card { display: flex; flex-direction: column; }
|
||||||
|
.mqtt-card .mqtt-group { order: 50; } /* 未显式设置 order 的组默认靠后 */
|
||||||
|
|
||||||
|
/* ============ 手机适配(响应式) ============ */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.main-grid { grid-template-columns: 1fr; padding: 16px; gap: 16px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.main-grid { padding: 10px; gap: 12px; }
|
||||||
|
.topbar { flex-wrap: wrap; row-gap: 8px; }
|
||||||
|
.topbar-center { order: 3; width: 100%; justify-content: flex-start; }
|
||||||
|
.page-switch-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
||||||
|
.mqtt-row { flex-wrap: wrap; gap: 8px; }
|
||||||
|
.page-switch-btn { flex: 1 1 45%; }
|
||||||
|
.form-inline { flex-direction: column; }
|
||||||
|
.form-row { flex-wrap: wrap; }
|
||||||
|
.conn-card .conn-status { flex-direction: column; gap: 6px; }
|
||||||
|
.card { padding: 16px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ 遥控器式快捷控制条 ============ */
|
||||||
|
.remote-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 18px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 16px auto 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
}
|
||||||
|
.remote-group { display: flex; gap: 6px; }
|
||||||
|
.remote-btn {
|
||||||
|
width: 42px; height: 42px;
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 24px; line-height: 1;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: #fff; color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
.remote-btn:hover { background: var(--primary-subtle); border-color: var(--primary); color: var(--primary); }
|
||||||
|
.remote-target {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
background: var(--primary-subtle);
|
||||||
|
border: 1px solid rgba(0,189,125,.2);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
.remote-label { font-size: 12px; font-weight: 600; color: var(--text-secondary); white-space: nowrap; }
|
||||||
|
.remote-count { font-size: 11px; color: var(--text-dim); white-space: nowrap; }
|
||||||
|
.remote-target select {
|
||||||
|
min-width: 160px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: #fff; color: var(--text); font-size: 13px;
|
||||||
|
}
|
||||||
|
.remote-pages { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.remote-bar { margin: 10px; padding: 10px; }
|
||||||
|
.remote-target { flex: 1 1 100%; }
|
||||||
|
.remote-target select { flex: 1; min-width: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 遥控条:页面切换合并模块 + 分隔线 */
|
||||||
|
.remote-nav { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.remote-sep { width: 1px; height: 26px; background: var(--border-default); margin: 0 4px; }
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.remote-nav { width: 100%; }
|
||||||
|
.remote-sep { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ 顶部控制模块(选择屏幕 / 页面切换,独立上下排列) ============ */
|
||||||
|
.control-stack {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 16px auto 0;
|
||||||
|
padding: 0 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.control-card { margin: 0; }
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.control-stack { padding: 0 10px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面切换:左右箭头独立一行,与下方页面按钮分隔开 */
|
||||||
|
.page-nav-prev {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
.page-nav-links { padding-top: 2px; }
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.page-nav-prev { justify-content: center; }
|
||||||
|
.page-nav-links { justify-content: flex-start; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片图标:继承主题主色(appica color -> currentColor) */
|
||||||
|
.card-header .card-icon { color: var(--primary); }
|
||||||
|
|
||||||
|
/* 页面按钮:当前激活页高亮 */
|
||||||
|
.page-switch-btn.active {
|
||||||
|
background: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,189,125,0.35);
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,20 @@
|
|||||||
var screenId = '';
|
var screenId = '';
|
||||||
var screenRole = '';
|
var screenRole = '';
|
||||||
|
|
||||||
|
/* 状态识别:点击某页面后,把该页操作模块前置到页面控制下方,并高亮当前页面按钮 */
|
||||||
|
function activatePage(page) {
|
||||||
|
if (!page) return;
|
||||||
|
var cards = document.querySelectorAll('.main-grid .card[data-page]');
|
||||||
|
for (var i = 0; i < cards.length; i++) cards[i].style.order = '';
|
||||||
|
var active = null;
|
||||||
|
for (var j = 0; j < cards.length; j++) {
|
||||||
|
if (cards[j].getAttribute('data-page') === page) { active = cards[j]; break; }
|
||||||
|
}
|
||||||
|
if (active) active.style.order = -10; // 前置到 main-grid 顶部(页面控制下方)
|
||||||
|
var navs = document.querySelectorAll('[data-nav]');
|
||||||
|
for (var k = 0; k < navs.length; k++) navs[k].classList.toggle('active', navs[k].getAttribute('data-nav') === page);
|
||||||
|
}
|
||||||
|
|
||||||
function applyScreenTarget(sel) {
|
function applyScreenTarget(sel) {
|
||||||
sel = sel || document.getElementById('screenTarget');
|
sel = sel || document.getElementById('screenTarget');
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
@@ -179,6 +193,8 @@
|
|||||||
try { cmd = JSON.parse(btn.getAttribute('data-mqtt')); } catch (err) { toast('指令 JSON 错误', 'error'); return; }
|
try { cmd = JSON.parse(btn.getAttribute('data-mqtt')); } catch (err) { toast('指令 JSON 错误', 'error'); return; }
|
||||||
// 屏幕终端精准控制:除「双屏开关」外,注入目标 client_id + role;全空=全局广播
|
// 屏幕终端精准控制:除「双屏开关」外,注入目标 client_id + role;全空=全局广播
|
||||||
if (cmd.action !== 'dual_screen') { cmd.screen_id = screenId; cmd.screen_role = screenRole; }
|
if (cmd.action !== 'dual_screen') { cmd.screen_id = screenId; cmd.screen_role = screenRole; }
|
||||||
|
// 状态识别:切页命令 → 前置对应页面操作模块
|
||||||
|
if (cmd.action === 'navigate' && cmd.params && cmd.params.page) activatePage(cmd.params.page);
|
||||||
var tgtTxt = screenId
|
var tgtTxt = screenId
|
||||||
? (cmd.screen_role === 'main' ? '主屏' : cmd.screen_role === 'secondary' ? '副屏' : '屏幕') + '·' + String(screenId).slice(0, 10)
|
? (cmd.screen_role === 'main' ? '主屏' : cmd.screen_role === 'secondary' ? '副屏' : '屏幕') + '·' + String(screenId).slice(0, 10)
|
||||||
: '全部';
|
: '全部';
|
||||||
@@ -212,6 +228,7 @@
|
|||||||
// 页面切换(绑定目标屏幕:screen_id 精确某台;空=全部)
|
// 页面切换(绑定目标屏幕:screen_id 精确某台;空=全部)
|
||||||
if (btn.hasAttribute('data-nav')) {
|
if (btn.hasAttribute('data-nav')) {
|
||||||
var page = btn.getAttribute('data-nav');
|
var page = btn.getAttribute('data-nav');
|
||||||
|
activatePage(page); // 状态识别:前置该页操作模块
|
||||||
var tgtTxt = screenId
|
var tgtTxt = screenId
|
||||||
? (screenRole === 'main' ? '主屏' : screenRole === 'secondary' ? '副屏' : '屏幕') + '·' + String(screenId).slice(0, 10)
|
? (screenRole === 'main' ? '主屏' : screenRole === 'secondary' ? '副屏' : '屏幕') + '·' + String(screenId).slice(0, 10)
|
||||||
: '全部';
|
: '全部';
|
||||||
|
|||||||
+225
-118
@@ -1,11 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>管理后台 · 昆明市大学生创业园</title>
|
<title>管理后台 · 昆明市大学生创业园</title>
|
||||||
<link rel="stylesheet" href="/static/admin.css">
|
<link rel="stylesheet" href="/static/admin.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% if not logged_in %}
|
{% if not logged_in %}
|
||||||
<!-- ==================== 登录页 ==================== -->
|
<!-- ==================== 登录页 ==================== -->
|
||||||
@@ -55,16 +57,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="topbar-right">
|
<div class="topbar-right">
|
||||||
<button class="btn-control" data-control="prev" title="上一个">
|
<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>
|
<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>
|
||||||
<button class="btn-control play-btn" data-control="play" title="播放">
|
<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>
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="currentColor">
|
||||||
|
<polygon points="5,3 15,9 5,15" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn-control play-btn" data-control="pause" title="暂停">
|
<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>
|
<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>
|
||||||
<button class="btn-control" data-control="next" title="下一个">
|
<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>
|
<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>
|
</button>
|
||||||
<span class="topbar-divider"></span>
|
<span class="topbar-divider"></span>
|
||||||
{% if from_screen %}<a class="btn-return" href="/screen">返回展播</a>{% endif %}
|
{% if from_screen %}<a class="btn-return" href="/screen">返回展播</a>{% endif %}
|
||||||
@@ -72,12 +85,43 @@
|
|||||||
<a class="btn-logout" href="/admin/logout">退出</a>
|
<a class="btn-logout" href="/admin/logout">退出</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 选择屏幕(独立模块) -->
|
||||||
|
<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="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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="main-grid">
|
<div class="main-grid">
|
||||||
<!-- 连接状态 -->
|
<!-- 连接状态(弱化:沉到底部、紧凑展示) -->
|
||||||
<div class="card">
|
<div class="card conn-card" style="order:99">
|
||||||
<div class="card-header">
|
<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>
|
<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>
|
<h3>连接状态</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="conn-status">
|
<div class="conn-status">
|
||||||
@@ -104,55 +148,99 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 页面切换 -->
|
<!-- 媒体轮播(独立模块) -->
|
||||||
<div class="card">
|
<div class="card" data-page="/screen">
|
||||||
<div class="card-header">
|
<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>
|
<div class="card-icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor"
|
||||||
<h3>目标屏幕(按 MQTT 客户端精准控制)</h3>
|
stroke-width="1.5">
|
||||||
|
<path d="M6 4v16l13-8z" />
|
||||||
|
</svg></div>
|
||||||
|
<h3>媒体轮播</h3>
|
||||||
</div>
|
</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>
|
|
||||||
</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-group">
|
|
||||||
<div class="mqtt-group-title">页面导航</div>
|
|
||||||
<div class="mqtt-row">
|
<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":"prev","params":{}}'>上一段</button>
|
||||||
<button class="page-switch-btn" data-mqtt='{"action":"navigate","params":{"page":"/wall"}}'>企业展示墙</button>
|
<button class="page-switch-btn" data-mqtt='{"action":"play","params":{}}'>播放</button>
|
||||||
<button class="page-switch-btn" data-mqtt='{"action":"navigate_rel","params":{"delta":-1}}'>← 向左</button>
|
<button class="page-switch-btn" data-mqtt='{"action":"pause","params":{}}'>暂停</button>
|
||||||
<button class="page-switch-btn" data-mqtt='{"action":"navigate_rel","params":{"delta":1}}'>向右 →</button>
|
<button class="page-switch-btn" data-mqtt='{"action":"next","params":{}}'>下一段</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 企业展示墙 -->
|
<!-- 语音对话(独立模块) -->
|
||||||
<div class="mqtt-group">
|
<div class="card" data-page="/voice">
|
||||||
<div class="mqtt-group-title">企业展示墙(/wall)</div>
|
<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">
|
<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_pause","params":{}}'>暂停滚动</button>
|
||||||
<button class="page-switch-btn" data-mqtt='{"action":"wall_resume","params":{}}'>继续滚动</button>
|
<button class="page-switch-btn" data-mqtt='{"action":"wall_resume","params":{}}'>继续滚动</button>
|
||||||
@@ -164,81 +252,65 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 视觉识别 -->
|
<!-- 人物识别(独立模块) -->
|
||||||
<div class="mqtt-group">
|
<!-- <div class="card">
|
||||||
<div class="mqtt-group-title">人物识别(全局)</div>
|
<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">
|
<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":true}}'>开启识别</button>
|
||||||
<button class="page-switch-btn" data-mqtt='{"action":"vision_set","params":{"enabled":false}}'>关闭识别</button>
|
<button class="page-switch-btn" data-mqtt='{"action":"vision_set","params":{"enabled":false}}'>关闭识别</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- AI 助手页 -->
|
<!-- 双屏(独立模块) -->
|
||||||
<div class="mqtt-group">
|
<!-- <div class="card">
|
||||||
<div class="mqtt-group-title">AI 助手页</div>
|
<div class="card-header">
|
||||||
<div class="mqtt-row">
|
<div class="card-icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor"
|
||||||
<input class="mqtt-input" id="mqttAiInput" type="text" placeholder="输入问题,回车发送到 AI 页">
|
stroke-width="1.5">
|
||||||
<button class="page-switch-btn" id="mqttAiSend">发送问题</button>
|
<rect x="2" y="3" width="20" height="14" rx="2" />
|
||||||
|
<path d="M8 21h8M12 17v4" />
|
||||||
|
</svg></div>
|
||||||
|
<h3>双屏控制</h3>
|
||||||
</div>
|
</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">
|
<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":true}}'>启动双屏</button>
|
||||||
<button class="page-switch-btn" data-mqtt='{"action":"dual_screen","params":{"on":false}}'>关闭双屏</button>
|
<button class="page-switch-btn" data-mqtt='{"action":"dual_screen","params":{"on":false}}'>关闭双屏</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mqtt-hint">在电脑另一块屏幕全屏展示(需展播机连接显示器)</div>
|
<div class="mqtt-hint">在电脑另一块屏幕全屏展示(需展播机连接显示器)</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- 媒体控制 -->
|
<!-- 全局通知(独立模块) -->
|
||||||
<div class="mqtt-group">
|
<!-- <div class="card">
|
||||||
<div class="mqtt-group-title">媒体轮播</div>
|
<div class="card-header">
|
||||||
<div class="mqtt-row">
|
<div class="card-icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor"
|
||||||
<button class="page-switch-btn" data-mqtt='{"action":"prev","params":{}}'>上一段</button>
|
stroke-width="1.5">
|
||||||
<button class="page-switch-btn" data-mqtt='{"action":"play","params":{}}'>播放</button>
|
<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" />
|
||||||
<button class="page-switch-btn" data-mqtt='{"action":"pause","params":{}}'>暂停</button>
|
<path d="M9 17v1a3 3 0 0 0 6 0v-1" />
|
||||||
<button class="page-switch-btn" data-mqtt='{"action":"next","params":{}}'>下一段</button>
|
</svg></div>
|
||||||
|
<h3>全局通知</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 通知 -->
|
|
||||||
<div class="mqtt-group">
|
|
||||||
<div class="mqtt-group-title">全局通知</div>
|
|
||||||
<div class="mqtt-row">
|
<div class="mqtt-row">
|
||||||
<input class="mqtt-input" id="mqttAlertText" type="text" placeholder="通知内容,回车广播">
|
<input class="mqtt-input" id="mqttAlertText" type="text" placeholder="通知内容,回车广播">
|
||||||
<button class="page-switch-btn" id="mqttAlertSend">广播通知</button>
|
<button class="page-switch-btn" id="mqttAlertSend">广播通知</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 播放设置 -->
|
<!-- 播放设置 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<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>
|
<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>
|
<h3>播放设置</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
<div class="form-inline">
|
||||||
@@ -269,7 +341,12 @@
|
|||||||
<!-- 语音 AI 测试(admin → 前端主动问候) -->
|
<!-- 语音 AI 测试(admin → 前端主动问候) -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<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>
|
<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>
|
<h3>语音 AI 测试</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
<div class="form-inline">
|
||||||
@@ -288,7 +365,11 @@
|
|||||||
<!-- 显示控制 -->
|
<!-- 显示控制 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<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>
|
<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>
|
<h3>显示控制</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-inline">
|
<div class="form-inline">
|
||||||
@@ -310,7 +391,11 @@
|
|||||||
<!-- 添加媒体 -->
|
<!-- 添加媒体 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<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>
|
<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>
|
<h3>添加媒体</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -320,7 +405,9 @@
|
|||||||
<button class="btn-accent" id="btnUpload">上传</button>
|
<button class="btn-accent" id="btnUpload">上传</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="upload-progress" id="uploadProgress" style="display:none;">
|
<div class="upload-progress" id="uploadProgress" style="display:none;">
|
||||||
<div class="progress-bar"><div class="progress-fill" id="progressFill" style="width:0%"></div></div>
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" id="progressFill" style="width:0%"></div>
|
||||||
|
</div>
|
||||||
<span class="progress-text" id="progressText"></span>
|
<span class="progress-text" id="progressText"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -337,13 +424,19 @@
|
|||||||
<!-- 媒体库 -->
|
<!-- 媒体库 -->
|
||||||
<div class="card full">
|
<div class="card full">
|
||||||
<div class="card-header">
|
<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>
|
<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>
|
<h3>媒体库({{ files|length }})</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-grid">
|
<div class="media-grid">
|
||||||
{% if files %}
|
{% if files %}
|
||||||
{% for item in 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' %}
|
{% if item.type == 'image' %}
|
||||||
<img src="{{ item.url }}" loading="lazy" class="thumb" alt="{{ item.name }}" data-preview>
|
<img src="{{ item.url }}" loading="lazy" class="thumb" alt="{{ item.name }}" data-preview>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -355,8 +448,10 @@
|
|||||||
<div class="body">
|
<div class="body">
|
||||||
<span class="name" title="{{ item.name }}">{{ item.name }}</span>
|
<span class="name" title="{{ item.name }}">{{ item.name }}</span>
|
||||||
<div class="meta-row">
|
<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-url' if item.source == 'url' else 'badge-local' }}">{{ '远程' if
|
||||||
<span class="badge {{ 'badge-video' if item.type == 'video' else 'badge-image' }}">{{ '视频' if item.type == 'video' else '图片' }}</span>
|
item.source == 'url' else '本地' }}</span>
|
||||||
|
<span class="badge {{ 'badge-video' if item.type == 'video' else 'badge-image' }}">{{ '视频' if
|
||||||
|
item.type == 'video' else '图片' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-url" data-url="{{ item.url }}" title="FastAPI 输出的资源 URL">
|
<div class="media-url" data-url="{{ item.url }}" title="FastAPI 输出的资源 URL">
|
||||||
<span>{{ item.url }}</span>
|
<span>{{ item.url }}</span>
|
||||||
@@ -372,7 +467,12 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<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 添加
|
暂无媒体文件<br>请上传或通过 URL 添加
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -382,13 +482,17 @@
|
|||||||
<!-- 播放列表 -->
|
<!-- 播放列表 -->
|
||||||
<div class="card full">
|
<div class="card full">
|
||||||
<div class="card-header">
|
<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>
|
<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>
|
<h3>播放列表({{ playlist|length }})</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-grid">
|
<div class="media-grid">
|
||||||
{% if playlist %}
|
{% if playlist %}
|
||||||
{% for item in 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' %}
|
{% if item.type == 'image' %}
|
||||||
<img src="{{ item.url }}" loading="lazy" class="thumb" alt="{{ item.name }}" data-preview>
|
<img src="{{ item.url }}" loading="lazy" class="thumb" alt="{{ item.name }}" data-preview>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -400,8 +504,10 @@
|
|||||||
<div class="body">
|
<div class="body">
|
||||||
<span class="name" title="{{ item.name }}">{{ item.name }}</span>
|
<span class="name" title="{{ item.name }}">{{ item.name }}</span>
|
||||||
<div class="meta-row">
|
<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-url' if item.source == 'url' else 'badge-local' }}">{{ '远程' if
|
||||||
<span class="badge {{ 'badge-video' if item.type == 'video' else 'badge-image' }}">{{ '视频' if item.type == 'video' else '图片' }}</span>
|
item.source == 'url' else '本地' }}</span>
|
||||||
|
<span class="badge {{ 'badge-video' if item.type == 'video' else 'badge-image' }}">{{ '视频' if
|
||||||
|
item.type == 'video' else '图片' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-url" data-url="{{ item.url }}" title="FastAPI 输出的资源 URL">
|
<div class="media-url" data-url="{{ item.url }}" title="FastAPI 输出的资源 URL">
|
||||||
<span>{{ item.url }}</span>
|
<span>{{ item.url }}</span>
|
||||||
@@ -451,10 +557,11 @@
|
|||||||
<script>
|
<script>
|
||||||
window.__DPM_INIT = {
|
window.__DPM_INIT = {
|
||||||
fullscreen: {{ 'true' if fullscreen else 'false' }},
|
fullscreen: {{ 'true' if fullscreen else 'false' }},
|
||||||
autostart: {{ 'true' if autostart else 'false' }}
|
autostart: {{ 'true' if autostart else 'false' }},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/admin.js"></script>
|
<script src="/static/admin.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user