feat(fasting): 添加周期性断食计划功能

实现完整的周期性断食计划系统,支持每日自动续订和通知管理:

- 新增周期性断食状态管理(activeCycle、currentCycleSession、cycleHistory)
- 实现周期性断食会话的自动完成和续订逻辑
- 添加独立的周期性断食通知系统,避免与单次断食通知冲突
- 支持暂停/恢复周期性断食计划
- 添加周期性断食数据持久化和水合功能
- 优化断食界面,优先显示周期性断食信息
- 新增空状态引导界面,提升用户体验
- 保持单次断食功能向后兼容
This commit is contained in:
richarjiang
2025-11-12 15:36:35 +08:00
parent 8687be10e8
commit 0bea454dca
7 changed files with 1317 additions and 55 deletions

View File

@@ -6,6 +6,7 @@ import type { RootState } from './index';
export type FastingScheduleOrigin = 'manual' | 'recommended' | 'quick-start' | 'auto';
// 保持向后兼容的单次断食计划
export type FastingSchedule = {
planId: string;
startISO: string;
@@ -15,14 +16,42 @@ export type FastingSchedule = {
origin: FastingScheduleOrigin;
};
// 新的周期性断食计划
export type FastingCycle = {
planId: string;
startHour: number;
startMinute: number;
enabled: boolean;
createdAtISO: string;
lastUpdatedISO: string;
};
// 周期性断食的单次会话
export type FastingCycleSession = {
planId: string;
startISO: string;
endISO: string;
cycleDate: string; // YYYY-MM-DD
completed: boolean;
};
type FastingState = {
// 保持向后兼容的单次计划
activeSchedule: FastingSchedule | null;
history: FastingSchedule[];
// 新的周期性计划
activeCycle: FastingCycle | null;
currentCycleSession: FastingCycleSession | null;
cycleHistory: FastingCycleSession[];
};
const initialState: FastingState = {
activeSchedule: null,
history: [],
activeCycle: null,
currentCycleSession: null,
cycleHistory: [],
};
const fastingSlice = createSlice({
@@ -126,6 +155,131 @@ const fastingSlice = createSlice({
clearActiveSchedule: (state) => {
state.activeSchedule = null;
},
// 周期性断食计划相关的 actions
startFastingCycle: (
state,
action: PayloadAction<{
planId: string;
startHour: number;
startMinute: number;
}>
) => {
const plan = getPlanById(action.payload.planId);
if (!plan) return;
const nowISO = new Date().toISOString();
state.activeCycle = {
planId: plan.id,
startHour: action.payload.startHour,
startMinute: action.payload.startMinute,
enabled: true,
createdAtISO: nowISO,
lastUpdatedISO: nowISO,
};
// 创建今天的断食会话,增加最小提前时间检查
const today = dayjs();
const todayStart = today.hour(action.payload.startHour).minute(action.payload.startMinute).second(0).millisecond(0);
// 要求至少提前10分钟设置周期性断食
const minAdvanceTime = 10; // 分钟
const minStartTime = today.add(minAdvanceTime, 'minute');
// 如果今天的开始时间已过或太接近,则从明天开始
const sessionStart = todayStart.isBefore(minStartTime)
? todayStart.add(1, 'day')
: todayStart;
const { start, end } = calculateFastingWindow(sessionStart.toDate(), plan.fastingHours);
state.currentCycleSession = {
planId: plan.id,
startISO: start.toISOString(),
endISO: end.toISOString(),
cycleDate: sessionStart.format('YYYY-MM-DD'),
completed: false,
};
},
pauseFastingCycle: (state) => {
if (state.activeCycle) {
state.activeCycle.enabled = false;
state.activeCycle.lastUpdatedISO = new Date().toISOString();
}
},
resumeFastingCycle: (state) => {
if (state.activeCycle) {
state.activeCycle.enabled = true;
state.activeCycle.lastUpdatedISO = new Date().toISOString();
}
},
stopFastingCycle: (state) => {
// 完成当前会话
if (state.currentCycleSession) {
state.cycleHistory.unshift(state.currentCycleSession);
}
// 清除周期性计划
state.activeCycle = null;
state.currentCycleSession = null;
},
updateFastingCycleTime: (
state,
action: PayloadAction<{ startHour: number; startMinute: number }>
) => {
if (!state.activeCycle) return;
state.activeCycle.startHour = action.payload.startHour;
state.activeCycle.startMinute = action.payload.startMinute;
state.activeCycle.lastUpdatedISO = new Date().toISOString();
},
completeCurrentCycleSession: (state) => {
if (!state.currentCycleSession) return;
// 标记当前会话为已完成
state.currentCycleSession.completed = true;
// 添加到历史记录
state.cycleHistory.unshift(state.currentCycleSession);
// 创建下一个周期的会话
if (state.activeCycle && state.activeCycle.enabled) {
const plan = getPlanById(state.activeCycle.planId);
if (plan) {
const nextDate = dayjs(state.currentCycleSession.cycleDate).add(1, 'day');
const nextStart = nextDate.hour(state.activeCycle.startHour).minute(state.activeCycle.startMinute).second(0).millisecond(0);
const { start, end } = calculateFastingWindow(nextStart.toDate(), plan.fastingHours);
state.currentCycleSession = {
planId: plan.id,
startISO: start.toISOString(),
endISO: end.toISOString(),
cycleDate: nextDate.format('YYYY-MM-DD'),
completed: false,
};
}
} else {
state.currentCycleSession = null;
}
},
hydrateFastingCycle: (
state,
action: PayloadAction<{
activeCycle: FastingCycle | null;
currentCycleSession: FastingCycleSession | null;
cycleHistory: FastingCycleSession[];
}>
) => {
state.activeCycle = action.payload.activeCycle;
state.currentCycleSession = action.payload.currentCycleSession;
state.cycleHistory = action.payload.cycleHistory;
},
},
});
@@ -136,6 +290,14 @@ export const {
setRecommendedSchedule,
completeActiveSchedule,
clearActiveSchedule,
// 周期性断食相关的 actions
startFastingCycle,
pauseFastingCycle,
resumeFastingCycle,
stopFastingCycle,
updateFastingCycleTime,
completeCurrentCycleSession,
hydrateFastingCycle,
} = fastingSlice.actions;
export default fastingSlice.reducer;
@@ -148,3 +310,56 @@ export const selectActiveFastingPlan = (state: RootState): FastingPlan | undefin
if (!schedule) return undefined;
return getPlanById(schedule.planId);
};
// 周期性断食相关的 selectors
export const selectActiveFastingCycle = (state: RootState) => state.fasting.activeCycle;
export const selectCurrentCycleSession = (state: RootState) => state.fasting.currentCycleSession;
export const selectCycleHistory = (state: RootState) => state.fasting.cycleHistory;
export const selectActiveCyclePlan = (state: RootState): FastingPlan | undefined => {
const cycle = state.fasting.activeCycle;
if (!cycle) return undefined;
return getPlanById(cycle.planId);
};
export const selectCurrentCyclePlan = (state: RootState): FastingPlan | undefined => {
const session = state.fasting.currentCycleSession;
if (!session) return undefined;
return getPlanById(session.planId);
};
// 获取当前应该显示的断食信息(优先显示周期性,其次显示单次)
export const selectCurrentFastingPlan = (state: RootState): FastingPlan | undefined => {
// 优先显示周期性断食
const cyclePlan = selectCurrentCyclePlan(state);
if (cyclePlan) return cyclePlan;
// 其次显示单次断食
return selectActiveFastingPlan(state);
};
// 获取当前应该显示的断食时间
export const selectCurrentFastingTimes = (state: RootState) => {
// 优先显示周期性断食
const cycleSession = state.fasting.currentCycleSession;
if (cycleSession) {
return {
startISO: cycleSession.startISO,
endISO: cycleSession.endISO,
};
}
// 其次显示单次断食
const schedule = state.fasting.activeSchedule;
if (schedule) {
return {
startISO: schedule.startISO,
endISO: schedule.endISO,
};
}
return null;
};
// 判断是否处于周期性断食模式
export const selectIsInCycleMode = (state: RootState) => {
return !!state.fasting.activeCycle;
};