import { AnimatedNumber } from '@/components/AnimatedNumber';
import { ROUTES } from '@/constants/Routes';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { NutritionSummary } from '@/services/dietRecords';
import { triggerLightHaptic } from '@/utils/haptics';
import { calculateRemainingCalories } from '@/utils/nutrition';
import dayjs from 'dayjs';
import { router } from 'expo-router';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Animated, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
export type NutritionRadarCardProps = {
nutritionSummary: NutritionSummary | null;
/** 基础代谢消耗的卡路里 */
burnedCalories?: number;
/** 基础代谢率 */
basalMetabolism?: number;
/** 运动消耗卡路里 */
activeCalories?: number;
/** 动画重置令牌 */
resetToken?: number;
};
// 简化的圆环进度组件
const SimpleRingProgress = ({
remainingCalories,
totalAvailable
}: {
remainingCalories: number;
totalAvailable: number;
}) => {
const animatedProgress = useRef(new Animated.Value(0)).current;
const radius = 32;
const strokeWidth = 8; // 增加圆环厚度
const center = radius + strokeWidth;
const circumference = 2 * Math.PI * radius;
// 计算进度:已消耗 / 总可用,进度越高表示剩余越少
const consumedAmount = totalAvailable - remainingCalories;
const calorieProgress = totalAvailable > 0 ? Math.min((consumedAmount / totalAvailable) * 100, 100) : 0;
useEffect(() => {
Animated.timing(animatedProgress, {
toValue: calorieProgress,
duration: 600,
useNativeDriver: false,
}).start();
}, [calorieProgress]);
const strokeDashoffset = animatedProgress.interpolate({
inputRange: [0, 100],
outputRange: [circumference, 0],
extrapolate: 'clamp',
});
return (
{Math.round(remainingCalories)}
还能吃
);
};
export function NutritionRadarCard({
nutritionSummary,
burnedCalories = 1618,
basalMetabolism,
activeCalories,
resetToken,
}: NutritionRadarCardProps) {
const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
const { pushIfAuthedElseLogin } = useAuthGuard()
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;
// 使用分离的代谢和运动数据,如果没有提供则从burnedCalories推算
const effectiveBasalMetabolism = basalMetabolism ?? (burnedCalories * 0.7); // 假设70%是基础代谢
const effectiveActiveCalories = activeCalories ?? (burnedCalories * 0.3); // 假设30%是运动消耗
const remainingCalories = calculateRemainingCalories({
basalMetabolism: effectiveBasalMetabolism,
activeCalories: effectiveActiveCalories,
consumedCalories,
});
const handleNavigateToRecords = () => {
triggerLightHaptic();
router.push(ROUTES.NUTRITION_RECORDS);
};
return (
饮食分析
更新: {dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')}
{nutritionStats.map((stat) => (
{stat.label}
{stat.value}
))}
{/* 卡路里计算区域 */}
还能吃
Math.round(v).toString()}
/>
千卡
=
基代
Math.round(v).toString()}
/>
+
运动
Math.round(v).toString()}
/>
-
饮食
Math.round(v).toString()}
/>
{/* 添加食物选项 */}
{
triggerLightHaptic();
pushIfAuthedElseLogin(`/food/camera?mealType=${currentMealType}`);
}}
activeOpacity={0.7}
>
AI识别
{
triggerLightHaptic();
pushIfAuthedElseLogin(`${ROUTES.FOOD_LIBRARY}?mealType=${currentMealType}`);
}}
activeOpacity={0.7}
>
食物库
{
triggerLightHaptic();
pushIfAuthedElseLogin(`${ROUTES.VOICE_RECORD}?mealType=${currentMealType}`);
}}
activeOpacity={0.7}
>
一句话记录
);
}
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,
marginTop: 12
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
},
titleIcon: {
width: 16,
height: 16,
marginRight: 6,
resizeMode: 'contain',
},
cardTitle: {
fontSize: 14,
color: '#192126',
fontWeight: '600'
},
cardSubtitle: {
fontSize: 10,
color: '#9AA3AE',
fontWeight: '600',
},
contentContainer: {
flexDirection: 'row',
alignItems: 'center',
},
radarContainer: {
alignItems: 'center',
justifyContent: 'center',
marginRight: 8,
width: 78, // Fixed width to match ring chart size
height: 78, // Fixed height to match ring chart size
},
statsContainer: {
flex: 1,
marginLeft: 4
},
statsBackground: {
backgroundColor: 'rgba(248, 250, 252, 0.8)', // 毛玻璃背景色
borderRadius: 12,
padding: 12,
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
justifyContent: 'space-between',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.06,
shadowRadius: 3,
elevation: 1,
// 添加边框增强毛玻璃效果
borderWidth: 0.5,
borderColor: 'rgba(255, 255, 255, 0.8)',
},
statItem: {
flexDirection: 'row',
alignItems: 'center',
width: '48%',
marginBottom: 8,
},
statDot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 8,
},
statLabel: {
fontSize: 10,
color: '#9AA3AE',
flex: 1,
},
statValue: {
fontSize: 12,
color: '#192126',
fontWeight: '600',
},
// 卡路里相关样式
calorieSection: {
marginTop: 6,
},
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: 14,
fontWeight: '600',
color: '#192126',
},
calculationText: {
fontSize: 10,
fontWeight: '600',
color: '#64748B',
},
calculationItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 2,
},
calculationLabel: {
fontSize: 9,
color: '#64748B',
fontWeight: '500',
},
calculationValue: {
fontSize: 11,
fontWeight: '700',
color: '#192126',
},
remainingCaloriesContainer: {
flexDirection: 'row',
alignItems: 'baseline',
gap: 2,
},
calorieUnit: {
fontSize: 10,
color: '#64748B',
fontWeight: '500',
},
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,
},
mealName: {
fontSize: 10,
color: '#64748B',
fontWeight: '600',
},
// 食物选项样式
foodOptionsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: 12,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: '#F1F5F9',
gap: 16,
},
foodOptionItem: {
alignItems: 'center',
flex: 1,
},
foodOptionIcon: {
width: 24,
height: 24,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 6,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.15,
shadowRadius: 4,
elevation: 4,
},
foodOptionEmoji: {
fontSize: 14,
},
foodOptionImage: {
width: 20,
height: 20,
resizeMode: 'contain',
},
foodOptionText: {
fontSize: 10,
fontWeight: '500',
color: '#192126',
textAlign: 'center',
},
});