Files
digital-pilates/components/NutritionRadarCard.tsx
2025-08-29 09:41:05 +08:00

365 lines
10 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 { AnimatedNumber } from '@/components/AnimatedNumber';
import { ROUTES } from '@/constants/Routes';
import { NutritionSummary } from '@/services/dietRecords';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { router } from 'expo-router';
import React, { useMemo, useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { RadarCategory, RadarChart } from './RadarChart';
export type NutritionRadarCardProps = {
nutritionSummary: NutritionSummary | null;
/** 基础代谢消耗的卡路里 */
burnedCalories?: number;
/** 卡路里缺口 */
calorieDeficit?: number;
/** 动画重置令牌 */
resetToken?: number;
/** 餐次点击回调 */
onMealPress?: (mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => void;
};
// 营养维度定义
const NUTRITION_DIMENSIONS: RadarCategory[] = [
{ key: 'calories', label: '热量' },
{ key: 'protein', label: '蛋白质' },
{ key: 'carbohydrate', label: '碳水' },
{ key: 'fat', label: '脂肪' },
{ key: 'fiber', label: '纤维' },
{ key: 'sodium', label: '钠' },
];
export function NutritionRadarCard({
nutritionSummary,
burnedCalories = 1618,
calorieDeficit = 0,
resetToken,
onMealPress
}: NutritionRadarCardProps) {
const [currentMealType, setCurrentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
const radarValues = useMemo(() => {
// 基于推荐日摄入量计算分数
const recommendations = {
calories: 2000, // 卡路里
protein: 50, // 蛋白质(g)
carbohydrate: 300, // 碳水化合物(g)
fat: 65, // 脂肪(g)
fiber: 25, // 膳食纤维(g)
sodium: 2300, // 钠(mg)
};
if (!nutritionSummary) return [0, 0, 0, 0, 0, 0];
// 检查每个营养素是否有实际值没有则返回0
const calories = nutritionSummary.totalCalories || 0;
const protein = nutritionSummary.totalProtein || 0;
const carbohydrate = nutritionSummary.totalCarbohydrate || 0;
const fat = nutritionSummary.totalFat || 0;
const fiber = nutritionSummary.totalFiber || 0;
const sodium = nutritionSummary.totalSodium || 0;
return [
calories > 0 ? Math.min(5, (calories / recommendations.calories) * 5) : 0,
protein > 0 ? Math.min(5, (protein / recommendations.protein) * 5) : 0,
carbohydrate > 0 ? Math.min(5, (carbohydrate / recommendations.carbohydrate) * 5) : 0,
fat > 0 ? Math.min(5, (fat / recommendations.fat) * 5) : 0,
fiber > 0 ? Math.min(5, (fiber / recommendations.fiber) * 5) : 0,
sodium > 0 ? Math.min(5, Math.max(0, 5 - (sodium / recommendations.sodium) * 5)) : 0, // 钠含量越低越好
];
}, [nutritionSummary]);
const nutritionStats = useMemo(() => {
return [
{ label: '热量', value: nutritionSummary ? `${Math.round(nutritionSummary.totalCalories)} 千卡` : '0 千卡', color: '#FF6B6B' },
{ label: '蛋白质', value: nutritionSummary ? `${nutritionSummary.totalProtein.toFixed(1)} g` : '0.0 g', color: '#4ECDC4' },
{ label: '碳水', value: nutritionSummary ? `${nutritionSummary.totalCarbohydrate.toFixed(1)} g` : '0.0 g', color: '#45B7D1' },
{ label: '脂肪', value: nutritionSummary ? `${nutritionSummary.totalFat.toFixed(1)} g` : '0.0 g', color: '#FFA07A' },
{ label: '纤维', value: nutritionSummary ? `${nutritionSummary.totalFiber.toFixed(1)} g` : '0.0 g', color: '#98D8C8' },
{ label: '钠', value: nutritionSummary ? `${Math.round(nutritionSummary.totalSodium)} mg` : '0 mg', color: '#F7DC6F' },
];
}, [nutritionSummary]);
// 计算还能吃的卡路里
const consumedCalories = nutritionSummary?.totalCalories || 0;
const remainingCalories = burnedCalories - consumedCalories - calorieDeficit;
// 餐次数据
const meals = [
{
type: 'breakfast' as const,
name: '早餐',
emoji: '🥚',
},
{
type: 'lunch' as const,
name: '午餐',
emoji: '🍔',
},
{
type: 'dinner' as const,
name: '晚餐',
emoji: '🥣',
},
{
type: 'snack' as const,
name: '加餐',
emoji: '🍎',
},
];
const handleNavigateToRecords = () => {
router.push(ROUTES.NUTRITION_RECORDS);
};
const handleAddFood = () => {
router.push(`/food-library?mealType=${currentMealType}`);
};
return (
<TouchableOpacity style={styles.card} onPress={handleNavigateToRecords} activeOpacity={0.8}>
<View style={styles.cardHeader}>
<Text style={styles.cardTitle}></Text>
<View style={styles.cardRightContainer}>
<Text style={styles.cardSubtitle}>: {dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')}</Text>
<TouchableOpacity style={styles.addButton} onPress={handleAddFood}>
<Ionicons name="add-circle" size={16} color="#FFFFFF" />
</TouchableOpacity>
</View>
</View>
<View style={styles.contentContainer}>
<View style={styles.radarContainer}>
<RadarChart
categories={NUTRITION_DIMENSIONS}
values={radarValues}
size="small"
maxValue={5}
/>
</View>
<View style={styles.statsContainer}>
{nutritionStats.map((stat, index) => (
<View key={stat.label} style={styles.statItem}>
<View style={[styles.statDot, { backgroundColor: stat.color }]} />
<Text style={styles.statLabel}>{stat.label}</Text>
<Text style={styles.statValue}>{stat.value}</Text>
</View>
))}
</View>
</View>
{/* 卡路里计算区域 */}
<View style={styles.calorieSection}>
<View style={styles.calorieContent}>
<View style={styles.calculationRow}>
<Text style={styles.calorieSubtitle}>()</Text>
<AnimatedNumber
value={remainingCalories}
resetToken={resetToken}
style={styles.mainValue}
format={(v) => Math.round(v).toString()}
/>
<Text style={styles.calculationText}> = </Text>
<View style={styles.calculationItem}>
<Ionicons name="flame" size={16} color="#FF6B6B" />
<Text style={styles.calculationLabel}></Text>
</View>
<AnimatedNumber
value={burnedCalories}
resetToken={resetToken}
style={styles.calculationValue}
format={(v) => Math.round(v).toString()}
/>
<Text style={styles.calculationText}> - </Text>
<View style={styles.calculationItem}>
<Ionicons name="restaurant" size={16} color="#4ECDC4" />
<Text style={styles.calculationLabel}></Text>
</View>
<AnimatedNumber
value={consumedCalories}
resetToken={resetToken}
style={styles.calculationValue}
format={(v) => Math.round(v).toString()}
/>
</View>
</View>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#FFFFFF',
borderRadius: 22,
padding: 18,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3.84,
elevation: 5,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
cardTitle: {
fontSize: 18,
fontWeight: '800',
color: '#192126',
},
cardRightContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
cardSubtitle: {
fontSize: 12,
color: '#9AA3AE',
fontWeight: '600',
},
contentContainer: {
flexDirection: 'row',
alignItems: 'center',
},
radarContainer: {
alignItems: 'center',
marginRight: 6,
},
statsContainer: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
marginLeft: 4
},
statItem: {
flexDirection: 'row',
alignItems: 'center',
width: '48%',
marginBottom: 16,
},
statDot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 8,
},
statLabel: {
fontSize: 12,
color: '#9AA3AE',
fontWeight: '600',
flex: 1,
},
statValue: {
fontSize: 12,
color: '#192126',
fontWeight: '700',
},
// 卡路里相关样式
calorieSection: {
marginTop: 12,
},
calorieTitleContainer: {
flexDirection: 'row',
alignItems: 'center',
},
calorieIcon: {
fontSize: 16,
marginRight: 8,
},
calorieTitle: {
fontSize: 16,
fontWeight: '800',
color: '#192126',
},
calorieContent: {
},
calorieSubtitle: {
fontSize: 10,
color: '#64748B',
fontWeight: '600',
marginRight: 4,
},
calculationRow: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
gap: 4,
},
mainValue: {
fontSize: 24,
fontWeight: '800',
color: '#192126',
},
calculationText: {
fontSize: 14,
fontWeight: '600',
color: '#64748B',
},
calculationItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 2,
},
calculationLabel: {
fontSize: 10,
color: '#64748B',
fontWeight: '500',
},
calculationValue: {
fontSize: 12,
fontWeight: '700',
color: '#192126',
},
mealsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
},
mealItem: {
alignItems: 'center',
flex: 1,
},
mealIconContainer: {
position: 'relative',
marginBottom: 6,
},
mealEmoji: {
fontSize: 24,
},
addButton: {
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: '#10B981',
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
mealName: {
fontSize: 10,
color: '#64748B',
fontWeight: '600',
},
});