refactor(coach): 重构教练组件,统一导入并简化UI实现与类型定义

This commit is contained in:
richarjiang
2025-08-28 09:46:14 +08:00
parent ba2d829e02
commit 5a59508b88
17 changed files with 2400 additions and 866 deletions

View 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;