567 lines
26 KiB
HTML
567 lines
26 KiB
HTML
<!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="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="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="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>
|
||
</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="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 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":"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 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 class="mqtt-row">
|
||
<input class="mqtt-input" id="mqttAlertText" type="text" placeholder="通知内容,回车广播">
|
||
<button class="page-switch-btn" id="mqttAlertSend">广播通知</button>
|
||
</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="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-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="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-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="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-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="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="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="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 }}">
|
||
{% 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="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 }}">
|
||
{% 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">×</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> |