feat(fasting): 重构断食通知系统并增强可靠性

- 新增 useFastingNotifications hook 统一管理通知状态和同步逻辑
- 实现四阶段通知提醒:开始前30分钟、开始时、结束前30分钟、结束时
- 添加通知验证机制,确保通知正确设置和避免重复
- 新增 NotificationErrorAlert 组件显示通知错误并提供重试选项
- 实现断食计划持久化存储,应用重启后自动恢复
- 添加开发者测试面板用于验证通知系统可靠性
- 优化通知同步策略,支持选择性更新减少不必要的操作
- 修复个人页面编辑按钮样式问题
- 更新应用版本号至 1.0.18
This commit is contained in:
richarjiang
2025-10-14 15:05:11 +08:00
parent e03b2b3032
commit cf069f3537
18 changed files with 1565 additions and 242 deletions

View File

@@ -1,7 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import dayjs from 'dayjs';
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';
@@ -29,14 +29,35 @@ 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: Date; origin?: FastingScheduleOrigin }>
action: PayloadAction<{ planId: string; start: string; origin?: FastingScheduleOrigin }>
) => {
const plan = getPlanById(action.payload.planId);
if (!plan) return;
const { start, end } = calculateFastingWindow(action.payload.start, plan.fastingHours);
const startDate = new Date(action.payload.start);
const { start, end } = calculateFastingWindow(startDate, plan.fastingHours);
const nowISO = new Date().toISOString();
state.activeSchedule = {
planId: plan.id,
@@ -49,13 +70,14 @@ const fastingSlice = createSlice({
},
rescheduleActivePlan: (
state,
action: PayloadAction<{ start: Date; origin?: FastingScheduleOrigin }>
action: PayloadAction<{ start: string; origin?: FastingScheduleOrigin }>
) => {
if (!state.activeSchedule) return;
const plan = getPlanById(state.activeSchedule.planId);
if (!plan) return;
const { start, end } = calculateFastingWindow(action.payload.start, plan.fastingHours);
const startDate = new Date(action.payload.start);
const { start, end } = calculateFastingWindow(startDate, plan.fastingHours);
state.activeSchedule = {
...state.activeSchedule,
startISO: start.toISOString(),
@@ -66,11 +88,12 @@ const fastingSlice = createSlice({
},
setRecommendedSchedule: (
state,
action: PayloadAction<{ planId: string; recommendedStart: Date }>
action: PayloadAction<{ planId: string; recommendedStart: string }>
) => {
const plan = getPlanById(action.payload.planId);
if (!plan) return;
const { start, end } = calculateFastingWindow(action.payload.recommendedStart, plan.fastingHours);
const startDate = new Date(action.payload.recommendedStart);
const { start, end } = calculateFastingWindow(startDate, plan.fastingHours);
const nowISO = new Date().toISOString();
state.activeSchedule = {
planId: plan.id,
@@ -107,6 +130,7 @@ const fastingSlice = createSlice({
});
export const {
hydrateActiveSchedule,
scheduleFastingPlan,
rescheduleActivePlan,
setRecommendedSchedule,

View File

@@ -7,7 +7,13 @@ import foodLibraryReducer from './foodLibrarySlice';
import foodRecognitionReducer from './foodRecognitionSlice';
import goalsReducer from './goalsSlice';
import healthReducer from './healthSlice';
import fastingReducer from './fastingSlice';
import fastingReducer, {
clearActiveSchedule,
completeActiveSchedule,
rescheduleActivePlan,
scheduleFastingPlan,
setRecommendedSchedule,
} from './fastingSlice';
import moodReducer from './moodSlice';
import nutritionReducer from './nutritionSlice';
import scheduleExerciseReducer from './scheduleExerciseSlice';
@@ -16,6 +22,7 @@ import trainingPlanReducer from './trainingPlanSlice';
import userReducer from './userSlice';
import waterReducer from './waterSlice';
import workoutReducer from './workoutSlice';
import { persistActiveFastingSchedule } from '@/utils/fasting';
// 创建监听器中间件来处理自动同步
const listenerMiddleware = createListenerMiddleware();
@@ -45,6 +52,46 @@ syncActions.forEach(action => {
});
});
const persistFastingState = async (listenerApi: any) => {
const state = listenerApi.getState() as { fasting?: { activeSchedule?: any } };
await persistActiveFastingSchedule(state?.fasting?.activeSchedule ?? null);
};
listenerMiddleware.startListening({
actionCreator: scheduleFastingPlan,
effect: async (_, listenerApi) => {
await persistFastingState(listenerApi);
},
});
listenerMiddleware.startListening({
actionCreator: rescheduleActivePlan,
effect: async (_, listenerApi) => {
await persistFastingState(listenerApi);
},
});
listenerMiddleware.startListening({
actionCreator: completeActiveSchedule,
effect: async (_, listenerApi) => {
await persistFastingState(listenerApi);
},
});
listenerMiddleware.startListening({
actionCreator: setRecommendedSchedule,
effect: async (_, listenerApi) => {
await persistFastingState(listenerApi);
},
});
listenerMiddleware.startListening({
actionCreator: clearActiveSchedule,
effect: async () => {
await persistActiveFastingSchedule(null);
},
});
export const store = configureStore({
reducer: {
user: userReducer,