fix: 管理后台连接状态 + 修复按钮无响应

连接状态(正式):
- /api/display/state 扩展:screens_online(大屏心跳统计,opc/display/heartbeat)+ last_command(最近指令与发布结果)
- 前端大屏每 10s 上报心跳(稳定 client_id),后端 30s TTL 统计在线大屏数
- 管理台新增「连接状态」卡片:后端/MQTT/在线大屏/最近指令,3s 轮询刷新

按钮无响应根治:
- admin.js 改为 document 事件委托(data-nav/data-control/data-action/复制/预览),不再依赖逐个绑定
- /static 与 /admin 响应加 no-cache 头,杜绝旧 JS/CSS 缓存导致的按钮失效
- SSE 不再自动重载页面(避免与操作回调双重重载清掉反馈);立即播放改为 toast 不重载
This commit is contained in:
Pine
2026-08-17 21:54:23 +08:00
parent 1a95ecbacb
commit c55835fb7d
10 changed files with 297 additions and 86 deletions
+115 -76
View File
@@ -1,6 +1,7 @@
/* =========================================================
Admin 管理后台前端逻辑(Jinja 页面配套)
动态操作调用 FastAPI REST,完成后整页刷新(服务端重渲染)
事件采用 document 委托绑定,保证按钮点击稳定响应;
连接状态每 3s 轮询 /api/display/state 刷新
========================================================= */
(function () {
'use strict';
@@ -34,12 +35,12 @@
})
.catch(function () { toast('网络错误', 'error'); });
}
function reload(msg) {
function reload(msg, delay) {
if (msg) toast(msg);
setTimeout(function () { location.reload(); }, 350);
setTimeout(function () { location.reload(); }, delay || 600);
}
/* ---------- 播放状态(SSE ---------- */
/* ---------- 播放状态(SSE,仅更新状态文字,不自动重载页面 ---------- */
var statusText = document.getElementById('statusText');
try {
var es = new EventSource('/api/events');
@@ -51,33 +52,118 @@
else if (s.status === 'playing') statusText.textContent = '正在播放 — ' + s.name;
else if (s.status === 'paused') statusText.textContent = '已暂停 — ' + s.name;
}
if (msg.action === 'playlist_changed') reload();
};
} catch (err) { /* ignore */ }
/* ---------- 页面切换(MQTT 控制大屏) ---------- */
document.querySelectorAll('[data-nav]').forEach(function (btn) {
btn.addEventListener('click', function () {
/* =========================================================
连接状态轮询(/api/display/state
========================================================= */
var connEls = {
api: document.getElementById('connApi'),
mqtt: document.getElementById('connMqtt'),
screens: document.getElementById('connScreens'),
lastCmd: document.getElementById('connLastCmd'),
};
function setConn(id, ok, value) {
var el = connEls[id];
if (!el) return;
var dot = el.querySelector('.conn-dot');
var val = el.querySelector('.conn-value');
if (dot) dot.className = 'conn-dot ' + (ok ? 'on' : 'off');
if (val) val.textContent = value;
}
function pollStatus() {
fetch('/api/display/state', { cache: 'no-store' })
.then(function (r) { return r.json(); })
.then(function (d) {
setConn('api', true, '在线');
setConn('mqtt', !!d.mqtt_connected, d.mqtt_connected ? '已连接(' + (d.mqtt_host || '') + '' : '未连接');
setConn('screens', (d.screens_online || 0) > 0, (d.screens_online || 0) + ' 台');
var lc = d.last_command;
if (lc) {
var names = { navigate: '页面切换', play: '播放', pause: '暂停', next: '下一项', prev: '上一项',
'play_target': '指定播放', alert: '通知', show_card: '卡片', set_mode: '播放模式' };
var when = new Date(lc.ts).toLocaleTimeString('zh-CN', { hour12: false });
setConn('lastCmd', !!lc.published,
(names[lc.action] || lc.action) + ' · ' + (lc.published ? '已广播' : '未广播') + ' · ' + when);
}
})
.catch(function () {
setConn('api', false, '离线');
setConn('mqtt', false, '未知');
setConn('screens', false, '未知');
});
}
if (connEls.api) { pollStatus(); setInterval(pollStatus, 3000); }
/* =========================================================
事件委托:页面切换 / 播放控制 / 媒体操作
========================================================= */
document.addEventListener('click', function (e) {
var btn = e.target.closest ? e.target.closest('[data-nav], [data-control], [data-action], .copy-btn, [data-preview]') : null;
if (!btn) return;
e.stopPropagation();
// 预览
if (btn.hasAttribute('data-preview')) { openPreview(btn.closest('.media-card')); return; }
// 复制 URL
if (btn.classList.contains('copy-btn')) {
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(); }
return;
}
// 页面切换
if (btn.hasAttribute('data-nav')) {
var page = btn.getAttribute('data-nav');
post('/api/display/command', { action: 'navigate', params: { page: page } }, function () {
toast('已切换大屏至「' + btn.textContent.trim() + '」');
});
});
});
return;
}
/* ---------- 播放控制 ---------- */
document.querySelectorAll('[data-control]').forEach(function (btn) {
btn.addEventListener('click', function () {
// 播放控制
if (btn.hasAttribute('data-control')) {
var action = btn.getAttribute('data-control');
if (action === 'minimize') {
post('/api/display/command', { action: 'minimize', params: {} });
} else {
post('/api/control', { action: action });
if (action === 'minimize') post('/api/display/command', { action: 'minimize', params: {} });
else post('/api/control', { action: action });
return;
}
// 媒体操作
if (btn.hasAttribute('data-action')) {
var card = btn.closest('.media-card');
var path = card.getAttribute('data-path');
var act = btn.getAttribute('data-action');
if (act === 'play-now') {
post('/api/playlist/play', { path: path }, function () { toast('已发送播放指令,大屏即将播放'); });
} else if (act === 'add-playlist') {
post('/api/playlist/add', { path: path }, function () { reload('已加入播放列表'); });
} else if (act === 'remove-playlist') {
post('/api/playlist/remove', { path: path }, function () { reload('已移出播放列表'); });
} else if (act === 'delete') {
if (!window.confirm('确定删除该文件?')) return;
post('/api/delete', { path: path }, function () { reload('已删除'); });
}
});
}
});
/* ---------- 播放设置 ---------- */
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);
}
/* =========================================================
播放设置(change/input 事件)
========================================================= */
var playMode = document.getElementById('playMode');
if (playMode) playMode.addEventListener('change', function () {
post('/api/settings', { play_mode: playMode.value }, function () { toast('播放模式已更新'); });
@@ -105,20 +191,17 @@
/* ---------- 显示控制 ---------- */
var btnFullscreen = document.getElementById('btnFullscreen');
var btnAutostart = document.getElementById('btnAutostart');
function renderDisplay() {
if (btnFullscreen) btnFullscreen.textContent = fullscreen ? '切换窗口模式' : '切换大屏模式';
var btnAutostart = document.getElementById('btnAutostart');
if (btnAutostart) { btnAutostart.textContent = autostart ? '已开启' : '已关闭'; btnAutostart.classList.toggle('active', autostart); }
}
if (btnFullscreen) btnFullscreen.addEventListener('click', function () {
fullscreen = !fullscreen;
renderDisplay();
fullscreen = !fullscreen; renderDisplay();
post('/api/settings', { fullscreen: fullscreen });
});
var btnAutostart = document.getElementById('btnAutostart');
if (btnAutostart) btnAutostart.addEventListener('click', function () {
autostart = !autostart;
renderDisplay();
autostart = !autostart; renderDisplay();
post('/api/settings', { autostart: autostart });
});
renderDisplay();
@@ -129,12 +212,10 @@
var inp = document.getElementById('fileInput');
var fileList = Array.from(inp.files || []);
if (fileList.length === 0) { toast('请选择文件', 'error'); return; }
var prog = document.getElementById('uploadProgress');
var fill = document.getElementById('progressFill');
var text = document.getElementById('progressText');
prog.style.display = 'block';
var done = 0;
fileList.forEach(function (file) {
var fd = new FormData();
@@ -167,7 +248,7 @@
if (!url) { toast('请输入 URL', 'error'); return; }
post('/api/media/add-url', { url: url }, function (d) {
if (play) {
post('/api/playlist/play', { path: url }, function () { reload('已加入并开始播放'); });
post('/api/playlist/play', { path: url }, function () { toast('已加入并发送播放指令'); });
} else {
reload(d && d.duplicate ? 'URL 已存在于媒体库' : '已添加到媒体库');
}
@@ -176,54 +257,15 @@
if (btnAddUrl) btnAddUrl.addEventListener('click', function () { addUrlThen(false); });
if (btnAddUrlPlay) btnAddUrlPlay.addEventListener('click', function () { addUrlThen(true); });
/* ---------- 复制资源 URLFastAPI 输出) ---------- */
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 === '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('已移出播放列表'); });
} else if (action === 'delete') {
if (!window.confirm('确定删除该文件?')) return;
post('/api/delete', { path: path }, function () { reload('已删除'); });
}
});
});
/* ---------- 预览弹窗 ---------- */
/* =========================================================
预览弹窗
========================================================= */
var modal = document.getElementById('previewModal');
var modalBody = document.getElementById('previewBody');
var current = null;
function openPreview(card) {
if (!card || !modal) return;
var path = card.getAttribute('data-path');
var type = card.getAttribute('data-type');
var source = card.getAttribute('data-source');
@@ -246,11 +288,8 @@
modal.style.display = 'none';
current = null;
}
document.querySelectorAll('[data-preview]').forEach(function (el) {
el.addEventListener('click', function () { openPreview(el.closest('.media-card')); });
});
document.getElementById('previewClose').addEventListener('click', closePreview);
modal.addEventListener('click', function (e) {
if (modal) modal.addEventListener('click', function (e) {
if (e.target === modal) closePreview();
});
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closePreview(); });
@@ -260,7 +299,7 @@
});
document.getElementById('previewPlay').addEventListener('click', function () {
if (!current) return;
post('/api/playlist/play', { path: current.path }, function () { closePreview(); toast('正在播放'); });
post('/api/playlist/play', { path: current.path }, function () { closePreview(); toast('已发送播放指令'); });
});
document.getElementById('previewDelete').addEventListener('click', function () {
if (!current) return;