feat: 支持营养圆环

This commit is contained in:
richarjiang
2025-08-31 16:30:08 +08:00
parent 4bb0576d92
commit fe634ba258
8 changed files with 400 additions and 158 deletions

View File

@@ -0,0 +1,300 @@
import { ThemedText } from '@/components/ThemedText';
import { useThemeColor } from '@/hooks/useThemeColor';
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
export type CalorieRingChartProps = {
metabolism: number;
exercise: number;
consumed: number;
goal: number;
protein: number;
fat: number;
carbs: number;
proteinGoal: number;
fatGoal: number;
carbsGoal: number;
};
export function CalorieRingChart({
metabolism,
exercise,
consumed,
goal,
protein,
fat,
carbs,
proteinGoal,
fatGoal,
carbsGoal,
}: CalorieRingChartProps) {
const surfaceColor = useThemeColor({}, 'surface');
const textColor = useThemeColor({}, 'text');
const textSecondaryColor = useThemeColor({}, 'textSecondary');
// 动画值
const animatedProgress = useRef(new Animated.Value(0)).current;
// 计算还能吃多少卡路里
const remainingCalories = metabolism + exercise - consumed - goal;
const canEat = Math.max(0, remainingCalories);
// 计算进度百分比 (用于圆环显示)
const totalAvailable = metabolism + exercise - goal;
const progressPercentage = totalAvailable > 0 ? Math.min((consumed / totalAvailable) * 100, 100) : 0;
// 圆环参数 - 更小的圆环以适应布局
const radius = 62;
const strokeWidth = 6;
const center = radius + strokeWidth;
const circumference = 2 * Math.PI * radius;
const strokeDasharray = circumference;
// 动画效果
useEffect(() => {
Animated.timing(animatedProgress, {
toValue: progressPercentage,
duration: 600,
useNativeDriver: false,
}).start();
}, [progressPercentage]);
// 使用动画值计算strokeDashoffset
const strokeDashoffset = animatedProgress.interpolate({
inputRange: [0, 100],
outputRange: [circumference, 0],
extrapolate: 'clamp',
});
return (
<View style={[styles.container, { backgroundColor: surfaceColor }]}>
{/* 左上角公式展示 */}
<View style={styles.formulaContainer}>
<ThemedText style={[styles.formulaText, { color: textSecondaryColor }]}>
= + - -
</ThemedText>
</View>
{/* 主要内容区域 */}
<View style={styles.mainContent}>
{/* 左侧圆环图 */}
<View style={styles.chartContainer}>
<Svg width={center * 2} height={center * 2}>
{/* 背景圆环 */}
<Circle
cx={center}
cy={center}
r={radius}
stroke="#F0F0F0"
strokeWidth={strokeWidth}
fill="none"
/>
{/* 进度圆环 - 保持固定颜色 */}
<AnimatedCircle
cx={center}
cy={center}
r={radius}
stroke={progressPercentage > 80 ? "#FF6B6B" : "#E0E0E0"}
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={`${strokeDasharray}`}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
transform={`rotate(-90 ${center} ${center})`}
/>
</Svg>
{/* 中心内容 */}
<View style={styles.centerContent}>
<ThemedText style={[styles.centerLabel, { color: textSecondaryColor }]}>
</ThemedText>
<ThemedText style={[styles.centerValue, { color: textColor }]}>
{canEat.toLocaleString()}
</ThemedText>
<ThemedText style={[styles.centerPercentage, { color: textSecondaryColor }]}>
{Math.round(progressPercentage)}%
</ThemedText>
</View>
</View>
{/* 右侧数据展示 */}
<View style={styles.dataContainer}>
{/* 各项数值 */}
<View style={styles.dataItem}>
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}></ThemedText>
<ThemedText style={[styles.dataValue, { color: textColor }]}>
{metabolism.toLocaleString()}
</ThemedText>
</View>
<View style={styles.dataItem}>
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}></ThemedText>
<ThemedText style={[styles.dataValue, { color: textColor }]}>
{exercise}
</ThemedText>
</View>
<View style={styles.dataItem}>
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}></ThemedText>
<ThemedText style={[styles.dataValue, { color: textColor }]}>
{consumed}
</ThemedText>
</View>
<View style={styles.dataItem}>
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}></ThemedText>
<ThemedText style={[styles.dataValue, { color: textColor }]}>
{goal}
</ThemedText>
</View>
</View>
</View>
{/* 底部营养素展示 */}
<View style={styles.nutritionContainer}>
<View style={styles.nutritionItem}>
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
</ThemedText>
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
{protein.toFixed(2)}/{proteinGoal.toFixed(2)}g
</ThemedText>
</View>
<View style={styles.nutritionItem}>
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
</ThemedText>
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
{fat.toFixed(2)}/{fatGoal.toFixed(2)}g
</ThemedText>
</View>
<View style={styles.nutritionItem}>
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
</ThemedText>
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
{carbs.toFixed(2)}/{carbsGoal.toFixed(2)}g
</ThemedText>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 16,
marginHorizontal: 16,
marginBottom: 16,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.04,
shadowRadius: 8,
elevation: 2,
},
formulaContainer: {
alignItems: 'flex-start',
marginBottom: 12,
},
formulaText: {
fontSize: 12,
fontWeight: '500',
color: '#999999',
lineHeight: 16,
},
mainContent: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 16,
paddingHorizontal: 8,
},
chartContainer: {
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
width: 140,
flexShrink: 0,
},
centerContent: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
},
centerLabel: {
fontSize: 11,
fontWeight: '500',
color: '#999999',
marginBottom: 2,
},
centerValue: {
fontSize: 16,
fontWeight: '700',
color: '#333333',
marginBottom: 1,
},
centerPercentage: {
fontSize: 11,
fontWeight: '500',
color: '#999999',
},
dataContainer: {
flex: 1,
marginLeft: 32,
gap: 4,
paddingLeft: 8,
},
dataItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingVertical: 2,
},
dataIcon: {
width: 6,
height: 6,
borderRadius: 3,
},
dataLabel: {
fontSize: 11,
fontWeight: '500',
color: '#999999',
minWidth: 28,
},
dataValue: {
fontSize: 11,
fontWeight: '600',
color: '#333333',
},
nutritionContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: 'rgba(0,0,0,0.06)',
},
nutritionItem: {
alignItems: 'center',
flex: 1,
},
nutritionLabel: {
fontSize: 10,
fontWeight: '500',
color: '#999999',
marginBottom: 3,
},
nutritionValue: {
fontSize: 11,
fontWeight: '600',
color: '#333333',
},
});