1. 心流舱里的那块表,为什么值得自己写一个
第一次在付费办公空间看到那块表的时候,我盯着它看了好一会儿。左边是一个番茄钟,25 分钟倒计时,红圈一点点收拢;右边是一个普通时钟,时针分针照常走。两块表盘并排放在一块小屏幕上,既让你盯住当下这 25 分钟,又让你随时知道现在几点、今天还剩多少时间。当时那块表标价大概 10 美元,我心想这东西逻辑这么简单,为什么不自己写一个。
心流舱、付费自习室、共享办公位这类空间,核心卖点就是「帮你专注」。但真正坐进去你会发现,手机上的番茄钟 App 一打开就容易被消息带走,手表又不想专门戴一块。桌面上如果有一块只干一件事的小屏幕——左边倒计时、右边看时间,反而最不容易分心。这就是「番茄风水钟」这个开源项目的出发点:用 Web 技术做一个双表盘时钟,左边番茄钟,右边时钟,还能把右侧表盘换成各种吉祥物图案,摆在桌面上既专注又有点意思。
它适合谁?适合想给自己桌面加一块专注屏的人,适合想练手 Cursor + Web 前端的人,也适合做智能硬件、桌面摆件、空间氛围类产品的同学拿去做原型。整个项目就是纯前端,一个 HTML 加 JS 就能跑,不需要后端、不需要数据库,本地双击就能打开。下面我会把项目结构、Cursor 提示词、可复制的配置和本地验证步骤都写清楚,你跟着做就能得到一个能用的双表盘时钟。
核心检索词先明确:这是一个开源的番茄钟 + 时钟双表盘 Web 项目,用 Cursor 辅助开发,可本地运行、可自定义表盘,适合桌面专注场景。
2. 用 Cursor 搭双表盘番茄钟:项目结构与提示词怎么写
先说清楚这个项目为什么用 Cursor 而不是纯手写。双表盘时钟的难点不在逻辑,而在「布局 + 表盘绘制 + 状态同步」这三块反复调。番茄钟要处理倒计时、暂停、重置、结束提醒;时钟要处理指针角度计算;右侧表盘还要支持点击切换图案。这些都是一改就要看效果的活,用 Cursor 边描述边生成、边预览边改,效率比纯手敲高很多。
项目结构我建议保持极简,一个目录搞定:
dual-time/ ├── index.html # 主页面,双表盘布局 ├── style.css # 布局与表盘样式 ├── app.js # 番茄钟逻辑 + 时钟指针 + 表盘切换 └── assets/ └── faces/ # 右侧表盘的吉祥物图案(svg/png)index.html负责把屏幕分成左右两半,左边放番茄钟容器,右边放时钟容器。style.css用 flex 做左右分栏,保证在小屏幕上也能并排显示。app.js里分三个模块:番茄钟计时器、时钟指针渲染、表盘切换。
在 Cursor 里,我建议你按模块给提示词,而不是一次性让它生成整个项目。一次性生成的东西往往布局能看、逻辑有坑。分模块的好处是每步都能验证。
第一步,先让 Cursor 生成布局骨架。提示词可以这样写:
用原生 HTML + CSS 做一个左右分栏的页面。 左边是一个圆形番茄钟表盘,中间显示剩余时间 MM:SS, 外圈有一圈进度环表示剩余比例。 右边是一个圆形时钟表盘,有 12 个刻度、时针、分针、秒针。 两个表盘尺寸一致,页面背景深色,整体居中。 不要用任何框架,纯 HTML/CSS/JS。第二步,补番茄钟逻辑。提示词:
在 app.js 里实现番茄钟: - 默认 25 分钟,可配置 - 开始 / 暂停 / 重置三个按钮 - 每秒更新剩余时间,同步更新外圈进度环 - 倒计时结束时播放一个提示音(用 Web Audio API 生成,不引入音频文件) - 时间格式 MM:SS第三步,补时钟指针。提示词:
在 app.js 里实现时钟: - 每秒更新一次 - 根据当前时间计算时针、分针、秒针的旋转角度 - 时针角度 = (小时 % 12) * 30 + 分钟 * 0.5 - 分针角度 = 分钟 * 6 + 秒 * 0.1 - 秒针角度 = 秒 * 6 - 用 CSS transform rotate 应用角度第四步,加表盘切换。提示词:
右侧时钟表盘支持点击切换背景图案。 把 assets/faces/ 下的图片列成一个数组, 点击表盘时循环切换到下一张,作为表盘背景。 切换时保持指针正常显示在图案上方。这四步走完,一个能跑的双表盘时钟就成型了。你会发现 Cursor 的价值在于:布局和角度计算这种「有标准答案但写起来烦」的部分,它一次就能给对,你只需要盯着预览调样式。真正需要你判断的是交互细节,比如番茄钟结束后是自动进入休息还是停住、表盘切换要不要加过渡动画,这些按你自己的使用习惯定。
这里有个我踩过的坑:一开始我让 Cursor 把番茄钟和时钟写在一个大函数里,结果改番茄钟逻辑时经常误伤时钟渲染。后来拆成两个独立的initPomodoro()和initClock(),各自维护自己的状态和定时器,互不干扰,改起来清爽很多。如果你也要用 Cursor 做类似项目,建议一开始就要求它「番茄钟和时钟逻辑分离,各自独立初始化」。
3. 可复制的项目配置与 Cursor 规则文件
这一节给你可以直接抄的配置。首先是index.html的骨架,重点是左右分栏和两个表盘容器:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>双表盘番茄风水钟</title> <link rel="stylesheet" href="style.css" /> </head> <body> <main class="stage"> <section class="dial pomodoro"> <div class="ring"></div> <div class="time" id="pomodoroTime">25:00</div> <div class="controls"> <button id="startBtn">开始</button> <button id="pauseBtn">暂停</button> <button id="resetBtn">重置</button> </div> </section> <section class="dial clock" id="clockDial"> <div class="hand hour" id="hourHand"></div> <div class="hand minute" id="minuteHand"></div> <div class="hand second" id="secondHand"></div> </section> </main> <script src="app.js"></script> </body> </html>然后是style.css里最关键的布局和表盘样式,左右分栏用 flex,表盘用border-radius: 50%:
.stage { display: flex; gap: 24px; justify-content: center; align-items: center; min-height: 100vh; background: #0f1115; } .dial { position: relative; width: 280px; height: 280px; border-radius: 50%; background: #1a1d24; display: flex; align-items: center; justify-content: center; } .clock { background-size: cover; background-position: center; cursor: pointer; } .hand { position: absolute; bottom: 50%; left: 50%; transform-origin: bottom center; background: #e8e8e8; } .hour { width: 6px; height: 70px; } .minute { width: 4px; height: 100px; } .second { width: 2px; height: 110px; background: #ff5a5f; }接着是app.js的核心逻辑,番茄钟和时钟分开写:
// 番茄钟 const POMODORO_SECONDS = 25 * 60; let remain = POMODORO_SECONDS; let timer = null; function renderPomodoro() { const m = String(Math.floor(remain / 60)).padStart(2, '0'); const s = String(remain % 60).padStart(2, '0'); document.getElementById('pomodoroTime').textContent = `${m}:${s}`; } function tick() { if (remain > 0) { remain -= 1; renderPomodoro(); } else { clearInterval(timer); timer = null; playBeep(); } } document.getElementById('startBtn').onclick = () => { if (!timer) timer = setInterval(tick, 1000); }; document.getElementById('pauseBtn').onclick = () => { clearInterval(timer); timer = null; }; document.getElementById('resetBtn').onclick = () => { clearInterval(timer); timer = null; remain = POMODORO_SECONDS; renderPomodoro(); }; // 时钟 function renderClock() { const now = new Date(); const h = now.getHours() % 12; const m = now.getMinutes(); const s = now.getSeconds(); document.getElementById('hourHand').style.transform = `rotate(${h * 30 + m * 0.5}deg)`; document.getElementById('minuteHand').style.transform = `rotate(${m * 6 + s * 0.1}deg)`; document.getElementById('secondHand').style.transform = `rotate(${s * 6}deg)`; } // 表盘切换 const faces = ['assets/faces/face1.svg', 'assets/faces/face2.svg']; let faceIndex = 0; document.getElementById('clockDial').onclick = () => { faceIndex = (faceIndex + 1) % faces.length; document.getElementById('clockDial').style.backgroundImage = `url(${faces[faceIndex]})`; }; // 提示音 function playBeep() { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); osc.frequency.value = 880; osc.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + 0.3); } renderPomodoro(); renderClock(); setInterval(renderClock, 1000);如果你用 Cursor 长期维护这个项目,建议在项目根目录放一个.cursorrules文件,把约定写进去,这样 Cursor 每次生成代码都会遵守:
本项目是纯前端双表盘时钟,不使用任何框架。 - 番茄钟逻辑写在 initPomodoro 相关函数中,时钟逻辑写在 initClock 相关函数中,两者状态独立。 - 所有 DOM 操作使用原生 API,不引入 jQuery。 - 样式统一写在 style.css,不在 JS 里写内联样式。 - 新增表盘图案放到 assets/faces/ 目录,并在 faces 数组中注册。 - 时间格式统一 MM:SS。这个规则文件的作用是让 Cursor 记住你的项目结构,避免它每次生成代码时又给你塞一个框架进来。实测下来,有了这个文件之后,Cursor 补全的代码风格会稳定很多。
4. 本地运行与验证:确认双表盘真的在走
配置写完,接下来是验证。这一步很多人会跳过,结果打开页面发现指针不动、倒计时不走,又回头找问题。我们一步步来。
最省事的运行方式是直接用浏览器打开index.html。但如果你后面要加载assets/faces/里的图片,用file://协议有时会有跨域限制,所以更稳妥的方式是起一个本地静态服务器。如果你装了 Python,一行命令就行:
cd dual-time python3 -m http.server 8080然后浏览器打开http://localhost:8080。如果你用 Node,也可以:
npx serve .打开后你应该看到:左边一个圆形表盘显示25:00,下面三个按钮;右边一个圆形表盘,时针分针秒针在走。验证分三步:
第一步,验证时钟。盯着右边的秒针看,它应该每秒跳一格,走满一圈是 60 秒。分针和时针也要跟着动,虽然肉眼看不明显,但你可以打开浏览器开发者工具,在renderClock里打个断点,看角度值是不是随时间变化。
第二步,验证番茄钟。点「开始」,左边的25:00应该开始往下减,每秒减 1。点「暂停」,数字停住。点「重置」,回到25:00。为了快速验证倒计时结束的行为,你可以临时把POMODORO_SECONDS改成10,跑一遍看结束时会不会响提示音、定时器会不会停。
第三步,验证表盘切换。点右边的时钟表盘,背景图案应该切换到下一张。前提是assets/faces/里确实放了图片,并且faces数组里的路径和文件名对得上。
如果你想让这块时钟真正摆在桌面上当专注屏,可以把它全屏打开,或者用手机浏览器打开同一个局域网地址,横屏放在桌上。项目本身是响应式的,小屏幕上两个表盘会自动缩小并排。
验证通过后,你得到的就是一个完全本地运行、不依赖任何外部服务的双表盘时钟。它不联网、不上传数据,番茄钟和时钟都在浏览器里跑,这也是纯前端方案的好处——简单、可控、随时能改。
5. 常见报错排查:指针不动、图片不显示、声音不响
这一节把几个高频问题列出来,对照着查。
问题一:秒针不动,或者只动一次。最常见的原因是setInterval(renderClock, 1000)没执行,或者renderClock里取 DOM 元素失败。打开控制台看有没有Cannot read properties of null这类报错。如果有,说明id对不上,检查 HTML 里的id="secondHand"和 JS 里getElementById('secondHand')是否一致。另一个可能是transform-origin没设对,指针在转但绕着中心转,看起来像没动。确认.hand的transform-origin: bottom center生效。
问题二:表盘图片不显示,控制台报 404。这是路径问题。faces数组里写的是assets/faces/face1.svg,那你的目录结构必须真的是dual-time/assets/faces/face1.svg。如果你用file://打开,路径大小写敏感,Face1.svg和face1.svg是两回事。建议统一用小写文件名。另外确认图片格式浏览器支持,svg 和 png 都没问题。
问题三:倒计时结束没有声音。浏览器的自动播放策略会拦截没有用户交互的音频。playBeep里的AudioContext如果在用户点击「开始」之前就创建,可能处于 suspended 状态。解决办法是在用户点击按钮时再创建或 resume 音频上下文。可以在startBtn的点击回调里加一句audioCtx.resume()。另外,如果你用的是new AudioContext()每次新建,频繁触发可能报错,建议全局维护一个实例。
问题四:番茄钟和时钟互相干扰。如果你发现点「暂停」之后时钟也停了,说明两个定时器共用了变量。检查是不是把timer这个变量同时用在了番茄钟和时钟上。正确做法是番茄钟用timer,时钟用独立的clockTimer,互不覆盖。
问题五:页面在手机上两个表盘挤在一起。这是布局没做响应式。在style.css里加一个媒体查询,窄屏时把.stage的flex-direction改成column,或者缩小表盘尺寸:
@media (max-width: 640px) { .stage { flex-direction: column; gap: 16px; } .dial { width: 200px; height: 200px; } }问题六:Cursor 生成的代码用了框架或外部 CDN。如果你明确要纯前端,但 Cursor 给你引入了 React 或某个 CDN 库,检查.cursorrules是否生效,或者在提示词里再强调一次「不使用任何框架和外部依赖」。纯前端的好处是离线可用,一旦引入 CDN,断网就废了。
排查这类问题的通用思路是:先看控制台报错,再看 DOM 元素是否存在,最后看定时器和状态变量。大部分「不动」的问题,要么是元素没取到,要么是定时器没起来,要么是角度算错。逐个排除,很快能定位。
6. 把这块钟用起来:从桌面摆件到空间氛围
项目跑通之后,怎么用是另一回事。我自己的用法是把它全屏打开放在一块旧平板上,横屏立在桌角。左边番茄钟开始跑,右边时钟随时看,中间不需要切任何 App。25 分钟一到提示音响,起身活动一下,再点重置进入下一轮。这种「只干一件事」的屏幕,比手机上的番茄钟 App 更容易让人进入状态。
如果你在做付费空间、自习室、共享办公这类场景,这个项目可以直接当氛围组件用。右侧表盘换成空间自己的 logo 或吉祥物,点击切换,既是个时钟又是个品牌展示位。因为它是纯前端,你可以把它打包进任何带浏览器的设备里,小到一块开发板接的屏幕,大到墙上的显示器,只要能跑浏览器就能显示。
想继续扩展的话,有几个方向:把番茄钟时长做成可配置,比如 25/45/60 分钟几档;加一个今日完成番茄数的统计,存在 localStorage 里;右侧表盘除了切换图案,还可以叠加农历、节气这类信息。这些都不需要后端,纯前端就能做。
如果你在接入过程中需要管理 API Key、调试模型调用,或者想把这类小工具和 AI 能力结合,可以走 TaoToken 的接入流程:先在 API Keys 页面创建密钥(https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite),再对照接入文档(https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite)把 Base URL 和 Key 配好。想先验证模型效果,可以直接在模型对话页(https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite)试几句。如果你打算长期用 Cursor 做这类小项目、把 AI 编码变成日常,Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite)会更合适。官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite,需要的时候从那里进就行。
最后留一个实用技巧:这个项目最大的价值不是它多复杂,而是它足够小、足够可控。你可以在一小时内用 Cursor 改出完全属于自己的版本——换个表盘、改个时长、加个统计。与其去买一块功能固定的表,不如自己写一块随时能改的。