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:
richarjiang
2025-10-15 19:06:18 +08:00
parent 039138f7e4
commit d39a32c0d8
9 changed files with 548 additions and 155 deletions

View File

@@ -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',
},
});