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 dayjs from 'dayjs'; import { useRouter } from 'expo-router'; 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); // 每次进入页面时验证通知 // 添加节流机制,避免频繁触发验证 const lastVerifyTimeRef = React.useRef(0); useFocusEffect( useCallback(() => { const now = Date.now(); const timeSinceLastVerify = now - lastVerifyTimeRef.current; // 如果距离上次验证不足 30 秒,跳过本次验证 if (timeSinceLastVerify < 30000) { return; } lastVerifyTimeRef.current = now; 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 timeSinceEnd = now.diff(end, 'minute'); if (timeSinceEnd > 60) { // 如果周期结束超过1小时,说明用户可能不再需要自动续订 if (__DEV__) { console.log('断食周期结束超过1小时,不自动续订'); } return; } // 使用每日固定时间计算下一个周期 // 保持原始的开始时间(小时和分钟),只增加日期 const originalStartHour = start.hour(); const originalStartMinute = start.minute(); // 计算下一个开始时间:明天的同一时刻 let nextStart = now.startOf('day').hour(originalStartHour).minute(originalStartMinute); // 如果计算出的时间在当前时间之前,则使用后天的同一时刻 if (nextStart.isBefore(now)) { nextStart = nextStart.add(1, 'day'); } const nextEnd = nextStart.add(currentPlan.fastingHours, 'hour'); if (__DEV__) { console.log('自动续订断食周期:', { oldStart: start.format('YYYY-MM-DD HH:mm'), oldEnd: end.format('YYYY-MM-DD HH:mm'), nextStart: nextStart.format('YYYY-MM-DD HH:mm'), nextEnd: nextEnd.format('YYYY-MM-DD HH:mm'), }); } dispatch(rescheduleActivePlan({ start: nextStart.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', }, });