refactor(coach): 重构教练组件,统一导入并简化UI实现与类型定义
This commit is contained in:
425
components/coach/DietPlanCard.tsx
Normal file
425
components/coach/DietPlanCard.tsx
Normal file
@@ -0,0 +1,425 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user