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 { useFastingCycleNotifications } from '@/hooks/useFastingCycleNotifications'; import { useFastingNotifications } from '@/hooks/useFastingNotifications'; import { clearActiveSchedule, completeCurrentCycleSession, hydrateFastingCycle, pauseFastingCycle, rescheduleActivePlan, resumeFastingCycle, scheduleFastingPlan, selectActiveCyclePlan, // 周期性断食相关的 selectors selectActiveFastingCycle, selectActiveFastingPlan, selectActiveFastingSchedule, selectCurrentCyclePlan, selectCurrentCycleSession, selectCurrentFastingPlan, selectCurrentFastingTimes, selectCycleHistory, selectIsInCycleMode, startFastingCycle, stopFastingCycle, updateFastingCycleTime } from '@/store/fastingSlice'; import { buildDisplayWindow, getFastingPhase, getPhaseLabel, // 周期性断食相关的工具函数 loadActiveFastingCycle, loadCurrentCycleSession, loadCycleHistory, loadPreferredPlanId, saveActiveFastingCycle, saveCurrentCycleSession, saveCycleHistory, savePreferredPlanId } from '@/utils/fasting'; import { Ionicons } from '@expo/vector-icons'; 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, TouchableOpacity, 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 scrollViewRef = React.useRef(null); // 单次断食计划的状态 const activeSchedule = useAppSelector(selectActiveFastingSchedule); const activePlan = useAppSelector(selectActiveFastingPlan); // 周期性断食计划的状态 const activeCycle = useAppSelector(selectActiveFastingCycle); const currentCycleSession = useAppSelector(selectCurrentCycleSession); const cycleHistory = useAppSelector(selectCycleHistory); const activeCyclePlan = useAppSelector(selectActiveCyclePlan); const currentCyclePlan = useAppSelector(selectCurrentCyclePlan); // 统一的当前断食信息(优先显示周期性) const currentPlan = useAppSelector(selectCurrentFastingPlan); const currentTimes = useAppSelector(selectCurrentFastingTimes); const isInCycleMode = useAppSelector(selectIsInCycleMode); const defaultPlan = FASTING_PLANS.find((plan) => plan.id === '14-10') ?? FASTING_PLANS[0]; const [preferredPlanId, setPreferredPlanId] = useState(currentPlan?.id ?? undefined); // 数据持久化 useEffect(() => { if (!currentPlan?.id) return; setPreferredPlanId(currentPlan.id); void savePreferredPlanId(currentPlan.id); }, [currentPlan?.id]); useEffect(() => { let cancelled = false; const hydratePreferredPlan = async () => { try { const savedPlanId = await loadPreferredPlanId(); if (cancelled) return; if (currentPlan?.id) return; if (savedPlanId && getPlanById(savedPlanId)) { setPreferredPlanId(savedPlanId); } } catch (error) { console.warn('读取断食首选计划失败', error); } }; hydratePreferredPlan(); return () => { cancelled = true; }; }, [currentPlan?.id]); // 加载周期性断食数据 useEffect(() => { let cancelled = false; const hydrateCycleData = async () => { try { if (cancelled) return; const [cycleData, sessionData, historyData] = await Promise.all([ loadActiveFastingCycle(), loadCurrentCycleSession(), loadCycleHistory(), ]); if (cancelled) return; dispatch(hydrateFastingCycle({ activeCycle: cycleData, currentCycleSession: sessionData, cycleHistory: historyData, })); } catch (error) { console.warn('加载周期性断食数据失败', error); } }; hydrateCycleData(); return () => { cancelled = true; }; }, [dispatch]); // 保存周期性断食数据,增加错误处理 useEffect(() => { const saveCycleData = async () => { try { if (activeCycle) { await saveActiveFastingCycle(activeCycle); } else { await saveActiveFastingCycle(null); } } catch (error) { console.error('保存周期性断食计划失败', error); // TODO: 可以在这里添加用户提示 } }; saveCycleData(); }, [activeCycle]); useEffect(() => { const saveSessionData = async () => { try { if (currentCycleSession) { await saveCurrentCycleSession(currentCycleSession); } else { await saveCurrentCycleSession(null); } } catch (error) { console.error('保存断食会话失败', error); // TODO: 可以在这里添加用户提示 } }; saveSessionData(); }, [currentCycleSession]); useEffect(() => { const saveHistoryData = async () => { try { await saveCycleHistory(cycleHistory); } catch (error) { console.error('保存断食历史失败', error); // TODO: 可以在这里添加用户提示 } }; saveHistoryData(); }, [cycleHistory]); // 使用单次断食通知管理 hook const { isReady: notificationsReady, isLoading: notificationsLoading, error: notificationError, notificationIds, lastSyncTime, verifyAndSync, forceSync, clearError, } = useFastingNotifications(activeSchedule, activePlan); // 使用周期性断食通知管理 hook const { isReady: cycleNotificationsReady, isLoading: cycleNotificationsLoading, error: cycleNotificationError, lastSyncTime: cycleLastSyncTime, verifyAndSync: verifyAndSyncCycle, forceSync: forceSyncCycle, clearError: clearCycleError, } = useFastingCycleNotifications(activeCycle, currentCycleSession, currentCyclePlan); // 每次进入页面时验证通知 // 添加节流机制,避免频繁触发验证 const lastVerifyTimeRef = React.useRef(0); const lastCycleVerifyTimeRef = React.useRef(0); useFocusEffect( useCallback(() => { const now = Date.now(); const timeSinceLastVerify = now - lastVerifyTimeRef.current; // 如果距离上次验证不足 30 秒,跳过本次验证 if (timeSinceLastVerify < 30000) { return; } lastVerifyTimeRef.current = now; verifyAndSync(); }, [verifyAndSync]) ); useFocusEffect( useCallback(() => { const now = Date.now(); const timeSinceLastVerify = now - lastCycleVerifyTimeRef.current; // 如果距离上次验证不足 30 秒,跳过本次验证 if (timeSinceLastVerify < 30000) { return; } lastCycleVerifyTimeRef.current = now; verifyAndSyncCycle(); }, [verifyAndSyncCycle]) ); // 使用统一的当前断食时间 const scheduleStart = useMemo(() => { if (currentTimes) { return new Date(currentTimes.startISO); } return undefined; }, [currentTimes]); const scheduleEnd = useMemo(() => { if (currentTimes) { return new Date(currentTimes.endISO); } return undefined; }, [currentTimes]); 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(() => { const planToUse = currentPlan || defaultPlan; return getRecommendedStart(planToUse); }, [currentPlan, defaultPlan]); // 调试信息(开发环境) useEffect(() => { if (__DEV__ && lastSyncTime) { console.log('单次断食通知状态:', { ready: notificationsReady, loading: notificationsLoading, error: notificationError, notificationIds, lastSyncTime, schedule: activeSchedule?.startISO, plan: activePlan?.id, }); } }, [notificationsReady, notificationsLoading, notificationError, notificationIds, lastSyncTime, activeSchedule?.startISO, activePlan?.id]); useEffect(() => { if (__DEV__ && cycleLastSyncTime) { console.log('周期性断食通知状态:', { ready: cycleNotificationsReady, loading: cycleNotificationsLoading, error: cycleNotificationError, lastSyncTime: cycleLastSyncTime, cycle: activeCycle?.planId, session: currentCycleSession?.cycleDate, }); } }, [cycleNotificationsReady, cycleNotificationsLoading, cycleNotificationError, cycleLastSyncTime, activeCycle?.planId, currentCycleSession?.cycleDate]); // 周期性断食的自动续订逻辑 // 移除1小时限制,但需要用户手动确认开始下一轮周期 useEffect(() => { if (!currentCycleSession || !activeCycle || !currentCyclePlan) return; if (!activeCycle.enabled) return; // 如果周期已暂停,不自动完成 if (phase !== 'completed') return; const end = dayjs(currentCycleSession.endISO); if (!end.isValid()) return; const now = dayjs(); if (now.isBefore(end)) return; // 检查当前会话是否已经标记为完成 if (currentCycleSession.completed) { if (__DEV__) { console.log('当前会话已完成,跳过自动完成'); } return; } if (__DEV__) { console.log('自动完成当前断食周期:', { cycleDate: currentCycleSession.cycleDate, planId: currentCycleSession.planId, endTime: end.format('YYYY-MM-DD HH:mm'), timeSinceEnd: now.diff(end, 'minute') + '分钟', }); } // 完成当前周期并创建下一个周期 // 这会自动创建下一天的会话,不需要用户手动操作 dispatch(completeCurrentCycleSession()); }, [dispatch, currentCycleSession, activeCycle, currentCyclePlan, phase]); // 保留原有的单次断食自动续订逻辑(向后兼容) useEffect(() => { if (!activeSchedule || !activePlan) 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(activePlan.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, activePlan, phase]); const handleAdjustStart = () => { setShowPicker(true); }; const handleConfirmStart = (date: Date) => { // 如果没有当前计划,使用默认计划 const planToUse = currentPlan || defaultPlan; // 如果处于周期性模式,更新周期性时间 if (isInCycleMode && activeCycle) { const hour = date.getHours(); const minute = date.getMinutes(); dispatch(updateFastingCycleTime({ startHour: hour, startMinute: minute })); } else if (activeSchedule) { // 单次断食模式,重新安排 dispatch(rescheduleActivePlan({ start: date.toISOString(), origin: 'manual' })); } else { // 创建新的单次断食计划 dispatch(scheduleFastingPlan({ planId: planToUse.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 = () => { // 如果没有活跃计划,不执行任何操作 if (!currentPlan) return; if (isInCycleMode) { // 停止周期性断食 dispatch(stopFastingCycle()); } else { // 清除单次断食 dispatch(clearActiveSchedule()); } }; // 新增:启动周期性断食 const handleStartCycle = async (plan: FastingPlan, startHour: number, startMinute: number) => { try { dispatch(startFastingCycle({ planId: plan.id, startHour, startMinute })); // 等待数据保存完成 // 注意:dispatch 是同步的,但我们需要确保数据被正确保存 console.log('周期性断食计划已启动', { planId: plan.id, startHour, startMinute }); } catch (error) { console.error('启动周期性断食失败', error); // TODO: 添加用户错误提示 } }; // 新增:暂停/恢复周期性断食 const handleToggleCycle = async () => { if (!activeCycle) return; try { if (activeCycle.enabled) { dispatch(pauseFastingCycle()); console.log('周期性断食已暂停'); } else { dispatch(resumeFastingCycle()); console.log('周期性断食已恢复'); } } catch (error) { console.error('切换周期性断食状态失败', error); // TODO: 添加用户错误提示 } }; return ( 轻断食 改善代谢 · 科学控脂 · 饮食不焦虑 {/* 通知错误提示 */} {currentPlan ? ( ) : ( 开始您的断食之旅 选择适合的断食计划,开启健康生活 断食可以帮助改善代谢、控制体重,让身体获得充分的休息和修复时间。 默认使用 14-10 热门计划(断食14小时,进食10小时) setShowPicker(true)} activeOpacity={0.8} > 开始断食计划 { // 滚动到计划列表 setTimeout(() => { scrollViewRef.current?.scrollTo({ y: 600, animated: true }); }, 100); }} activeOpacity={0.8} > 浏览断食方案 )} {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', }, emptyStateCard: { borderRadius: 28, padding: 24, backgroundColor: '#FFFFFF', shadowColor: '#000', shadowOffset: { width: 0, height: 16 }, shadowOpacity: 0.08, shadowRadius: 24, elevation: 6, marginBottom: 20, }, emptyStateHeader: { alignItems: 'center', marginBottom: 24, }, emptyStateTitle: { fontSize: 24, fontWeight: '700', color: '#2E3142', marginBottom: 8, textAlign: 'center', }, emptyStateSubtitle: { fontSize: 16, color: '#6F7D87', textAlign: 'center', lineHeight: 22, }, emptyStateContent: { alignItems: 'center', marginBottom: 32, }, emptyStateIcon: { width: 80, height: 80, borderRadius: 40, backgroundColor: 'rgba(111, 125, 135, 0.1)', alignItems: 'center', justifyContent: 'center', marginBottom: 20, }, emptyStateDescription: { fontSize: 15, color: '#4A5460', textAlign: 'center', lineHeight: 22, paddingHorizontal: 20, marginBottom: 12, }, defaultPlanInfo: { fontSize: 13, color: '#8A96A3', textAlign: 'center', lineHeight: 18, paddingHorizontal: 20, fontStyle: 'italic', }, emptyStateActions: { gap: 12, }, primaryButton: { backgroundColor: '#2E3142', paddingVertical: 16, paddingHorizontal: 24, borderRadius: 24, alignItems: 'center', justifyContent: 'center', }, primaryButtonText: { fontSize: 16, fontWeight: '600', color: '#FFFFFF', }, secondaryButton: { backgroundColor: 'transparent', paddingVertical: 16, paddingHorizontal: 24, borderRadius: 24, alignItems: 'center', justifyContent: 'center', borderWidth: 1.5, borderColor: '#2E3142', }, secondaryButtonText: { fontSize: 16, fontWeight: '600', color: '#2E3142', }, });