心流艙付費(fèi)空間番茄風(fēng)水鐘:用Cursor+Web打造專注時(shí)鐘)
1. 心流艙里的那塊表為什么值得自己寫一個(gè)第一次在付費(fèi)辦公空間看到那塊表的時(shí)候我盯著它看了好一會(huì)兒。左邊是一個(gè)番茄鐘25 分鐘倒計(jì)時(shí)紅圈一點(diǎn)點(diǎn)收攏右邊是一個(gè)普通時(shí)鐘時(shí)針分針照常走。兩塊表盤并排放在一塊小屏幕上既讓你盯住當(dāng)下這 25 分鐘又讓你隨時(shí)知道現(xiàn)在幾點(diǎn)、今天還剩多少時(shí)間。當(dāng)時(shí)那塊表標(biāo)價(jià)大概 10 美元我心想這東西邏輯這么簡(jiǎn)單為什么不自己寫一個(gè)。心流艙、付費(fèi)自習(xí)室、共享辦公位這類空間核心賣點(diǎn)就是「幫你專注」。但真正坐進(jìn)去你會(huì)發(fā)現(xiàn)手機(jī)上的番茄鐘 App 一打開就容易被消息帶走手表又不想專門戴一塊。桌面上如果有一塊只干一件事的小屏幕——左邊倒計(jì)時(shí)、右邊看時(shí)間反而最不容易分心。這就是「番茄風(fēng)水鐘」這個(gè)開源項(xiàng)目的出發(fā)點(diǎn)用 Web 技術(shù)做一個(gè)雙表盤時(shí)鐘左邊番茄鐘右邊時(shí)鐘還能把右側(cè)表盤換成各種吉祥物圖案擺在桌面上既專注又有點(diǎn)意思。它適合誰適合想給自己桌面加一塊專注屏的人適合想練手 Cursor Web 前端的人也適合做智能硬件、桌面擺件、空間氛圍類產(chǎn)品的同學(xué)拿去做原型。整個(gè)項(xiàng)目就是純前端一個(gè) HTML 加 JS 就能跑不需要后端、不需要數(shù)據(jù)庫本地雙擊就能打開。下面我會(huì)把項(xiàng)目結(jié)構(gòu)、Cursor 提示詞、可復(fù)制的配置和本地驗(yàn)證步驟都寫清楚你跟著做就能得到一個(gè)能用的雙表盤時(shí)鐘。核心檢索詞先明確這是一個(gè)開源的番茄鐘 時(shí)鐘雙表盤 Web 項(xiàng)目用 Cursor 輔助開發(fā)可本地運(yùn)行、可自定義表盤適合桌面專注場(chǎng)景。2. 用 Cursor 搭雙表盤番茄鐘項(xiàng)目結(jié)構(gòu)與提示詞怎么寫先說清楚這個(gè)項(xiàng)目為什么用 Cursor 而不是純手寫。雙表盤時(shí)鐘的難點(diǎn)不在邏輯而在「布局 表盤繪制 狀態(tài)同步」這三塊反復(fù)調(diào)。番茄鐘要處理倒計(jì)時(shí)、暫停、重置、結(jié)束提醒時(shí)鐘要處理指針角度計(jì)算右側(cè)表盤還要支持點(diǎn)擊切換圖案。這些都是一改就要看效果的活用 Cursor 邊描述邊生成、邊預(yù)覽邊改效率比純手敲高很多。項(xiàng)目結(jié)構(gòu)我建議保持極簡(jiǎn)一個(gè)目錄搞定dual-time/ ├── index.html # 主頁面雙表盤布局 ├── style.css # 布局與表盤樣式 ├── app.js # 番茄鐘邏輯 時(shí)鐘指針 表盤切換 └── assets/ └── faces/ # 右側(cè)表盤的吉祥物圖案svg/pngindex.html負(fù)責(zé)把屏幕分成左右兩半左邊放番茄鐘容器右邊放時(shí)鐘容器。style.css用 flex 做左右分欄保證在小屏幕上也能并排顯示。app.js里分三個(gè)模塊番茄鐘計(jì)時(shí)器、時(shí)鐘指針渲染、表盤切換。在 Cursor 里我建議你按模塊給提示詞而不是一次性讓它生成整個(gè)項(xiàng)目。一次性生成的東西往往布局能看、邏輯有坑。分模塊的好處是每步都能驗(yàn)證。第一步先讓 Cursor 生成布局骨架。提示詞可以這樣寫用原生 HTML CSS 做一個(gè)左右分欄的頁面。 左邊是一個(gè)圓形番茄鐘表盤中間顯示剩余時(shí)間 MM:SS 外圈有一圈進(jìn)度環(huán)表示剩余比例。 右邊是一個(gè)圓形時(shí)鐘表盤有 12 個(gè)刻度、時(shí)針、分針、秒針。 兩個(gè)表盤尺寸一致頁面背景深色整體居中。 不要用任何框架純 HTML/CSS/JS。第二步補(bǔ)番茄鐘邏輯。提示詞在 app.js 里實(shí)現(xiàn)番茄鐘 - 默認(rèn) 25 分鐘可配置 - 開始 / 暫停 / 重置三個(gè)按鈕 - 每秒更新剩余時(shí)間同步更新外圈進(jìn)度環(huán) - 倒計(jì)時(shí)結(jié)束時(shí)播放一個(gè)提示音用 Web Audio API 生成不引入音頻文件 - 時(shí)間格式 MM:SS第三步補(bǔ)時(shí)鐘指針。提示詞在 app.js 里實(shí)現(xiàn)時(shí)鐘 - 每秒更新一次 - 根據(jù)當(dāng)前時(shí)間計(jì)算時(shí)針、分針、秒針的旋轉(zhuǎn)角度 - 時(shí)針角度 (小時(shí) % 12) * 30 分鐘 * 0.5 - 分針角度 分鐘 * 6 秒 * 0.1 - 秒針角度 秒 * 6 - 用 CSS transform rotate 應(yīng)用角度第四步加表盤切換。提示詞右側(cè)時(shí)鐘表盤支持點(diǎn)擊切換背景圖案。 把 assets/faces/ 下的圖片列成一個(gè)數(shù)組 點(diǎn)擊表盤時(shí)循環(huán)切換到下一張作為表盤背景。 切換時(shí)保持指針正常顯示在圖案上方。這四步走完一個(gè)能跑的雙表盤時(shí)鐘就成型了。你會(huì)發(fā)現(xiàn) Cursor 的價(jià)值在于布局和角度計(jì)算這種「有標(biāo)準(zhǔn)答案但寫起來煩」的部分它一次就能給對(duì)你只需要盯著預(yù)覽調(diào)樣式。真正需要你判斷的是交互細(xì)節(jié)比如番茄鐘結(jié)束后是自動(dòng)進(jìn)入休息還是停住、表盤切換要不要加過渡動(dòng)畫這些按你自己的使用習(xí)慣定。這里有個(gè)我踩過的坑一開始我讓 Cursor 把番茄鐘和時(shí)鐘寫在一個(gè)大函數(shù)里結(jié)果改番茄鐘邏輯時(shí)經(jīng)常誤傷時(shí)鐘渲染。后來拆成兩個(gè)獨(dú)立的initPomodoro()和initClock()各自維護(hù)自己的狀態(tài)和定時(shí)器互不干擾改起來清爽很多。如果你也要用 Cursor 做類似項(xiàng)目建議一開始就要求它「番茄鐘和時(shí)鐘邏輯分離各自獨(dú)立初始化」。3. 可復(fù)制的項(xiàng)目配置與 Cursor 規(guī)則文件這一節(jié)給你可以直接抄的配置。首先是index.html的骨架重點(diǎn)是左右分欄和兩個(gè)表盤容器!DOCTYPE html html langzh-CN head meta charsetUTF-8 / meta nameviewport contentwidthdevice-width, initial-scale1.0 / title雙表盤番茄風(fēng)水鐘/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里最關(guān)鍵的布局和表盤樣式左右分欄用 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的核心邏輯番茄鐘和時(shí)鐘分開寫// 番茄鐘 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(); }; // 時(shí)鐘 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 長(zhǎng)期維護(hù)這個(gè)項(xiàng)目建議在項(xiàng)目根目錄放一個(gè).cursorrules文件把約定寫進(jìn)去這樣 Cursor 每次生成代碼都會(huì)遵守本項(xiàng)目是純前端雙表盤時(shí)鐘不使用任何框架。 - 番茄鐘邏輯寫在 initPomodoro 相關(guān)函數(shù)中時(shí)鐘邏輯寫在 initClock 相關(guān)函數(shù)中兩者狀態(tài)獨(dú)立。 - 所有 DOM 操作使用原生 API不引入 jQuery。 - 樣式統(tǒng)一寫在 style.css不在 JS 里寫內(nèi)聯(lián)樣式。 - 新增表盤圖案放到 assets/faces/ 目錄并在 faces 數(shù)組中注冊(cè)。 - 時(shí)間格式統(tǒng)一 MM:SS。這個(gè)規(guī)則文件的作用是讓 Cursor 記住你的項(xiàng)目結(jié)構(gòu)避免它每次生成代碼時(shí)又給你塞一個(gè)框架進(jìn)來。實(shí)測(cè)下來有了這個(gè)文件之后Cursor 補(bǔ)全的代碼風(fēng)格會(huì)穩(wěn)定很多。4. 本地運(yùn)行與驗(yàn)證確認(rèn)雙表盤真的在走配置寫完接下來是驗(yàn)證。這一步很多人會(huì)跳過結(jié)果打開頁面發(fā)現(xiàn)指針不動(dòng)、倒計(jì)時(shí)不走又回頭找問題。我們一步步來。最省事的運(yùn)行方式是直接用瀏覽器打開index.html。但如果你后面要加載assets/faces/里的圖片用file://協(xié)議有時(shí)會(huì)有跨域限制所以更穩(wěn)妥的方式是起一個(gè)本地靜態(tài)服務(wù)器。如果你裝了 Python一行命令就行cd dual-time python3 -m http.server 8080然后瀏覽器打開http://localhost:8080。如果你用 Node也可以npx serve .打開后你應(yīng)該看到左邊一個(gè)圓形表盤顯示25:00下面三個(gè)按鈕右邊一個(gè)圓形表盤時(shí)針分針秒針在走。驗(yàn)證分三步第一步驗(yàn)證時(shí)鐘。盯著右邊的秒針看它應(yīng)該每秒跳一格走滿一圈是 60 秒。分針和時(shí)針也要跟著動(dòng)雖然肉眼看不明顯但你可以打開瀏覽器開發(fā)者工具在renderClock里打個(gè)斷點(diǎn)看角度值是不是隨時(shí)間變化。第二步驗(yàn)證番茄鐘。點(diǎn)「開始」左邊的25:00應(yīng)該開始往下減每秒減 1。點(diǎn)「暫?!箶?shù)字停住。點(diǎn)「重置」回到25:00。為了快速驗(yàn)證倒計(jì)時(shí)結(jié)束的行為你可以臨時(shí)把POMODORO_SECONDS改成10跑一遍看結(jié)束時(shí)會(huì)不會(huì)響提示音、定時(shí)器會(huì)不會(huì)停。第三步驗(yàn)證表盤切換。點(diǎn)右邊的時(shí)鐘表盤背景圖案應(yīng)該切換到下一張。前提是assets/faces/里確實(shí)放了圖片并且faces數(shù)組里的路徑和文件名對(duì)得上。如果你想讓這塊時(shí)鐘真正擺在桌面上當(dāng)專注屏可以把它全屏打開或者用手機(jī)瀏覽器打開同一個(gè)局域網(wǎng)地址橫屏放在桌上。項(xiàng)目本身是響應(yīng)式的小屏幕上兩個(gè)表盤會(huì)自動(dòng)縮小并排。驗(yàn)證通過后你得到的就是一個(gè)完全本地運(yùn)行、不依賴任何外部服務(wù)的雙表盤時(shí)鐘。它不聯(lián)網(wǎng)、不上傳數(shù)據(jù)番茄鐘和時(shí)鐘都在瀏覽器里跑這也是純前端方案的好處——簡(jiǎn)單、可控、隨時(shí)能改。5. 常見報(bào)錯(cuò)排查指針不動(dòng)、圖片不顯示、聲音不響這一節(jié)把幾個(gè)高頻問題列出來對(duì)照著查。問題一秒針不動(dòng)或者只動(dòng)一次。最常見的原因是setInterval(renderClock, 1000)沒執(zhí)行或者renderClock里取 DOM 元素失敗。打開控制臺(tái)看有沒有Cannot read properties of null這類報(bào)錯(cuò)。如果有說明id對(duì)不上檢查 HTML 里的idsecondHand和 JS 里getElementById(secondHand)是否一致。另一個(gè)可能是transform-origin沒設(shè)對(duì)指針在轉(zhuǎn)但繞著中心轉(zhuǎn)看起來像沒動(dòng)。確認(rèn).hand的transform-origin: bottom center生效。問題二表盤圖片不顯示控制臺(tái)報(bào) 404。這是路徑問題。faces數(shù)組里寫的是assets/faces/face1.svg那你的目錄結(jié)構(gòu)必須真的是dual-time/assets/faces/face1.svg。如果你用file://打開路徑大小寫敏感Face1.svg和face1.svg是兩回事。建議統(tǒng)一用小寫文件名。另外確認(rèn)圖片格式瀏覽器支持svg 和 png 都沒問題。問題三倒計(jì)時(shí)結(jié)束沒有聲音。瀏覽器的自動(dòng)播放策略會(huì)攔截沒有用戶交互的音頻。playBeep里的AudioContext如果在用戶點(diǎn)擊「開始」之前就創(chuàng)建可能處于 suspended 狀態(tài)。解決辦法是在用戶點(diǎn)擊按鈕時(shí)再創(chuàng)建或 resume 音頻上下文。可以在startBtn的點(diǎn)擊回調(diào)里加一句audioCtx.resume()。另外如果你用的是new AudioContext()每次新建頻繁觸發(fā)可能報(bào)錯(cuò)建議全局維護(hù)一個(gè)實(shí)例。問題四番茄鐘和時(shí)鐘互相干擾。如果你發(fā)現(xiàn)點(diǎn)「暫停」之后時(shí)鐘也停了說明兩個(gè)定時(shí)器共用了變量。檢查是不是把timer這個(gè)變量同時(shí)用在了番茄鐘和時(shí)鐘上。正確做法是番茄鐘用timer時(shí)鐘用獨(dú)立的clockTimer互不覆蓋。問題五頁面在手機(jī)上兩個(gè)表盤擠在一起。這是布局沒做響應(yīng)式。在style.css里加一個(gè)媒體查詢窄屏?xí)r把.stage的flex-direction改成column或者縮小表盤尺寸media (max-width: 640px) { .stage { flex-direction: column; gap: 16px; } .dial { width: 200px; height: 200px; } }問題六Cursor 生成的代碼用了框架或外部 CDN。如果你明確要純前端但 Cursor 給你引入了 React 或某個(gè) CDN 庫檢查.cursorrules是否生效或者在提示詞里再強(qiáng)調(diào)一次「不使用任何框架和外部依賴」。純前端的好處是離線可用一旦引入 CDN斷網(wǎng)就廢了。排查這類問題的通用思路是先看控制臺(tái)報(bào)錯(cuò)再看 DOM 元素是否存在最后看定時(shí)器和狀態(tài)變量。大部分「不動(dòng)」的問題要么是元素沒取到要么是定時(shí)器沒起來要么是角度算錯(cuò)。逐個(gè)排除很快能定位。6. 把這塊鐘用起來從桌面擺件到空間氛圍項(xiàng)目跑通之后怎么用是另一回事。我自己的用法是把它全屏打開放在一塊舊平板上橫屏立在桌角。左邊番茄鐘開始跑右邊時(shí)鐘隨時(shí)看中間不需要切任何 App。25 分鐘一到提示音響起身活動(dòng)一下再點(diǎn)重置進(jìn)入下一輪。這種「只干一件事」的屏幕比手機(jī)上的番茄鐘 App 更容易讓人進(jìn)入狀態(tài)。如果你在做付費(fèi)空間、自習(xí)室、共享辦公這類場(chǎng)景這個(gè)項(xiàng)目可以直接當(dāng)氛圍組件用。右側(cè)表盤換成空間自己的 logo 或吉祥物點(diǎn)擊切換既是個(gè)時(shí)鐘又是個(gè)品牌展示位。因?yàn)樗羌兦岸四憧梢园阉虬M(jìn)任何帶瀏覽器的設(shè)備里小到一塊開發(fā)板接的屏幕大到墻上的顯示器只要能跑瀏覽器就能顯示。想繼續(xù)擴(kuò)展的話有幾個(gè)方向把番茄鐘時(shí)長(zhǎng)做成可配置比如 25/45/60 分鐘幾檔加一個(gè)今日完成番茄數(shù)的統(tǒng)計(jì)存在 localStorage 里右側(cè)表盤除了切換圖案還可以疊加農(nóng)歷、節(jié)氣這類信息。這些都不需要后端純前端就能做。如果你在接入過程中需要管理 API Key、調(diào)試模型調(diào)用或者想把這類小工具和 AI 能力結(jié)合可以走 TaoToken 的接入流程先在 API Keys 頁面創(chuàng)建密鑰https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite再對(duì)照接入文檔https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite把 Base URL 和 Key 配好。想先驗(yàn)證模型效果可以直接在模型對(duì)話頁https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite試幾句。如果你打算長(zhǎng)期用 Cursor 做這類小項(xiàng)目、把 AI 編碼變成日常Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite會(huì)更合適。官網(wǎng)入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite需要的時(shí)候從那里進(jìn)就行。最后留一個(gè)實(shí)用技巧這個(gè)項(xiàng)目最大的價(jià)值不是它多復(fù)雜而是它足夠小、足夠可控。你可以在一小時(shí)內(nèi)用 Cursor 改出完全屬于自己的版本——換個(gè)表盤、改個(gè)時(shí)長(zhǎng)、加個(gè)統(tǒng)計(jì)。與其去買一塊功能固定的表不如自己寫一塊隨時(shí)能改的。