Files
DPM/backend/static/admin.js
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

226 lines
9.6 KiB
JavaScript
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.
/* =========================================================
Admin 管理后台前端逻辑(Jinja 页面配套)
动态操作调用 FastAPI REST,完成后整页刷新(服务端重渲染)
========================================================= */
(function () {
'use strict';
var init = window.__DPM_INIT || { fullscreen: true, autostart: false };
var fullscreen = !!init.fullscreen;
var autostart = !!init.autostart;
/* ---------- Toast ---------- */
var toastContainer = document.getElementById('toastContainer');
function toast(msg, type) {
if (!toastContainer) return;
var el = document.createElement('div');
el.className = 'toast ' + (type || 'success');
el.textContent = msg;
toastContainer.appendChild(el);
setTimeout(function () { el.remove(); }, 2800);
}
/* ---------- 通用请求 ---------- */
function post(url, body, cb) {
fetch(url, {
method: 'POST',
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
})
.then(function (r) { return r.json().catch(function () { return {}; }); })
.then(function (d) {
if (d && d.ok === false) { toast(d.error || '操作失败', 'error'); return; }
if (cb) cb(d);
})
.catch(function () { toast('网络错误', 'error'); });
}
function reload(msg) {
if (msg) toast(msg);
setTimeout(function () { location.reload(); }, 350);
}
/* ---------- 播放状态(SSE ---------- */
var statusText = document.getElementById('statusText');
try {
var es = new EventSource('/api/events');
es.onmessage = function (e) {
var msg; try { msg = JSON.parse(e.data); } catch (err) { return; }
if (msg.action === 'state_update' && msg.state && statusText) {
var s = msg.state;
if (!s.status || s.status === 'idle' || !s.name) statusText.textContent = '等待大屏连接...';
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 */ }
/* ---------- 播放控制 ---------- */
document.querySelectorAll('[data-control]').forEach(function (btn) {
btn.addEventListener('click', function () {
var action = btn.getAttribute('data-control');
if (action === 'minimize') {
post('/api/display/command', { action: 'minimize', params: {} });
} else {
post('/api/control', { action: action });
}
});
});
/* ---------- 播放设置 ---------- */
var playMode = document.getElementById('playMode');
if (playMode) playMode.addEventListener('change', function () {
post('/api/settings', { play_mode: playMode.value }, function () { toast('播放模式已更新'); });
});
var imageDuration = document.getElementById('imageDuration');
if (imageDuration) imageDuration.addEventListener('change', function () {
var v = Number(imageDuration.value);
if (v < 1 || v > 300) { toast('时长需在 1-300 秒', 'error'); imageDuration.value = 5; return; }
post('/api/settings', { image_duration: v }, function () { toast('图片时长已更新'); });
});
var volumeRange = document.getElementById('volumeRange');
var volumeLabel = document.getElementById('volumeLabel');
var volTimer = null;
if (volumeRange && volumeLabel) {
volumeRange.addEventListener('input', function () {
volumeLabel.textContent = volumeRange.value + '%';
if (volTimer) clearTimeout(volTimer);
volTimer = setTimeout(function () {
post('/api/settings', { volume: Number(volumeRange.value) }, function () { toast('音量已更新'); });
}, 300);
});
}
/* ---------- 显示控制 ---------- */
var btnFullscreen = document.getElementById('btnFullscreen');
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();
post('/api/settings', { fullscreen: fullscreen });
});
var btnAutostart = document.getElementById('btnAutostart');
if (btnAutostart) btnAutostart.addEventListener('click', function () {
autostart = !autostart;
renderDisplay();
post('/api/settings', { autostart: autostart });
});
renderDisplay();
/* ---------- 上传(带进度) ---------- */
var btnUpload = document.getElementById('btnUpload');
if (btnUpload) btnUpload.addEventListener('click', function () {
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();
fd.append('file', file);
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
var pct = Math.round((e.loaded / e.total) * 100);
var overall = Math.round(((done + e.loaded / e.total) / fileList.length) * 100);
fill.style.width = overall + '%';
text.textContent = file.name + ' (' + (done + 1) + '/' + fileList.length + ') — ' + pct + '%';
}
};
xhr.onload = function () {
done += 1;
if (done === fileList.length) reload('上传完成');
};
xhr.onerror = function () { toast(file.name + ' 上传失败', 'error'); done += 1; };
xhr.send(fd);
});
});
/* ---------- 远程 URL ---------- */
var btnAddUrl = document.getElementById('btnAddUrl');
if (btnAddUrl) btnAddUrl.addEventListener('click', function () {
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 已存在于媒体库' : '已添加到媒体库');
});
});
/* ---------- 媒体操作(加入播放 / 移出 / 删除) ---------- */
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') {
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) {
var path = card.getAttribute('data-path');
var type = card.getAttribute('data-type');
var source = card.getAttribute('data-source');
var name = card.getAttribute('data-name');
var url = source === 'url' ? path : '/file/' + encodeURI(path);
current = { path: path, url: url, type: type, name: name };
document.getElementById('previewTitle').textContent = name;
modalBody.innerHTML = type === 'image'
? '<img src="' + url + '" alt="' + name + '">'
: '<video src="' + url + '" controls autoplay playsinline></video>';
document.getElementById('previewMeta').innerHTML =
'<span class="badge ' + (source === 'url' ? 'badge-url' : 'badge-local') + '">' + (source === 'url' ? '远程' : '本地') + '</span>' +
'<span class="badge ' + (type === 'video' ? 'badge-video' : 'badge-image') + '">' + (type === 'video' ? '视频' : '图片') + '</span>';
modal.style.display = 'flex';
}
function closePreview() {
var v = modalBody.querySelector('video');
if (v) { v.pause(); v.removeAttribute('src'); v.load(); }
modalBody.innerHTML = '';
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 (e.target === modal) closePreview();
});
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closePreview(); });
document.getElementById('previewAdd').addEventListener('click', function () {
if (!current) return;
post('/api/playlist/add', { path: current.path }, function () { closePreview(); reload('已加入播放列表'); });
});
document.getElementById('previewDelete').addEventListener('click', function () {
if (!current) return;
if (!window.confirm('确定删除该文件?')) return;
post('/api/delete', { path: current.path }, function () { closePreview(); reload('已删除'); });
});
})();