当前位置: 首页 > news >正文

科技感网页计时器.html

一、科技感计网页时器应用介绍

        这是一个功能丰富、界面精美的网页计时器应用,集成了秒表和倒计时两大核心功能,具有以下特点:

1. 界面特色

        现代科技感设计 :采用深色/浅色双主题自适应,搭配优雅的蓝色渐变和发光效果
响应式布局 :完美适配从手机到桌面的各种设备尺寸
环形进度指示 :直观展示计时进度,增强视觉体验
精致UI元素 :包含精心设计的按钮、面板、标签和数字显示

2. 秒表功能

        高精度计时 :精确到厘秒(0.01秒)的计时显示
圈数记录 :支持记录和显示多个计时点,适合运动训练
会话保持 :可选择在页面刷新后保留计时状态
声音反馈 :可开启按键音效增强使用体验
震动反馈 :在支持的设备上提供触觉反馈

3. 倒计时功能

        自定义时间 :支持手动输入分钟和秒数
快捷预设 :内置多个常用时长预设(1分钟、5分钟、25分钟等)
一键调整 :可快速增减1分钟
结束提醒 :倒计时结束时提供声音和/或震动提醒
会话保持 :同样支持页面刷新后恢复倒计时状态

4. 实用特性

        键盘快捷键 :支持空格键(开始/暂停)、L键(记录圈数)、R键(重置)等快捷操作
模式切换 :可通过标签或数字键1/2快速切换秒表和倒计时模式
后台计时 :即使切换到其他标签页,计时器仍保持精确计时
无外部依赖 :所有功能(包括音效)均通过原生JavaScript实现,无需外部库
本地存储 :使用localStorage保存用户偏好设置和计时状态


二、具体代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
<title>科技感计时器 · 秒表 & 倒计时</title>
<meta name="color-scheme" content="dark light">
<style>:root{--bg: #0b0f1a;--panel: rgba(255,255,255,0.06);--panel-border: rgba(255,255,255,0.12);--text: #e8f0ff;--muted: #9fb3ff;--accent: #7aa2ff;--accent-2: #4df3ff;--good: #4ae3a0;--warn: #ffcf51;--bad:  #ff6b7a;--glow: 0 0 16px rgba(122,162,255,.55), 0 0 32px rgba(77,243,255,.35);--radius: 20px;}@media (prefers-color-scheme: light){:root{--bg: #f7fafc;--panel: rgba(10,20,60,0.06);--panel-border: rgba(10,20,60,0.1);--text: #142237;--muted: #3b5bcc;--accent: #385aff;--accent-2: #0abdc6;--glow: 0 0 16px rgba(56,90,255,.25), 0 0 32px rgba(10,189,198,.2);}}*{box-sizing:border-box}html,body{height:100%}body{margin:0; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI",Roboto, "PingFang SC","Hiragino Sans GB","Microsoft YaHei", Arial, "Noto Sans", sans-serif;background:radial-gradient(1200px 800px at 10% -20%, rgba(122,162,255,.20), transparent 60%),radial-gradient(1200px 800px at 110% 0%, rgba(77,243,255,.15), transparent 60%),linear-gradient(180deg, var(--bg), #060913 70%);color: var(--text);display:grid; place-items:center; padding:24px;}.app{width:min(980px,100%); display:grid; gap:20px;}.brand{display:flex; align-items:center; gap:12px; user-select:none;letter-spacing: .5px;}.logo{width:40px; height:40px; border-radius:12px; position:relative; isolation:isolate;background: linear-gradient(135deg, var(--accent), var(--accent-2));box-shadow: var(--glow);}.logo::after{content:""; position:absolute; inset:3px; border-radius:10px;background: radial-gradient(120% 120% at 20% 20%, rgba(255,255,255,.55), rgba(255,255,255,.05) 45%, transparent 60%);mix-blend-mode: screen; pointer-events:none;}.brand h1{ font-size:20px; margin:0; font-weight:700 }.brand .sub{ opacity:.7; font-size:12px }.panel{background: var(--panel);border: 1px solid var(--panel-border);border-radius: var(--radius);backdrop-filter: blur(10px) saturate(140%);-webkit-backdrop-filter: blur(10px) saturate(140%);box-shadow: 0 10px 30px rgba(0,0,0,.25);}/* 确保 hidden 属性生效 */[hidden] { display:none !important; }.tabs{ display:flex; gap:8px; padding:8px; }.tab{flex:1; text-align:center; padding:10px 14px; cursor:pointer; user-select:none;border-radius:14px; border:1px solid transparent; transition:.2s ease all;background: linear-gradient(180deg, transparent, rgba(255,255,255,.04));}.tab[aria-selected="true"]{border-color: rgba(122,162,255,.5);box-shadow: var(--glow);background:linear-gradient(180deg, rgba(122,162,255,.12), rgba(77,243,255,.10));}.timer{display:grid; grid-template-columns: 1.1fr .9fr; gap:20px; padding:20px;}@media (max-width: 840px){ .timer{ grid-template-columns: 1fr; } }.display{min-height: 280px; display:grid; place-items:center; position:relative; overflow:hidden;border-radius: var(--radius);}/* digital time */.digits{font-variant-numeric: tabular-nums;font-size: clamp(36px, 8vw, 72px);line-height:1;letter-spacing: 2px;text-shadow: 0 2px 16px rgba(0,0,0,.25);}.subdigits{opacity:.8; margin-top:8px; font-size: clamp(12px, 2.6vw, 16px);}/* circular progress container */.ring{position:absolute; inset:0; display:grid; place-items:center; pointer-events:none;}.ring .circle{width:min(440px, 80vw); aspect-ratio:1/1; border-radius:50%;background:radial-gradient(60% 60% at 50% 50%, rgba(255,255,255,.08), transparent 60%),conic-gradient(var(--accent) var(--p,0%), rgba(255,255,255,.08) 0);mask: radial-gradient(circle at 50% 50%, transparent 63%, black 64%);box-shadow: var(--glow);}.controls{display:grid; gap:12px; padding:16px; align-content:start;}.row{ display:flex; flex-wrap:wrap; gap:10px }button, .chip, .toggle{appearance:none; border:1px solid var(--panel-border);background: linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.02));color: var(--text);padding:10px 14px; border-radius:14px; cursor:pointer; transition:.15s ease all;}button:hover{ transform: translateY(-1px); box-shadow: 0 6px 20px rgba(0,0,0,.25), var(--glow) }button:active{ transform: translateY(0) scale(.99) }.primary{ border-color: color-mix(in oklab, var(--accent) 60%, transparent); }.good{ border-color: color-mix(in oklab, var(--good) 60%, transparent); }.warn{ border-color: color-mix(in oklab, var(--warn) 60%, transparent); }.bad{  border-color: color-mix(in oklab, var(--bad)  60%, transparent);  }.chip{ user-select:none }.chip[aria-pressed="true"]{outline: 1px solid var(--accent);box-shadow: var(--glow);}.grid{display:grid; grid-template-columns: repeat(3, 1fr); gap:10px;}@media (max-width: 520px){ .grid{ grid-template-columns: repeat(2, 1fr);} }.laps{padding:16px; max-height:260px; overflow:auto; border-top:1px dashed var(--panel-border);scrollbar-width: thin;}.laps .item{display:flex; justify-content:space-between; padding:8px 0; font-variant-numeric:tabular-nums;border-bottom:1px dashed rgba(255,255,255,.06);}.laps .item:last-child{ border-bottom:none }.hint{ opacity:.7; font-size:12px; padding-inline:16px; }.footer{display:flex; justify-content:space-between; align-items:center; padding:10px 16px;border-top:1px solid var(--panel-border); border-radius:0 0 var(--radius) var(--radius);font-size:12px; opacity:.8;}.kbd{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;padding:2px 6px; border-radius:6px; border:1px solid var(--panel-border); opacity:.9 }.sr-only{position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0;}/* input for countdown */.time-inputs{display:flex; gap:8px; align-items:center; justify-content:center; margin-top:8px;}.time-inputs input{width:80px; padding:10px 12px; border-radius:12px;background:rgba(255,255,255,.04); border:1px solid var(--panel-border); color:var(--text);text-align:center; font-variant-numeric:tabular-nums;}.time-inputs label{ font-size:12px; opacity:.8 }
</style>
</head>
<body><div class="app"><div class="brand"><div class="logo" aria-hidden="true"></div><div><h1>计时器 · Timer</h1><div class="sub">秒表 & 倒计时 · 科技感 · 响应式 · 键盘快捷键</div></div></div><!-- Tabs --><div class="panel tabs" role="tablist" aria-label="Mode"><button id="tabStopwatch" class="tab" role="tab" aria-selected="true" aria-controls="panelStopwatch">⏱ 秒表</button><button id="tabCountdown" class="tab" role="tab" aria-selected="false" aria-controls="panelCountdown">⏳ 倒计时</button></div><!-- Stopwatch --><section id="panelStopwatch" class="panel timer" role="tabpanel" aria-labelledby="tabStopwatch"><div class="display"><div class="ring"><div class="circle" id="swRing"></div></div><div style="text-align:center"><div class="digits" id="swDigits">00:00.00</div><div class="subdigits" id="swSub">累计 00:00.00</div></div></div><div class="controls"><div class="row"><button class="primary" id="swStart">▶ 开始</button><button id="swLap" class="good" disabled>⏺ 记录圈 (L)</button><button id="swReset" class="bad" disabled>↻ 重置</button></div><div class="row"><span class="chip" aria-pressed="true" id="persistToggle">🧠 会话保持</span><span class="chip" aria-pressed="true" id="soundToggle">🔔 结束提示</span><span class="chip" aria-pressed="false" id="vibrateToggle">📳 震动</span></div><div class="hint">快捷键:<span class="kbd">Space</span> 开始/暂停 · <span class="kbd">L</span> 记录圈 · <span class="kbd">R</span> 重置 · <span class="kbd">1</span>/<span class="kbd">2</span> 切换模式</div><div class="laps" id="laps" aria-live="polite" aria-label="Lap list"></div></div></section><!-- Countdown --><section id="panelCountdown" class="panel timer" role="tabpanel" aria-labelledby="tabCountdown" hidden><div class="display"><div class="ring"><div class="circle" id="cdRing" style="--p:0%"></div></div><div style="text-align:center"><div class="digits" id="cdDigits">00:00</div><div class="subdigits" id="cdSub">准备就绪</div><div class="time-inputs" aria-label="设定时间"><label for="minInput">分</label><input id="minInput" type="number" min="0" max="999" value="0" inputmode="numeric"><label for="secInput">秒</label><input id="secInput" type="number" min="0" max="59" value="30" inputmode="numeric"></div></div></div><div class="controls"><div class="row"><button class="primary" id="cdStart">▶ 开始</button><button id="cdAdd" class="good">+1 分</button><button id="cdSubMin" class="warn">-1 分</button><button id="cdReset" class="bad" disabled>↻ 重置</button></div><div class="grid"><button class="chip" data-preset="60">🍵 1 分钟</button><button class="chip" data-preset="300">☕ 5 分钟</button><button class="chip" data-preset="600">🧘 10 分钟</button><button class="chip" data-preset="1500">🍅 25 分钟</button><button class="chip" data-preset="1800">📚 30 分钟</button><button class="chip" data-preset="3600">⏲️ 60 分钟</button></div><div class="row"><span class="chip" aria-pressed="true" id="cdSoundToggle">🔔 结束提示</span><span class="chip" aria-pressed="false" id="cdVibrateToggle">📳 震动</span><span class="chip" aria-pressed="true" id="cdPersistToggle">🧠 会话保持</span></div><div class="hint">快捷键:<span class="kbd">Space</span> 开始/暂停 · <span class="kbd">↑/↓</span> ±1 分 · <span class="kbd">R</span> 重置 · <span class="kbd">1</span>/<span class="kbd">2</span> 切换模式</div></div></section><div class="panel footer"><div>💡 提示:切换到其它标签页也会保持计时精度(基于高精度时间戳计算)。</div><div>© <span id="year"></span> Timer UI</div></div></div><!-- 简单的提示音(WebAudio 动态合成,无需外链音频) --><audio id="beep" class="sr-only"></audio><script>
(function(){"use strict";// ========= 工具函数 =========const $ = sel => document.querySelector(sel);const $$ = sel => Array.from(document.querySelectorAll(sel));const fmt2 = n => n.toString().padStart(2,'0');const storage = {get(k, def){ try{ return JSON.parse(localStorage.getItem(k)) ?? def }catch{ return def } },set(k, v){ localStorage.setItem(k, JSON.stringify(v)); }};const now = () => performance.now();// ========= 年份 =========$('#year').textContent = new Date().getFullYear();// ========= 标签切换 =========const tabStopwatch = $('#tabStopwatch');const tabCountdown = $('#tabCountdown');const panelStopwatch = $('#panelStopwatch');const panelCountdown = $('#panelCountdown');function selectTab(which){const isSW = which === 'sw';tabStopwatch.setAttribute('aria-selected', String(isSW));tabCountdown.setAttribute('aria-selected', String(!isSW));panelStopwatch.hidden = !isSW;panelCountdown.hidden = isSW;// 记忆上次模式storage.set('timer:lastTab', which);}// 恢复上次模式selectTab(storage.get('timer:lastTab','sw'));tabStopwatch.addEventListener('click', () => selectTab('sw'));tabCountdown.addEventListener('click', () => selectTab('cd'));// ========= 提示音(动态合成一段叮铃) =========const audioCtx = new (window.AudioContext || window.webkitAudioContext || function(){})();function playBeep(){if(!audioCtx || audioCtx.state === 'suspended'){ try{ audioCtx.resume(); }catch{} }const o = audioCtx.createOscillator();const g = audioCtx.createGain();o.type = 'sine';const t0 = audioCtx.currentTime;o.frequency.setValueAtTime(880, t0);o.frequency.exponentialRampToValueAtTime(1760, t0 + 0.15);g.gain.setValueAtTime(0.001, t0);g.gain.exponentialRampToValueAtTime(0.3, t0 + 0.02);g.gain.exponentialRampToValueAtTime(0.001, t0 + 0.5);o.connect(g).connect(audioCtx.destination);o.start(); o.stop(t0 + 0.5);}function vibrate(ms=200){ if(navigator.vibrate) navigator.vibrate(ms); }// ========= 秒表 =========const swDigits = $('#swDigits');const swSub = $('#swSub');const swStart = $('#swStart');const swLap = $('#swLap');const swReset = $('#swReset');const swRing = $('#swRing');const lapsEl = $('#laps');let swRunning = false;let swStartTime = 0;    // 本轮开始时刻let swElapsed = 0;      // 已累计毫秒let swRAF = 0;let swLastLapAt = 0;let swPersist = storage.get('sw:persist', true);let swSoundOn = storage.get('sw:sound', true);let swVibrateOn = storage.get('sw:vibrate', false);const persistToggle = $('#persistToggle');const soundToggle = $('#soundToggle');const vibrateToggle = $('#vibrateToggle');function syncChip(el, on){ el.setAttribute('aria-pressed', String(on)); }syncChip(persistToggle, swPersist);syncChip(soundToggle, swSoundOn);syncChip(vibrateToggle, swVibrateOn);persistToggle.addEventListener('click', ()=>{ swPersist = !JSON.parse(persistToggle.getAttribute('aria-pressed')); syncChip(persistToggle, swPersist); storage.set('sw:persist', swPersist); });soundToggle.addEventListener('click', ()=>{ swSoundOn = !JSON.parse(soundToggle.getAttribute('aria-pressed')); syncChip(soundToggle, swSoundOn); storage.set('sw:sound', swSoundOn); });vibrateToggle.addEventListener('click', ()=>{ swVibrateOn = !JSON.parse(vibrateToggle.getAttribute('aria-pressed')); syncChip(vibrateToggle, swVibrateOn); storage.set('sw:vibrate', swVibrateOn); });function formatSW(ms){const total = Math.floor(ms);const mm = Math.floor(total/60000);const ss = Math.floor((total%60000)/1000);const cs = Math.floor((total%1000)/10); // 厘秒return `${fmt2(mm)}:${fmt2(ss)}.${fmt2(cs)}`;}function updateSWUI(){const elapsed = swRunning ? swElapsed + (now() - swStartTime) : swElapsed;swDigits.textContent = formatSW(elapsed);swSub.textContent = `累计 ${formatSW(elapsed)}`;// 秒表环按 3 分钟一圈示例const p = Math.min(100, (elapsed % (3*60*1000)) / (3*60*10));swRing.style.setProperty('--p', p + '%');}function swTick(){updateSWUI();swRAF = requestAnimationFrame(swTick);}function swStartPause(){if(!swRunning){swRunning = true;swStartTime = now();swLastLapAt = swLastLapAt || swElapsed;swStart.textContent = '⏸ 暂停';swLap.disabled = false;swReset.disabled = false;swRAF = requestAnimationFrame(swTick);}else{swRunning = false;swElapsed += now() - swStartTime;swStart.textContent = '▶ 继续';cancelAnimationFrame(swRAF);updateSWUI();}if(swPersist) storage.set('sw:state', {swRunning, swStartTime: swStartTime ? Date.now() - (now() - swStartTime) : 0, swElapsed, laps: readLaps()});}function swDoReset(){swRunning = false;swElapsed = 0;swStartTime = 0;swStart.textContent = '▶ 开始';swLap.disabled = true;swReset.disabled = true;cancelAnimationFrame(swRAF);lapsEl.innerHTML = '';swLastLapAt = 0;updateSWUI();if(swPersist) storage.set('sw:state', {swRunning, swStartTime:0, swElapsed:0, laps:[]});}function addLap(){const t = swRunning ? swElapsed + (now() - swStartTime) : swElapsed;const lapDur = t - swLastLapAt;swLastLapAt = t;const item = document.createElement('div');item.className = 'item';const idx = lapsEl.children.length + 1;item.innerHTML = `<span>#${idx}</span><span>${formatSW(lapDur)}</span><span>${formatSW(t)}</span>`;lapsEl.prepend(item);if(swPersist) storage.set('sw:state', {swRunning, swStartTime: swStartTime ? Date.now() - (now() - swStartTime) : 0, swElapsed, laps: readLaps()});// 节奏反馈if(swSoundOn) playBeep();if(swVibrateOn) vibrate(30);}function readLaps(){return Array.from(lapsEl.children).reverse().map(node=>{const spans = node.querySelectorAll('span');return { idx: spans[0].textContent, lap: spans[1].textContent, total: spans[2].textContent };});}function restoreSW(){const s = storage.get('sw:state', null);if(!s) return;swElapsed = s.swElapsed || 0;lapsEl.innerHTML = '';(s.laps || []).forEach(rec=>{const item = document.createElement('div'); item.className='item';item.innerHTML = `<span>${rec.idx}</span><span>${rec.lap}</span><span>${rec.total}</span>`;lapsEl.appendChild(item);const [m,sec,cs] = rec.total.split(/[:.]/).map(Number);swLastLapAt = Math.max(swLastLapAt, (m*60+sec)*1000 + cs*10);});if(s.swRunning){// 依据保存时的 wall clock 恢复const startedAtWall = s.swStartTime || 0;if(startedAtWall){const delta = Date.now() - startedAtWall;swStartTime = now() - delta;swRunning = true;swStart.textContent = '⏸ 暂停';swLap.disabled = false; swReset.disabled = false;swRAF = requestAnimationFrame(swTick);}}else{updateSWUI();if(swElapsed>0){ swReset.disabled = false; }}}restoreSW();swStart.addEventListener('click', swStartPause);swReset.addEventListener('click', swDoReset);swLap.addEventListener('click', ()=>{ if(!swRunning) return; addLap(); });// ========= 倒计时 =========const cdDigits = $('#cdDigits');const cdSub    = $('#cdSub');const cdRing   = $('#cdRing');const minInput = $('#minInput');const secInput = $('#secInput');const cdStart  = $('#cdStart');const cdAdd    = $('#cdAdd');const cdSubMin = $('#cdSubMin');const cdReset  = $('#cdReset');let cdTotal = 30_000; // mslet cdRemain = cdTotal;let cdRunning = false;let cdStartWall = 0;let cdRAF = 0;let cdPersist = storage.get('cd:persist', true);let cdSoundOn = storage.get('cd:sound', true);let cdVibrateOn = storage.get('cd:vibrate', false);const cdPersistToggle = $('#cdPersistToggle');const cdSoundToggle = $('#cdSoundToggle');const cdVibrateToggle = $('#cdVibrateToggle');syncChip(cdPersistToggle, cdPersist);syncChip(cdSoundToggle, cdSoundOn);syncChip(cdVibrateToggle, cdVibrateOn);cdPersistToggle.addEventListener('click', ()=>{ cdPersist = !JSON.parse(cdPersistToggle.getAttribute('aria-pressed')); syncChip(cdPersistToggle, cdPersist); storage.set('cd:persist', cdPersist); });cdSoundToggle.addEventListener('click', ()=>{ cdSoundOn = !JSON.parse(cdSoundToggle.getAttribute('aria-pressed')); syncChip(cdSoundToggle, cdSoundOn); storage.set('cd:sound', cdSoundOn); });cdVibrateToggle.addEventListener('click', ()=>{ cdVibrateOn = !JSON.parse(cdVibrateToggle.getAttribute('aria-pressed')); syncChip(cdVibrateToggle, cdVibrateOn); storage.set('cd:vibrate', cdVibrateOn); });function setFromInputs(){const m = Math.max(0, Math.min(999, parseInt(minInput.value||'0',10)));const s = Math.max(0, Math.min(59, parseInt(secInput.value||'0',10)));cdTotal = (m*60 + s) * 1000;cdRemain = cdTotal;updateCDUI();}minInput.addEventListener('change', setFromInputs);secInput.addEventListener('change', setFromInputs);function formatCD(ms){ms = Math.max(0, Math.floor(ms));const mm = Math.floor(ms/60000);const ss = Math.floor((ms%60000)/1000);return `${fmt2(mm)}:${fmt2(ss)}`;}function updateCDUI(){cdDigits.textContent = formatCD(cdRemain);cdSub.textContent = cdRunning ? '计时中…' : (cdRemain===0 ? '已结束' : '准备就绪');const p = cdTotal === 0 ? 0 : (100 - (cdRemain / cdTotal) * 100);cdRing.style.setProperty('--p', `${p}%`);cdReset.disabled = cdRemain === cdTotal && !cdRunning;}function cdTick(){const elapsed = Date.now() - cdStartWall;cdRemain = Math.max(0, cdTotal - elapsed);updateCDUI();if(cdRemain === 0){cdRunning = false;cdStart.textContent = '▶ 重新开始';cancelAnimationFrame(cdRAF);if(cdSoundOn) playBeep();if(cdVibrateOn) vibrate(400);return;}cdRAF = requestAnimationFrame(cdTick);}function cdStartPause(){if(cdTotal === 0 && !cdRunning){ return; }if(!cdRunning){// 如果是暂停后继续,cdTotal 应改为 remainif(cdRemain !== cdTotal){ cdTotal = cdRemain; }cdRunning = true;cdStartWall = Date.now();cdStart.textContent = '⏸ 暂停';cdRAF = requestAnimationFrame(cdTick);}else{// 暂停:固化剩余cdRunning = false;cdRemain = Math.max(0, cdTotal - (Date.now() - cdStartWall));cdStart.textContent = '▶ 继续';cancelAnimationFrame(cdRAF);updateCDUI();}if(cdPersist) storage.set('cd:state', {cdRunning, cdTotal, cdRemain, startedAt: cdStartWall});}function cdDoReset(){cdRunning = false; cancelAnimationFrame(cdRAF);cdRemain = cdTotal = Math.max(0, cdTotal); // 保持设定值cdStart.textContent = '▶ 开始';updateCDUI();if(cdPersist) storage.set('cd:state', {cdRunning:false, cdTotal, cdRemain:cdTotal, startedAt:0});}function setPreset(sec){cdRunning = false; cancelAnimationFrame(cdRAF);cdRemain = cdTotal = sec*1000;const mm = Math.floor(sec/60), ss = sec%60;minInput.value = mm; secInput.value = ss;cdStart.textContent = '▶ 开始';updateCDUI();if(cdPersist) storage.set('cd:state', {cdRunning:false, cdTotal, cdRemain:cdTotal, startedAt:0});}// 按钮事件cdStart.addEventListener('click', cdStartPause);cdReset.addEventListener('click', cdDoReset);cdAdd.addEventListener('click', ()=> setPreset(Math.floor(cdTotal/1000) + 60));cdSubMin.addEventListener('click', ()=> setPreset(Math.max(0, Math.floor(cdTotal/1000) - 60)));$$('#panelCountdown .grid .chip').forEach(btn=>{btn.addEventListener('click', ()=> setPreset(parseInt(btn.dataset.preset,10)));});// 初始同步 UI(function initCD(){const saved = storage.get('cd:state', null);if(saved){cdTotal = saved.cdTotal || 0;cdRemain = saved.cdRemain ?? cdTotal;if(saved.cdRunning){// 恢复运行中:根据 wall clock 计算const elapsed = Date.now() - (saved.startedAt || Date.now());cdRemain = Math.max(0, cdTotal - elapsed);cdRunning = cdRemain > 0;if(cdRunning){cdStart.textContent = '⏸ 暂停';cdStartWall = Date.now() - Math.min(cdTotal, elapsed);cdRAF = requestAnimationFrame(cdTick);}else{cdStart.textContent = '▶ 重新开始';}}minInput.value = Math.floor(cdTotal/60000);secInput.value = Math.floor((cdTotal%60000)/1000);}else{minInput.value = 0; secInput.value = 30;setFromInputs();}updateCDUI();})();// ========= 全局键盘快捷键 =========window.addEventListener('keydown', (e)=>{if (['INPUT','TEXTAREA'].includes(document.activeElement.tagName)) return;if(e.code === 'Space'){ e.preventDefault();if(!panelStopwatch.hidden) swStartPause(); else cdStartPause();}else if(e.key==='l' || e.key==='L'){if(!panelStopwatch.hidden && swRunning) addLap();}else if(e.key==='r' || e.key==='R'){if(!panelStopwatch.hidden) swDoReset(); else cdDoReset();}else if(e.key==='1'){ selectTab('sw'); }else if(e.key==='2'){ selectTab('cd'); }else if(e.key==='ArrowUp' && !panelCountdown.hidden){ setPreset(Math.floor(cdTotal/1000)+60); }else if(e.key==='ArrowDown' && !panelCountdown.hidden){ setPreset(Math.max(0, Math.floor(cdTotal/1000)-60)); }});// ========= 页面可见性处理:保持精度 =========document.addEventListener('visibilitychange', ()=>{// 秒表:切后台不做特殊处理,因基于 high-res 时间戳// 倒计时:切前台时强制刷新剩余if(!panelCountdown.hidden && cdRunning){cdRemain = Math.max(0, cdTotal - (Date.now() - cdStartWall));updateCDUI();}});
})();
</script>
</body>
</html>

http://www.dtcms.com/a/357985.html

相关文章:

  • 设计模式:抽象工厂模式(Abstract Factory Pattern)
  • 在word以及latex中引用zotero中的参考文献
  • 单例模式的mock类注入单元测试与友元类解决方案
  • 云存储(参考自腾讯云计算工程师认证)
  • Twitter舆情裂变链:指纹云手机跨账号协同机制提升互动率200%
  • 使用电脑操作Android11手机,连接步骤
  • 【序列晋升】21 Spring Cloud Gateway 云原生网关演进之路
  • DVWA靶场通关笔记-CSRF(Impossible级别)
  • 【90页PPT】新能源汽车数字化转型SAP解决方案(附下载方式)
  • 汽车加气站操作工证考试的复习重点是什么?
  • 【自然语言处理与大模型】多机多卡分布式微调训练的有哪些方式
  • C++ constexpr:编译时计算的高效秘籍
  • 复现论文块体不锈钢上的光栅耦合表面等离子体共振
  • 10.2 工程学中的矩阵
  • hadoop安欣医院挂号看诊管理系统(代码+数据库+LW)
  • 使用 Ansible 和 Azure Pipelines 增强您的 DevOps
  • Midjourney绘画创作入门操作创作(广告创意与设计)
  • 腾讯云centos7.6的运维笔记——从yum的安装与更新源开始
  • C++ 之 【map和set的模拟实现】(只涉及map和set的插入、迭代器以及map的operator[]函数)
  • Altium Designer中电路板设计
  • 流式HTTP MCP服务器开发
  • Android中handler机制
  • 《RANKGUESS: Password Guessing Using Adversarial Ranking》——论文解读
  • 主从DNS和Web服务器搭建过程
  • Windows系统提示“找不到文件‘javaw‘”
  • 【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day16
  • Azure DevOps cherry pick
  • 基于IEC61499开放自动化PLC数据储存方案
  • Python 多线程日志错乱:logging.Handler 的并发问题
  • 42-Ansible-Inventory