diff --git a/frontend/static/css/display.css b/frontend/static/css/display.css index 9742bf0..957d7e5 100644 --- a/frontend/static/css/display.css +++ b/frontend/static/css/display.css @@ -865,3 +865,9 @@ html, body { .cd-item .cd-remain.overdue { color: var(--accent-red); } +/* ── Count-up timer (正计时) ──────────────── */ +.countup-section { margin-top: clamp(8px, 1.5vh, 18px); } +.countup-label { font-size: clamp(12px, 1.6vw, 18px); color: var(--text-dim); margin-bottom: clamp(2px, 0.4vh, 8px); } +#countup-value { font-size: clamp(24px, 5vw, 64px); margin-bottom: 0; } +.countup-btns { display: flex; gap: clamp(10px, 1.5vw, 20px); margin-top: clamp(8px, 1.2vh, 16px); } +.countup-btns .pomo-btn { padding: clamp(6px, 1vh, 12px) clamp(20px, 3vw, 44px); } diff --git a/frontend/static/js/display.js b/frontend/static/js/display.js index a96dc38..49e34f7 100644 --- a/frontend/static/js/display.js +++ b/frontend/static/js/display.js @@ -36,6 +36,7 @@ _cdModalMode: false, // true = countdown modal, false = alarm modal _editingCountdown: null, timezone: 'Asia/Shanghai', // 账户独立时区(来自 /api/display/state) + countUp: { status: 'idle', startedAtMs: null, baseSec: 0, frozenSec: 0 }, }; // ─── DOM refs ───────────────────────────── @@ -107,6 +108,10 @@ if (name === 'pomodoro') { fetchPomodoro(); } + // Immediately render count-up when switching back to time panel + if (name === 'time') { + renderCountUpPanel(); + } } cards.forEach(function (card) { @@ -1298,6 +1303,108 @@ n.y + '年' + n.mo + '月' + n.d + '日 星期' + days[n.wd]; } + function _calcCountUpElapsed() { + var c = state.countUp; + if (!c) return 0; + if (c.status !== 'running') return c.frozenSec || 0; + try { + if (c.startedAtMs == null) return c.frozenSec || 0; + return (c.baseSec || 0) + Math.floor((Date.now() - c.startedAtMs) / 1000); + } catch (_) { + return c.frozenSec || 0; + } + } + + function _formatCountUp(totalSec) { + if (typeof totalSec !== 'number' || !isFinite(totalSec) || totalSec < 0) { + return '00:00:00'; + } + var h = Math.floor(totalSec / 3600); + var m = Math.floor((totalSec % 3600) / 60); + var s = totalSec % 60; + return _pad2(h) + ':' + _pad2(m) + ':' + _pad2(s); + } + + function renderCountUpPanel() { + try { + var c = state.countUp || {}; + var el = $('countup-value'); + if (!el) return; + el.textContent = _formatCountUp(_calcCountUpElapsed()); + var btnStart = $('countup-start'); + var btnStop = $('countup-stop'); + var btnReset = $('countup-reset'); + var running = c.status === 'running'; + if (btnStart) btnStart.style.display = running ? 'none' : ''; + if (btnReset) btnReset.style.display = running ? 'none' : ''; + if (btnStop) btnStop.style.display = running ? '' : 'none'; + } catch (e) { + console.warn('countUp render failed:', e); + } + } + + function countUpStart() { + try { + var c = state.countUp; + if (c.status === 'running') return; + c.baseSec = 0; + c.startedAtMs = Date.now(); + c.status = 'running'; + renderCountUpPanel(); + } catch (e) { + console.warn('countUp start failed:', e); + } + } + + function countUpStop() { + try { + var c = state.countUp; + if (c.status !== 'running') return; + c.frozenSec = _calcCountUpElapsed(); + c.startedAtMs = null; + c.status = 'idle'; + renderCountUpPanel(); + } catch (e) { + console.warn('countUp stop failed:', e); + } + } + + function countUpReset() { + try { + var c = state.countUp; + c.frozenSec = 0; + c.startedAtMs = null; + c.status = 'idle'; + renderCountUpPanel(); + } catch (e) { + console.warn('countUp reset failed:', e); + } + } + + function setupCountUpControls() { + var btnStart = $('countup-start'); + var btnStop = $('countup-stop'); + var btnReset = $('countup-reset'); + if (btnStart) { + btnStart.addEventListener('click', function (e) { + e.stopPropagation(); + countUpStart(); + }); + } + if (btnStop) { + btnStop.addEventListener('click', function (e) { + e.stopPropagation(); + countUpStop(); + }); + } + if (btnReset) { + btnReset.addEventListener('click', function (e) { + e.stopPropagation(); + countUpReset(); + }); + } + } + function renderWeatherPanel() { var w = state.weather || {}; if (w.temp === undefined) { @@ -1621,6 +1728,7 @@ renderCountdownCard(); renderPomodoroCard(); if (state.activePanel === 'time') renderTimePanel(); + if (state.activePanel === 'time') renderCountUpPanel(); if (state.activePanel === 'pomodoro') renderPomodoro(); if (state.activePanel === 'countdown') renderCountdownPanel(); // Refresh update schedule info every 10s to keep next-update times current @@ -1648,5 +1756,13 @@ fetchState(); connectSSE(); switchPanel('time'); + setupCountUpControls(); + document.addEventListener('visibilitychange', function () { + try { + if (document.visibilityState === 'visible' && state.activePanel === 'time') { + renderCountUpPanel(); + } + } catch (_) {} + }); })(); diff --git a/frontend/templates/display.html b/frontend/templates/display.html index 30f7d5a..3e08594 100644 --- a/frontend/templates/display.html +++ b/frontend/templates/display.html @@ -76,6 +76,15 @@