@@ -54,6 +54,8 @@ interface PomodoroState {
5454 isPaused : boolean ;
5555 remainingSeconds : number ;
5656 totalSeconds : number ;
57+ /** 当前运行阶段的绝对截止时间戳(ms)——墙钟校准源,休眠唤醒/计时漂移后自愈 */
58+ endAt : number | null ;
5759 completedCount : number ;
5860 /** @deprecated 兼容层:由 activePreset 派生,下版本移除 */
5961 mode : Mode ;
@@ -197,6 +199,7 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
197199 isPaused : false ,
198200 remainingSeconds : defaultSettings . workDuration * 60 ,
199201 totalSeconds : defaultSettings . workDuration * 60 ,
202+ endAt : null ,
200203 completedCount : 0 ,
201204 mode : 'self_study' ,
202205 settings : defaultSettings ,
@@ -254,6 +257,8 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
254257 start : ( ) => {
255258 set ( ( s ) => ( {
256259 isRunning : true , isPaused : false , sessionStartTime : Date . now ( ) ,
260+ // 墙钟截止点:tick 据此校准,避免 interval 漂移/后台节流累积误差
261+ endAt : Date . now ( ) + s . remainingSeconds * 1000 ,
257262 lastAction : 'start' as PomodoroAction , lastActionCounter : s . lastActionCounter + 1 ,
258263 } ) ) ;
259264 soundPlayer . play ( 'pomodoro_start' ) ;
@@ -264,6 +269,7 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
264269 set ( ( s ) => ( {
265270 mode : 'self_study' , phase : 'work' ,
266271 remainingSeconds : MINI_DIVE_SECONDS , totalSeconds : MINI_DIVE_SECONDS ,
272+ endAt : Date . now ( ) + MINI_DIVE_SECONDS * 1000 ,
267273 isMiniDive : true , isRunning : true , isPaused : false ,
268274 sessionStartTime : Date . now ( ) , currentGoal : '首潜 · 3 分钟体验' ,
269275 lastAction : 'start' as PomodoroAction , lastActionCounter : s . lastActionCounter + 1 ,
@@ -276,6 +282,7 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
276282 set ( ( s ) => ( {
277283 mode : 'self_study' , phase : 'work' ,
278284 remainingSeconds : COMMIT_DIVE_SECONDS , totalSeconds : COMMIT_DIVE_SECONDS ,
285+ endAt : Date . now ( ) + COMMIT_DIVE_SECONDS * 1000 ,
279286 isMiniDive : true , isRunning : true , isPaused : false ,
280287 sessionStartTime : Date . now ( ) , currentGoal : '就 5 分钟 · 随时可以停' ,
281288 lastAction : 'start' as PomodoroAction , lastActionCounter : s . lastActionCounter + 1 ,
@@ -285,23 +292,25 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
285292
286293 pause : ( ) => {
287294 set ( ( s ) => ( {
288- isRunning : false , isPaused : true ,
295+ isRunning : false , isPaused : true , endAt : null ,
289296 lastAction : 'pause' as PomodoroAction , lastActionCounter : s . lastActionCounter + 1 ,
290297 } ) ) ;
291298 soundPlayer . play ( 'pomodoro_pause' ) ;
292299 } ,
293300
294301 resume : ( ) => {
295302 const { sessionStartTime, wasImmersive } = get ( ) ;
296- set ( {
303+ set ( ( s ) => ( {
297304 isRunning : true ,
298305 isPaused : false ,
306+ // 恢复运行重设墙钟截止点(暂停期间时间不计入)
307+ endAt : Date . now ( ) + s . remainingSeconds * 1000 ,
299308 // 如果 sessionStartTime 为空(重置后),重新记录
300309 sessionStartTime : sessionStartTime ?? Date . now ( ) ,
301310 // 若上次是从沉浸模式退出的,自动重新进入沉浸
302311 isImmersive : wasImmersive ? true : get ( ) . isImmersive ,
303312 wasImmersive : false ,
304- } ) ;
313+ } ) ) ;
305314 } ,
306315
307316 reset : ( ) => {
@@ -312,6 +321,7 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
312321 totalSeconds : duration ,
313322 isRunning : false ,
314323 isPaused : false ,
324+ endAt : null ,
315325 sessionStartTime : null ,
316326 wasImmersive : false ,
317327 isMiniDive : false ,
@@ -331,6 +341,7 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
331341 completedCount : newCount ,
332342 isRunning : false ,
333343 isPaused : false ,
344+ endAt : null ,
334345 isMiniDive : false ,
335346 } ) ;
336347 } ,
@@ -376,7 +387,7 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
376387 soundPlayer . play ( 'pomodoro_pause' ) ;
377388 }
378389 set ( ( s ) => ( {
379- isImmersive : false , wasImmersive : true , isRunning : false , isPaused : true ,
390+ isImmersive : false , wasImmersive : true , isRunning : false , isPaused : true , endAt : null ,
380391 lastAction : 'exit_immersive' as PomodoroAction , lastActionCounter : s . lastActionCounter + 1 ,
381392 } ) ) ;
382393 } ,
@@ -476,6 +487,8 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
476487 completedCount : newCount ,
477488 isRunning : shouldAutoStart ,
478489 isPaused : ! shouldAutoStart ,
490+ // 自动进入下一阶段时重设墙钟截止点;否则清空等待下次 start/resume
491+ endAt : shouldAutoStart ? Date . now ( ) + duration * 1000 : null ,
479492 // 迷你潜水仅限一个工作阶段,阶段切换即恢复常规节律
480493 isMiniDive : false ,
481494 // 切换到新阶段时清空计时,下一个 start/resume 会重新设置
@@ -490,7 +503,16 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
490503 showCompletionOverlay : phase === 'work' ? true : s . showCompletionOverlay ,
491504 } ) ) ;
492505 } else {
493- const nextRemaining = remainingSeconds - 1 ;
506+ let nextRemaining = remainingSeconds - 1 ;
507+ // 墙钟校准:系统休眠唤醒/setInterval 漂移后,与 endAt 误差超过 1s 时直接吸附,
508+ // 避免递减模型累积误差(误差 ≤1s 时保持 -1 节奏,防止 UI 跳秒)
509+ const { endAt } = get ( ) ;
510+ if ( endAt != null ) {
511+ const wallRemaining = Math . max ( 0 , Math . round ( ( endAt - Date . now ( ) ) / 1000 ) ) ;
512+ if ( Math . abs ( wallRemaining - nextRemaining ) > 1 ) {
513+ nextRemaining = wallRemaining ;
514+ }
515+ }
494516 // 静默预设跳过预警和滴答音
495517 if ( ! isSilent ) {
496518 // 预警(工作阶段)—— 支持自定义时点
0 commit comments