feat(fasting): add auto-renewal and reset functionality for fasting plans
- Implement auto-renewal logic for completed fasting cycles using dayjs - Add reset button with information modal in FastingOverviewCard - Configure iOS push notifications for production environment - Add expo-media-library and react-native-view-shot dependencies - Update FastingScheduleOrigin type to include 'auto' origin
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
} 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';
|
||||
@@ -154,6 +155,53 @@ export default function FastingTabScreen() {
|
||||
}
|
||||
}, [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);
|
||||
@@ -213,6 +261,7 @@ export default function FastingTabScreen() {
|
||||
endTimeLabel={displayWindow.endTimeLabel}
|
||||
onAdjustStartPress={handleAdjustStart}
|
||||
onViewMealsPress={handleViewMeal}
|
||||
onResetPress={handleResetPlan}
|
||||
progress={progress}
|
||||
/>
|
||||
)}
|
||||
@@ -233,9 +282,6 @@ export default function FastingTabScreen() {
|
||||
<Text style={styles.resetHint}>
|
||||
如果计划与作息不符,可重新选择方案或调整开始时间。
|
||||
</Text>
|
||||
<Text style={styles.resetAction} onPress={handleResetPlan}>
|
||||
重置
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
@@ -325,20 +371,10 @@ const styles = StyleSheet.create({
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user