番茄钟专注助手 · Pomodoro Focus

举报
yd_217436577 发表于 2026/10/22 23:09:38 2026/10/22
【摘要】 一个基于 React + Vite 的番茄工作法计时应用,帮助你提高专注力。

用 React + Vite 从零打造番茄钟专注助手 · Pomodoro Focus

发布日期:2026-10-22
仓库地址:https://gitcode.com/sdf2342/pomodoro-focus
技术栈:React 18 + Vite 5 + Web Audio API + localStorage

前言

番茄工作法(Pomodoro Technique)是一种经典的时间管理方法:25 分钟专注、5 分钟休息、每 4 轮来一次 15 分钟长休息。市面上番茄钟 App 不少,但大多要么功能臃肿、要么不能离线使用。于是我决定自己写一个——纯前端、零依赖、开箱即用的 Pomodoro Focus。

这篇文章记录了整个项目的设计思路和关键实现,希望能给同样想写小工具的你一些参考。

项目亮点

先看最终实现了哪些功能:

功能 实现方式
番茄钟计时(25/5/15 分钟) React Hooks + setInterval
SVG 圆形进度条 环形动画 + 倒计时数字居中
专注/休息自动切换 每完成 4 轮专注触发长休息
任务管理 添加/选择/删除任务,累计番茄数
完成统计 今日番茄数 + 总专注时长
桌面通知 Notification API
白噪音(雨声/海浪/森林) Web Audio API 实时合成
主题切换 专注深色沉浸 / 休息浅色轻松
数据持久化 localStorage
响应式设计 移动端适配

架构设计

项目采用 Hooks 抽离 + 组件组合 的架构,核心逻辑全部放在自定义 Hooks 中,组件只负责渲染:

src/
├── main.jsx                # 入口
├── App.jsx                 # 主应用(状态编排)
├── App.css                 # 样式(主题切换+响应式)
├── constants.js            # 常量定义
├── hooks/
│   ├── usePomodoro.js      # 番茄钟核心逻辑
│   ├── useWhiteNoise.js    # 白噪音生成
│   └── useLocalStorage.js  # 本地持久化
└── components/
    ├── TimerCircle.jsx     # 圆形进度条
    ├── ModeTabs.jsx       # 模式切换
    ├── Controls.jsx       # 控制按钮
    ├── TaskPanel.jsx      # 任务面板
    ├── StatsPanel.jsx     # 统计面板
    └── WhiteNoise.jsx     # 白噪音切换

这样做的好处是:每个 Hook 职责单一、可独立测试,组件层非常薄,后续替换 UI 框架(比如换成 Vue)也只需改组件层。

核心实现解析

1. 番茄钟计时引擎:usePomodoro

这是整个应用的心脏。它管理四个状态:当前模式(专注/短休/长休)、剩余秒数、是否运行、已完成专注轮数。

export function usePomodoro({ onCycleComplete, currentTaskId, onTaskPomodoro }) {
  const [mode, setMode] = useState('focus')
  const [timeLeft, setTimeLeft] = useState(MODES.focus.duration) // 25 * 60 = 1500
  const [isRunning, setIsRunning] = useState(false)
  const [completedFocusCount, setCompletedFocusCount] = useState(0)
  // ...
}

自动切换逻辑是关键:专注结束后,根据已完成轮数判断下一个模式——每 4 轮触发一次长休息:

if (mode === 'focus') {
  const newCount = completedFocusCount + 1
  setCompletedFocusCount(newCount)
  // 每 4 轮专注后进入长休息,否则短休息
  const nextMode = newCount % 4 === 0 ? 'long' : 'short'
  setMode(nextMode)
  setTimeLeft(MODES[nextMode].duration)
}

计时器使用 useEffect + setInterval,每秒减 1。当 timeLeft 归零时,通过另一个 useEffect 监听并触发 handleComplete,实现自动切换:

useEffect(() => {
  if (timeLeft === 0 && !isRunning) {
    handleComplete()
  }
}, [timeLeft, isRunning, handleComplete])

设计要点:把"计时"和"完成处理"拆成两个 Effect,是为了避免在 setInterval 回调中直接处理完成逻辑导致状态更新竞态。先让 timeLeft 归零,再由监听器统一处理,干净利落。

2. 白噪音合成:useWhiteNoise

这是整个项目最有意思的部分。没有引入任何音频文件,而是用 Web Audio API 实时合成 三种白噪音:雨声、海浪、森林。

以雨声为例,核心是用 BiquadFilter 滤波器把白噪声"染"成雨声的频谱特征:

// 雨声 = 白噪声 → 高通 100Hz → 低通 800Hz
const lp = ctx.createBiquadFilter()
lp.type = 'lowpass'
lp.frequency.value = 800

const hp = ctx.createBiquadFilter()
hp.type = 'highpass'
hp.frequency.value = 100

source.connect(hp)
hp.connect(lp)
lp.connect(gain)
gain.connect(ctx.destination)

海浪声则在滤波基础上叠加了一个低频振荡器(LFO),模拟海浪的起伏感:

// 海浪 = 滤波后白噪声 × LFO 振幅调制
lfo = ctx.createOscillator()
lfo.frequency.value = 0.15  // 每 ~7 秒一个浪
lfoGain = ctx.createGain()
lfo.connect(lfoGain)
lfoGain.connect(gain.gain)   // 调制音量
lfo.start()

森林声用 bandpass 滤波 + 更高频的 LFO(0.3Hz),听起来像风穿过树叶。

为什么不用音频文件? 三个原因:(1) 零网络请求,离线可用;(2) 仓库体积小,没有二进制文件;(3) 音量可以精确控制,不会有文件解码的延迟。代价是浏览器兼容性——但现代浏览器都支持 Web Audio API,问题不大。

3. 数据持久化:useLocalStorage

一个通用的 localStorage Hook,封装了序列化/反序列化和跨标签页同步:

export function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key)
    return stored ? JSON.parse(stored) : initialValue
  })

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value))
  }, [key, value])

  return [value, setValue]
}

App.jsx 中用它保存三份数据:任务列表、当前选中任务、统计数据(按日期键值存储今日番茄数和总专注分钟)。

4. 桌面通知

每个番茄周期结束时,通过 Notification API 推送通知:

if ('Notification' in window && Notification.permission === 'granted') {
  const title = mode === 'focus' ? '专注完成!🎉' : '休息结束!💪'
  new Notification(title, { body: '...', icon: '/favicon.svg' })
}

应用启动时自动请求权限,用户授权后就能在浏览器后台收到提醒。

5. 主题切换

专注模式使用深色背景(沉浸感),休息模式切换为浅色背景(放松感)。通过在根 div 上切换 CSS class 实现,配合 CSS transition 平滑过渡:

<div className={`app ${isBreakMode ? 'theme-break' : 'theme-focus'}`}>

技术选型思考

为什么选 React + Vite?

  • Vite 的冷启动 < 1 秒,HMR 几乎瞬时,开发体验远好于 CRA/Webpack
  • React 18 的 Hooks 模型非常适合这种状态驱动的应用
  • 整个项目只有两个依赖(react + react-dom),极简

为什么不用 Redux / Zustand?

这个应用的状态很简单,全部可以用 useState + 自定义 Hooks 搞定。引入状态管理库是过度设计。

为什么不用 Tailwind?

项目总共就一个 App.css,手写 CSS 更直接,也方便做主题切换的 CSS 变量。Tailwind 对于这种单页小工具来说,配置成本大于收益。

项目结构一览

pomodoro-focus/
├── index.html
├── package.json          # 仅 react + react-dom 两个依赖
├── vite.config.js
└── src/
    ├── main.jsx          # React 挂载入口
    ├── App.jsx           # 主应用:状态编排 + 组件组合
    ├── App.css           # 全部样式(含主题 + 响应式)
    ├── constants.js      # 模式定义:focus 25min / short 5min / long 15min
    ├── hooks/
    │   ├── usePomodoro.js     # 计时引擎
    │   ├── useWhiteNoise.js   # 白噪音合成
    │   └── useLocalStorage.js # 本地存储
    └── components/
        ├── TimerCircle.jsx    # SVG 圆形进度条
        ├── ModeTabs.jsx       # 专注/短休/长休切换
        ├── Controls.jsx       # 开始/暂停/重置
        ├── TaskPanel.jsx      # 任务列表
        ├── StatsPanel.jsx     # 今日统计
        └── WhiteNoise.jsx     # 白噪音面板

快速开始

# 克隆仓库
git clone https://gitcode.com/sdf2342/pomodoro-focus.git
cd pomodoro-focus

# 安装依赖
npm install

# 启动开发服务器
npm run dev
# → http://localhost:5173

# 构建生产版本
npm run build

总结与反思

这个项目虽然不大,但有几个点我觉得值得分享:

  1. Hooks 抽离逻辑 让组件保持纯粹,后续维护和测试都很舒服
  2. Web Audio API 合成白噪音 是个很实用的技巧,零依赖实现环境音
  3. 专注/休息主题切换 看起来是个小细节,但实际使用时对心理暗示的效果很明显
  4. localStorage 按日期存储统计 简单有效,不需要后端就能有"今日数据"的概念

如果你也在找一个简洁、离线可用的番茄钟,或者想学习 React Hooks 的实际应用,欢迎 Star 和 Fork:

👉 仓库地址:https://gitcode.com/sdf2342/pomodoro-focus


本文由 coolfier 发布于 GitCode,转载请注明出处。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。