22 * ImmersiveTimer — 「潮汐穹顶」沉浸模式
33 *
44 * 全屏渐变色场背景(随进度变化),
5- * 弧形光带进度条 + 呼吸缩放大字号倒计时(ImmersiveRing),
6- * 底部极简 icon-only 操作(ImmersiveControls)。
5+ * 中央 Chronos 时间生物(full 模式)+ 底部极简 icon-only 操作(ImmersiveControls)。
76 *
87 * @ai -context: 3.8 心流音乐引擎 + 3.13 具身学习休息引导集成于此;
9- * 中央圆环与底部操作区已拆至 ImmersiveRing / ImmersiveControls
10- * (单文件 ≤300 行规范)。
8+ * 底部操作区已拆至 ImmersiveControls(单文件 ≤300 行规范)。
119 */
1210import { useMemo , useState , useEffect } from 'react' ;
1311import { motion } from 'framer-motion' ;
1412import { usePomodoroStore } from '../store/usePomodoroStore' ;
15- import { useShallow } from 'zustand/react/shallow' ;
16- import { useReducedMotion } from '@/hooks/useReducedMotion' ;
1713import { BEAT } from '@/lib/animation/springConfig' ;
1814import { useAudioPrefsStore } from '@/lib/audio/audioPrefsStore' ;
1915import { AnchorReminderOverlay } from './AnchorReminderOverlay' ;
2016import { ChronosCanvas } from './chronos/ChronosCanvas' ;
21- import { ImmersiveRing } from './ImmersiveRing' ;
17+ import { ChronosStateRow } from './chronos/ChronosStateRow' ;
18+ import { toChronosState } from './chronos/chronosState' ;
2219import { ImmersiveControls } from './ImmersiveControls' ;
2320import { useFlowMusic } from '@/hooks/useFlowMusic' ;
2421
25- const PHASE_LABELS : Record < string , string > = {
26- work : '专注中' ,
27- short_break : '休息中' ,
28- long_break : '休息中' ,
29- } ;
30-
3122interface ImmersiveTimerProps {
3223 /** 背景音(白噪音)开关状态 */
3324 whiteNoiseEnabled ?: boolean ;
@@ -43,12 +34,6 @@ interface ImmersiveTimerProps {
4334 focusScore ?: number ;
4435 /** 3.8 心流音乐引擎是否激活 */
4536 flowMusicEnabled ?: boolean ;
46- /** 专注完成绽放触发(Chronos 时间生物) */
47- bloom ?: boolean ;
48- /** 环境光亮度 0-1(P2 暗环境自发光补偿) */
49- ambientLight ?: number ;
50- /** Chronos 生物形态开关(缺省 true,关闭回退经典环) */
51- chronosEnabled ?: boolean ;
5237}
5338
5439/** 3.13 具身学习休息活动列表 */
@@ -101,33 +86,42 @@ export default function ImmersiveTimer({
10186 lastSessionKeywords,
10287 focusScore = 0 ,
10388 flowMusicEnabled = false ,
104- bloom = false ,
105- ambientLight = 0.5 ,
106- chronosEnabled = true ,
10789} : ImmersiveTimerProps ) {
108- const prefersReduced = useReducedMotion ( ) ;
109-
110- const {
111- remainingSeconds,
112- totalSeconds,
113- phase,
114- isRunning,
115- currentGoal,
116- pause,
117- resume,
118- reset,
119- skip,
120- } = usePomodoroStore ( useShallow ( s => s ) ) ;
90+ // P0-1 细粒度订阅:整 store 订阅(useShallow(s => s))会在任何字段变化时
91+ // 重渲染沉浸层;remainingSeconds 每秒 tick 不可避免,但其余字段独立订阅
92+ // 避免 settings/presets 等无关变化波及本层
93+ const remainingSeconds = usePomodoroStore ( ( s ) => s . remainingSeconds ) ;
94+ const totalSeconds = usePomodoroStore ( ( s ) => s . totalSeconds ) ;
95+ const phase = usePomodoroStore ( ( s ) => s . phase ) ;
96+ const isRunning = usePomodoroStore ( ( s ) => s . isRunning ) ;
97+ const isPaused = usePomodoroStore ( ( s ) => s . isPaused ) ;
98+ const isArmed = usePomodoroStore ( ( s ) => s . isArmed ) ;
99+ const activePreset = usePomodoroStore ( ( s ) => s . activePreset ) ;
100+ const isStepDive = usePomodoroStore ( ( s ) => s . isStepDive ) ;
101+ const currentGoal = usePomodoroStore ( ( s ) => s . currentGoal ) ;
102+ // 动作(稳定引用)
103+ const pause = usePomodoroStore ( ( s ) => s . pause ) ;
104+ const startBreathingDive = usePomodoroStore ( ( s ) => s . startBreathingDive ) ;
105+ const skipBreathingDive = usePomodoroStore ( ( s ) => s . skipBreathingDive ) ;
106+ const resume = usePomodoroStore ( ( s ) => s . resume ) ;
107+ const reset = usePomodoroStore ( ( s ) => s . reset ) ;
108+ const skip = usePomodoroStore ( ( s ) => s . skip ) ;
121109
122- // Chronos 点击交互(时间生物 = 核心交互点 ):
123- // 专注 → 暂停 /继续;休息 → 提前结束休息 ;长按 → 沉睡
110+ // Chronos 点击交互(设计详解状态机 ):
111+ // 专注 → 呼吸缓解(30s) /继续;呼吸(运行中) → 跳过呼吸直接专注; 休息 → 提前结束 ;长按 → 沉睡
124112 const handleChronosTap = ( ) => {
125- if ( phase === 'work' ) {
126- if ( isRunning ) pause ( ) ;
113+ const cs = toChronosState ( { isArmed, isRunning, isPaused, phase, isStepDive } ) ;
114+ if ( cs === 'focus' ) {
115+ if ( isRunning ) startBreathingDive ( ) ;
127116 else resume ( ) ;
128- } else {
117+ } else if ( cs === 'breathing' ) {
118+ // 呼吸态(迈步/呼吸缓解)运行中 → 跳过呼吸直接进入专注
119+ if ( isRunning ) skipBreathingDive ( ) ;
120+ else resume ( ) ;
121+ } else if ( cs === 'short_break' || cs === 'long_break' ) {
129122 skip ( ) ;
130123 }
124+ // asleep 在沉浸模式不应出现(进入沉浸必经运行/暂停),忽略
131125 } ;
132126 const handleChronosLongPress = ( ) => {
133127 reset ( ) ;
@@ -155,7 +149,6 @@ export default function ImmersiveTimer({
155149 const minutes = Math . floor ( remainingSeconds / 60 ) ;
156150 const seconds = remainingSeconds % 60 ;
157151 const timeStr = `${ String ( minutes ) . padStart ( 2 , '0' ) } :${ String ( seconds ) . padStart ( 2 , '0' ) } ` ;
158- const label = PHASE_LABELS [ phase ] ?? '专注中' ;
159152
160153 const backgroundGradient = useMemo (
161154 ( ) => getBackgroundGradient ( progressPercent ) ,
@@ -164,18 +157,6 @@ export default function ImmersiveTimer({
164157
165158 const isBreak = phase === 'short_break' || phase === 'long_break' ;
166159
167- // 经典环呼吸动画参数(chronosEnabled=false 时回退)
168- const breatheAnimation = prefersReduced
169- ? { }
170- : {
171- scale : [ 1 , 1.02 , 1 ] ,
172- transition : {
173- duration : BEAT . x5 / 100 ,
174- repeat : Infinity ,
175- ease : 'easeInOut' as const ,
176- } ,
177- } ;
178-
179160 // 3.13 休息活动轮换索引
180161 const [ activityIndex , setActivityIndex ] = useState ( 0 ) ;
181162 useEffect ( ( ) => {
@@ -245,23 +226,29 @@ export default function ImmersiveTimer({
245226 </ motion . div >
246227 ) }
247228
248- { /* 中央计时器区域 — Chronos 时间生物(可回退经典环) */ }
249- { chronosEnabled ? (
250- < ChronosCanvas
251- mode = "full"
252- phase = { phase }
253- isRunning = { isRunning }
254- remainingSeconds = { remainingSeconds }
255- started
256- intensity = { focusScore > 0 ? focusScore : 50 }
257- ambientLight = { ambientLight }
258- bloom = { bloom }
259- onTap = { handleChronosTap }
260- onLongPress = { handleChronosLongPress }
261- timeStr = { timeStr }
229+ { /* 中央计时器区域 — Chronos 时间生物(唯一形态) */ }
230+ < ChronosCanvas
231+ mode = "full"
232+ phase = { phase }
233+ isRunning = { isRunning }
234+ isPaused = { isPaused }
235+ isArmed = { isArmed }
236+ remainingSeconds = { remainingSeconds }
237+ totalSeconds = { totalSeconds }
238+ isStepDive = { isStepDive }
239+ mood = { activePreset ?. mood }
240+ onTap = { handleChronosTap }
241+ onLongPress = { handleChronosLongPress }
242+ timeStr = { timeStr }
243+ />
244+ { /* 状态行(沉浸深色背景专用变体):仅 work 阶段显示(休息时由活动建议卡占据该区域) */ }
245+ { phase === 'work' && (
246+ < ChronosStateRow
247+ input = { { isArmed, isRunning, isPaused, phase } }
248+ variant = "immersive"
249+ hint = { isRunning ? '点击暂停 · 长按放弃' : '点击继续' }
250+ className = "absolute bottom-32 left-0 right-0"
262251 />
263- ) : (
264- < ImmersiveRing progress = { progress } timeStr = { timeStr } label = { label } breatheAnimation = { breatheAnimation } />
265252 ) }
266253
267254 { /* T2 记忆锚点提醒浮层 — work 阶段每 12 分钟一句话要点,15 秒自动消失 */ }
0 commit comments