/* ========================================================= Admin 管理后台前端逻辑(Jinja 页面配套) 事件采用 document 委托绑定,保证按钮点击稳定响应; 连接状态每 3s 轮询 /api/display/state 刷新 ========================================================= */ (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, delay) { if (msg) toast(msg); setTimeout(function () { location.reload(); }, delay || 600); } /* ---------- 播放状态(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; } }; } catch (err) { /* ignore */ } /* ========================================================= 连接状态轮询(/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('keydown', function (e) { if (e.key !== 'Enter') return; if (e.target && e.target.id === 'mqttAiInput') { document.getElementById('mqttAiSend').click(); } else if (e.target && e.target.id === 'mqttAlertText') { document.getElementById('mqttAlertSend').click(); } }); document.addEventListener('click', function (e) { var btn = e.target.closest ? e.target.closest('[data-nav], [data-mqtt], [data-control], [data-action], .copy-btn, [data-preview], #mqttAiSend, #mqttAlertSend') : 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; } // 大屏 MQTT 指令(data-mqtt 携带完整 {action, params} JSON) if (btn.hasAttribute('data-mqtt')) { var cmd = null; try { cmd = JSON.parse(btn.getAttribute('data-mqtt')); } catch (err) { toast('指令 JSON 错误', 'error'); return; } post('/api/display/command', cmd, function (d) { toast(d && d.ok !== false ? '指令已发送:' + (cmd.action || '') : '发送失败(MQTT 未连接?)', d && d.ok !== false ? 'ok' : 'error'); }); return; } // AI 提问输入框 if (btn.id === 'mqttAiSend') { var text = (document.getElementById('mqttAiInput').value || '').trim(); if (!text) { toast('请输入问题', 'error'); return; } post('/api/display/command', { action: 'ai_input', params: { text: text } }, function () { toast('已发送问题到大屏 AI 页'); }); return; } // 通知输入框 if (btn.id === 'mqttAlertSend') { var alertText = (document.getElementById('mqttAlertText').value || '').trim(); if (!alertText) { toast('请输入通知内容', 'error'); return; } post('/api/display/command', { action: 'alert', params: { text: alertText } }, function () { toast('通知已广播'); }); 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; } // 播放控制 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 }); 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('播放模式已更新'); }); }); 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 sfxVolumeRange = document.getElementById('sfxVolumeRange'); var sfxVolumeLabel = document.getElementById('sfxVolumeLabel'); var sfxVolTimer = null; if (sfxVolumeRange && sfxVolumeLabel) { sfxVolumeRange.addEventListener('input', function () { sfxVolumeLabel.textContent = sfxVolumeRange.value + '%'; if (sfxVolTimer) clearTimeout(sfxVolTimer); sfxVolTimer = setTimeout(function () { post('/api/settings', { sfx_volume: Number(sfxVolumeRange.value) }, function () { toast('音效音量已更新'); }); }, 300); }); } /* ---------- 显示控制 ---------- */ var btnFullscreen = document.getElementById('btnFullscreen'); var btnAutostart = document.getElementById('btnAutostart'); function renderDisplay() { if (btnFullscreen) btnFullscreen.textContent = fullscreen ? '切换窗口模式' : '切换大屏模式'; if (btnAutostart) { btnAutostart.textContent = autostart ? '已开启' : '已关闭'; btnAutostart.classList.toggle('active', autostart); } } if (btnFullscreen) btnFullscreen.addEventListener('click', function () { fullscreen = !fullscreen; renderDisplay(); post('/api/settings', { fullscreen: fullscreen }); }); 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'); 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) { if (play) { post('/api/playlist/play', { path: url }, function () { toast('已加入并发送播放指令'); }); } else { reload(d && d.duplicate ? 'URL 已存在于媒体库' : '已添加到媒体库'); } }); } if (btnAddUrl) btnAddUrl.addEventListener('click', function () { addUrlThen(false); }); if (btnAddUrlPlay) btnAddUrlPlay.addEventListener('click', function () { addUrlThen(true); }); /* ========================================================= 预览弹窗 ========================================================= */ 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'); 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' ? '' + name + '' : ''; document.getElementById('previewMeta').innerHTML = '' + (source === 'url' ? '远程' : '本地') + '' + '' + (type === 'video' ? '视频' : '图片') + ''; 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.getElementById('previewClose').addEventListener('click', closePreview); if (modal) 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('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; post('/api/delete', { path: current.path }, function () { closePreview(); reload('已删除'); }); }); })();