- 移除模拟HRV数据,改为从健康数据中获取实际HRV值 - 新增HRV更新时间显示,提升用户信息获取体验 - 优化日期推导逻辑,确保数据加载一致性 - 更新BMI卡片和营养雷达图组件,支持紧凑模式展示 - 移除不再使用的图片资源,简化项目结构
172 lines
5.3 KiB
TypeScript
172 lines
5.3 KiB
TypeScript
import { ROUTES } from '@/constants/Routes';
|
|
import { NutritionSummary } from '@/services/dietRecords';
|
|
import Feather from '@expo/vector-icons/Feather';
|
|
import dayjs from 'dayjs';
|
|
import { router } from 'expo-router';
|
|
import React, { useMemo } from 'react';
|
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
import { RadarCategory, RadarChart } from './RadarChart';
|
|
|
|
export type NutritionRadarCardProps = {
|
|
nutritionSummary: NutritionSummary | null;
|
|
};
|
|
|
|
// 营养维度定义
|
|
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 }: NutritionRadarCardProps) {
|
|
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];
|
|
|
|
return [
|
|
Math.min(5, (nutritionSummary.totalCalories / recommendations.calories) * 5),
|
|
Math.min(5, (nutritionSummary.totalProtein / recommendations.protein) * 5),
|
|
Math.min(5, (nutritionSummary.totalCarbohydrate / recommendations.carbohydrate) * 5),
|
|
Math.min(5, (nutritionSummary.totalFat / recommendations.fat) * 5),
|
|
Math.min(5, (nutritionSummary.totalFiber / recommendations.fiber) * 5),
|
|
Math.min(5, Math.max(0, 5 - (nutritionSummary.totalSodium / recommendations.sodium) * 5)), // 钠含量越低越好
|
|
];
|
|
}, [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 handleNavigateToRecords = () => {
|
|
router.push(ROUTES.NUTRITION_RECORDS);
|
|
};
|
|
|
|
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('YYYY-MM-DD HH:mm')}</Text>
|
|
<Feather name="more-vertical" size={16} color="#9AA3AE" />
|
|
</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>
|
|
</TouchableOpacity>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
card: {
|
|
backgroundColor: '#FFFFFF',
|
|
borderRadius: 22,
|
|
padding: 18,
|
|
marginBottom: 16,
|
|
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',
|
|
},
|
|
});
|