带轮动画间隔计时器:锁屏异常、重复进度圈及代码正确性问询
嘿,我来帮你搞定这几个问题,顺便把代码给你理顺~
问题拆解与代码修复
1. 手机锁屏后计时器异常的原因
手机锁屏后,浏览器会把页面切到后台,定时器(setInterval/setTimeout)会被系统节流甚至暂停,导致你的计时逻辑彻底乱掉。你虽然用了new Date().getTime()计算剩余时间,但外层循环的定时器(countDownI)被暂停后,整个循环节奏会偏离预期,等回到前台时,时间差会突然拉大,就出现了负数或跳变的情况。
解决核心:不要依赖外层定时器来控制循环,而是每完成一个阶段(工作/休息)后,再决定是否进入下一个阶段,同时始终用当前时间和阶段开始时间的差值计算剩余时间,这样即使后台暂停,回到前台也能立刻算出正确剩余值。
2. 最小Circle随重复次数缩减进度的实现
你需要跟踪已完成的重复次数,用「已完成次数/总重复次数」的百分比来更新最小Circle(circle3)的进度。比如总重复3次,完成1次就把进度设为33%,完成2次设为66%,全部完成设为100%,这样就能实现逐步缩减的效果。
3. 当前代码的问题与完整修复版
你的代码存在几个关键问题:变量作用域混乱、提前设置的结束定时器时间不准、缺少重复次数跟踪、负数处理不严谨。下面是修正后的完整代码,我会标注关键修改点:
修正后的代码
<!DOCTYPE html> <html> <head> <style> svg circle { fill: none; stroke: #f90; stroke-width: 21; stroke-linecap: round; transition: 0.3s; /* 缩短过渡时间,让进度变化更顺滑 */ transform: rotate(-90deg); transform-origin: center center; } text { font-size: 74px; font-family: sans-serif; fill: #333; } </style> </head> <body> <svg width="233" height="233"> <circle cx="50%" cy="50%" r="calc(50% - 10.5px)"/> <circle cx="50%" cy="50%" r="calc(50% - 34.5px)"/> <circle cx="50%" cy="50%" r="calc(50% - 58.5px)"/> <text x="50%" y="50%" text-anchor="middle" alignment-baseline="middle"></text> </svg> <p>Work</p> <input type="number" id="work-seconds" placeholder="seconds" min="1" value="25"> <!-- 设min为1避免无效输入 --> <br> <p>Break</p> <input type="number" id="break-seconds" placeholder="break" min="0" value="5"> <br> <p>Repeat</p> <input type="number" id="repeat" placeholder="repeat" min="0" value="3"> <br> <br> <button id="btn">START</button> <script> // 元素获取与初始化 const circle1 = document.querySelectorAll('circle')[0]; const circle2 = document.querySelectorAll('circle')[1]; const circle3 = document.querySelectorAll('circle')[2]; const circumference1 = circle1.getTotalLength(); const circumference2 = circle2.getTotalLength(); const circumference3 = circle3.getTotalLength(); const showSec = document.querySelector('text'); const btn = document.getElementById('btn'); circle1.style.strokeDasharray = circumference1; circle2.style.strokeDasharray = circumference2; circle3.style.strokeDasharray = circumference3; // 初始进度设置 setProgress1(100); setProgress2(100); setProgress3(0); // 全局状态变量 let currentTimer = null; let completedRepeats = 0; let totalRepeats = 0; let workSec = 0; let breakSec = 0; // 进度更新函数 function setProgress1(percent) { const offset = circumference1 - (percent / 100) * circumference1; circle1.style.strokeDashoffset = offset; } function setProgress2(percent) { const offset = circumference2 - (percent / 100) * circumference2; circle2.style.strokeDashoffset = offset; } function setProgress3(percent) { const offset = circumference3 - (percent / 100) * circumference3; circle3.style.strokeDashoffset = offset; } // 开始按钮事件 btn.addEventListener('click', function(){ // 清除之前的定时器 if(currentTimer) clearInterval(currentTimer); // 获取并校验输入值 workSec = Math.max(1, Math.ceil(document.getElementById('work-seconds').value) || 1); breakSec = Math.max(0, Math.ceil(document.getElementById('break-seconds').value) || 0); totalRepeats = Math.max(0, Math.ceil(document.getElementById('repeat').value) || 0); completedRepeats = 0; // 重置所有进度 setProgress1(100); setProgress2(100); setProgress3(0); // 启动第一个工作阶段 startWork(); }); // 工作阶段逻辑 function startWork() { const endTime = Date.now() + workSec * 1000; // 更新重复进度(如果设置了重复次数) if(totalRepeats > 0) { setProgress3((completedRepeats / totalRepeats) * 100); } currentTimer = setInterval(() => { // 计算剩余时间,确保不会出现负数 const remaining = Math.max(0, Math.ceil((endTime - Date.now()) / 1000)); updateDisplay(remaining); setProgress1((remaining / workSec) * 100); if(remaining <= 0) { clearInterval(currentTimer); setProgress1(0); // 延迟1秒后切换阶段 setTimeout(() => { setProgress1(100); if(breakSec > 0) { setTimeout(startBreak, 1000); } else { finishRepeat(); } }, 1000); } }, 1000); } // 休息阶段逻辑 function startBreak() { const endTime = Date.now() + breakSec * 1000; currentTimer = setInterval(() => { const remaining = Math.max(0, Math.ceil((endTime - Date.now()) / 1000)); updateDisplay(remaining); setProgress2((remaining / breakSec) * 100); if(remaining <= 0) { clearInterval(currentTimer); setProgress2(0); setTimeout(() => { setProgress2(100); finishRepeat(); }, 1000); } }, 1000); } // 完成一轮后的逻辑 function finishRepeat() { completedRepeats++; // 检查是否完成所有重复 if(totalRepeats > 0 && completedRepeats >= totalRepeats) { setProgress3(100); showSec.innerHTML = '–'; return; } // 继续下一轮 startWork(); } // 更新倒计时显示 function updateDisplay(seconds) { const displayText = seconds < 10 ? `0${seconds}` : `${seconds}`; showSec.textContent = displayText; showSec.style.fontSize = `calc(74px / ${displayText.length})`; } </script> </body> </html>
关键修改点说明
- 用
const/let替代var,彻底解决变量作用域混乱的问题 - 拆分工作/休息阶段为独立函数,逻辑更清晰,也避免了嵌套过深的问题
- 增加了重复次数跟踪,每次完成一轮就更新最小Circle的进度
- 所有倒计时计算都用
Date.now(),即使后台暂停,回到前台也能立刻算出正确剩余时间,且用Math.max(0, ...)避免负数 - 移除了提前设置的结束定时器,改成每轮结束后检查是否完成所有重复,逻辑更可靠
- 输入值做了有效性校验,避免0或负数导致的异常
现在你可以测试一下:
- 锁屏后再打开,倒计时会立刻显示正确的剩余时间
- 最小的Circle会随着重复次数逐步填满
- 各种输入组合(仅工作、工作+休息、工作+休息+重复)都能正常循环
内容的提问来源于stack exchange,提问作者test
相关产品推荐
相关产品推荐

