- 新增 useFastingNotifications hook 统一管理通知状态和同步逻辑 - 实现四阶段通知提醒:开始前30分钟、开始时、结束前30分钟、结束时 - 添加通知验证机制,确保通知正确设置和避免重复 - 新增 NotificationErrorAlert 组件显示通知错误并提供重试选项 - 实现断食计划持久化存储,应用重启后自动恢复 - 添加开发者测试面板用于验证通知系统可靠性 - 优化通知同步策略,支持选择性更新减少不必要的操作 - 修复个人页面编辑按钮样式问题 - 更新应用版本号至 1.0.18
151 lines
4.7 KiB
TypeScript
151 lines
4.7 KiB
TypeScript
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';
|
|
|
|
export type FastingSchedule = {
|
|
planId: string;
|
|
startISO: string;
|
|
endISO: string;
|
|
createdAtISO: string;
|
|
updatedAtISO: string;
|
|
origin: FastingScheduleOrigin;
|
|
};
|
|
|
|
type FastingState = {
|
|
activeSchedule: FastingSchedule | null;
|
|
history: FastingSchedule[];
|
|
};
|
|
|
|
const initialState: FastingState = {
|
|
activeSchedule: null,
|
|
history: [],
|
|
};
|
|
|
|
const fastingSlice = createSlice({
|
|
name: 'fasting',
|
|
initialState,
|
|
reducers: {
|
|
hydrateActiveSchedule: (
|
|
state,
|
|
action: PayloadAction<FastingSchedule | null>
|
|
) => {
|
|
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;
|
|
},
|
|
},
|
|
});
|
|
|
|
export const {
|
|
hydrateActiveSchedule,
|
|
scheduleFastingPlan,
|
|
rescheduleActivePlan,
|
|
setRecommendedSchedule,
|
|
completeActiveSchedule,
|
|
clearActiveSchedule,
|
|
} = 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);
|
|
};
|