资讯动态

番茄时钟小程序源码拆解:状态机与时间戳让计时不再漂移

发布时间:2026/9/14 20:11:20 来源:尧图企业网站定制
简介番茄时钟小程序源码实现基于番茄工作法的专注计时功能适合小程序开发者、移动开发初学者以及希望提升个人时间管理效率的用户参考。压缩包共20个文件包含5个脚本文件、4个页面结构文件、4个样式表文件另有图片与动图素材、配置文件和说明文档整体大小约1.28MB目录结构清晰便于按模块查看。目前已有88人学习下载。通过阅读这套源码可以了解小程序从页面布局、交互绑定、状态管理到计时器控制、通知提醒的完整实现路径也能学到弹窗、动画、时间格式化、定时器清理等具体编写技巧。源码中前端事件处理与业务逻辑分层清楚适合在现有基础上二次调整比如修改番茄时长、增加任务记录或更换提醒方式对搭建同类效率工具具有直接的参考价值整个项目体量轻、结构简单适合快速上手实践。1. 番茄时钟小程序源码能直接改的代码比想象中更依赖状态设计拿到一个「番茄时钟.zip」小程序源码包第一反应是解压、导入微信开发者工具、换个 AppID 跑起来。但真正写过一个番茄钟的人都知道这个项目的难点不在倒计时界面的 UI而在「计时状态怎么切、切后台之后还准不准、数据存哪、提醒怎么发」。小程序不像网页页面隐藏后定时器会被系统挂起经典的setInterval倒计时方案在真机上必定漂移。这篇就顺着番茄时钟源码包里的常见工程结构把状态机、存储、统计和真机调试这几个绕不开的坎逐一拆开讲。不管你是准备拿这套源码二次开发成自己的效率工具还是想锻炼小程序基础能力重点是先理解计时逻辑为什么不能无脑写、改了 AppID 之后要动哪些配置、以及数据模型怎样设计才不会在加功能时推翻重来。下面按我自己会落地的方式展开。2. 番茄时钟源码的工程结构先搞清哪几个文件必须改2.1 解压后常见的目录责任划分番茄时钟小程序源码的 zip 包解压后工程结构通常长这样tomato-clock/ ├── app.js # 全局逻辑启动时恢复任务状态 ├── app.json # 页面注册、窗口外观、tabBar 配置 ├── app.wxss # 全局样式变量与公共类 ├── project.config.json # 开发者工具项目配置appid 必改 ├── sitemap.json # 搜索索引开关 ├── pages/ │ ├── index/ # 计时主页 │ ├── tasks/ # 任务清单页 │ ├── stats/ # 统计页 │ └── settings/ # 参数设置页 ├── utils/ │ ├── timer.js # 番茄状态机与倒计时核心 │ └── storage.js # 本地存储封装 ├── components/ │ └── progress-ring/ # 环形进度条组件 └── static/ └── icons/ # tabBar 图标拿到任何一份源码我的习惯是先看app.json和utils/目录。app.json决定页面能不能跑通utils/决定业务逻辑能不能按自己的需求改。app.js在小程序冷启动时执行番茄钟这里有一段关键时刻把上次未完成的工作阶段恢复出来。如果你的源码包里没做这件事那补上做了的话重点看它读的是不是globalData后面改存储结构时两边必须同步。project.config.json是导入开发者工具后第一个报错来源这里的appid还是源码作者的账号。把它改成「测试号」或者你自己的小程序 AppID是让工程跑起来的第一步下面单独说。2.2 让源码跑通必须改的配置与参数表打开微信开发者工具选择「导入项目」时直接选到解压后的目录工具会提示 AppID 不匹配。常见做法是选「测试号」它不占注册名额适合本地调试。推荐先确认这几个配置项再点编译配置项位置必须改说明appidproject.config.json的appid字段必须不改成自己的或测试号真机预览和云开发都用不了compilerVersionproject.config.json通常不用读者手上的开发者工具版本比我这边新时低版本编译器配置也能自动升级styleapp.json的window节点按需v2表示新版的组件样式老源码可能是v1界面错位时先查这navigationBarTitleTextapp.json每个页面节点建议改各页面的静态标题首页改成「番茄时钟」permissionapp.json用到定位才需要番茄钟一般不碰定位没有这个节点属正常改完配置点编译如果页面白屏打开调试器 Console最常见的报错是app.json里注册了源码包里不存在的页面路径。删除多余注册或者补齐缺失文件两者选前者更省事。注意如果源码包附带miniprogram_npm目录说明用了 npm 依赖。导入项目后需要在工具菜单栏执行「工具 → 构建 npm」否则会出现require报错。番茄钟此类工具类源码大概率不会引第三方库但确认一下成本很低。2.3 补上小程序备案相关的信息现在微信小程序上架需要完成小程序备案。源码包给你的是代码它不包含备案信息因为备案是绑定你注册的小程序账号的。如果你准备发布而不是只做本地体验在「小程序管理后台 → 设置 → 基本设置」里把备案信息填完整代码里不需要也尽量不要写死任何主体信息。这里提一句是因为很多从 zip 包起步的开发者会把备案和代码混淆实际上两端互不影响。3. 计时状态机与动态标题番茄钟核心逻辑的落地方式3.1 为什么不能在 WXML 里直接倒计时很多第一次接触番茄钟源码的人第一个想法是「页面加载后setInterval每秒减 1」。这在开发者工具模拟器上表现完美一到真机就露馅小程序切到后台比如回微信消息渲染线程被挂起前台的setInterval不再触发等用户回到小程序界面恢复的瞬间连续跳了几秒番茄钟实际已经超时了。源码里真正可靠的写法是不依赖累加 tick而是「用时间戳算差值」。核心思想每次更新界面时拿目标结束时间戳 - 当前时间戳算剩余秒数。就算定时器被挂起 5 分钟回到前台算出来的剩余时间依然准确。3.2 番茄钟状态机的设计与参数设定番茄工作法的基础循环是25 分钟工作5 分钟短休息每完成 4 个番茄插入一次 15–20 分钟长休息。源码里对应的状态枚举一般是const Phase { IDLE: idle, // 未开始 FOCUS: focus, // 专注中 SHORT_BREAK: shortBreak, // 短休息 LONG_BREAK: longBreak, // 长休息 PAUSED: paused // 暂停 };状态转换规则如下表当前状态触发行为下一个状态附加操作IDLE点击开始FOCUS记录开始时间戳写入当前任务FOCUS倒计时归零SHORT_BREAK震动 提示音今日完成数 1SHORT_BREAK休息归零FOCUS若连续完成数达到 4进入LONG_BREAKFOCUS或SHORT_BREAK点击暂停PAUSED保存剩余毫秒数清定时器PAUSED点击继续FOCUS基于剩余毫秒重建结束时间戳任意非IDLE状态点击重置IDLE清空计时数据不写入统计设计时要注意一个容易踩的坑完成 4 个番茄后的长休息判定应该在「FOCUS → 休息」时检查连续完成数而不是在进入短休息后重新倒计时时才判断。不然用户只要经历一次短休息判断时点就乱了。utils/timer.js中的倒计时核心逻辑用时间戳写法大致是这样class PomodoroTimer { constructor(options) { this.phase Phase.IDLE; this.duration options.focusMinutes * 60 * 1000; this.endTime 0; // 目标结束时间戳 this.remainMs 0; // 暂停时保存的剩余毫秒 this.timer null; this.onTick options.onTick || function () {}; this.onComplete options.onComplete || function () {}; } start() { this.endTime Date.now() this.duration; this.phase Phase.FOCUS; this.timer setInterval(() this.tick(), 500); this.tick(); } pause() { this.remainMs Math.max(0, this.endTime - Date.now()); clearInterval(this.timer); this.phase Phase.PAUSED; } resume() { this.endTime Date.now() this.remainMs; this.phase Phase.FOCUS; this.timer setInterval(() this.tick(), 500); this.tick(); } tick() { const remain Math.max(0, this.endTime - Date.now()); this.onTick(Math.ceil(remain / 1000)); if (remain 0) { clearInterval(this.timer); this.onComplete(); } } reset() { clearInterval(this.timer); this.phase Phase.IDLE; this.remainMs 0; } }这里的要点是setInterval(..., 500)每 500 毫秒比对一次时间戳而不是把duration每次减 500。Math.ceil保证显示 0 秒时才真正结束避免跳秒。定时器的间隔没必要设成 1000因为时间戳比对天然免疫了单次间隔不准的问题500 毫秒只是让界面在回前台后能更快刷新实际不耗电。3.3 动态设置标题把桌面小组件逻辑搬进小程序番茄钟运行时的另一个细节是页面标题。很多人忽略wx.setNavigationBarTitle的用法番茄钟恰恰是它的典型场景工作时标题显示「 专注中 24:31」休息时显示「☕ 休息中 04:12」比在页面里贴一个text更直观。// pages/index/index.js 片段 function updateTitle(phase, remainSeconds) { const mm String(Math.floor(remainSeconds / 60)).padStart(2, 0); const ss String(remainSeconds % 60).padStart(2, 0); const titleMap { focus: 工作中 ${mm}:${ss}, shortBreak: 休息中 ${mm}:${ss}, longBreak: 长休中 ${mm}:${ss}, paused: 已暂停 ${mm}:${ss}, idle: 番茄时钟 }; wx.setNavigationBarTitle({ title: titleMap[phase] }); }这个能力不需要额外配置但要注意两点wx.setNavigationBarTitle的调用时机必须是在onReady之后页面还没渲染完成时设置会被忽略其次如果想要标题栏背景色跟随状态切换要同步调用wx.setNavigationBarColor否则只有文字在变视觉反馈不完整。这个场景对源码改造的启发是动态标题不是「锦上添花」而是番茄钟「瞥一眼就知道状态」的核心体验。4. 任务记录与统计从能计时到能复盘数据闭环怎么搭4.1 存储结构设计一份能支撑统计的本地数据模型番茄钟源码里统计页做得好不好完全取决于存储层的数据结构设计。如果只存「今天完成了几个番茄」那明天的历史记录、上周的趋势、每个任务的累计时长统统做不了。设计存储结构时我建议按事件流而不是按汇总值存。// utils/storage.js const KEY_POMODOROS pomodoro_records_v2; const KEY_TASKS tasks_v1; function addPomodoroRecord(taskId, completedAt) { const records wx.getStorageSync(KEY_POMODOROS) || []; records.push({ id: ${Date.now()}_${Math.random().toString(36).slice(2, 6)}, taskId: taskId || , completedAt: completedAt || Date.now(), phase: focus, // 预留之后如果支持自定义时长区分 25/50 分钟 duration: 25 // 单位分钟 }); // 保留最近 1000 条即可防止存储无限膨胀 const trimmed records.slice(-1000); wx.setStorageSync(KEY_POMODOROS, trimmed); return trimmed.length; }completedAt存时间戳而不是日期字符串是为了统计页能用new Date(completedAt)做任意时区换算也方便按「自然日」还是「自然周」聚合。taskId暂时可为空但字段必须预留否则后续加待办清单功能时存档数据没有任务归属迁移成本极高。这里使用同步的wx.setStorageSync没有性能问题因为番茄钟写入频率极低一天撑死几十次同步写法逻辑更直白。不要为了潮流改成异步wx.setStorage然后在回调里处理then在小数据量场景纯属自找麻烦。4.2 统计查询按天、按任务维度聚合的三种写法统计页最常见的是「今日完成」「连续天数」「累计时长」三个指标。连续天数要从记录里反向求从今天往回逐日检查一旦某天为 0 就断开。这三类查询对应三种代码模式// 场景一今日完成数量 function getTodayCount() { const records wx.getStorageSync(KEY_POMODOROS) || []; const start new Date(); start.setHours(0, 0, 0, 0); return records.filter(r r.completedAt start.getTime()).length; } // 场景二按周聚合画柱状图的数据源 function getWeeklyTrend() { const records wx.getStorageSync(KEY_POMODOROS) || []; const days []; for (let i 6; i 0; i--) { const d new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() - i); const next new Date(d); next.setDate(d.getDate() 1); days.push({ label: ${d.getMonth() 1}/${d.getDate()}, count: records.filter(r r.completedAt d.getTime() r.completedAt next.getTime()).length }); } return days; } // 场景三连续专注天数有记录即算一天不要求必须 4 个番茄 function getStreakDays() { const records wx.getStorageSync(KEY_POMODOROS) || []; const daySet new Set(); records.forEach(r { const d new Date(r.completedAt); daySet.add(${d.getFullYear()}-${d.getMonth() 1}-${d.getDate()}); }); let streak 0; const cursor new Date(); cursor.setHours(0, 0, 0, 0); while (true) { const key ${cursor.getFullYear()}-${cursor.getMonth() 1}-${cursor.getDate()}; if (!daySet.has(key)) break; streak; cursor.setDate(cursor.getDate() - 1); } return streak; }getWeeklyTrend里面每次循环都 filter 一遍全量数组1000 条记录内完全没有压力不需要提前建立索引或做 Map 预聚合。代码可读性优先这是工具类小程序源码应有的取舍。4.3 设置页参数与本地持久化的联动番茄钟必然要暴露几个设置项工作时长、短休息时长、长休息时长、每天目标数。这些参数的流向是设置页写入wx.setStorageSync(settings, {...})app.js的onLaunch里读取后合并到globalData计时页在onShow时再读一次——因为你可能在设置页改了参数返回首页onShow是唯一可靠的刷新时机。{ focusMinutes: 25, shortBreakMinutes: 5, longBreakMinutes: 15, dailyGoal: 8, soundEnabled: true, vibrateEnabled: true }你可能会踩的坑是修改设置后返回计时页发现duration没有变。原因多半是计时器实例在页面onLoad时被创建后续没有再同步设置项。解法很简单在onShow里重置计时器的duration字段但要顺带判断当前状态是不是IDLE计时进行中不允许改时长这是符合直觉的设计约束——没有人希望一个番茄钟跑到一半突然变成 50 分钟。统计页的数据不需要实时刷新onShow里重新调一遍getTodayCount()即可。真正的性能瓶颈不在本地查询而在图表组件的渲染。如果你的源码里统计页用了ec-canvas之类的大组件页面切换会有明显卡顿后面第五部分细说。5. 真机调试与三个高频坑计时不准、没提醒、数据变空5.1 为什么模拟器一切正常真机一锁屏就出问题微信开发者工具的模拟器不会完全模拟小程序在系统级的行为。你对源码做了修改之后在模拟器上计时流畅、提醒准时一上真机锁屏几分钟就发现回来时页面停在上次的时间点倒计时已经结束了但没有通知。这是正常的系统限制小程序在后台被冻结JS 不再执行只有特定能力如wx.requestSubscribeMessage订阅消息可以做到离线触达。番茄钟这类工具的常见做法是降级但不失效回前台时用时间戳判定是否已超时超时则立即补发一次本地提示震动 提示音并把「完成」事件补齐写入记录。界面显示上不假装「一直在计时」而是直接呈现「已完成」这个体验反而比纠结后台保活更诚实。苹果 iOS 和 Android 在定时器行为上也有细微差别部分 Android 厂商的省电策略更激进小程序在后台可能被完全杀掉iOS 相对稳定但同样不能保证后台执行。所以源码里凡是依赖setInterval长期运行的设计最终都要回归到那一个核心函数endTime - Date.now()。5.2 提醒失效的排查顺序与震动参数用户反馈「没提醒」一般集中在三种原因现象排查点解决办法工作结束后有响声但没震动真机上wx.vibrateLong在某些 Android 机型静音模式下被拦截在设置项里增加震动开关并提示用户关闭系统级静音声音和震动都无wx.createInnerAudioContext的音频文件路径写错或src用了网络地址但没配合法域名把提示音文件放到static/audio/使用本地相对路径锁屏后再打开只补了一次提示符合预期不是 bug代码里对onShow做超时补偿避免重复入账震动参数建议统一写在utils/feedback.js里方便真机调试时关闭或调强度// utils/feedback.js function vibrate(strength medium) { const mode { light: () wx.vibrateShort({ type: light }), medium: () wx.vibrateShort({ type: medium }), heavy: () wx.vibrateLong() }; (mode[strength] || mode.medium)(); } function playCompleteSound() { const ctx wx.createInnerAudioContext(); ctx.src /static/audio/complete.mp3; ctx.play(); ctx.onEnded(() ctx.destroy()); }调试时在微信开发者工具中wx.vibrateShort默认无效这是模拟器的限制别在模拟器上验证震动。必须要真机预览才能确认——用开发者工具右上角的「预览」生成二维码手机扫码即可。5.3 数据丢失与迁移为什么清除缓存不等于删库番茄钟源码如果默认把所有数据放在wx.setStorageSync的 key 下用户一旦在「设置 → 清除缓存」就会全部丢失。这个行为的坑在于小程序缓存在 UI 上只有一个「清除」入口用户操作成本极低误触概率高。防范思路是把「关键业务数据」和「临时缓存」分开。临时数据比如设置页的未保存草稿、计时页的剩余秒数用普通setStorageSync没问题但完成记录这类不可再生的数据要么提供导出能力// 统计页里放一个导出按钮 function exportRecords() { const records wx.getStorageSync(KEY_POMODOROS) || []; wx.setClipboardData({ data: JSON.stringify(records), success: () wx.showToast({ title: 已复制到剪贴板, icon: success }) }); }要么在用户手动清缓存之前提醒他去设置页点一次「备份」。导出到剪贴板的格式可以在 Excel 里清洗后续要接后端也不难。最后如果你打算把这个源码改完发布建议再做一件事在开发者工具里跑一遍「体验评分」这里会列出所有自定义组件样式上不必要的setData调用。番茄钟的tick每秒更新一次界面是setData的高频场景如果页面里同时渲染了环形进度条、数字、标题、任务列表单次setData的数据量就会超标。优化技巧是只在剩余秒数变化时setData把环形进度条的样式计算提前到WXS里做而不是每次都传一个计算好的 style 对象。这条改动对低端 Android 机的流畅度提升是最明显的。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价