Files
digital-pilates/components/coach/DietPlanCard.tsx

425 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;