Files
DPM/backend/templates/admin.html
T
Pine 0af159a175 feat: 数据真实化、企业展示墙、全局手势识别与 3D 模型完善
- 数据大屏改用真实数据(xls 统计表 + 宣传册口径);TOKEN/工具调用基于真实锚点测算
- 新增企业展示墙页(/wall,39 家全量档案 + MQTT/admin/ESP32 控制文档)
- 新增全局手势识别(GlobalVision 全局挂载 + dpm:gesture 事件分发)
- 3D 模型墙面逐间修复、去除门/过道顶棚/调试标签、房间资产不穿墙、会议室多小桌
- 侧栏企业分布扩展至 39 家完整信息,分区按官方四大孵化区域
- 语音页多轮对话展示优化、去除页脚与检测横条、背景对齐
2026-08-18 05:48:56 +08:00

413 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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">
</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>
</div>
<h2>昆明市大学生创业园</h2>
<p>大屏幕轮播控制系统</p>
</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-page">
<div class="dashboard active">
<!-- 顶栏 -->
<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>
<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="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">
<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 class="mqtt-group">
<div class="mqtt-group-title">企业展示墙(/wall</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>
</div>
<div class="mqtt-row">
<button class="page-switch-btn" data-mqtt='{"action":"wall_speed","params":{"speed":"slow"}}'>慢速</button>
<button class="page-switch-btn" data-mqtt='{"action":"wall_speed","params":{"speed":"normal"}}'>标准</button>
<button class="page-switch-btn" data-mqtt='{"action":"wall_speed","params":{"speed":"fast"}}'>快速</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">
<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>
<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>
</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>
<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 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>
<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="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 %}
{% 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 }}">
{% if item.type == 'image' %}
<img src="{{ item.url }}" loading="lazy" class="thumb" alt="{{ item.name }}" data-preview>
{% else %}
<div class="thumb-video-wrap" data-preview>
<video src="{{ item.url }}#t=0.5" preload="auto" muted playsinline class="thumb"></video>
<span class="play-badge"></span>
</div>
{% endif %}
<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.type == 'video' else '图片' }}</span>
</div>
<div class="media-url" data-url="{{ item.url }}" title="FastAPI 输出的资源 URL">
<span>{{ item.url }}</span>
<button class="copy-btn" title="复制链接">复制</button>
</div>
<div class="actions">
<button class="btn-accent btn-sm" data-action="play-now">立即播放</button>
<button class="btn-accent btn-sm" data-action="add-playlist">加入播放</button>
<button class="btn-danger btn-sm" data-action="delete">删除</button>
</div>
</div>
</div>
{% endfor %}
{% 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>
暂无媒体文件<br>请上传或通过 URL 添加
</div>
{% endif %}
</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 %}
{% 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 }}">
{% if item.type == 'image' %}
<img src="{{ item.url }}" loading="lazy" class="thumb" alt="{{ item.name }}" data-preview>
{% else %}
<div class="thumb-video-wrap" data-preview>
<video src="{{ item.url }}#t=0.5" preload="auto" muted playsinline class="thumb"></video>
<span class="play-badge"></span>
</div>
{% endif %}
<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.type == 'video' else '图片' }}</span>
</div>
<div class="media-url" data-url="{{ item.url }}" title="FastAPI 输出的资源 URL">
<span>{{ item.url }}</span>
<button class="copy-btn" title="复制链接">复制</button>
</div>
<div class="actions">
<button class="btn-accent btn-sm" data-action="play-now">立即播放</button>
<button class="btn-danger btn-sm" data-action="remove-playlist">移出</button>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<div class="empty-icon"></div>
播放列表为空<br>从上方媒体库添加内容
</div>
{% endif %}
</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">&times;</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>
<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>