diff --git a/backend/app/routers.py b/backend/app/routers.py index f03d553..bfc6904 100644 --- a/backend/app/routers.py +++ b/backend/app/routers.py @@ -223,6 +223,16 @@ async def remove_playlist(body: PathBody): return {"ok": True} +@router.post("/api/playlist/play") +async def play_playlist_item(body: PathBody): + """指定媒体立即播放:加入播放列表(如未在)→ MQTT 广播 play_target → 大屏跳转播放""" + path = body.path + storage.add_to_playlist(path) + hub.publish_command("playlist_changed") + cmd = hub.publish_command("play_target", {"path": path}) + return {"ok": True, "cmd_id": cmd.get("cmd_id")} + + # ==================== 播放控制 / 状态 ==================== _CONTROL_ACTIONS = {"play", "pause", "next", "prev"} diff --git a/backend/main.py b/backend/main.py index 805c424..f327d3f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -5,4 +5,4 @@ import uvicorn from app.config import settings if __name__ == "__main__": - uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, reload=False) + uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, reload=True) diff --git a/backend/static/admin.css b/backend/static/admin.css index 4aa5e95..41c702e 100644 --- a/backend/static/admin.css +++ b/backend/static/admin.css @@ -1123,3 +1123,67 @@ button { font-family: "Poppins", "PingFang SC", sans-serif; cursor: pointer; tra .form-inline { gap: var(--space-8); align-items: stretch; } .form-group { margin-bottom: var(--space-8); } } + +/* ========================================================= + 管理后台增强:页面切换 / 立即播放 / URL 展示 + ========================================================= */ +.page-switch-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; +} +.page-switch-btn { + padding: 12px 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary, #00BD7D); + background: var(--primary-subtle, rgba(0,189,125,0.07)); + border: 1px solid rgba(0, 189, 125, 0.28); + border-radius: 10px; + cursor: pointer; + transition: all 0.2s; +} +.page-switch-btn:hover { + background: rgba(0, 189, 125, 0.14); + transform: translateY(-1px); +} +.media-url { + display: flex; + align-items: center; + gap: 6px; + margin: 4px 0 6px; + padding: 3px 6px; + min-width: 0; + background: rgba(0, 189, 125, 0.05); + border: 1px solid rgba(0, 189, 125, 0.14); + border-radius: 6px; +} +.media-url span { + flex: 1; + min-width: 0; + font-size: 10px; + font-family: "SF Mono", "JetBrains Mono", Menlo, monospace; + color: #6b7280; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.copy-btn { + flex-shrink: 0; + padding: 2px 7px; + font-size: 10px; + border: none; + border-radius: 5px; + background: rgba(0, 189, 125, 0.14); + color: var(--primary, #00BD7D); + cursor: pointer; + transition: all 0.2s; +} +.copy-btn:hover { background: rgba(0, 189, 125, 0.24); } +.media-card .actions { + flex-wrap: wrap; + gap: 5px; +} +@media (max-width: 900px) { + .page-switch-grid { grid-template-columns: repeat(2, 1fr); } +} diff --git a/backend/static/admin.js b/backend/static/admin.js index f0d7295..3cd0861 100644 --- a/backend/static/admin.js +++ b/backend/static/admin.js @@ -55,6 +55,16 @@ }; } catch (err) { /* ignore */ } + /* ---------- 页面切换(MQTT 控制大屏) ---------- */ + document.querySelectorAll('[data-nav]').forEach(function (btn) { + btn.addEventListener('click', function () { + var page = btn.getAttribute('data-nav'); + post('/api/display/command', { action: 'navigate', params: { page: page } }, function () { + toast('已切换大屏至「' + btn.textContent.trim() + '」'); + }); + }); + }); + /* ---------- 播放控制 ---------- */ document.querySelectorAll('[data-control]').forEach(function (btn) { btn.addEventListener('click', function () { @@ -150,23 +160,54 @@ /* ---------- 远程 URL ---------- */ var btnAddUrl = document.getElementById('btnAddUrl'); - if (btnAddUrl) btnAddUrl.addEventListener('click', function () { + var btnAddUrlPlay = document.getElementById('btnAddUrlPlay'); + function addUrlThen(play) { var inp = document.getElementById('urlInput'); var url = (inp.value || '').trim(); if (!url) { toast('请输入 URL', 'error'); return; } post('/api/media/add-url', { url: url }, function (d) { - reload(d && d.duplicate ? 'URL 已存在于媒体库' : '已添加到媒体库'); + if (play) { + post('/api/playlist/play', { path: url }, function () { reload('已加入并开始播放'); }); + } else { + reload(d && d.duplicate ? 'URL 已存在于媒体库' : '已添加到媒体库'); + } + }); + } + if (btnAddUrl) btnAddUrl.addEventListener('click', function () { addUrlThen(false); }); + if (btnAddUrlPlay) btnAddUrlPlay.addEventListener('click', function () { addUrlThen(true); }); + + /* ---------- 复制资源 URL(FastAPI 输出) ---------- */ + document.querySelectorAll('.copy-btn').forEach(function (btn) { + btn.addEventListener('click', function (e) { + e.stopPropagation(); + var url = btn.closest('.media-url').getAttribute('data-url') || ''; + var full = /^https?:\/\//.test(url) ? url : window.location.origin + url; + var done = function () { toast('链接已复制'); }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(full).then(done).catch(function () { fallbackCopy(full); done(); }); + } else { + fallbackCopy(full); done(); + } }); }); + function fallbackCopy(text) { + var ta = document.createElement('textarea'); + ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); ta.select(); + try { document.execCommand('copy'); } catch (err) { /* ignore */ } + document.body.removeChild(ta); + } - /* ---------- 媒体操作(加入播放 / 移出 / 删除) ---------- */ + /* ---------- 媒体操作(立即播放 / 加入播放 / 移出 / 删除) ---------- */ document.querySelectorAll('[data-action]').forEach(function (btn) { btn.addEventListener('click', function (e) { e.stopPropagation(); var card = btn.closest('.media-card'); var path = card.getAttribute('data-path'); var action = btn.getAttribute('data-action'); - if (action === 'add-playlist') { + if (action === 'play-now') { + post('/api/playlist/play', { path: path }, function () { toast('正在播放'); }); + } else if (action === 'add-playlist') { post('/api/playlist/add', { path: path }, function () { reload('已加入播放列表'); }); } else if (action === 'remove-playlist') { post('/api/playlist/remove', { path: path }, function () { reload('已移出播放列表'); }); @@ -217,6 +258,10 @@ if (!current) return; post('/api/playlist/add', { path: current.path }, function () { closePreview(); reload('已加入播放列表'); }); }); + document.getElementById('previewPlay').addEventListener('click', function () { + if (!current) return; + post('/api/playlist/play', { path: current.path }, function () { closePreview(); toast('正在播放'); }); + }); document.getElementById('previewDelete').addEventListener('click', function () { if (!current) return; if (!window.confirm('确定删除该文件?')) return; diff --git a/backend/templates/admin.html b/backend/templates/admin.html index f9779ae..f415c8c 100644 --- a/backend/templates/admin.html +++ b/backend/templates/admin.html @@ -74,6 +74,20 @@
+ +
+
+
+

页面切换(控制大屏展示)

+
+
+ + + + +
+
+
@@ -140,10 +154,11 @@
- +
- + +
@@ -172,7 +187,12 @@ {{ '远程' if item.source == 'url' else '本地' }} {{ '视频' if item.type == 'video' else '图片' }} +
+ {{ item.url }} + +
+
@@ -212,7 +232,12 @@ {{ '远程' if item.source == 'url' else '本地' }} {{ '视频' if item.type == 'video' else '图片' }} +
+ {{ item.url }} + +
+
@@ -240,6 +265,7 @@