import { FastingOverviewCard } from '@/components/fasting/FastingOverviewCard'; import { FastingPlanList } from '@/components/fasting/FastingPlanList'; import { FastingStartPickerModal } from '@/components/fasting/FastingStartPickerModal'; import { Colors } from '@/constants/Colors'; import { FASTING_PLANS, FastingPlan, getPlanById, getRecommendedStart } from '@/constants/Fasting'; import { ROUTES } from '@/constants/Routes'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useFocusEffect } from '@react-navigation/native'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useCountdown } from '@/hooks/useCountdown'; import { clearActiveSchedule, rescheduleActivePlan, scheduleFastingPlan, selectActiveFastingPlan, selectActiveFastingSchedule, } from '@/store/fastingSlice'; import { buildDisplayWindow, calculateFastingWindow, getFastingPhase, getPhaseLabel, loadPreferredPlanId, loadStoredFastingNotificationIds, savePreferredPlanId, } from '@/utils/fasting'; import type { FastingNotificationIds } from '@/utils/fasting'; import { ensureFastingNotificationsReady, resyncFastingNotifications } from '@/services/fastingNotifications'; import { getNotificationEnabled } from '@/utils/userPreferences'; import { useRouter } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ScrollView, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; export default function FastingTabScreen() { const router = useRouter(); const dispatch = useAppDispatch(); const theme = useColorScheme() ?? 'light'; const colorTokens = Colors[theme]; 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); const [notificationsReady, setNotificationsReady] = useState(false); const notificationsLoadedRef = useRef(false); const notificationIdsRef = useRef({}); 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]); useFocusEffect( useCallback(() => { let cancelled = false; const checkNotifications = async () => { const ready = await ensureFastingNotificationsReady(); if (!cancelled) { setNotificationsReady(ready); if (!ready) { notificationsLoadedRef.current = false; } } }; checkNotifications(); return () => { cancelled = true; }; }, []) ); const currentPlan: FastingPlan | undefined = useMemo(() => { if (activePlan) return activePlan; if (preferredPlanId) return getPlanById(preferredPlanId) ?? defaultPlan; return defaultPlan; }, [activePlan, preferredPlanId, defaultPlan]); 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 (!notificationsReady) return; let cancelled = false; const verifyPreference = async () => { const enabled = await getNotificationEnabled(); if (!cancelled && !enabled) { setNotificationsReady(false); notificationsLoadedRef.current = false; } }; verifyPreference(); return () => { cancelled = true; }; }, [notificationsReady]); const recommendedDate = useMemo(() => { if (!currentPlan) return undefined; return getRecommendedStart(currentPlan); }, [currentPlan]); useEffect(() => { let cancelled = false; const syncNotifications = async () => { if (!notificationsLoadedRef.current) { const storedIds = await loadStoredFastingNotificationIds(); if (cancelled) return; notificationIdsRef.current = storedIds; notificationsLoadedRef.current = true; } const nextIds = await resyncFastingNotifications({ schedule: activeSchedule ?? null, plan: notificationsReady ? currentPlan : undefined, previousIds: notificationIdsRef.current, enabled: notificationsReady, }); if (!cancelled) { notificationIdsRef.current = nextIds; } }; syncNotifications(); return () => { cancelled = true; }; }, [notificationsReady, activeSchedule?.startISO, activeSchedule?.endISO, currentPlan?.id]); const handleAdjustStart = () => { if (!currentPlan) return; setShowPicker(true); }; const handleConfirmStart = (date: Date) => { if (!currentPlan) return; if (activeSchedule) { dispatch(rescheduleActivePlan({ start: date, origin: 'manual' })); } else { dispatch(scheduleFastingPlan({ planId: currentPlan.id, start: date, 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, }, 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: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 16, }, resetHint: { flex: 1, fontSize: 12, color: '#8A96A3', marginRight: 12, }, resetAction: { fontSize: 12, fontWeight: '600', color: '#6366F1', }, });