You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

带轮动画间隔计时器:锁屏异常、重复进度圈及代码正确性问询

嘿,我来帮你搞定这几个问题,顺便把代码给你理顺~

问题拆解与代码修复

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 = '&ndash;';
                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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 14:42:42