@@ -44,6 +44,8 @@ interface PomodoroState {
4444 sessionStartTime : number | null ;
4545 /** 当前番茄目标文字 */
4646 currentGoal : string | null ;
47+ /** 首潜迷你会话标记:3 分钟体验潜水,会话时长按实际记录而非 settings 时长 */
48+ isMiniDive : boolean ;
4749 /** 是否处于沉浸专注模式 */
4850 isImmersive : boolean ;
4951 /** 退出沉浸后标记,用于 resume 时自动重入 */
@@ -64,6 +66,8 @@ interface PomodoroState {
6466 lastSessionActualDuration : number | null ;
6567
6668 start : ( ) => void ;
69+ /** 开始首潜 3 分钟迷你体验(新手引导专用,不改动用户设置) */
70+ startMiniDive : ( ) => void ;
6771 pause : ( ) => void ;
6872 resume : ( ) => void ;
6973 reset : ( ) => void ;
@@ -90,6 +94,9 @@ const getPhaseDuration = (phase: Phase, settings: PomodoroSettings, mode?: Mode)
9094 }
9195} ;
9296
97+ /** 首潜迷你体验时长(3 分钟),见新手引导系统 */
98+ export const MINI_DIVE_SECONDS = 180 ;
99+
93100const getNextPhase = (
94101 currentPhase : Phase ,
95102 completedCount : number ,
@@ -106,6 +113,24 @@ const getNextPhase = (
106113 return 'work' ;
107114} ;
108115
116+ /**
117+ * 计算阶段结束后的完成计数:
118+ * - 长休结束 → 归零(一轮完成)
119+ * - 工作结束 → +1;上课模式无长休,计数达到周期上限后回绕,避免无限累加
120+ * - 其他阶段 → 不变
121+ */
122+ const getNextCount = (
123+ phase : Phase ,
124+ completedCount : number ,
125+ longBreakInterval : number ,
126+ mode ?: Mode ,
127+ ) : number => {
128+ if ( phase === 'long_break' ) return 0 ;
129+ if ( phase !== 'work' ) return completedCount ;
130+ if ( mode === 'class' ) return ( completedCount % longBreakInterval ) + 1 ;
131+ return completedCount + 1 ;
132+ } ;
133+
109134export const usePomodoroStore = create < PomodoroState > ( ( set , get ) => {
110135 const defaultSettings : PomodoroSettings = {
111136 workDuration : 25 ,
@@ -130,6 +155,7 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
130155 settings : defaultSettings ,
131156 sessionStartTime : null ,
132157 currentGoal : null ,
158+ isMiniDive : false ,
133159 isImmersive : false ,
134160 wasImmersive : false ,
135161 aiRecommendedDuration : undefined ,
@@ -168,6 +194,18 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
168194 soundPlayer . play ( 'pomodoro_start' ) ;
169195 } ,
170196
197+ startMiniDive : ( ) => {
198+ // 3 分钟真实专注:走完整 tick 链路(记会话/触发成就),duration 按 180s 如实记录
199+ set ( ( s ) => ( {
200+ mode : 'self_study' , phase : 'work' ,
201+ remainingSeconds : MINI_DIVE_SECONDS , totalSeconds : MINI_DIVE_SECONDS ,
202+ isMiniDive : true , isRunning : true , isPaused : false ,
203+ sessionStartTime : Date . now ( ) , currentGoal : '首潜 · 3 分钟体验' ,
204+ lastAction : 'start' as PomodoroAction , lastActionCounter : s . lastActionCounter + 1 ,
205+ } ) ) ;
206+ soundPlayer . play ( 'pomodoro_start' ) ;
207+ } ,
208+
171209 pause : ( ) => {
172210 set ( ( s ) => ( {
173211 isRunning : false , isPaused : true ,
@@ -199,12 +237,13 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
199237 isPaused : false ,
200238 sessionStartTime : null ,
201239 wasImmersive : false ,
240+ isMiniDive : false ,
202241 } ) ;
203242 } ,
204243
205244 skip : ( ) => {
206245 const { phase, completedCount, settings, mode } = get ( ) ;
207- const newCount = phase === 'long_break' ? 0 : ( phase === 'work' ? completedCount + 1 : completedCount ) ;
246+ const newCount = getNextCount ( phase , completedCount , settings . longBreakInterval , mode ) ;
208247 const nextPhase = getNextPhase ( phase , completedCount , settings . longBreakInterval , mode ) ;
209248 const duration = getPhaseDuration ( nextPhase , settings , mode ) ;
210249 set ( {
@@ -214,12 +253,15 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
214253 completedCount : newCount ,
215254 isRunning : false ,
216255 isPaused : false ,
256+ isMiniDive : false ,
217257 } ) ;
218258 } ,
219259
220260 setMode : ( mode ) => {
221- const { settings, phase, isRunning, isPaused } = get ( ) ;
222- set ( { mode } ) ;
261+ const { settings, phase, isRunning, isPaused, mode : prevMode } = get ( ) ;
262+ if ( mode === prevMode ) return ;
263+ // 切换模式 = 开启新周期:计数归零,避免跨模式累计(自习首轮跳到 8 的根因)
264+ set ( { mode, completedCount : 0 } ) ;
223265 // 切换模式后,若计时器未运行,重置当前阶段时长
224266 if ( ! isRunning && ! isPaused ) {
225267 const duration = getPhaseDuration ( phase , settings , mode ) ;
@@ -252,7 +294,7 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
252294 if ( remainingSeconds <= 1 ) {
253295 // Phase completed
254296 const wasRunning = isRunning ;
255- const newCount = phase === 'long_break' ? 0 : ( phase === 'work' ? completedCount + 1 : completedCount ) ;
297+ const newCount = getNextCount ( phase , completedCount , settings . longBreakInterval , mode ) ;
256298 const nextPhase = getNextPhase ( phase , completedCount , settings . longBreakInterval , mode ) ;
257299 const duration = getPhaseDuration ( nextPhase , settings , mode ) ;
258300 const isCycleComplete = phase === 'long_break' ;
@@ -272,14 +314,17 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
272314 // 记录完成的番茄会话
273315 let actualDuration : number | null = null ;
274316 if ( phase === 'work' ) {
275- const { sessionStartTime : sst } = get ( ) ;
276- const workMinutes = mode === 'class' ? settings . classDuration : settings . workDuration ;
317+ const { sessionStartTime : sst , isMiniDive } = get ( ) ;
318+ // 迷你潜水按实际 180s 记录,避免污染效率统计(首潜决策:计入成就)
319+ const plannedSeconds = isMiniDive
320+ ? MINI_DIVE_SECONDS
321+ : ( mode === 'class' ? settings . classDuration : settings . workDuration ) * 60 ;
277322 actualDuration = sst
278323 ? Math . round ( ( Date . now ( ) - sst ) / 1000 )
279- : workMinutes * 60 ;
324+ : plannedSeconds ;
280325 recordSession ( {
281326 mode : get ( ) . mode ,
282- duration : workMinutes * 60 ,
327+ duration : plannedSeconds ,
283328 actualDuration,
284329 completedAt : new Date ( ) ,
285330 interrupted : false ,
@@ -328,6 +373,8 @@ export const usePomodoroStore = create<PomodoroState>((set, get) => {
328373 completedCount : newCount ,
329374 isRunning : shouldAutoStart ,
330375 isPaused : ! shouldAutoStart ,
376+ // 迷你潜水仅限一个工作阶段,阶段切换即恢复常规节律
377+ isMiniDive : false ,
331378 // 切换到新阶段时清空计时,下一个 start/resume 会重新设置
332379 sessionStartTime : null ,
333380 // 发出 phase_complete 动作信号
0 commit comments