feat: 支持营养圆环
This commit is contained in:
300
components/CalorieRingChart.tsx
Normal file
300
components/CalorieRingChart.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user