feat(fasting): 新增轻断食功能模块

新增完整的轻断食功能,包括:
- 断食计划列表和详情页面,支持12-12、14-10、16-8、18-6四种计划
- 断食状态实时追踪和倒计时显示
- 自定义开始时间选择器
- 断食通知提醒功能
- Redux状态管理和数据持久化
- 新增tab导航入口和路由配置
This commit is contained in:
richarjiang
2025-10-13 19:21:29 +08:00
parent 971aebd560
commit e03b2b3032
17 changed files with 2390 additions and 7 deletions

View File

@@ -0,0 +1,185 @@
import type { FastingPlan } from '@/constants/Fasting';
import { Colors } from '@/constants/Colors';
import { IconSymbol } from '@/components/ui/IconSymbol';
import { useColorScheme } from '@/hooks/useColorScheme';
import React, { useMemo } from 'react';
import {
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
type FastingPlanListProps = {
plans: FastingPlan[];
activePlanId?: string | null;
onSelectPlan: (plan: FastingPlan) => void;
};
const difficultyLabel: Record<FastingPlan['difficulty'], string> = {
: '适合入门',
: '脂代提升',
: '平台突破',
};
export function FastingPlanList({ plans, activePlanId, onSelectPlan }: FastingPlanListProps) {
const theme = useColorScheme() ?? 'light';
const colors = Colors[theme];
const sortedPlans = useMemo(
() => plans.slice().sort((a, b) => a.fastingHours - b.fastingHours),
[plans]
);
return (
<View style={styles.wrapper}>
<View style={styles.headerRow}>
<Text style={styles.headerTitle}></Text>
<View style={styles.headerBadge}>
<Text style={styles.headerBadgeText}></Text>
</View>
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
{sortedPlans.map((plan) => {
const isActive = plan.id === activePlanId;
return (
<TouchableOpacity
key={plan.id}
style={[
styles.card,
{
backgroundColor: plan.theme.backdrop,
borderColor: isActive ? plan.theme.accent : 'transparent',
},
]}
activeOpacity={0.85}
onPress={() => onSelectPlan(plan)}
>
<View style={styles.cardTopRow}>
<View style={[styles.difficultyPill, { backgroundColor: `${plan.theme.accent}1A` }]}>
<Text style={[styles.difficultyText, { color: plan.theme.accent }]}>
{plan.difficulty}
</Text>
</View>
{plan.badge && (
<View style={[styles.badgePill, { backgroundColor: `${plan.theme.accent}26` }]}>
<Text style={[styles.badgeText, { color: plan.theme.accent }]}>
{plan.badge}
</Text>
</View>
)}
</View>
<Text style={styles.cardTitle}>{plan.title}</Text>
<Text style={styles.cardSubtitle}>{plan.subtitle}</Text>
<View style={styles.metaRow}>
<View style={styles.metaItem}>
<IconSymbol name="chart.pie.fill" color={colors.icon} size={16} />
<Text style={styles.metaText}>{plan.fastingHours} </Text>
</View>
<View style={styles.metaItem}>
<IconSymbol name="flag.fill" color={colors.icon} size={16} />
<Text style={styles.metaText}>{difficultyLabel[plan.difficulty]}</Text>
</View>
</View>
</TouchableOpacity>
);
})}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
wrapper: {
marginTop: 32,
},
headerRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 12,
paddingHorizontal: 4,
},
headerTitle: {
fontSize: 18,
fontWeight: '700',
color: '#2E3142',
},
headerBadge: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 14,
backgroundColor: 'rgba(53, 52, 69, 0.08)',
},
headerBadgeText: {
fontSize: 12,
fontWeight: '600',
color: '#353445',
},
scrollContent: {
paddingRight: 20,
},
card: {
width: 220,
borderRadius: 24,
paddingVertical: 18,
paddingHorizontal: 18,
marginRight: 16,
borderWidth: 2,
},
cardTopRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
difficultyPill: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
},
difficultyText: {
fontSize: 12,
fontWeight: '600',
},
badgePill: {
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 14,
},
badgeText: {
fontSize: 11,
fontWeight: '600',
},
cardTitle: {
fontSize: 16,
fontWeight: '700',
color: '#2E3142',
marginBottom: 6,
},
cardSubtitle: {
fontSize: 13,
fontWeight: '500',
color: '#5B6572',
marginBottom: 12,
},
metaRow: {
marginTop: 'auto',
},
metaItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 6,
},
metaText: {
marginLeft: 6,
fontSize: 12,
color: '#5B6572',
fontWeight: '500',
},
});