Files
digital-pilates/components/CalorieRingChart.tsx
richarjiang bca6670390 Add Chinese translations for medication management and personal settings
- Introduced new translation files for medication, personal, and weight management in Chinese.
- Updated the main index file to include the new translation modules.
- Enhanced the medication type definitions to include 'ointment'.
- Refactored workout type labels to utilize i18n for better localization support.
- Improved sleep quality descriptions and recommendations with i18n integration.
2025-11-28 17:29:51 +08:00

305 lines
8.8 KiB
TypeScript

import { ThemedText } from '@/components/ThemedText';
import { useI18n } from '@/hooks/useI18n';
import { useThemeColor } from '@/hooks/useThemeColor';
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import Svg, { Circle, Defs, Stop, LinearGradient as SvgLinearGradient } 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,
}: CalorieRingChartProps) {
const { t } = useI18n();
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 = 42;
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}>
<View style={styles.mainContent}>
{/* 左侧圆环图 */}
<View style={styles.chartContainer}>
<Svg width={center * 2} height={center * 2}>
<Defs>
<SvgLinearGradient id="progressGradient" x1="0" y1="0" x2="1" y2="1">
<Stop offset="0" stopColor={progressPercentage > 80 ? "#FF9966" : "#4facfe"} stopOpacity="1" />
<Stop offset="1" stopColor={progressPercentage > 80 ? "#FF5E62" : "#00f2fe"} stopOpacity="1" />
</SvgLinearGradient>
</Defs>
{/* 背景圆环 */}
<Circle
cx={center}
cy={center}
r={radius}
stroke="#F5F7FA"
strokeWidth={strokeWidth}
fill="none"
/>
{/* 进度圆环 */}
<AnimatedCircle
cx={center}
cy={center}
r={radius}
stroke="url(#progressGradient)"
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={`${strokeDasharray}`}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
transform={`rotate(-90 ${center} ${center})`}
/>
</Svg>
{/* 中心内容 */}
<View style={styles.centerContent}>
<ThemedText style={styles.centerLabel}>
{t('nutritionRecords.chart.remaining')}
</ThemedText>
<ThemedText style={styles.centerValue}>
{Math.round(canEat)}
</ThemedText>
<ThemedText style={styles.centerUnit}>
{t('nutritionRecords.nutrients.caloriesUnit')}
</ThemedText>
</View>
</View>
{/* 右侧数据展示 - 优化布局 */}
<View style={styles.dataContainer}>
{/* 公式 */}
<View style={styles.formulaContainer}>
<ThemedText style={styles.formulaText}>
{t('nutritionRecords.chart.formula')}
</ThemedText>
</View>
{/* 代谢 & 运动 & 饮食 */}
<View style={styles.statsGroup}>
<View style={styles.statRowCompact}>
<View style={styles.labelWithDot}>
<View style={styles.dotMetabolism} />
<ThemedText style={styles.statLabel}>{t('nutritionRecords.chart.metabolism')}</ThemedText>
</View>
<ThemedText style={styles.statValue}>{Math.round(metabolism)}</ThemedText>
</View>
<View style={styles.statRowCompact}>
<View style={styles.labelWithDot}>
<View style={styles.dotExercise} />
<ThemedText style={styles.statLabel}>{t('nutritionRecords.chart.exercise')}</ThemedText>
</View>
<ThemedText style={styles.statValue}>{Math.round(exercise)}</ThemedText>
</View>
<View style={styles.statRowCompact}>
<View style={styles.labelWithDot}>
<View style={styles.dotConsumed} />
<ThemedText style={styles.statLabel}>{t('nutritionRecords.chart.diet')}</ThemedText>
</View>
<ThemedText style={styles.statValue}>{Math.round(consumed)}</ThemedText>
</View>
</View>
<View style={styles.divider} />
{/* 营养素 - 水平排布 */}
<View style={styles.nutritionRow}>
<View style={styles.nutritionItem}>
<ThemedText style={styles.statValueSmall}>{Math.round(protein)}{t('nutritionRecords.nutrients.unit')}</ThemedText>
<ThemedText style={styles.statLabelSmall}>{t('nutritionRecords.nutrients.protein')}</ThemedText>
</View>
<View style={styles.nutritionItem}>
<ThemedText style={styles.statValueSmall}>{Math.round(fat)}{t('nutritionRecords.nutrients.unit')}</ThemedText>
<ThemedText style={styles.statLabelSmall}>{t('nutritionRecords.nutrients.fat')}</ThemedText>
</View>
<View style={styles.nutritionItem}>
<ThemedText style={styles.statValueSmall}>{Math.round(carbs)}{t('nutritionRecords.nutrients.unit')}</ThemedText>
<ThemedText style={styles.statLabelSmall}>{t('nutritionRecords.nutrients.carbs')}</ThemedText>
</View>
</View>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
borderRadius: 24,
padding: 16,
marginHorizontal: 20,
shadowColor: 'rgba(30, 41, 59, 0.08)',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.12,
shadowRadius: 16,
elevation: 6,
},
formulaContainer: {
marginBottom: 12,
},
formulaText: {
fontSize: 10,
fontWeight: '500',
color: '#94A3B8',
fontFamily: 'AliRegular',
},
mainContent: {
flexDirection: 'row',
alignItems: 'flex-start',
},
chartContainer: {
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
width: 100,
height: 100,
marginTop: 8,
},
centerContent: {
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
},
centerLabel: {
fontSize: 10,
fontWeight: '500',
color: '#94A3B8',
marginBottom: 1,
fontFamily: 'AliRegular',
},
centerValue: {
fontSize: 20,
fontWeight: '800',
color: '#1E293B',
lineHeight: 24,
fontFamily: 'AliBold',
},
centerUnit: {
fontSize: 10,
fontWeight: '600',
color: '#64748B',
fontFamily: 'AliRegular',
},
dataContainer: {
flex: 1,
marginLeft: 20,
},
statsGroup: {
gap: 6,
},
statRowCompact: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
labelWithDot: {
flexDirection: 'row',
alignItems: 'center',
},
dotMetabolism: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: '#94A3B8',
marginRight: 6,
},
dotExercise: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: '#4facfe',
marginRight: 6,
},
dotConsumed: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: '#FF9966',
marginRight: 6,
},
statLabel: {
fontSize: 12,
color: '#64748B',
fontFamily: 'AliRegular',
},
statValue: {
fontSize: 13,
fontWeight: '600',
color: '#334155',
fontFamily: 'AliBold',
},
divider: {
height: 1,
backgroundColor: '#F1F5F9',
marginVertical: 10,
},
nutritionRow: {
flexDirection: 'row',
justifyContent: 'space-between',
},
nutritionItem: {
alignItems: 'center',
},
statLabelSmall: {
fontSize: 10,
color: '#94A3B8',
marginTop: 2,
fontFamily: 'AliRegular',
},
statValueSmall: {
fontSize: 13,
fontWeight: '600',
color: '#475569',
fontFamily: 'AliBold',
},
});