import React, { useState, useEffect, useRef } from 'react'; import { Play, Pause, RotateCcw, BellRing, Settings } from 'lucide-react'; export default function CountdownTimer() { // 狀態管理 const [initialTime, setInitialTime] = useState(10); // 預設 10 秒 const [timeLeft, setTimeLeft] = useState(10); const [isRunning, setIsRunning] = useState(false); const [isEditing, setIsEditing] = useState(false); const [isFinished, setIsFinished] = useState(false); // 自訂輸入狀態 const [inputHours, setInputHours] = useState('00'); const [inputMinutes, setInputMinutes] = useState('00'); const [inputSeconds, setInputSeconds] = useState('10'); // 新增:快速輸入秒數的狀態 const [quickSeconds, setQuickSeconds] = useState(''); // 新增:提醒聲音選擇狀態 const [selectedSound, setSelectedSound] = useState('beep'); // 新增:保存共用的 AudioContext 實例,避免被瀏覽器阻擋 const audioCtxRef = useRef(null); // 環形進度條設定 const circleRadius = 120; const circumference = 2 * Math.PI * circleRadius; const strokeDashoffset = initialTime > 0 ? circumference - (timeLeft / initialTime) * circumference : 0; // 播放提示音效 (使用 Web Audio API) const playAlarmSound = (soundOverride) => { const soundToPlay = soundOverride || selectedSound; try { // 使用共用的 AudioContext,取代每次都建立新的 if (!audioCtxRef.current) { audioCtxRef.current = new (window.AudioContext || window.webkitAudioContext)(); } const audioCtx = audioCtxRef.current; // 確保音效引擎處於啟動狀態 (喚醒它) if (audioCtx.state === 'suspended') { audioCtx.resume(); } const now = audioCtx.currentTime; const playOscillator = (type, freq, startTime, duration, startVol = 0.2, endVol = 0.001) => { const oscillator = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); oscillator.type = type; oscillator.frequency.setValueAtTime(freq, startTime); // 設定音量與淡出效果 gainNode.gain.setValueAtTime(startVol, startTime); gainNode.gain.exponentialRampToValueAtTime(endVol, startTime + duration); oscillator.connect(gainNode); gainNode.connect(audioCtx.destination); oscillator.start(startTime); oscillator.stop(startTime + duration); }; if (soundToPlay === 'beep') { // 傳統短嗶聲 (3 次) playOscillator('sine', 880, now, 0.2); playOscillator('sine', 880, now + 0.3, 0.2); playOscillator('sine', 880, now + 0.6, 0.2); } else if (soundToPlay === 'digital') { // 電子鬧鐘 (6 次急促聲) for (let i = 0; i < 6; i++) { playOscillator('square', 600, now + i * 0.15, 0.08, 0.1, 0.001); } } else if (soundToPlay === 'chime') { // 柔和門鈴/和弦聲 playOscillator('sine', 659.25, now, 0.6, 0.3, 0.001); // E5 playOscillator('sine', 523.25, now + 0.4, 1.0, 0.3, 0.001); // C5 } } catch (error) { console.log("瀏覽器不支援或阻擋了音效播放", error); } }; // 計時器邏輯 useEffect(() => { let interval = null; if (isRunning && timeLeft > 0) { interval = setInterval(() => { setTimeLeft((prev) => prev - 1); }, 1000); } else if (timeLeft === 0 && isRunning) { setIsRunning(false); setIsFinished(true); playAlarmSound(); // 呼叫播放音效函數 } return () => clearInterval(interval); }, [isRunning, timeLeft, selectedSound]); // 格式化時間顯示 const formatTime = (timeInSeconds) => { const h = Math.floor(timeInSeconds / 3600); const m = Math.floor((timeInSeconds % 3600) / 60); const s = timeInSeconds % 60; if (h > 0) { return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; }; // 控制功能 const toggleTimer = () => { if (timeLeft === 0) return; // 【關鍵修復】:在使用者點擊「播放」的瞬間,初始化並解鎖音效權限 if (!audioCtxRef.current) { audioCtxRef.current = new (window.AudioContext || window.webkitAudioContext)(); } if (audioCtxRef.current.state === 'suspended') { audioCtxRef.current.resume(); } setIsRunning(!isRunning); setIsFinished(false); setIsEditing(false); }; const resetTimer = () => { setIsRunning(false); setTimeLeft(initialTime); setIsFinished(false); }; const setPresetTime = (minutes) => { const seconds = minutes * 60; setInitialTime(seconds); setTimeLeft(seconds); setIsRunning(false); setIsFinished(false); setIsEditing(false); // 更新輸入框顯示 setInputHours(Math.floor(seconds / 3600).toString().padStart(2, '0')); setInputMinutes(Math.floor((seconds % 3600) / 60).toString().padStart(2, '0')); setInputSeconds((seconds % 60).toString().padStart(2, '0')); }; const handleCustomTimeSubmit = (e) => { e.preventDefault(); const h = parseInt(inputHours) || 0; const m = parseInt(inputMinutes) || 0; const s = parseInt(inputSeconds) || 0; const totalSeconds = (h * 3600) + (m * 60) + s; if (totalSeconds > 0) { setInitialTime(totalSeconds); setTimeLeft(totalSeconds); setIsEditing(false); setIsFinished(false); } }; // 新增:處理快速輸入秒數的邏輯 const handleQuickSecondsSubmit = (e) => { e.preventDefault(); const secs = parseInt(quickSeconds); if (secs > 0) { setInitialTime(secs); setTimeLeft(secs); setIsRunning(false); setIsFinished(false); setQuickSeconds(''); // 清空輸入框 // 同步更新齒輪設定視窗的輸入框顯示 setInputHours(Math.floor(secs / 3600).toString().padStart(2, '0')); setInputMinutes(Math.floor((secs % 3600) / 60).toString().padStart(2, '0')); setInputSeconds((secs % 60).toString().padStart(2, '0')); } }; // 決定進度條顏色 (最後 10 秒變紅) const progressColor = timeLeft <= 10 && initialTime > 10 ? 'text-red-500' : 'text-emerald-500'; return (