- 新增晚餐提醒(18:00)和心情提醒(21:00)的定时通知 - 实现基于HRV数据的压力检测和智能鼓励通知 - 添加后台任务处理支持,修改iOS后台模式为processing - 优化营养记录页面使用Redux状态管理,支持实时数据更新 - 重构卡路里计算公式,移除目标卡路里概念,改为基代+运动-饮食 - 新增营养目标动态计算功能,基于用户身体数据智能推荐 - 完善通知点击跳转逻辑,支持多种提醒类型的路由处理
298 lines
8.0 KiB
TypeScript
298 lines
8.0 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;
|
|
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;
|
|
const canEat = Math.max(0, remainingCalories);
|
|
|
|
// 计算进度百分比 (用于圆环显示)
|
|
const totalAvailable = metabolism + exercise;
|
|
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.toFixed(1)}千卡
|
|
</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 }]}>
|
|
{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>
|
|
|
|
{/* 底部营养素展示 */}
|
|
<View style={styles.nutritionContainer}>
|
|
<View style={styles.nutritionItem}>
|
|
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
|
|
蛋白质
|
|
</ThemedText>
|
|
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
|
|
{Math.round(protein)}/{Math.round(proteinGoal)}g
|
|
</ThemedText>
|
|
</View>
|
|
|
|
<View style={styles.nutritionItem}>
|
|
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
|
|
脂肪
|
|
</ThemedText>
|
|
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
|
|
{Math.round(fat)}/{Math.round(fatGoal)}g
|
|
</ThemedText>
|
|
</View>
|
|
|
|
<View style={styles.nutritionItem}>
|
|
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
|
|
碳水化合物
|
|
</ThemedText>
|
|
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
|
|
{Math.round(carbs)}/{Math.round(carbsGoal)}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',
|
|
},
|
|
}); |