feat: 新增饮食方案卡片及相关计算功能
- 在教练页面中新增饮食方案卡片,展示用户的饮食计划和相关数据 - 实现BMI计算、每日推荐摄入热量计算及营养素分配功能 - 优化体重历史记录卡片,增加动画效果提升用户体验 - 更新统计页面样式,增加体重卡片样式和按钮功能 - 修复获取HRV值的逻辑,确保数据准确性
This commit is contained in:
@@ -109,6 +109,7 @@ const CardType = {
|
||||
WEIGHT_INPUT: '__WEIGHT_INPUT_CARD__',
|
||||
DIET_INPUT: '__DIET_INPUT_CARD__',
|
||||
DIET_TEXT_INPUT: '__DIET_TEXT_INPUT__',
|
||||
DIET_PLAN: '__DIET_PLAN_CARD__',
|
||||
} as const;
|
||||
|
||||
type CardType = typeof CardType[keyof typeof CardType];
|
||||
@@ -279,6 +280,7 @@ export default function CoachScreen() {
|
||||
// { key: 'analyze', label: '分析运动记录', action: () => handleAnalyzeRecords() },
|
||||
{ key: 'weight', label: '#记体重', action: () => insertWeightInputCard() },
|
||||
{ key: 'diet', label: '#记饮食', action: () => insertDietInputCard() },
|
||||
{ key: 'dietPlan', label: '#饮食方案', action: () => insertDietPlanCard() },
|
||||
], [router, planDraft, checkin]);
|
||||
|
||||
const scrollToEnd = useCallback(() => {
|
||||
@@ -1281,6 +1283,167 @@ export default function CoachScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
if (item.content?.startsWith(CardType.DIET_PLAN)) {
|
||||
const cardId = item.content.split('\n')?.[1] || '';
|
||||
|
||||
// 获取用户数据
|
||||
const weight = userProfile?.weight ? Number(userProfile.weight) : 58;
|
||||
const height = userProfile?.height ? Number(userProfile.height) : 160;
|
||||
|
||||
// 计算年龄
|
||||
const calculateAge = (birthday?: string): number => {
|
||||
if (!birthday) return 25; // 默认年龄
|
||||
try {
|
||||
const birthDate = new Date(birthday);
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - birthDate.getFullYear();
|
||||
const monthDiff = today.getMonth() - birthDate.getMonth();
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
||||
age--;
|
||||
}
|
||||
return age > 0 ? age : 25;
|
||||
} catch {
|
||||
return 25;
|
||||
}
|
||||
};
|
||||
|
||||
const age = calculateAge(userProfile?.birthDate);
|
||||
const gender = userProfile?.gender || 'female';
|
||||
const name = userProfile?.name || '用户';
|
||||
|
||||
// 计算相关数据
|
||||
const bmi = calculateBMI(weight, height);
|
||||
const bmiStatus = getBMIStatus(bmi);
|
||||
const dailyCalories = calculateDailyCalories(weight, height, age, gender);
|
||||
const nutrition = calculateNutritionDistribution(dailyCalories);
|
||||
|
||||
return (
|
||||
<View style={styles.dietPlanContainer}>
|
||||
{/* 标题部分 */}
|
||||
<View style={styles.dietPlanHeader}>
|
||||
<View style={styles.dietPlanTitleContainer}>
|
||||
<Ionicons name="restaurant-outline" size={20} color={Colors.light.accentGreenDark} />
|
||||
<Text style={styles.dietPlanTitle}>我的饮食方案</Text>
|
||||
</View>
|
||||
<Text style={styles.dietPlanSubtitle}>MY DIET PLAN</Text>
|
||||
</View>
|
||||
|
||||
{/* 我的档案数据 */}
|
||||
<View style={styles.profileSection}>
|
||||
<Text style={styles.sectionTitle}>我的档案数据</Text>
|
||||
<View style={styles.profileDataRow}>
|
||||
<View style={styles.avatarContainer}>
|
||||
<View style={styles.avatar}>
|
||||
<Text style={styles.avatarText}>{name.charAt(0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.profileStats}>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{age}</Text>
|
||||
<Text style={styles.statLabel}>年龄/岁</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{height.toFixed(1)}</Text>
|
||||
<Text style={styles.statLabel}>身高/CM</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{weight.toFixed(1)}</Text>
|
||||
<Text style={styles.statLabel}>体重/KG</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* BMI部分 */}
|
||||
<View style={styles.bmiSection}>
|
||||
<View style={styles.bmiHeader}>
|
||||
<Text style={styles.sectionTitle}>当前BMI</Text>
|
||||
<View style={[styles.bmiStatusBadge, { backgroundColor: bmiStatus.color }]}>
|
||||
<Text style={styles.bmiStatusText}>{bmiStatus.status}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.bmiValue}>{bmi.toFixed(1)}</Text>
|
||||
<View style={styles.bmiScale}>
|
||||
<View style={[styles.bmiBar, { backgroundColor: '#87CEEB' }]} />
|
||||
<View style={[styles.bmiBar, { backgroundColor: '#90EE90' }]} />
|
||||
<View style={[styles.bmiBar, { backgroundColor: '#FFD700' }]} />
|
||||
<View style={[styles.bmiBar, { backgroundColor: '#FFA07A' }]} />
|
||||
</View>
|
||||
<View style={styles.bmiLabels}>
|
||||
<Text style={styles.bmiLabel}>偏瘦</Text>
|
||||
<Text style={styles.bmiLabel}>正常</Text>
|
||||
<Text style={styles.bmiLabel}>偏胖</Text>
|
||||
<Text style={styles.bmiLabel}>肥胖</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 饮食目标 */}
|
||||
<View style={styles.collapsibleSection}>
|
||||
<View style={styles.collapsibleHeader}>
|
||||
<Text style={styles.sectionTitle}>饮食目标</Text>
|
||||
<Ionicons name="chevron-down" size={16} color="#687076" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 目标体重 */}
|
||||
<View style={styles.collapsibleSection}>
|
||||
<View style={styles.collapsibleHeader}>
|
||||
<Text style={styles.sectionTitle}>目标体重</Text>
|
||||
<Ionicons name="chevron-down" size={16} color="#687076" />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 每日推荐摄入 */}
|
||||
<View style={styles.caloriesSection}>
|
||||
<View style={styles.caloriesHeader}>
|
||||
<Text style={styles.sectionTitle}>每日推荐摄入</Text>
|
||||
<Text style={styles.caloriesValue}>{dailyCalories}千卡</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.nutritionGrid}>
|
||||
<View style={styles.nutritionItem}>
|
||||
<Text style={styles.nutritionValue}>{nutrition.carbs}g</Text>
|
||||
<View style={styles.nutritionLabelRow}>
|
||||
<Ionicons name="nutrition-outline" size={16} color="#687076" />
|
||||
<Text style={styles.nutritionLabel}>碳水</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.nutritionItem}>
|
||||
<Text style={styles.nutritionValue}>{nutrition.protein}g</Text>
|
||||
<View style={styles.nutritionLabelRow}>
|
||||
<Ionicons name="fitness-outline" size={16} color="#687076" />
|
||||
<Text style={styles.nutritionLabel}>蛋白质</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.nutritionItem}>
|
||||
<Text style={styles.nutritionValue}>{nutrition.fat}g</Text>
|
||||
<View style={styles.nutritionLabelRow}>
|
||||
<Ionicons name="water-outline" size={16} color="#687076" />
|
||||
<Text style={styles.nutritionLabel}>脂肪</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.nutritionNote}>
|
||||
根据您的基础代谢率,这到理想体重所需要的热量缺口计算得出每日的热量推荐值。
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<TouchableOpacity
|
||||
style={styles.dietPlanButton}
|
||||
onPress={() => {
|
||||
// 这里可以添加跳转到详细饮食方案页面的逻辑
|
||||
console.log('跳转到饮食方案详情');
|
||||
}}
|
||||
>
|
||||
<Ionicons name="restaurant" size={16} color="#FFFFFF" />
|
||||
<Text style={styles.dietPlanButtonText}>饮食方案</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 在流式回复过程中显示取消按钮
|
||||
if (isStreaming && pendingAssistantIdRef.current === item.id && item.content?.trim()) {
|
||||
return (
|
||||
@@ -1380,6 +1543,58 @@ export default function CoachScreen() {
|
||||
setTimeout(scrollToEnd, 100);
|
||||
}
|
||||
|
||||
function insertDietPlanCard() {
|
||||
const id = `dpcard_${Date.now()}`;
|
||||
const payload = `${CardType.DIET_PLAN}\n${id}`;
|
||||
setMessages((prev) => [...prev, { id, role: 'assistant', content: payload }]);
|
||||
setTimeout(scrollToEnd, 100);
|
||||
}
|
||||
|
||||
// 计算BMI
|
||||
function calculateBMI(weight: number, height: number): number {
|
||||
if (!weight || !height || weight <= 0 || height <= 0) return 0;
|
||||
const heightInMeters = height / 100;
|
||||
return Number((weight / (heightInMeters * heightInMeters)).toFixed(1));
|
||||
}
|
||||
|
||||
// 获取BMI状态
|
||||
function getBMIStatus(bmi: number): { status: string; color: string } {
|
||||
if (bmi < 18.5) return { status: '偏瘦', color: '#87CEEB' };
|
||||
if (bmi < 24) return { status: '正常', color: '#90EE90' };
|
||||
if (bmi < 28) return { status: '偏胖', color: '#FFD700' };
|
||||
return { status: '肥胖', color: '#FFA07A' };
|
||||
}
|
||||
|
||||
// 计算每日推荐摄入热量
|
||||
function calculateDailyCalories(weight: number, height: number, age: number, gender: string = 'female'): number {
|
||||
if (!weight || !height || !age) return 1376; // 默认值
|
||||
|
||||
// 使用Harris-Benedict公式计算基础代谢率
|
||||
let bmr: number;
|
||||
if (gender === 'male') {
|
||||
bmr = 88.362 + (13.397 * weight) + (4.799 * height) - (5.677 * age);
|
||||
} else {
|
||||
bmr = 447.593 + (9.247 * weight) + (3.098 * height) - (4.330 * age);
|
||||
}
|
||||
|
||||
// 考虑活动水平,这里使用轻度活动的系数1.375
|
||||
return Math.round(bmr * 1.375);
|
||||
}
|
||||
|
||||
// 计算营养素分配
|
||||
function calculateNutritionDistribution(calories: number) {
|
||||
// 碳水化合物 50%,蛋白质 15%,脂肪 35%
|
||||
const carbCalories = calories * 0.5;
|
||||
const proteinCalories = calories * 0.15;
|
||||
const fatCalories = calories * 0.35;
|
||||
|
||||
return {
|
||||
carbs: Math.round(carbCalories / 4), // 1g碳水 = 4卡路里
|
||||
protein: Math.round(proteinCalories / 4), // 1g蛋白质 = 4卡路里
|
||||
fat: Math.round(fatCalories / 9), // 1g脂肪 = 9卡路里
|
||||
};
|
||||
}
|
||||
|
||||
async function handleSubmitWeight(text?: string, cardId?: string) {
|
||||
const val = parseFloat(String(text ?? '').trim());
|
||||
if (isNaN(val) || val <= 0 || val > 500) {
|
||||
@@ -2419,6 +2634,181 @@ const styles = StyleSheet.create({
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
// 饮食方案卡片样式
|
||||
dietPlanContainer: {
|
||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
gap: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: `${Colors.light.accentGreen}33`, // 20% opacity
|
||||
},
|
||||
dietPlanHeader: {
|
||||
gap: 4,
|
||||
},
|
||||
dietPlanTitleContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
dietPlanTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
},
|
||||
dietPlanSubtitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#687076',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
profileSection: {
|
||||
gap: 12,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
color: '#192126',
|
||||
},
|
||||
profileDataRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
profileStats: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
justifyContent: 'space-around',
|
||||
},
|
||||
statItem: {
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
statValue: {
|
||||
fontSize: 20,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: 12,
|
||||
color: '#687076',
|
||||
},
|
||||
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',
|
||||
color: '#192126',
|
||||
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,
|
||||
color: '#687076',
|
||||
},
|
||||
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',
|
||||
color: Colors.light.accentGreenDark,
|
||||
},
|
||||
nutritionGrid: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
gap: 16,
|
||||
},
|
||||
nutritionItem: {
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
nutritionValue: {
|
||||
fontSize: 24,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
},
|
||||
nutritionLabelRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
nutritionLabel: {
|
||||
fontSize: 12,
|
||||
color: '#687076',
|
||||
},
|
||||
nutritionNote: {
|
||||
fontSize: 12,
|
||||
color: '#687076',
|
||||
lineHeight: 16,
|
||||
textAlign: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
dietPlanButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
backgroundColor: Colors.light.accentGreenDark,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 12,
|
||||
marginTop: 8,
|
||||
},
|
||||
dietPlanButtonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
|
||||
const markdownStyles = {
|
||||
|
||||
Reference in New Issue
Block a user