425 lines
12 KiB
TypeScript
425 lines
12 KiB
TypeScript
import { Colors } from '@/constants/Colors';
|
||
import { useAppSelector } from '@/hooks/redux';
|
||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||
import { selectUserAge } from '@/store/userSlice';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import React, { useState } from 'react';
|
||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||
|
||
interface DietPlanCardProps {
|
||
onGeneratePlan: () => void;
|
||
}
|
||
|
||
const DietPlanCard: React.FC<DietPlanCardProps> = ({ onGeneratePlan }) => {
|
||
const colorScheme = useColorScheme();
|
||
const theme = Colors[colorScheme ?? 'light'];
|
||
const [isExpanded, setIsExpanded] = useState(false);
|
||
|
||
const userProfile = useAppSelector((s) => s.user?.profile);
|
||
const userAge = useAppSelector(selectUserAge);
|
||
|
||
// 计算BMI
|
||
const calculateBMI = () => {
|
||
if (!userProfile?.weight || !userProfile?.height) return null;
|
||
const weight = Number(userProfile.weight);
|
||
const height = Number(userProfile.height) / 100; // 转换为米
|
||
return weight / (height * height);
|
||
};
|
||
|
||
const bmi = calculateBMI();
|
||
|
||
// 获取BMI状态
|
||
const getBMIStatus = (bmi: number) => {
|
||
if (bmi < 18.5) return { text: '偏瘦', color: '#3B82F6' };
|
||
if (bmi < 24) return { text: '正常', color: '#10B981' };
|
||
if (bmi < 28) return { text: '超重', color: '#F59E0B' };
|
||
return { text: '肥胖', color: '#EF4444' };
|
||
};
|
||
|
||
const bmiStatus = bmi ? getBMIStatus(bmi) : null;
|
||
|
||
// 估算基础代谢率 (BMR)
|
||
const calculateBMR = () => {
|
||
if (!userProfile?.weight || !userProfile?.height || userAge === null) return null;
|
||
|
||
const weight = Number(userProfile.weight);
|
||
const height = Number(userProfile.height);
|
||
const age = userAge;
|
||
const gender = userProfile.gender;
|
||
|
||
// 使用 Mifflin-St Jeor 公式
|
||
if (gender === 'male') {
|
||
return Math.round(10 * weight + 6.25 * height - 5 * age + 5);
|
||
} else {
|
||
return Math.round(10 * weight + 6.25 * height - 5 * age - 161);
|
||
}
|
||
};
|
||
|
||
const bmr = calculateBMR();
|
||
const dailyCalories = bmr ? Math.round(bmr * 1.4) : null; // 轻度活动系数
|
||
|
||
return (
|
||
<View style={[styles.dietPlanContainer, {
|
||
backgroundColor: theme.surface,
|
||
borderColor: `${theme.primary}33`
|
||
}]}>
|
||
{/* 头部 */}
|
||
<View style={styles.dietPlanHeader}>
|
||
<View style={styles.dietPlanTitleContainer}>
|
||
<Ionicons name="nutrition" size={24} color={theme.primary} />
|
||
<Text style={[styles.dietPlanTitle, { color: theme.text }]}>个性化饮食方案</Text>
|
||
</View>
|
||
<Text style={[styles.dietPlanSubtitle, { color: theme.textMuted }]}>
|
||
基于你的身体数据定制
|
||
</Text>
|
||
</View>
|
||
|
||
{/* 用户资料概览 */}
|
||
{userProfile && (
|
||
<View style={styles.profileSection}>
|
||
<Text style={[styles.sectionTitle, { color: theme.text }]}>个人资料</Text>
|
||
<View style={styles.profileDataRow}>
|
||
<View style={styles.avatarContainer}>
|
||
<View style={[styles.avatar, { backgroundColor: theme.primary }]}>
|
||
<Text style={styles.avatarText}>
|
||
{userProfile.name?.charAt(0) || 'U'}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
<View style={styles.profileStats}>
|
||
{userProfile.weight && (
|
||
<View style={styles.statItem}>
|
||
<Text style={[styles.statValue, { color: theme.text }]}>
|
||
{userProfile.weight}
|
||
</Text>
|
||
<Text style={[styles.statLabel, { color: theme.textMuted }]}>kg</Text>
|
||
</View>
|
||
)}
|
||
{userProfile.height && (
|
||
<View style={styles.statItem}>
|
||
<Text style={[styles.statValue, { color: theme.text }]}>
|
||
{userProfile.height}
|
||
</Text>
|
||
<Text style={[styles.statLabel, { color: theme.textMuted }]}>cm</Text>
|
||
</View>
|
||
)}
|
||
{userAge !== null && (
|
||
<View style={styles.statItem}>
|
||
<Text style={[styles.statValue, { color: theme.text }]}>
|
||
{userAge}
|
||
</Text>
|
||
<Text style={[styles.statLabel, { color: theme.textMuted }]}>岁</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* BMI 部分 */}
|
||
{bmi && bmiStatus && (
|
||
<View style={styles.bmiSection}>
|
||
<View style={styles.bmiHeader}>
|
||
<Text style={[styles.sectionTitle, { color: theme.text }]}>BMI 指数</Text>
|
||
<View style={[styles.bmiStatusBadge, { backgroundColor: bmiStatus.color }]}>
|
||
<Text style={styles.bmiStatusText}>{bmiStatus.text}</Text>
|
||
</View>
|
||
</View>
|
||
<Text style={[styles.bmiValue, { color: theme.text }]}>
|
||
{bmi.toFixed(1)}
|
||
</Text>
|
||
|
||
{/* BMI 刻度条 */}
|
||
<View style={styles.bmiScale}>
|
||
<View style={[styles.bmiBar, { backgroundColor: '#3B82F6' }]} />
|
||
<View style={[styles.bmiBar, { backgroundColor: '#10B981' }]} />
|
||
<View style={[styles.bmiBar, { backgroundColor: '#F59E0B' }]} />
|
||
<View style={[styles.bmiBar, { backgroundColor: '#EF4444' }]} />
|
||
</View>
|
||
<View style={styles.bmiLabels}>
|
||
<Text style={[styles.bmiLabel, { color: theme.textMuted }]}>偏瘦</Text>
|
||
<Text style={[styles.bmiLabel, { color: theme.textMuted }]}>正常</Text>
|
||
<Text style={[styles.bmiLabel, { color: theme.textMuted }]}>超重</Text>
|
||
<Text style={[styles.bmiLabel, { color: theme.textMuted }]}>肥胖</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* 可折叠的详细信息 */}
|
||
<TouchableOpacity
|
||
style={styles.collapsibleSection}
|
||
onPress={() => setIsExpanded(!isExpanded)}
|
||
>
|
||
<View style={styles.collapsibleHeader}>
|
||
<Text style={[styles.sectionTitle, { color: theme.text }]}>营养需求分析</Text>
|
||
<Ionicons
|
||
name={isExpanded ? 'chevron-up' : 'chevron-down'}
|
||
size={20}
|
||
color={theme.text}
|
||
/>
|
||
</View>
|
||
</TouchableOpacity>
|
||
|
||
{isExpanded && (
|
||
<>
|
||
{/* 卡路里需求 */}
|
||
{dailyCalories && (
|
||
<View style={styles.caloriesSection}>
|
||
<View style={styles.caloriesHeader}>
|
||
<Text style={[styles.sectionTitle, { color: theme.text }]}>每日卡路里需求</Text>
|
||
<Text style={[styles.caloriesValue, { color: '#10B981' }]}>
|
||
{dailyCalories} kcal
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* 营养素分配 */}
|
||
<View style={styles.nutritionGrid}>
|
||
<View style={styles.nutritionItem}>
|
||
<Text style={[styles.nutritionValue, { color: theme.text }]}>55%</Text>
|
||
<View style={styles.nutritionLabelRow}>
|
||
<View style={[styles.nutritionDot, { backgroundColor: '#3B82F6' }]} />
|
||
<Text style={[styles.nutritionLabel, { color: theme.textMuted }]}>碳水</Text>
|
||
</View>
|
||
</View>
|
||
<View style={styles.nutritionItem}>
|
||
<Text style={[styles.nutritionValue, { color: theme.text }]}>20%</Text>
|
||
<View style={styles.nutritionLabelRow}>
|
||
<View style={[styles.nutritionDot, { backgroundColor: '#10B981' }]} />
|
||
<Text style={[styles.nutritionLabel, { color: theme.textMuted }]}>蛋白质</Text>
|
||
</View>
|
||
</View>
|
||
<View style={styles.nutritionItem}>
|
||
<Text style={[styles.nutritionValue, { color: theme.text }]}>25%</Text>
|
||
<View style={styles.nutritionLabelRow}>
|
||
<View style={[styles.nutritionDot, { backgroundColor: '#F59E0B' }]} />
|
||
<Text style={[styles.nutritionLabel, { color: theme.textMuted }]}>脂肪</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
<Text style={[styles.nutritionNote, { color: theme.textMuted }]}>
|
||
* 营养素比例基于一般健康成人推荐标准,具体需求因人而异
|
||
</Text>
|
||
</>
|
||
)}
|
||
|
||
{/* 生成方案按钮 */}
|
||
<TouchableOpacity
|
||
style={[styles.dietPlanButton, { backgroundColor: '#10B981' }]}
|
||
onPress={onGeneratePlan}
|
||
>
|
||
<Ionicons name="sparkles" size={16} color="#FFFFFF" />
|
||
<Text style={styles.dietPlanButtonText}>生成个性化饮食方案</Text>
|
||
</TouchableOpacity>
|
||
|
||
{/* 使用次数提示 */}
|
||
<View style={styles.usageCountContainer}>
|
||
<Ionicons name="information-circle" size={16} color={theme.primary} />
|
||
<Text style={[styles.usageText, { color: theme.primary }]}>
|
||
AI 将根据你的身体数据和健康目标制定专属方案
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
);
|
||
};
|
||
|
||
const styles = StyleSheet.create({
|
||
dietPlanContainer: {
|
||
borderRadius: 16,
|
||
padding: 16,
|
||
gap: 16,
|
||
borderWidth: 1,
|
||
maxWidth: '85%',
|
||
alignSelf: 'flex-start',
|
||
minWidth: 300
|
||
},
|
||
dietPlanHeader: {
|
||
gap: 4,
|
||
},
|
||
dietPlanTitleContainer: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
},
|
||
dietPlanTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '800',
|
||
},
|
||
dietPlanSubtitle: {
|
||
fontSize: 12,
|
||
fontWeight: '600',
|
||
letterSpacing: 1,
|
||
},
|
||
profileSection: {
|
||
gap: 12,
|
||
},
|
||
sectionTitle: {
|
||
fontSize: 14,
|
||
fontWeight: '700',
|
||
},
|
||
profileDataRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 16,
|
||
},
|
||
avatarContainer: {
|
||
alignItems: 'center',
|
||
},
|
||
avatar: {
|
||
width: 48,
|
||
height: 48,
|
||
borderRadius: 24,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
avatarText: {
|
||
color: '#FFFFFF',
|
||
fontSize: 18,
|
||
fontWeight: '800',
|
||
},
|
||
profileStats: {
|
||
flexDirection: 'row',
|
||
flex: 1,
|
||
justifyContent: 'space-around',
|
||
},
|
||
statItem: {
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
},
|
||
statValue: {
|
||
fontSize: 20,
|
||
fontWeight: '800',
|
||
},
|
||
statLabel: {
|
||
fontSize: 12,
|
||
},
|
||
bmiSection: {
|
||
gap: 12,
|
||
},
|
||
bmiHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
},
|
||
bmiStatusBadge: {
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 4,
|
||
borderRadius: 12,
|
||
},
|
||
bmiStatusText: {
|
||
fontSize: 12,
|
||
fontWeight: '700',
|
||
color: '#FFFFFF',
|
||
},
|
||
bmiValue: {
|
||
fontSize: 32,
|
||
fontWeight: '800',
|
||
textAlign: 'center',
|
||
},
|
||
bmiScale: {
|
||
flexDirection: 'row',
|
||
height: 8,
|
||
borderRadius: 4,
|
||
overflow: 'hidden',
|
||
gap: 1,
|
||
},
|
||
bmiBar: {
|
||
flex: 1,
|
||
height: '100%',
|
||
},
|
||
bmiLabels: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-between',
|
||
paddingHorizontal: 4,
|
||
},
|
||
bmiLabel: {
|
||
fontSize: 11,
|
||
},
|
||
collapsibleSection: {
|
||
paddingVertical: 8,
|
||
borderBottomWidth: 1,
|
||
borderBottomColor: 'rgba(0,0,0,0.06)',
|
||
},
|
||
collapsibleHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
},
|
||
caloriesSection: {
|
||
gap: 12,
|
||
},
|
||
caloriesHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
},
|
||
caloriesValue: {
|
||
fontSize: 18,
|
||
fontWeight: '800',
|
||
},
|
||
nutritionGrid: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-around',
|
||
gap: 16,
|
||
},
|
||
nutritionItem: {
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
},
|
||
nutritionValue: {
|
||
fontSize: 24,
|
||
fontWeight: '800',
|
||
},
|
||
nutritionLabelRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
},
|
||
nutritionDot: {
|
||
width: 8,
|
||
height: 8,
|
||
borderRadius: 4,
|
||
},
|
||
nutritionLabel: {
|
||
fontSize: 12,
|
||
},
|
||
nutritionNote: {
|
||
fontSize: 12,
|
||
lineHeight: 16,
|
||
textAlign: 'center',
|
||
marginTop: 8,
|
||
},
|
||
dietPlanButton: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 8,
|
||
paddingVertical: 12,
|
||
paddingHorizontal: 16,
|
||
borderRadius: 12,
|
||
marginTop: 8,
|
||
},
|
||
dietPlanButtonText: {
|
||
fontSize: 14,
|
||
fontWeight: '700',
|
||
color: '#FFFFFF',
|
||
},
|
||
usageCountContainer: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
paddingHorizontal: 8,
|
||
paddingVertical: 4,
|
||
borderRadius: 12,
|
||
backgroundColor: 'rgba(122,90,248,0.08)',
|
||
},
|
||
usageText: {
|
||
fontSize: 12,
|
||
fontWeight: '600',
|
||
flex: 1,
|
||
},
|
||
});
|
||
|
||
export default DietPlanCard; |