Files
DPM/backend/templates/admin.html
T

437 lines
22 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 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>
<!-- 语音 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%;">
</div>
<div class="form-group">
<label>&nbsp;</label>
<button class="btn-primary" id="btnVoiceGreet" style="width:100%;">播报问候</button>
</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>
<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>