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

React中页面刷新/关闭后仍运行的Boss倒计时计时器问题

React Boss倒计时计时器修复方案

问题现状

  • 页面刷新后,计时器冻结在刷新瞬间的时间点,无法继续倒计时
  • 其他设备访问站点时,计时器重置为代码设置的默认时间
  • 使用localStorage存储计时数据后,数据虽能保留,但倒计时功能停止工作

问题根源

  1. 刷新后定时器未自动启动:原代码中isRunning默认值为false,刷新后不会自动触发定时器,导致倒计时中断。
  2. 存储逻辑错误:直接存储剩余秒数而非目标结束时间戳,刷新后无法计算当前时刻到结束时刻的真实剩余时间,导致时间偏差。
  3. 跨设备同步限制:localStorage是本地浏览器存储,无法在多设备间共享数据,因此其他设备访问会使用默认时间(此问题需后端支持,本文先解决本地持久化问题)。

修复后的代码

import React, { useState, useEffect } from "react";
import "./App.css";

const BossTimer = ({ boss }) => {
  const { name, race, elements, bossRespawnTime, image, raceImage } = boss;
  const timerKey = `timer_end_${name}`;

  // 初始化剩余时间和运行状态
  const [remainingTime, setRemainingTime] = useState(() => {
    const storedEndTime = parseInt(localStorage.getItem(timerKey));
    if (storedEndTime) {
      // 计算当前到结束时间的剩余秒数,最小为0
      const diff = Math.floor((storedEndTime - Date.now()) / 1000);
      return Math.max(diff, 0);
    }
    // 无存储数据则用默认时间
    return bossRespawnTime;
  });
  const [isRunning, setIsRunning] = useState(() => {
    const storedEndTime = parseInt(localStorage.getItem(timerKey));
    // 如果有存储的结束时间且未到点,自动启动
    return storedEndTime && storedEndTime > Date.now();
  });

  useEffect(() => {
    let interval;
    if (isRunning && remainingTime > 0) {
      interval = setInterval(() => {
        setRemainingTime(prev => {
          const newTime = prev - 1;
          // 更新存储的结束时间戳
          const newEndTime = Date.now() + newTime * 1000;
          localStorage.setItem(timerKey, newEndTime.toString());
          return newTime;
        });
      }, 1000);
    } else if (remainingTime <= 0) {
      // 倒计时结束,停止并清除存储
      setIsRunning(false);
      localStorage.removeItem(timerKey);
    }

    return () => clearInterval(interval);
  }, [isRunning, remainingTime, timerKey]);

  const startTimer = () => {
    // 启动时设置结束时间戳
    const endTime = Date.now() + bossRespawnTime * 1000;
    localStorage.setItem(timerKey, endTime.toString());
    setRemainingTime(bossRespawnTime);
    setIsRunning(true);
  };

  const resetTimer = () => {
    setIsRunning(false);
    setRemainingTime(bossRespawnTime);
    localStorage.removeItem(timerKey);
  };

  const formatTime = (timeInSeconds) => {
    const hours = Math.floor(Math.abs(timeInSeconds) / 3600);
    const minutes = Math.floor((Math.abs(timeInSeconds) % 3600) / 60);
    const seconds = Math.abs(timeInSeconds) % 60;

    return `${timeInSeconds < 0 ? "-" : ""}${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
  };

  return (
    <tr>
      <td>
        <div className="boss-info">
          <img src={image} alt={`${name} 头像`} width="50" height="50" />
          <span className="boss-name">{name}</span>
        </div>
      </td>
      <td>
        <img src={raceImage} alt={`${race} 种族图标`} width="30" height="30" />
        <span className="boss-race">{race}</span>
      </td>
      <td>
        <div className="element-image">
          {elements.map((element, index) => (
            <img
              key={index}
              src={element}
              alt={`属性图标 ${index + 1}`}
              width="20"
              height="20"
            />
          ))}
        </div>
      </td>
      <td>
        <div className="timer">
          <span>{formatTime(remainingTime)}</span>
          <div className="timer-buttons">
            <button onClick={startTimer} disabled={isRunning}>Start</button>
            <button onClick={resetTimer}>Restart</button>
          </div>
        </div>
      </td>
    </tr>
  );
};

function App() {
  const bosses = [
    {
      name: "Архон",
      bossRespawnTime: 4 * 60 * 60 + 20 * 60,
      image: "URL_OF_IMAGE_1",
      race: "Undead",
      elements: ["URL_OF_ELEMENT_2_1"],
      raceImage: "URL_OF_RACE_IMAGE_2"
    },
    {
      name: "Boss 2",
      bossRespawnTime: 3 * 60 * 60 + 15 * 60,
      image: "URL_OF_IMAGE_2",
      race: "Undead",
      elements: ["URL_OF_ELEMENT_2_1"],
      raceImage: "URL_OF_RACE_IMAGE_2"
    },
    // 可添加更多Boss数据
  ];

  return (
    <div className="App">
      <h1>Boss 倒计时</h1>
      <table>
        <thead>
          <tr>
            <th>Boss信息</th>
            <th>种族</th>
            <th>属性</th>
            <th>计时器/操作</th>
          </tr>
        </thead>
        <tbody>
          {bosses.map((boss, index) => (
            <BossTimer key={index} boss={boss} />
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default App;

关键修改说明

  1. 存储结束时间戳:不再存储剩余秒数,改为存储倒计时结束的时间戳(Date.now() + 剩余秒数*1000),刷新后可通过当前时间与结束时间的差值计算真实剩余时间。
  2. 自动启动定时器:初始化时判断存储的结束时间是否未到期,自动设置isRunning为true,无需手动点击Start按钮。
  3. 倒计时结束处理:当剩余时间≤0时,自动停止定时器并清除localStorage数据。
  4. 修复语法错误:修正了原代码中bosses数组内的多余逗号。

跨设备同步说明

如果需要实现多设备间计时器同步,仅靠前端localStorage无法完成,需要后端存储每个Boss的倒计时结束时间,前端从后端拉取最新数据并同步更新,同时前端操作(启动/重置)也要通知后端更新数据。

内容的提问来源于stack exchange,提问作者Irakli Gvelesiani

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 23:39:53