import { FASTING_PLANS, FastingPlan, getPlanById } from '@/constants/Fasting'; import { calculateFastingWindow, getFastingPhase } from '@/utils/fasting'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import dayjs from 'dayjs'; import type { RootState } from './index'; export type FastingScheduleOrigin = 'manual' | 'recommended' | 'quick-start' | 'auto'; // 保持向后兼容的单次断食计划 export type FastingSchedule = { planId: string; startISO: string; endISO: string; createdAtISO: string; updatedAtISO: string; 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({ name: 'fasting', initialState, reducers: { hydrateActiveSchedule: ( state, action: PayloadAction ) => { const incoming = action.payload; if (!incoming) { state.activeSchedule = null; return; } if (state.activeSchedule) { const currentUpdated = dayjs(state.activeSchedule.updatedAtISO ?? state.activeSchedule.startISO); const incomingUpdated = dayjs(incoming.updatedAtISO ?? incoming.startISO); if (currentUpdated.isSame(incomingUpdated) || currentUpdated.isAfter(incomingUpdated)) { return; } } state.activeSchedule = incoming; }, scheduleFastingPlan: ( state, action: PayloadAction<{ planId: string; start: string; origin?: FastingScheduleOrigin }> ) => { const plan = getPlanById(action.payload.planId); if (!plan) return; const startDate = new Date(action.payload.start); const { start, end } = calculateFastingWindow(startDate, plan.fastingHours); const nowISO = new Date().toISOString(); state.activeSchedule = { planId: plan.id, startISO: start.toISOString(), endISO: end.toISOString(), createdAtISO: nowISO, updatedAtISO: nowISO, origin: action.payload.origin ?? 'manual', }; }, rescheduleActivePlan: ( state, action: PayloadAction<{ start: string; origin?: FastingScheduleOrigin }> ) => { if (!state.activeSchedule) return; const plan = getPlanById(state.activeSchedule.planId); if (!plan) return; const startDate = new Date(action.payload.start); const { start, end } = calculateFastingWindow(startDate, plan.fastingHours); state.activeSchedule = { ...state.activeSchedule, startISO: start.toISOString(), endISO: end.toISOString(), updatedAtISO: new Date().toISOString(), origin: action.payload.origin ?? state.activeSchedule.origin, }; }, setRecommendedSchedule: ( state, action: PayloadAction<{ planId: string; recommendedStart: string }> ) => { const plan = getPlanById(action.payload.planId); if (!plan) return; const startDate = new Date(action.payload.recommendedStart); const { start, end } = calculateFastingWindow(startDate, plan.fastingHours); const nowISO = new Date().toISOString(); state.activeSchedule = { planId: plan.id, startISO: start.toISOString(), endISO: end.toISOString(), createdAtISO: nowISO, updatedAtISO: nowISO, origin: 'recommended', }; }, completeActiveSchedule: (state) => { if (!state.activeSchedule) return; const phase = getFastingPhase( new Date(state.activeSchedule.startISO), new Date(state.activeSchedule.endISO) ); if (phase === 'fasting') { // Allow manual completion only when fasting window已经结束 state.activeSchedule = { ...state.activeSchedule, endISO: dayjs().toISOString(), updatedAtISO: new Date().toISOString(), }; } state.history.unshift(state.activeSchedule); state.activeSchedule = null; }, 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; }, }, }); export const { hydrateActiveSchedule, scheduleFastingPlan, rescheduleActivePlan, setRecommendedSchedule, completeActiveSchedule, clearActiveSchedule, // 周期性断食相关的 actions startFastingCycle, pauseFastingCycle, resumeFastingCycle, stopFastingCycle, updateFastingCycleTime, completeCurrentCycleSession, hydrateFastingCycle, } = fastingSlice.actions; export default fastingSlice.reducer; export const selectFastingRoot = (state: RootState) => state.fasting; export const selectActiveFastingSchedule = (state: RootState) => state.fasting.activeSchedule; export const selectFastingPlans = () => FASTING_PLANS; export const selectActiveFastingPlan = (state: RootState): FastingPlan | undefined => { const schedule = state.fasting.activeSchedule; 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; };