番茄钟专注助手 · Pomodoro Focus
用 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
总结与反思
这个项目虽然不大,但有几个点我觉得值得分享:
- Hooks 抽离逻辑 让组件保持纯粹,后续维护和测试都很舒服
- Web Audio API 合成白噪音 是个很实用的技巧,零依赖实现环境音
- 专注/休息主题切换 看起来是个小细节,但实际使用时对心理暗示的效果很明显
- localStorage 按日期存储统计 简单有效,不需要后端就能有"今日数据"的概念
如果你也在找一个简洁、离线可用的番茄钟,或者想学习 React Hooks 的实际应用,欢迎 Star 和 Fork:
👉 仓库地址:https://gitcode.com/sdf2342/pomodoro-focus
本文由 coolfier 发布于 GitCode,转载请注明出处。
- 点赞
- 收藏
- 关注作者
评论(0)