番茄鐘計時器 × 任務管理器 — 專為開發者打造的生產力工具
- 四種模式:專注(25分)、短休息(5分)、長休息(15分)、自訂(精確到秒)
- SVG 圓形進度環 + 頁面頂部進度條,雙重視覺呈現
- 倒數結束後按「開始」自動重設並重新倒數
- 計時結束後持續循環播放,直到手動關閉
- 計時器跳出脈衝動畫橫幅,提示對應模式訊息
- 5 種鈴聲可選,設定內支援即時試聽
| 鈴聲 | 描述 |
|---|---|
| 🔔 經典三音 | 三音階上升 |
| 🎵 清脆鐘聲 | 餘韻悠長 |
| 🤖 電子嗶嗶 | 四連電子音 |
| ✨ 空靈風鈴 | 四音漸升 |
| 🪘 禪意深鐘 | 低沉厚重 |
- 新增、刪除、完成任務,優先度三段(高 / 中 / 低)
- 點擊任務設為當前專注目標,顯示於計時器中央
- HTML5 原生拖拉排序,無外部套件依賴
- 每個任務自動累積完成番茄數 🍅
- 新增任務時可附加多個標籤(工作、學習、設計、開發…)
- 支援自訂標籤,不受預設限制
- 任務清單上方標籤篩選 bar,一鍵過濾
- 今日番茄數 / 本週番茄數 / 單日最高 三張數字卡
- 長條圖顯示近 7 天每日番茄數(Recharts)
- 標籤分布橫條圖,一眼看出時間花在哪裡
- 今日 / 本週切換視圖
- 右上角一鍵切換,全畫面平滑過渡
- 偏好設定自動儲存
- 可安裝至手機或桌面,像原生 App 使用
- 完整離線快取,無網路也能正常使用
- 首次訪問時自動顯示安裝提示橫幅
關掉頁面再打開,以下全部保留:任務清單、計時剩餘時間、今日番茄、統計紀錄、標籤、鈴聲偏好、主題。
| 技術 | 用途 |
|---|---|
| React 18 | 核心框架,Function Component |
| Vite 5 | 開發環境與打包 |
| vite-plugin-pwa | Service Worker 與 PWA manifest 自動生成 |
| Recharts | 統計圖表 |
| Web Audio API | 瀏覽器原生音效合成,零音效檔 |
| HTML5 Drag & Drop API | 原生拖拉排序,零套件 |
| localStorage | 資料持久化 |
| SVG + CSS Animation | 圓形進度環動畫 |
// 直接在 setInterval callback 裡讀 state 會拿到舊值
// 改用 ref 確保永遠是最新狀態
const isRingingRef = useRef(false);
const stopAlarm = useCallback(() => {
clearInterval(alarmIntervalRef.current);
isRingingRef.current = false; // ← ref 即時更新
setIsRinging(false);
}, []);
alarmIntervalRef.current = setInterval(() => {
if (!isRingingRef.current) { // ← 不會讀到 stale closure
clearInterval(alarmIntervalRef.current);
return;
}
RINGTONES[ringRef.current]?.play(playTone);
}, interval);const playTone = useCallback((freq, dur, type = "sine", gain = 0.3) => {
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gn = ctx.createGain();
osc.connect(gn);
gn.connect(ctx.destination);
osc.type = type;
osc.frequency.setValueAtTime(freq, ctx.currentTime);
gn.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + dur);
}, []);const handleDrop = (e, targetId) => {
const result = [...tasks];
const fromIdx = result.findIndex(t => t.id === dragId);
const toIdx = result.findIndex(t => t.id === targetId);
const [moved] = result.splice(fromIdx, 1);
result.splice(toIdx, 0, moved);
setTasks(result);
};const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference * (1 - progress);
<circle
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
style={{ transition: "stroke-dashoffset 0.9s linear" }}
/>useEffect(() => {
const handler = (e) => {
e.preventDefault(); // 阻止預設提示
setInstallPrompt(e); // 儲存事件,稍後自訂時機觸發
setShowInstallBanner(true);
};
window.addEventListener("beforeinstallprompt", handler);
return () => window.removeEventListener("beforeinstallprompt", handler);
}, []);git clone https://github.com/az860917/focusflow.git
cd focusflow
npm install
npm run dev瀏覽器前往 http://localhost:5173
npm run build # 產生 dist/推薦部署到 Vercel(直接連 GitHub repo 即可,零設定)。
focusflow/
├── public/
│ ├── icon-192.png # PWA 圖示(需自行準備)
│ └── icon-512.png # PWA 圖示(需自行準備)
├── src/
│ ├── App.jsx # 主元件(所有邏輯與 UI)
│ └── main.jsx # 應用程式入口
├── index.html
├── vite.config.js # Vite + PWA 設定
└── package.json
npm install vite-plugin-pwa -DMIT © 2025 林冠宇
