import { AnimatedNumber } from '@/components/AnimatedNumber'; import { ROUTES } from '@/constants/Routes'; import { useActiveCalories } from '@/hooks/useActiveCalories'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { fetchDailyBasalMetabolism, fetchDailyNutritionData, selectBasalMetabolismByDate, selectNutritionSummaryByDate } from '@/store/nutritionSlice'; 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 = { selectedDate?: Date; style?: object; /** 动画重置令牌 */ 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 ( 80 ? "#FF6B6B" : "#4ECDC4"} strokeWidth={strokeWidth} fill="none" strokeDasharray={`${circumference}`} strokeDashoffset={strokeDashoffset} strokeLinecap="round" transform={`rotate(-90 ${center} ${center})`} /> {Math.round(remainingCalories)} 还能吃 ); }; export function NutritionRadarCard({ selectedDate, style, resetToken, }: NutritionRadarCardProps) { const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast'); const [loading, setLoading] = useState(false); const { pushIfAuthedElseLogin } = useAuthGuard(); const dispatch = useAppDispatch(); const dateKey = useMemo(() => { return selectedDate ? dayjs(selectedDate).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD'); }, [selectedDate]); // 使用专用的选择器获取营养数据和基础代谢 const nutritionSummary = useAppSelector(selectNutritionSummaryByDate(dateKey)); const basalMetabolism = useAppSelector(selectBasalMetabolismByDate(dateKey)); // 使用专用的hook获取运动消耗卡路里 const { activeCalories: effectiveActiveCalories, loading: activeCaloriesLoading } = useActiveCalories(selectedDate); // 获取营养数据和基础代谢数据 useEffect(() => { const loadNutritionCardData = async () => { const targetDate = selectedDate || new Date(); try { setLoading(true); await Promise.all([ dispatch(fetchDailyNutritionData(targetDate)).unwrap(), dispatch(fetchDailyBasalMetabolism(targetDate)).unwrap(), ]); } catch (error) { console.error('NutritionRadarCard: 获取营养卡片数据失败:', error); } finally { setLoading(false); } }; loadNutritionCardData(); }, [selectedDate, dispatch]); 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; // 使用从HealthKit获取的数据,如果没有则使用默认值 const effectiveBasalMetabolism = basalMetabolism || 0; // 基础代谢默认值 const remainingCalories = calculateRemainingCalories({ basalMetabolism: effectiveBasalMetabolism, activeCalories: effectiveActiveCalories, consumedCalories, }); const handleNavigateToRecords = () => { triggerLightHaptic(); router.push(ROUTES.NUTRITION_RECORDS); }; return ( 饮食分析 {loading ? '加载中...' : `更新: ${dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')}`} {nutritionStats.map((stat) => ( {stat.label} {stat.value} ))} {/* 卡路里计算区域 */} 还能吃 (loading || activeCaloriesLoading) ? '--' : Math.round(v).toString()} /> 千卡 = 基代 loading ? '--' : Math.round(v).toString()} /> + 运动 activeCaloriesLoading ? '--' : Math.round(v).toString()} /> - 饮食 loading ? '--' : 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', }, });