11/**
22 * @ai -context: pomodoro 功能模块页面:PomodoroPage。
33 */
4- import { useEffect , useRef , useState , useMemo } from 'react' ;
4+ import { useEffect , useRef , useState , useMemo , useCallback } from 'react' ;
55import { createPortal } from 'react-dom' ;
66import { AnimatePresence , motion } from 'framer-motion' ;
7- import { Play , Pause , RotateCcw , SkipForward , Clock , Volume2 , VolumeX , Focus , Plus , GraduationCap , BookOpen , PenLine , BookMarked , Brain , Timer , Moon , Coffee , Dumbbell , Music , Languages , Calculator , Microscope } from 'lucide-react' ;
7+ import { Play , Pause , RotateCcw , SkipForward , Clock , Volume2 , VolumeX , Focus } from 'lucide-react' ;
88import { cn } from '@/lib/utils' ;
99import { db } from '@/lib/storage' ;
1010import TimerRing from '../components/TimerRing' ;
@@ -13,6 +13,8 @@ import ImmersiveTimer from '../components/ImmersiveTimer';
1313import SlideToExit from '../components/SlideToExit' ;
1414import CycleMarkers from '../components/CycleMarkers' ;
1515import PresetEditor from '../components/PresetEditor' ;
16+ import PresetTabs from '../components/PresetTabs' ;
17+ import { CompletionCelebration } from '../components/CompletionCelebration' ;
1618import { usePomodoroStore } from '../store/usePomodoroStore' ;
1719import { useShallow } from 'zustand/react/shallow' ;
1820import { useAudioPlayer } from '@/lib/audio/useAudioPlayer' ;
@@ -27,20 +29,14 @@ const WHITE_NOISE_FADE_MS = 1000;
2729const WHITE_NOISE_FADE_OUT_MS = 1500 ;
2830const TIMER_TICK_INTERVAL_MS = 1000 ;
2931
30- /** 预设图标映射(lucide 图标名 → 组件) */
31- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32- const PRESET_ICONS : Record < string , React . ComponentType < any > > = {
33- GraduationCap, BookOpen, PenLine, BookMarked, Brain, Timer,
34- Moon, Coffee, Dumbbell, Music, Languages, Calculator, Microscope,
35- } ;
36-
3732export default function PomodoroPage ( ) {
3833 const {
3934 phase, isRunning, isPaused, remainingSeconds, totalSeconds,
4035 completedCount, settings, currentGoal, isImmersive,
4136 activePreset, presets,
4237 start, pause, resume, reset, skip, setPreset, setCurrentGoal,
4338 enterImmersive, exitImmersive, tick, createPreset,
39+ showCompletionOverlay, dismissCompletionOverlay, lastSessionActualDuration,
4440 } = usePomodoroStore ( useShallow ( s => s ) ) ;
4541
4642 usePomodoroEffects ( ) ;
@@ -106,6 +102,9 @@ export default function PomodoroPage() {
106102 else setGoalModalOpen ( true ) ;
107103 } ;
108104
105+ // P3-19 稳定回调引用,供 memo 化的 PresetTabs 避免每秒重渲染
106+ const openPresetEditor = useCallback ( ( ) => setPresetEditorOpen ( true ) , [ ] ) ;
107+
109108 /**
110109 * 提交目标并开始番茄。goal 为空字符串时表示“跳过目标”:
111110 * 仍然启动计时,但不记录目标(currentGoal 置 null,也不写目标记忆库)。
@@ -180,55 +179,14 @@ export default function PomodoroPage() {
180179 >
181180 { /* 背景环境光 — 由3D场景提供,已移除 */ }
182181
183- { /* Preset tabs — 横向滚动预设列表 */ }
184- < motion . div
185- className = "flex items-center gap-0.5 p-1 bg-bg-secondary/60 backdrop-blur-sm rounded-full border border-border/20 max-w-full overflow-x-auto"
186- initial = { { opacity : 0 , y : - 10 } }
187- animate = { { opacity : 1 , y : 0 } }
188- transition = { { delay : 0.1 , ...SPRING . gentle } }
189- >
190- { presets . map ( ( preset ) => {
191- const Icon = PRESET_ICONS [ preset . icon ] ?? BookOpen ;
192- const isActive = activePreset ?. id === preset . id ;
193- return (
194- < motion . button
195- key = { preset . id }
196- onClick = { ( ) => setPreset ( preset . id ) }
197- whileTap = { { scale : 0.97 } }
198- className = { cn (
199- 'relative flex items-center gap-1.5 px-4 py-2 rounded-full text-[13px] font-medium transition-all duration-300 whitespace-nowrap' ,
200- isActive
201- ? 'text-white shadow-[0_2px_12px_rgba(91,138,114,0.3)]'
202- : 'text-text-secondary hover:text-text-primary' ,
203- ) }
204- >
205- { isActive && (
206- < motion . div
207- layoutId = "pomo-mode-bg"
208- className = "absolute inset-0 rounded-full bg-brand-500"
209- transition = { SPRING . default }
210- />
211- ) }
212- < span className = "relative flex items-center gap-1.5" >
213- < Icon className = "w-4 h-4" strokeWidth = { 1.5 } />
214- { preset . name }
215- < span className = { cn ( 'text-[10px] opacity-60' , isActive && 'opacity-80' ) } > · { preset . workDuration } min</ span >
216- </ span >
217- </ motion . button >
218- ) ;
219- } ) }
220- { /* "+" 快捷创建预设入口(达上限时隐藏) */ }
221- { presets . length < MAX_PRESETS && (
222- < motion . button
223- onClick = { ( ) => setPresetEditorOpen ( true ) }
224- whileTap = { { scale : 0.9 } }
225- className = "flex items-center justify-center w-8 h-8 rounded-full text-text-tertiary hover:text-text-primary hover:bg-bg-tertiary/60 transition-all duration-200 flex-shrink-0"
226- aria-label = "新建预设"
227- >
228- < Plus className = "w-4 h-4" strokeWidth = { 1.5 } />
229- </ motion . button >
230- ) }
231- </ motion . div >
182+ { /* Preset tabs — 横向滚动预设列表(P3-19 memo 化) */ }
183+ < PresetTabs
184+ presets = { presets }
185+ activePresetId = { activePreset ?. id }
186+ canCreate = { presets . length < MAX_PRESETS }
187+ onSelect = { setPreset }
188+ onCreate = { openPresetEditor }
189+ />
232190
233191 { /* 预设提示 */ }
234192 < motion . div
@@ -383,6 +341,18 @@ export default function PomodoroPage() {
383341 </ motion . div >
384342 ) }
385343 </ AnimatePresence >
344+
345+ { /* v0.29: 深潜完成庆祝覆盖层 */ }
346+ < CompletionCelebration
347+ visible = { showCompletionOverlay }
348+ durationSeconds = { lastSessionActualDuration ?? ( activePreset ?. workDuration ?? settings . workDuration ) * 60 }
349+ goal = { currentGoal }
350+ presetName = { activePreset ?. name ?? null }
351+ totalDepth = { ( ( completedCount + 1 ) * ( activePreset ?. workDuration ?? 25 ) ) * 4 }
352+ depthGained = { ( lastSessionActualDuration ?? ( activePreset ?. workDuration ?? 25 ) * 60 ) / 60 * 4 }
353+ onClose = { dismissCompletionOverlay }
354+ onContinue = { ( ) => { dismissCompletionOverlay ( ) ; setGoalModalOpen ( true ) ; } }
355+ />
386356 </ >
387357 ) ;
388358}
0 commit comments