import { FastingOverviewCard } from '@/components/fasting/FastingOverviewCard'; import { FastingPlanList } from '@/components/fasting/FastingPlanList'; import { FastingStartPickerModal } from '@/components/fasting/FastingStartPickerModal'; import { NotificationErrorAlert } from '@/components/ui/NotificationErrorAlert'; import { FASTING_PLANS, FastingPlan, getPlanById, getRecommendedStart } from '@/constants/Fasting'; import { ROUTES } from '@/constants/Routes'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useCountdown } from '@/hooks/useCountdown'; import { useFastingNotifications } from '@/hooks/useFastingNotifications'; import { clearActiveSchedule, rescheduleActivePlan, scheduleFastingPlan, selectActiveFastingPlan, selectActiveFastingSchedule, } from '@/store/fastingSlice'; import { buildDisplayWindow, calculateFastingWindow, getFastingPhase, getPhaseLabel, loadPreferredPlanId, savePreferredPlanId } from '@/utils/fasting'; import { useFocusEffect } from '@react-navigation/native'; import { useRouter } from 'expo-router'; import dayjs from 'dayjs'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ScrollView, StyleSheet, Text, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; export default function FastingTabScreen() { const router = useRouter(); const dispatch = useAppDispatch(); const insets = useSafeAreaInsets(); const activeSchedule = useAppSelector(selectActiveFastingSchedule); const activePlan = useAppSelector(selectActiveFastingPlan); const defaultPlan = FASTING_PLANS.find((plan) => plan.id === '14-10') ?? FASTING_PLANS[0]; const [preferredPlanId, setPreferredPlanId] = useState(activePlan?.id ?? undefined); useEffect(() => { if (!activePlan?.id) return; setPreferredPlanId(activePlan.id); void savePreferredPlanId(activePlan.id); }, [activePlan?.id]); useEffect(() => { let cancelled = false; const hydratePreferredPlan = async () => { try { const savedPlanId = await loadPreferredPlanId(); if (cancelled) return; if (activePlan?.id) return; if (savedPlanId && getPlanById(savedPlanId)) { setPreferredPlanId(savedPlanId); } } catch (error) { console.warn('读取断食首选计划失败', error); } }; hydratePreferredPlan(); return () => { cancelled = true; }; }, [activePlan?.id]); const currentPlan: FastingPlan | undefined = useMemo(() => { if (activePlan) return activePlan; if (preferredPlanId) return getPlanById(preferredPlanId) ?? defaultPlan; return defaultPlan; }, [activePlan, preferredPlanId, defaultPlan]); // 使用新的通知管理 hook const { isReady: notificationsReady, isLoading: notificationsLoading, error: notificationError, notificationIds, lastSyncTime, verifyAndSync, forceSync, clearError, } = useFastingNotifications(activeSchedule, currentPlan); // 每次进入页面时验证通知 useFocusEffect( useCallback(() => { verifyAndSync(); }, [verifyAndSync]) ); const scheduleStart = useMemo(() => { if (activeSchedule) return new Date(activeSchedule.startISO); if (currentPlan) return getRecommendedStart(currentPlan); return undefined; }, [activeSchedule, currentPlan]); const scheduleEnd = useMemo(() => { if (activeSchedule && currentPlan) { return new Date(activeSchedule.endISO); } if (currentPlan && scheduleStart) { return calculateFastingWindow(scheduleStart, currentPlan.fastingHours).end; } return undefined; }, [activeSchedule, currentPlan, scheduleStart]); const phase = getFastingPhase(scheduleStart ?? null, scheduleEnd ?? null); const countdownTarget = phase === 'fasting' ? scheduleEnd : scheduleStart; const { formatted: countdownValue } = useCountdown({ target: countdownTarget ?? null }); const progress = useMemo(() => { if (!scheduleStart || !scheduleEnd) return 0; const total = scheduleEnd.getTime() - scheduleStart.getTime(); if (total <= 0) return 0; const now = Date.now(); if (now <= scheduleStart.getTime()) return 0; if (now >= scheduleEnd.getTime()) return 1; return (now - scheduleStart.getTime()) / total; }, [scheduleStart, scheduleEnd]); const displayWindow = buildDisplayWindow(scheduleStart ?? null, scheduleEnd ?? null); const [showPicker, setShowPicker] = useState(false); // 显示通知错误(如果有) useEffect(() => { if (notificationError) { console.warn('断食通知错误:', notificationError); // 可以在这里添加用户提示,比如 Toast 或 Snackbar } }, [notificationError]); const recommendedDate = useMemo(() => { if (!currentPlan) return undefined; return getRecommendedStart(currentPlan); }, [currentPlan]); // 调试信息(开发环境) useEffect(() => { if (__DEV__ && lastSyncTime) { console.log('断食通知状态:', { ready: notificationsReady, loading: notificationsLoading, error: notificationError, notificationIds, lastSyncTime, schedule: activeSchedule?.startISO, plan: currentPlan?.id, }); } }, [notificationsReady, notificationsLoading, notificationError, notificationIds, lastSyncTime, activeSchedule?.startISO, currentPlan?.id]); useEffect(() => { if (!activeSchedule || !currentPlan) return; if (phase !== 'completed') return; const start = dayjs(activeSchedule.startISO); const end = dayjs(activeSchedule.endISO); if (!start.isValid() || !end.isValid()) return; const now = dayjs(); if (now.isBefore(end)) return; const fastingHours = currentPlan.fastingHours; const eatingHours = currentPlan.eatingHours; const cycleHours = fastingHours + eatingHours; if (fastingHours <= 0 || cycleHours <= 0) return; let nextStart = start; let nextEnd = end; let iterations = 0; const maxIterations = 60; while (!now.isBefore(nextEnd)) { nextStart = nextStart.add(cycleHours, 'hour'); nextEnd = nextStart.add(fastingHours, 'hour'); iterations += 1; if (iterations >= maxIterations) { if (__DEV__) { console.warn('自动续订断食周期失败: 超出最大迭代次数', { start: activeSchedule.startISO, end: activeSchedule.endISO, planId: currentPlan.id, }); } return; } } if (iterations === 0) return; dispatch(rescheduleActivePlan({ start: nextStart.toDate().toISOString(), origin: 'auto', })); }, [dispatch, activeSchedule, currentPlan, phase]); const handleAdjustStart = () => { if (!currentPlan) return; setShowPicker(true); }; const handleConfirmStart = (date: Date) => { if (!currentPlan) return; if (activeSchedule) { dispatch(rescheduleActivePlan({ start: date.toISOString(), origin: 'manual' })); } else { dispatch(scheduleFastingPlan({ planId: currentPlan.id, start: date.toISOString(), origin: 'manual' })); } }; const handleSelectPlan = (plan: FastingPlan) => { router.push(`${ROUTES.FASTING_PLAN_DETAIL}/${plan.id}`); }; const handleViewMeal = () => { router.push(ROUTES.FOOD_LIBRARY); }; const handleResetPlan = () => { dispatch(clearActiveSchedule()); }; return ( 轻断食 改善代谢 · 科学控脂 · 饮食不焦虑 {/* 通知错误提示 */} {currentPlan && ( )} {currentPlan && ( 计划亮点 {currentPlan.subtitle} {currentPlan.highlights.map((highlight) => ( {highlight} ))} 如果计划与作息不符,可重新选择方案或调整开始时间。 )} setShowPicker(false)} initialDate={scheduleStart} recommendedDate={recommendedDate} onConfirm={handleConfirmStart} /> ); } const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: 'white' }, scrollContainer: { paddingHorizontal: 20, paddingTop: 12, }, headerRow: { marginBottom: 20, }, screenTitle: { fontSize: 28, fontWeight: '800', color: '#2E3142', marginBottom: 6, }, screenSubtitle: { fontSize: 14, color: '#6F7D87', fontWeight: '500', }, highlightCard: { marginTop: 28, padding: 20, borderRadius: 24, backgroundColor: '#FFFFFF', shadowColor: '#000', shadowOffset: { width: 0, height: 12 }, shadowOpacity: 0.06, shadowRadius: 20, elevation: 4, }, highlightHeader: { marginBottom: 12, }, highlightTitle: { fontSize: 18, fontWeight: '700', color: '#2E3142', }, highlightSubtitle: { fontSize: 13, color: '#6F7D87', marginTop: 6, }, highlightItem: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 10, }, highlightDot: { width: 6, height: 6, borderRadius: 3, marginTop: 7, marginRight: 10, }, highlightText: { flex: 1, fontSize: 14, color: '#4A5460', lineHeight: 20, }, resetRow: { marginTop: 16, }, resetHint: { fontSize: 12, color: '#8A96A3', }, });