Files
digital-pilates/components/NutritionRadarCard.tsx
richarjiang df2afeb5a1 feat: 更新营养雷达图组件,优化营养分析卡片
- 在营养分析卡片中引入更新日期和图标,提升信息展示
- 修改营养维度标签,简化文本内容
- 更新雷达图组件,支持自定义尺寸和标签显示
- 增加尺寸配置,优化图表显示效果
- 更新饮食记录服务,添加更新时间字段以支持新功能
2025-08-19 11:03:18 +08:00

184 lines
5.4 KiB
TypeScript

import { NutritionSummary } from '@/services/dietRecords';
import Feather from '@expo/vector-icons/Feather';
import dayjs from 'dayjs';
import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { RadarCategory, RadarChart } from './RadarChart';
export type NutritionRadarCardProps = {
nutritionSummary: NutritionSummary | null;
isLoading?: boolean;
};
// 营养维度定义
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, isLoading = false }: 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]);
return (
<View style={styles.card}>
<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>
{isLoading ? (
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>...</Text>
</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>
);
}
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',
},
loadingContainer: {
alignItems: 'center',
justifyContent: 'center',
height: 80,
},
loadingText: {
fontSize: 16,
color: '#9AA3AE',
fontWeight: '600',
},
});