292 lines
8.3 KiB
TypeScript
292 lines
8.3 KiB
TypeScript
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;
|
|
protein: number;
|
|
fat: number;
|
|
carbs: number;
|
|
proteinGoal: number;
|
|
fatGoal: number;
|
|
carbsGoal: number;
|
|
};
|
|
|
|
export function CalorieRingChart({
|
|
metabolism,
|
|
exercise,
|
|
consumed,
|
|
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;
|
|
const canEat = Math.max(0, remainingCalories);
|
|
|
|
// 计算进度百分比 (用于圆环显示)
|
|
const totalAvailable = metabolism + exercise;
|
|
const progressPercentage = totalAvailable > 0 ? Math.min((consumed / totalAvailable) * 100, 100) : 0;
|
|
|
|
// 圆环参数 - 减小尺寸以优化空间占用
|
|
const radius = 48;
|
|
const strokeWidth = 8; // 增加圆环厚度
|
|
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" : "#4ECDC4"}
|
|
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 }]}>
|
|
{Math.round(canEat)}千卡
|
|
</ThemedText>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 右侧数据展示 */}
|
|
<View style={styles.dataContainer}>
|
|
<View style={styles.dataBackground}>
|
|
{/* 左右两列布局 */}
|
|
<View style={styles.dataColumns}>
|
|
{/* 左列:卡路里数据 */}
|
|
<View style={styles.dataColumn}>
|
|
<View style={styles.dataItem}>
|
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>代谢</ThemedText>
|
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
|
{Math.round(metabolism)}千卡
|
|
</ThemedText>
|
|
</View>
|
|
|
|
<View style={styles.dataItem}>
|
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>运动</ThemedText>
|
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
|
{Math.round(exercise)}千卡
|
|
</ThemedText>
|
|
</View>
|
|
|
|
<View style={styles.dataItem}>
|
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>饮食</ThemedText>
|
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
|
{Math.round(consumed)}千卡
|
|
</ThemedText>
|
|
</View>
|
|
</View>
|
|
|
|
{/* 右列:营养数据 */}
|
|
<View style={styles.dataColumn}>
|
|
<View style={styles.dataItem}>
|
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>蛋白质</ThemedText>
|
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
|
{Math.round(protein)}g
|
|
</ThemedText>
|
|
</View>
|
|
|
|
<View style={styles.dataItem}>
|
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>脂肪</ThemedText>
|
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
|
{Math.round(fat)}g
|
|
</ThemedText>
|
|
</View>
|
|
|
|
<View style={styles.dataItem}>
|
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>碳水</ThemedText>
|
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
|
{Math.round(carbs)}g
|
|
</ThemedText>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
backgroundColor: '#FFFFFF',
|
|
borderRadius: 16,
|
|
padding: 16,
|
|
marginHorizontal: 16,
|
|
marginBottom: 8,
|
|
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: 0, // 移除底部间距,因为不再有底部营养容器
|
|
paddingHorizontal: 8,
|
|
},
|
|
chartContainer: {
|
|
position: 'relative',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
width: 112, // 减少宽度以匹配更小的圆环 (48*2 + 8*2)
|
|
flexShrink: 0,
|
|
},
|
|
centerContent: {
|
|
position: 'absolute',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
centerLabel: {
|
|
fontSize: 11,
|
|
fontWeight: '500',
|
|
color: '#999999',
|
|
marginBottom: 2,
|
|
},
|
|
centerValue: {
|
|
fontSize: 14,
|
|
fontWeight: '600',
|
|
color: '#333333',
|
|
marginBottom: 1,
|
|
},
|
|
centerPercentage: {
|
|
fontSize: 11,
|
|
fontWeight: '500',
|
|
color: '#999999',
|
|
},
|
|
dataContainer: {
|
|
flex: 1,
|
|
marginLeft: 16,
|
|
},
|
|
dataBackground: {
|
|
backgroundColor: 'rgba(248, 250, 252, 0.8)', // 毛玻璃背景色
|
|
borderRadius: 12,
|
|
padding: 12,
|
|
shadowColor: '#000',
|
|
shadowOffset: {
|
|
width: 0,
|
|
height: 1,
|
|
},
|
|
shadowOpacity: 0.06,
|
|
shadowRadius: 3,
|
|
elevation: 1,
|
|
// 添加边框增强毛玻璃效果
|
|
borderWidth: 0.5,
|
|
borderColor: 'rgba(255, 255, 255, 0.8)',
|
|
gap: 4,
|
|
},
|
|
dataItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
},
|
|
dataIcon: {
|
|
width: 6,
|
|
height: 6,
|
|
borderRadius: 3,
|
|
},
|
|
dataLabel: {
|
|
fontSize: 11,
|
|
fontWeight: '500',
|
|
color: '#999999',
|
|
minWidth: 28,
|
|
},
|
|
dataValue: {
|
|
fontSize: 11,
|
|
fontWeight: '600',
|
|
color: '#333333',
|
|
},
|
|
dataColumns: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
gap: 12,
|
|
},
|
|
dataColumn: {
|
|
flex: 1,
|
|
gap: 4,
|
|
},
|
|
}); |