Files
digital-pilates/hooks/useFastingCycleNotifications.ts
richarjiang 0bea454dca feat(fasting): 添加周期性断食计划功能
实现完整的周期性断食计划系统,支持每日自动续订和通知管理:

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

175 lines
5.3 KiB
TypeScript

import { FastingPlan } from '@/constants/Fasting';
import { FastingCycleNotificationManager } from '@/services/fastingCycleNotifications';
import { FastingCycle, FastingCycleSession } from '@/store/fastingSlice';
import { useCallback, useEffect, useRef, useState } from 'react';
export interface UseFastingCycleNotificationsState {
isReady: boolean;
isLoading: boolean;
error: string | null;
lastSyncTime: Date | null;
}
export interface UseFastingCycleNotificationsActions {
verifyAndSync: () => Promise<void>;
forceSync: () => Promise<void>;
clearError: () => void;
}
export const useFastingCycleNotifications = (
cycle: FastingCycle | null,
session: FastingCycleSession | null,
plan: FastingPlan | undefined
): UseFastingCycleNotificationsState & UseFastingCycleNotificationsActions => {
const [state, setState] = useState<UseFastingCycleNotificationsState>({
isReady: false,
isLoading: true,
error: null,
lastSyncTime: null,
});
const isInitializedRef = useRef(false);
const isSyncingRef = useRef(false);
// 初始化通知系统
const initialize = useCallback(async () => {
if (isInitializedRef.current) return;
try {
setState(prev => ({ ...prev, isLoading: true, error: null }));
// 周期性断食通知不需要额外的权限检查,因为它使用相同的通知系统
setState(prev => ({
...prev,
isReady: true,
isLoading: false,
}));
isInitializedRef.current = true;
} catch (error) {
console.error('初始化周期性断食通知失败', error);
setState(prev => ({
...prev,
isReady: false,
isLoading: false,
error: error instanceof Error ? error.message : '初始化失败',
}));
}
}, []);
// 验证和同步通知
const verifyAndSync = useCallback(async () => {
if (!state.isReady || isSyncingRef.current) return;
try {
isSyncingRef.current = true;
setState(prev => ({ ...prev, error: null }));
if (cycle && session) {
const isValid = await FastingCycleNotificationManager.verifyCycleNotifications(cycle, session);
if (!isValid) {
console.log('周期性断食通知需要重新同步');
try {
await FastingCycleNotificationManager.scheduleCycleNotifications(cycle, session);
} catch (syncError) {
// 如果同步失败,记录错误但不阻止验证流程
console.error('重新安排通知失败', syncError);
setState(prev => ({
...prev,
error: '重新安排通知失败:' + (syncError instanceof Error ? syncError.message : '未知错误'),
}));
return;
}
}
} else {
// 如果没有周期性计划,确保清理所有相关通知
await FastingCycleNotificationManager.cancelAllCycleNotifications();
}
setState(prev => ({
...prev,
lastSyncTime: new Date(),
}));
} catch (error) {
console.error('验证周期性断食通知失败', error);
setState(prev => ({
...prev,
error: '同步周期性断食通知失败:' + (error instanceof Error ? error.message : '未知错误'),
}));
} finally {
isSyncingRef.current = false;
}
}, [state.isReady, cycle, session]);
// 强制同步通知
const forceSync = useCallback(async () => {
if (!state.isReady) {
console.warn('通知系统未就绪,无法强制同步');
return;
}
if (isSyncingRef.current) {
console.warn('通知同步正在进行中,请稍后再试');
return;
}
try {
isSyncingRef.current = true;
setState(prev => ({ ...prev, error: null }));
if (cycle && session) {
await FastingCycleNotificationManager.scheduleCycleNotifications(cycle, session);
console.log('周期性断食通知已强制同步', {
cycle: cycle.planId,
session: session.cycleDate,
});
} else {
await FastingCycleNotificationManager.cancelAllCycleNotifications();
}
setState(prev => ({
...prev,
lastSyncTime: new Date(),
}));
} catch (error) {
console.error('强制同步周期性断食通知失败', error);
setState(prev => ({
...prev,
error: '强制同步周期性断食通知失败:' + (error instanceof Error ? error.message : '未知错误'),
}));
throw error; // 重新抛出错误以便调用方处理
} finally {
isSyncingRef.current = false;
}
}, [state.isReady, cycle, session]);
// 清除错误
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
// 初始化
useEffect(() => {
initialize();
}, [initialize]);
// 当周期性计划或会话变化时验证和同步
useEffect(() => {
if (!state.isReady) return;
// 使用防抖延迟执行,避免在快速状态变化时重复触发
const debounceTimer = setTimeout(() => {
verifyAndSync();
}, 1000); // 1秒防抖
return () => clearTimeout(debounceTimer);
}, [state.isReady, cycle?.planId, cycle?.enabled, session?.cycleDate, session?.startISO, session?.endISO]);
return {
...state,
verifyAndSync,
forceSync,
clearError,
};
};