Files
DPM/backend/templates/admin.html
T
Pine 00cb76c73f feat: 管理后台迁移至 FastAPI + Jinja2 服务端渲染
- GET /admin:Jinja 模板渲染(登录页 / 管理台),媒体库、播放列表、设置由服务端渲染
- POST /admin/login:表单登录 + HMAC 签名 Cookie(DPM_ADMIN_SECRET),GET /admin/logout 退出
- /static/admin.css + admin.js:动态操作(上传进度/播放控制/设置/SSE 状态/预览弹窗)调 REST 后整页刷新
- 前端移除 React Admin 页面与 /admin 路由(Admin.jsx 删除);媒体/播放列表接口保持不变
- 依赖改用 uv 管理(pyproject.toml + uv.lock,含 jinja2),README 更新启动方式
2026-08-17 21:30:44 +08:00

264 lines
12 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"><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>
</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="actions">
<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="actions">
<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="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>