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/pngindex.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 langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / title双表盘番茄风水钟/title link relstylesheet hrefstyle.css / /head body main classstage section classdial pomodoro div classring/div div classtime idpomodoroTime25:00/div div classcontrols button idstartBtn开始/button button idpauseBtn暂停/button button idresetBtn重置/button /div /section section classdial clock idclockDial div classhand hour idhourHand/div div classhand minute idminuteHand/div div classhand second idsecondHand/div /section /main script srcapp.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 里的idsecondHand和 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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite再对照接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite把 Base URL 和 Key 配好。想先验证模型效果可以直接在模型对话页https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite试几句。如果你打算长期用 Cursor 做这类小项目、把 AI 编码变成日常Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite会更合适。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite需要的时候从那里进就行。最后留一个实用技巧这个项目最大的价值不是它多复杂而是它足够小、足够可控。你可以在一小时内用 Cursor 改出完全属于自己的版本——换个表盘、改个时长、加个统计。与其去买一块功能固定的表不如自己写一块随时能改的。
阅读完成 · 觉得有帮助?