Skip to content

Commit a7d69de

Browse files
committed
refactor(pomodoro): usePomodoroStore 拆分为 timer/settings/session/preset 四 slice
1 parent 3c725ed commit a7d69de

6 files changed

Lines changed: 807 additions & 730 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/**
2+
* 番茄钟状态仓库 — 类型定义与共享纯函数(slice 拆分第一文件)
3+
* Pomodoro store types — shared types and pure helpers for slices
4+
*
5+
* @ai-context: 2026-08 拆分自 usePomodoroStore(811 行超限)。PomodoroState
6+
* 为全 store 契约,timerSlice/settingsSlice/presetSlice 各实现子集;
7+
* 阶段时长/下一阶段/计数迁移等纯函数集中于此,禁止在各 slice 内重复。
8+
* @ai-context: Split from the 811-line monolith; PomodoroState is the full
9+
* contract while slices implement subsets. Phase math stays here as pure
10+
* functions shared by all slices.
11+
*/
12+
import type { StateCreator } from 'zustand';
13+
import type { PomodoroPreset } from '@/types/models';
14+
15+
export type Phase = 'work' | 'short_break' | 'long_break';
16+
/** @deprecated 仅为兼容旧会话记录保留,新代码使用 preset */
17+
export type Mode = 'class' | 'self_study';
18+
19+
/** 番茄钟动作信号类型(供 usePomodoroEffects 监听) */
20+
export type PomodoroAction =
21+
| 'start'
22+
| 'pause'
23+
| 'exit_immersive'
24+
| 'tick_5min_warning'
25+
| 'tick_final'
26+
| 'phase_complete'
27+
| null;
28+
29+
export interface PomodoroSettings {
30+
workDuration: number;
31+
shortBreakDuration: number;
32+
longBreakDuration: number;
33+
longBreakInterval: number;
34+
autoStartBreak: boolean;
35+
autoStartWork: boolean;
36+
soundEnabled: boolean;
37+
notificationEnabled: boolean;
38+
classDuration: number;
39+
// v0.28 扩展
40+
activePresetId?: string;
41+
warningMinutes?: number;
42+
tickFinalEnabled?: boolean;
43+
completionSoundId?: string;
44+
warningSoundId?: string;
45+
}
46+
47+
export interface PomodoroState {
48+
// ── 计时器状态 ──
49+
phase: Phase;
50+
isRunning: boolean;
51+
isPaused: boolean;
52+
remainingSeconds: number;
53+
totalSeconds: number;
54+
/** 当前运行阶段的绝对截止时间戳(ms)——墙钟校准源,休眠唤醒/计时漂移后自愈 */
55+
endAt: number | null;
56+
completedCount: number;
57+
/** @deprecated 兼容层:由 activePreset 派生,下版本移除 */
58+
mode: Mode;
59+
/** 当前工作会话开始时间戳(ms),用于计算 actualDuration */
60+
sessionStartTime: number | null;
61+
/** 暂停时刻时间戳(ms),用于排除暂停时间对 actualDuration 的影响 */
62+
pausedAt: number | null;
63+
/** 累计暂停时间(ms),actualDuration = (now - sessionStartTime - totalPausedMs) / 1000 */
64+
totalPausedMs: number;
65+
/** 当前番茄目标文字 */
66+
currentGoal: string | null;
67+
/** 首潜迷你会话标记:3 分钟体验潜水,会话时长按实际记录而非 settings 时长 */
68+
isMiniDive: boolean;
69+
/** 是否处于沉浸专注模式 */
70+
isImmersive: boolean;
71+
/** 退出沉浸后标记,用于 resume 时自动重入 */
72+
wasImmersive: boolean;
73+
/** AI 推荐的工作时长(分钟) */
74+
aiRecommendedDuration?: number;
75+
/** AI 推荐理由文本 */
76+
aiReasoning?: string;
77+
/** 动作信号(供 usePomodoroEffects 消费) */
78+
lastAction: PomodoroAction;
79+
lastActionCounter: number;
80+
/** phase_complete 时附带的已完成阶段 */
81+
lastCompletedPhase: Phase | null;
82+
/** phase_complete 时是否完成整轮 */
83+
isCycleComplete: boolean;
84+
/** phase_complete 时实际持续秒数 */
85+
lastSessionActualDuration: number | null;
86+
/** v0.29: 深潜完成庆祝覆盖层可见性 */
87+
showCompletionOverlay: boolean;
88+
dismissCompletionOverlay: () => void;
89+
90+
// ── 设置与预设状态 ──
91+
settings: PomodoroSettings;
92+
presets: PomodoroPreset[];
93+
activePreset: PomodoroPreset | null;
94+
95+
// ── 计时器操作 ──
96+
start: () => void;
97+
/** 开始首潜 3 分钟迷你体验(新手引导专用,不改动用户设置) */
98+
startMiniDive: () => void;
99+
/** T3: 开始 5 分钟承诺深潜(拖延重启专用,最小承诺降低启动门槛) */
100+
startCommitDive: () => void;
101+
pause: () => void;
102+
resume: () => void;
103+
reset: () => void;
104+
skip: () => void;
105+
tick: () => void;
106+
enterImmersive: () => void;
107+
exitImmersive: () => void;
108+
/** @deprecated 使用 setPreset 替代 */
109+
setMode: (mode: Mode) => void;
110+
setPreset: (presetId: string) => void;
111+
setCurrentGoal: (goal: string | null) => void;
112+
updateSettings: (settings: Partial<PomodoroSettings>) => void;
113+
/** 设置 AI 推荐结果 */
114+
setAIRecommendation: (duration: number, reasoning: string) => void;
115+
initialize: () => Promise<void>;
116+
117+
// ── 预设 CRUD ──
118+
createPreset: (data: Omit<PomodoroPreset, 'id' | 'sortOrder' | 'createdAt' | 'builtin'>) => Promise<PomodoroPreset>;
119+
updatePreset: (id: string, changes: Partial<Omit<PomodoroPreset, 'id' | 'builtin'>>) => Promise<void>;
120+
deletePreset: (id: string) => Promise<void>;
121+
reorderPresets: (orderedIds: string[]) => Promise<void>;
122+
}
123+
124+
/** slice 创建函数类型(全 state 可见,实现自身子集) */
125+
export type PomodoroSlice<T> = StateCreator<PomodoroState, [], [], T>;
126+
127+
// ── 纯函数:阶段时长与计数迁移 ─────────────────────────────
128+
129+
/** 根据预设获取阶段时长(秒) */
130+
export function getPhaseDuration(phase: Phase, preset: PomodoroPreset | null, settings: PomodoroSettings): number {
131+
if (!preset) {
132+
// 回退到旧 settings(初始化前)
133+
switch (phase) {
134+
case 'work': return settings.workDuration * 60;
135+
case 'short_break': return settings.shortBreakDuration * 60;
136+
case 'long_break': return settings.longBreakDuration * 60;
137+
}
138+
}
139+
switch (phase) {
140+
case 'work': return preset.workDuration * 60;
141+
case 'short_break': return preset.shortBreakDuration * 60;
142+
case 'long_break': return preset.longBreakDuration * 60;
143+
}
144+
}
145+
146+
/** 首潜迷你体验时长(3 分钟),见新手引导系统 */
147+
export const MINI_DIVE_SECONDS = 180;
148+
149+
/** T3 5 分钟承诺深潜时长(拖延情绪调节:不要求完美,只要求开始) */
150+
export const COMMIT_DIVE_SECONDS = 300;
151+
152+
/** 获取预设的有效 longBreakInterval(0 = 无长休) */
153+
export function getInterval(preset: PomodoroPreset | null, settings: PomodoroSettings): number {
154+
return preset ? preset.longBreakInterval : settings.longBreakInterval;
155+
}
156+
157+
export function getNextPhase(
158+
currentPhase: Phase,
159+
completedCount: number,
160+
longBreakInterval: number,
161+
): Phase {
162+
if (currentPhase === 'work') {
163+
// longBreakInterval=0 表示无长休(原上课模式),始终短休
164+
if (longBreakInterval === 0) return 'short_break';
165+
return (completedCount + 1) % longBreakInterval === 0
166+
? 'long_break'
167+
: 'short_break';
168+
}
169+
return 'work';
170+
}
171+
172+
/**
173+
* 计算阶段结束后的完成计数:
174+
* - 长休结束 → 归零(一轮完成)
175+
* - 工作结束 → +1;无长休模式计数达到周期上限后回绕,避免无限累加
176+
* - 其他阶段 → 不变
177+
*/
178+
export function getNextCount(
179+
phase: Phase,
180+
completedCount: number,
181+
longBreakInterval: number,
182+
): number {
183+
if (phase === 'long_break') return 0;
184+
if (phase !== 'work') return completedCount;
185+
// 无长休模式:回绕计数(用固定 4 作为显示上限)
186+
if (longBreakInterval === 0) return (completedCount % 4) + 1;
187+
return completedCount + 1;
188+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* 番茄钟状态仓库 — 预设 slice(CRUD 与活动预设)
3+
* Pomodoro preset slice — preset CRUD and active preset
4+
*
5+
* @ai-context: 拆分自 usePomodoroStore。CRUD 委托给 ../lib/presetService,
6+
* 本 slice 仅同步 store 状态。删除活动预设时回退第一个并重置计时时长;
7+
* reorder 保持 orderedIds 全量顺序。activePreset/mode 与 settingsSlice 的
8+
* setPreset/setMode 共享同一状态槽(Zustand 组合时后者覆盖初始值)。
9+
* @ai-context: Extracted from the monolith; CRUD delegates to presetService
10+
* while this slice keeps store state in sync. Deleting the active preset
11+
* falls back to the first and resets timer duration.
12+
*/
13+
import { getPhaseDuration, type PomodoroSlice, type PomodoroState } from './pomodoroStoreTypes';
14+
import {
15+
createPreset as svcCreatePreset, updatePreset as svcUpdatePreset,
16+
deletePreset as svcDeletePreset, reorderPresets as svcReorderPresets,
17+
} from '../lib/presetService';
18+
import type { PomodoroPreset } from '@/types/models';
19+
20+
export const createPresetSlice: PomodoroSlice<Pick<PomodoroState, 'presets' | 'activePreset' | 'createPreset' | 'updatePreset' | 'deletePreset' | 'reorderPresets'>> = (set, get) => ({
21+
presets: [],
22+
activePreset: null,
23+
24+
createPreset: async (data) => {
25+
const preset = await svcCreatePreset(data);
26+
set((s) => ({ presets: [...s.presets, preset] }));
27+
return preset;
28+
},
29+
30+
updatePreset: async (id, changes) => {
31+
await svcUpdatePreset(id, changes);
32+
set((s) => ({
33+
presets: s.presets.map(p => (p.id === id ? { ...p, ...changes } : p)),
34+
activePreset: s.activePreset?.id === id
35+
? { ...s.activePreset, ...changes } as PomodoroPreset
36+
: s.activePreset,
37+
}));
38+
},
39+
40+
deletePreset: async (id) => {
41+
await svcDeletePreset(id);
42+
const { activePreset, presets } = get();
43+
const remaining = presets.filter(p => p.id !== id);
44+
// 若删除的是当前活动预设,回退到第一个
45+
let newActive = activePreset;
46+
if (activePreset?.id === id) {
47+
newActive = remaining[0] ?? null;
48+
const duration = getPhaseDuration('work', newActive, get().settings);
49+
set({ remainingSeconds: duration, totalSeconds: duration, phase: 'work', completedCount: 0 });
50+
}
51+
set({ presets: remaining, activePreset: newActive, mode: newActive?.silent ? 'class' : 'self_study' });
52+
},
53+
54+
reorderPresets: async (orderedIds) => {
55+
await svcReorderPresets(orderedIds);
56+
set((s) => ({
57+
presets: orderedIds
58+
.map(id => s.presets.find(p => p.id === id))
59+
.filter((p): p is PomodoroPreset => p != null),
60+
}));
61+
},
62+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* 番茄钟状态仓库 — 设置 slice(配置与预设切换)
3+
* Pomodoro settings slice — config and preset switching
4+
*
5+
* @ai-context: 拆分自 usePomodoroStore。updateSettings 在计时器未运行时
6+
* 同步刷新阶段时长;setPreset 切换=开启新周期(计数归零);setMode 为
7+
* @deprecated 兼容层映射到内置预设。saveSettings 持久化失败静默降级。
8+
* @ai-context: Extracted from the monolith. updateSettings refreshes phase
9+
* duration when idle; switching presets starts a fresh cycle (count resets);
10+
* setMode is the deprecated compat shim mapping onto builtin presets.
11+
*/
12+
import { saveSettings } from './usePomodoroPersistence';
13+
import { getPhaseDuration, type Mode, type PomodoroSlice, type PomodoroState } from './pomodoroStoreTypes';
14+
15+
export const createSettingsSlice: PomodoroSlice<Pick<PomodoroState, 'mode' | 'settings' | 'currentGoal' | 'aiRecommendedDuration' | 'aiReasoning' | 'setMode' | 'setPreset' | 'setCurrentGoal' | 'updateSettings' | 'setAIRecommendation'>> = (set, get) => ({
16+
mode: 'self_study',
17+
settings: {
18+
workDuration: 25,
19+
shortBreakDuration: 5,
20+
longBreakDuration: 15,
21+
longBreakInterval: 4,
22+
autoStartBreak: true,
23+
autoStartWork: false,
24+
soundEnabled: true,
25+
notificationEnabled: false,
26+
classDuration: 45,
27+
},
28+
currentGoal: null,
29+
aiRecommendedDuration: undefined,
30+
aiReasoning: undefined,
31+
32+
/** @deprecated 兼容层:将 mode 映射到内置预设 */
33+
setMode: (mode) => {
34+
const { presets } = get();
35+
const target = mode === 'class'
36+
? presets.find(p => p.silent)
37+
: presets.find(p => !p.silent);
38+
if (target) get().setPreset(target.id);
39+
},
40+
41+
setPreset: (presetId) => {
42+
const { settings, phase, isRunning, isPaused, activePreset } = get();
43+
if (activePreset?.id === presetId) return;
44+
const preset = get().presets.find(p => p.id === presetId) ?? null;
45+
if (!preset) return;
46+
// 切换预设 = 开启新周期:计数归零,避免跨预设累计
47+
const mode: Mode = preset.silent ? 'class' : 'self_study';
48+
set({ activePreset: preset, mode, completedCount: 0, phase: 'work' });
49+
// 切换预设后,若计时器未运行,重置当前阶段时长
50+
if (!isRunning && !isPaused) {
51+
const duration = getPhaseDuration('work', preset, settings);
52+
set({ remainingSeconds: duration, totalSeconds: duration });
53+
}
54+
// 持久化 activePresetId
55+
saveSettings({ ...settings, activePresetId: presetId }).catch(() => {});
56+
},
57+
58+
setCurrentGoal: (goal) => set({ currentGoal: goal }),
59+
60+
setAIRecommendation: (duration, reasoning) =>
61+
set({ aiRecommendedDuration: duration, aiReasoning: reasoning }),
62+
63+
updateSettings: (newSettings) => {
64+
const { settings, phase, isRunning, isPaused, activePreset } = get();
65+
const merged = { ...settings, ...newSettings };
66+
67+
// If not running, update timer to reflect new duration
68+
if (!isRunning && !isPaused) {
69+
const duration = getPhaseDuration(phase, activePreset, merged);
70+
set({
71+
settings: merged,
72+
remainingSeconds: duration,
73+
totalSeconds: duration,
74+
});
75+
} else {
76+
set({ settings: merged });
77+
}
78+
79+
// 持久化设置
80+
saveSettings(merged).catch(() => {});
81+
},
82+
});

0 commit comments

Comments
 (0)