feat: 优化营养记录和卡路里环图组件,增加毛玻璃背景和动画效果
This commit is contained in:
@@ -660,13 +660,7 @@ export default function ExploreScreen() {
|
|||||||
/>
|
/>
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
||||||
{/* 饮水记录卡片 */}
|
|
||||||
<FloatingCard style={styles.masonryCard} delay={500}>
|
|
||||||
<WaterIntakeCard
|
|
||||||
selectedDate={currentSelectedDateString}
|
|
||||||
style={styles.waterCardOverride}
|
|
||||||
/>
|
|
||||||
</FloatingCard>
|
|
||||||
|
|
||||||
<FloatingCard style={styles.masonryCard} delay={0}>
|
<FloatingCard style={styles.masonryCard} delay={0}>
|
||||||
<StressMeter
|
<StressMeter
|
||||||
@@ -684,6 +678,19 @@ export default function ExploreScreen() {
|
|||||||
heartRate={heartRate}
|
heartRate={heartRate}
|
||||||
/>
|
/>
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
||||||
|
<FloatingCard style={styles.masonryCard}>
|
||||||
|
<View style={styles.cardHeaderRow}>
|
||||||
|
<Text style={styles.cardTitle}>睡眠</Text>
|
||||||
|
</View>
|
||||||
|
{sleepDuration != null ? (
|
||||||
|
<Text style={styles.sleepValue}>
|
||||||
|
{Math.floor(sleepDuration / 60)}小时{Math.floor(sleepDuration % 60)}分钟
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.sleepValue}>——</Text>
|
||||||
|
)}
|
||||||
|
</FloatingCard>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 右列 */}
|
{/* 右列 */}
|
||||||
@@ -699,20 +706,15 @@ export default function ExploreScreen() {
|
|||||||
resetToken={animToken}
|
resetToken={animToken}
|
||||||
/>
|
/>
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
{/* 饮水记录卡片 */}
|
||||||
<FloatingCard style={styles.masonryCard}>
|
<FloatingCard style={styles.masonryCard} delay={500}>
|
||||||
<View style={styles.cardHeaderRow}>
|
<WaterIntakeCard
|
||||||
<Text style={styles.cardTitle}>睡眠</Text>
|
selectedDate={currentSelectedDateString}
|
||||||
</View>
|
style={styles.waterCardOverride}
|
||||||
{sleepDuration != null ? (
|
/>
|
||||||
<Text style={styles.sleepValue}>
|
|
||||||
{Math.floor(sleepDuration / 60)}小时{Math.floor(sleepDuration % 60)}分钟
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Text style={styles.sleepValue}>——</Text>
|
|
||||||
)}
|
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
||||||
|
|
||||||
{/* 基础代谢卡片 */}
|
{/* 基础代谢卡片 */}
|
||||||
<FloatingCard style={styles.masonryCard} delay={1250}>
|
<FloatingCard style={styles.masonryCard} delay={1250}>
|
||||||
<BasalMetabolismCard
|
<BasalMetabolismCard
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import dayjs from 'dayjs';
|
|||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
|
||||||
FlatList,
|
FlatList,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -394,7 +393,6 @@ export default function NutritionRecordsScreen() {
|
|||||||
metabolism={healthData?.basalEnergyBurned || 1482}
|
metabolism={healthData?.basalEnergyBurned || 1482}
|
||||||
exercise={healthData?.activeEnergyBurned || 0}
|
exercise={healthData?.activeEnergyBurned || 0}
|
||||||
consumed={nutritionSummary?.totalCalories || 0}
|
consumed={nutritionSummary?.totalCalories || 0}
|
||||||
goal={0}
|
|
||||||
protein={nutritionSummary?.totalProtein || 0}
|
protein={nutritionSummary?.totalProtein || 0}
|
||||||
fat={nutritionSummary?.totalFat || 0}
|
fat={nutritionSummary?.totalFat || 0}
|
||||||
carbs={nutritionSummary?.totalCarbohydrate || 0}
|
carbs={nutritionSummary?.totalCarbohydrate || 0}
|
||||||
@@ -403,14 +401,7 @@ export default function NutritionRecordsScreen() {
|
|||||||
carbsGoal={nutritionGoals.carbsGoal}
|
carbsGoal={nutritionGoals.carbsGoal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{loading ? (
|
{(
|
||||||
<View style={styles.loadingContainer}>
|
|
||||||
<ActivityIndicator size="large" color={colorTokens.primary} />
|
|
||||||
<Text style={[styles.loadingText, { color: colorTokens.textSecondary }]}>
|
|
||||||
加载中...
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={displayRecords}
|
data={displayRecords}
|
||||||
renderItem={({ item, index }) => renderRecord({ item, index })}
|
renderItem={({ item, index }) => renderRecord({ item, index })}
|
||||||
|
|||||||
@@ -11,21 +11,18 @@ export type CalorieRingChartProps = {
|
|||||||
metabolism: number;
|
metabolism: number;
|
||||||
exercise: number;
|
exercise: number;
|
||||||
consumed: number;
|
consumed: number;
|
||||||
goal: number;
|
|
||||||
protein: number;
|
protein: number;
|
||||||
fat: number;
|
fat: number;
|
||||||
carbs: number;
|
carbs: number;
|
||||||
proteinGoal: number;
|
proteinGoal: number;
|
||||||
fatGoal: number;
|
fatGoal: number;
|
||||||
carbsGoal: number;
|
carbsGoal: number;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CalorieRingChart({
|
export function CalorieRingChart({
|
||||||
metabolism,
|
metabolism,
|
||||||
exercise,
|
exercise,
|
||||||
consumed,
|
consumed,
|
||||||
goal,
|
|
||||||
protein,
|
protein,
|
||||||
fat,
|
fat,
|
||||||
carbs,
|
carbs,
|
||||||
@@ -49,9 +46,9 @@ export function CalorieRingChart({
|
|||||||
const totalAvailable = metabolism + exercise;
|
const totalAvailable = metabolism + exercise;
|
||||||
const progressPercentage = totalAvailable > 0 ? Math.min((consumed / totalAvailable) * 100, 100) : 0;
|
const progressPercentage = totalAvailable > 0 ? Math.min((consumed / totalAvailable) * 100, 100) : 0;
|
||||||
|
|
||||||
// 圆环参数 - 更小的圆环以适应布局
|
// 圆环参数 - 减小尺寸以优化空间占用
|
||||||
const radius = 62;
|
const radius = 48;
|
||||||
const strokeWidth = 6;
|
const strokeWidth = 8; // 增加圆环厚度
|
||||||
const center = radius + strokeWidth;
|
const center = radius + strokeWidth;
|
||||||
const circumference = 2 * Math.PI * radius;
|
const circumference = 2 * Math.PI * radius;
|
||||||
const strokeDasharray = circumference;
|
const strokeDasharray = circumference;
|
||||||
@@ -100,7 +97,7 @@ export function CalorieRingChart({
|
|||||||
cx={center}
|
cx={center}
|
||||||
cy={center}
|
cy={center}
|
||||||
r={radius}
|
r={radius}
|
||||||
stroke={progressPercentage > 80 ? "#FF6B6B" : "#E0E0E0"}
|
stroke={progressPercentage > 80 ? "#FF6B6B" : "#4ECDC4"}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
fill="none"
|
fill="none"
|
||||||
strokeDasharray={`${strokeDasharray}`}
|
strokeDasharray={`${strokeDasharray}`}
|
||||||
@@ -116,7 +113,7 @@ export function CalorieRingChart({
|
|||||||
还能吃
|
还能吃
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText style={[styles.centerValue, { color: textColor }]}>
|
<ThemedText style={[styles.centerValue, { color: textColor }]}>
|
||||||
{canEat.toFixed(1)}千卡
|
{Math.round(canEat)}千卡
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText style={[styles.centerPercentage, { color: textSecondaryColor }]}>
|
<ThemedText style={[styles.centerPercentage, { color: textSecondaryColor }]}>
|
||||||
{Math.round(progressPercentage)}%
|
{Math.round(progressPercentage)}%
|
||||||
@@ -126,7 +123,11 @@ export function CalorieRingChart({
|
|||||||
|
|
||||||
{/* 右侧数据展示 */}
|
{/* 右侧数据展示 */}
|
||||||
<View style={styles.dataContainer}>
|
<View style={styles.dataContainer}>
|
||||||
{/* 各项数值 */}
|
<View style={styles.dataBackground}>
|
||||||
|
{/* 左右两列布局 */}
|
||||||
|
<View style={styles.dataColumns}>
|
||||||
|
{/* 左列:卡路里数据 */}
|
||||||
|
<View style={styles.dataColumn}>
|
||||||
<View style={styles.dataItem}>
|
<View style={styles.dataItem}>
|
||||||
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>代谢</ThemedText>
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>代谢</ThemedText>
|
||||||
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||||
@@ -147,41 +148,36 @@ export function CalorieRingChart({
|
|||||||
{Math.round(consumed)}千卡
|
{Math.round(consumed)}千卡
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 底部营养素展示 */}
|
{/* 右列:营养数据 */}
|
||||||
<View style={styles.nutritionContainer}>
|
<View style={styles.dataColumn}>
|
||||||
<View style={styles.nutritionItem}>
|
<View style={styles.dataItem}>
|
||||||
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>蛋白质</ThemedText>
|
||||||
蛋白质
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||||
</ThemedText>
|
{Math.round(protein)}g
|
||||||
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
|
|
||||||
{Math.round(protein)}/{Math.round(proteinGoal)}g
|
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.nutritionItem}>
|
<View style={styles.dataItem}>
|
||||||
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>脂肪</ThemedText>
|
||||||
脂肪
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||||
</ThemedText>
|
{Math.round(fat)}g
|
||||||
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
|
|
||||||
{Math.round(fat)}/{Math.round(fatGoal)}g
|
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.nutritionItem}>
|
<View style={styles.dataItem}>
|
||||||
<ThemedText style={[styles.nutritionLabel, { color: textSecondaryColor }]}>
|
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}>碳水</ThemedText>
|
||||||
碳水化合物
|
<ThemedText style={[styles.dataValue, { color: textColor }]}>
|
||||||
</ThemedText>
|
{Math.round(carbs)}g
|
||||||
<ThemedText style={[styles.nutritionValue, { color: textColor }]}>
|
|
||||||
{Math.round(carbs)}/{Math.round(carbsGoal)}g
|
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,14 +209,14 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginBottom: 16,
|
marginBottom: 0, // 移除底部间距,因为不再有底部营养容器
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
},
|
},
|
||||||
chartContainer: {
|
chartContainer: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: 140,
|
width: 112, // 减少宽度以匹配更小的圆环 (48*2 + 8*2)
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
centerContent: {
|
centerContent: {
|
||||||
@@ -235,8 +231,8 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 2,
|
marginBottom: 2,
|
||||||
},
|
},
|
||||||
centerValue: {
|
centerValue: {
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: '700',
|
fontWeight: '600',
|
||||||
color: '#333333',
|
color: '#333333',
|
||||||
marginBottom: 1,
|
marginBottom: 1,
|
||||||
},
|
},
|
||||||
@@ -247,15 +243,29 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
dataContainer: {
|
dataContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
marginLeft: 32,
|
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,
|
gap: 4,
|
||||||
paddingLeft: 8,
|
|
||||||
},
|
},
|
||||||
dataItem: {
|
dataItem: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
paddingVertical: 2,
|
|
||||||
},
|
},
|
||||||
dataIcon: {
|
dataIcon: {
|
||||||
width: 6,
|
width: 6,
|
||||||
@@ -273,26 +283,13 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#333333',
|
color: '#333333',
|
||||||
},
|
},
|
||||||
nutritionContainer: {
|
dataColumns: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingTop: 12,
|
gap: 12,
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: 'rgba(0,0,0,0.06)',
|
|
||||||
},
|
},
|
||||||
nutritionItem: {
|
dataColumn: {
|
||||||
alignItems: 'center',
|
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
gap: 4,
|
||||||
nutritionLabel: {
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '#999999',
|
|
||||||
marginBottom: 3,
|
|
||||||
},
|
|
||||||
nutritionValue: {
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#333333',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -6,9 +6,11 @@ import { NutritionGoals, calculateRemainingCalories } from '@/utils/nutrition';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { Animated, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { RadarCategory, RadarChart } from './RadarChart';
|
import Svg, { Circle } from 'react-native-svg';
|
||||||
|
|
||||||
|
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
||||||
|
|
||||||
|
|
||||||
export type NutritionRadarCardProps = {
|
export type NutritionRadarCardProps = {
|
||||||
@@ -28,15 +30,71 @@ export type NutritionRadarCardProps = {
|
|||||||
onMealPress?: (mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => void;
|
onMealPress?: (mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 营养维度定义
|
// 简化的圆环进度组件
|
||||||
const NUTRITION_DIMENSIONS: RadarCategory[] = [
|
const SimpleRingProgress = ({
|
||||||
{ key: 'calories', label: '热量' },
|
remainingCalories,
|
||||||
{ key: 'protein', label: '蛋白质' },
|
totalAvailable
|
||||||
{ key: 'carbohydrate', label: '碳水' },
|
}: {
|
||||||
{ key: 'fat', label: '脂肪' },
|
remainingCalories: number;
|
||||||
{ key: 'fiber', label: '纤维' },
|
totalAvailable: number;
|
||||||
{ key: 'sodium', label: '钠' },
|
}) => {
|
||||||
];
|
const animatedProgress = useRef(new Animated.Value(0)).current;
|
||||||
|
const radius = 32;
|
||||||
|
const strokeWidth = 8; // 增加圆环厚度
|
||||||
|
const center = radius + strokeWidth;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
|
||||||
|
// 计算进度:已消耗 / 总可用,进度越高表示剩余越少
|
||||||
|
const consumedAmount = totalAvailable - remainingCalories;
|
||||||
|
const calorieProgress = totalAvailable > 0 ? Math.min((consumedAmount / totalAvailable) * 100, 100) : 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(animatedProgress, {
|
||||||
|
toValue: calorieProgress,
|
||||||
|
duration: 600,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
}, [calorieProgress]);
|
||||||
|
|
||||||
|
const strokeDashoffset = animatedProgress.interpolate({
|
||||||
|
inputRange: [0, 100],
|
||||||
|
outputRange: [circumference, 0],
|
||||||
|
extrapolate: 'clamp',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ alignItems: 'center' }}>
|
||||||
|
<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={calorieProgress > 80 ? "#FF6B6B" : "#4ECDC4"}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray={`${circumference}`}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
transform={`rotate(-90 ${center} ${center})`}
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
<View style={{ position: 'absolute', alignItems: 'center', justifyContent: 'center', top: 0, left: 0, right: 0, bottom: 0 }}>
|
||||||
|
<Text style={{ fontSize: 12, fontWeight: '600', color: '#192126' }}>
|
||||||
|
{Math.round(remainingCalories)}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 8, color: '#9AA3AE' }}>还能吃</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function NutritionRadarCard({
|
export function NutritionRadarCard({
|
||||||
nutritionSummary,
|
nutritionSummary,
|
||||||
@@ -48,38 +106,11 @@ export function NutritionRadarCard({
|
|||||||
resetToken,
|
resetToken,
|
||||||
onMealPress
|
onMealPress
|
||||||
}: NutritionRadarCardProps) {
|
}: NutritionRadarCardProps) {
|
||||||
const [currentMealType, setCurrentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
|
const [currentMealType] = useState<'breakfast' | 'lunch' | 'dinner' | 'snack'>('breakfast');
|
||||||
const [showFoodOverlay, setShowFoodOverlay] = useState(false);
|
const [showFoodOverlay, setShowFoodOverlay] = useState(false);
|
||||||
const radarValues = useMemo(() => {
|
// 计算营养目标
|
||||||
// 基于动态计算的营养目标或默认推荐值
|
const calorieGoal = nutritionGoals?.calories ?? 2000;
|
||||||
const recommendations = {
|
const proteinGoal = nutritionGoals?.proteinGoal ?? 50;
|
||||||
calories: nutritionGoals?.calories ?? 2000, // 卡路里
|
|
||||||
protein: nutritionGoals?.proteinGoal ?? 50, // 蛋白质(g)
|
|
||||||
carbohydrate: nutritionGoals?.carbsGoal ?? 300, // 碳水化合物(g)
|
|
||||||
fat: nutritionGoals?.fatGoal ?? 65, // 脂肪(g)
|
|
||||||
fiber: nutritionGoals?.fiberGoal ?? 25, // 膳食纤维(g)
|
|
||||||
sodium: nutritionGoals?.sodiumGoal ?? 2300, // 钠(mg)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!nutritionSummary) return [0, 0, 0, 0, 0, 0];
|
|
||||||
|
|
||||||
// 检查每个营养素是否有实际值,没有则返回0
|
|
||||||
const calories = nutritionSummary.totalCalories || 0;
|
|
||||||
const protein = nutritionSummary.totalProtein || 0;
|
|
||||||
const carbohydrate = nutritionSummary.totalCarbohydrate || 0;
|
|
||||||
const fat = nutritionSummary.totalFat || 0;
|
|
||||||
const fiber = nutritionSummary.totalFiber || 0;
|
|
||||||
const sodium = nutritionSummary.totalSodium || 0;
|
|
||||||
|
|
||||||
return [
|
|
||||||
calories > 0 ? Math.min(5, (calories / recommendations.calories) * 5) : 0,
|
|
||||||
protein > 0 ? Math.min(5, (protein / recommendations.protein) * 5) : 0,
|
|
||||||
carbohydrate > 0 ? Math.min(5, (carbohydrate / recommendations.carbohydrate) * 5) : 0,
|
|
||||||
fat > 0 ? Math.min(5, (fat / recommendations.fat) * 5) : 0,
|
|
||||||
fiber > 0 ? Math.min(5, (fiber / recommendations.fiber) * 5) : 0,
|
|
||||||
sodium > 0 ? Math.min(5, Math.max(0, 5 - (sodium / recommendations.sodium) * 5)) : 0, // 钠含量越低越好
|
|
||||||
];
|
|
||||||
}, [nutritionSummary, nutritionGoals]);
|
|
||||||
|
|
||||||
const nutritionStats = useMemo(() => {
|
const nutritionStats = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
@@ -127,16 +158,15 @@ export function NutritionRadarCard({
|
|||||||
|
|
||||||
<View style={styles.contentContainer}>
|
<View style={styles.contentContainer}>
|
||||||
<View style={styles.radarContainer}>
|
<View style={styles.radarContainer}>
|
||||||
<RadarChart
|
<SimpleRingProgress
|
||||||
categories={NUTRITION_DIMENSIONS}
|
remainingCalories={remainingCalories}
|
||||||
values={radarValues}
|
totalAvailable={effectiveBasalMetabolism + effectiveActiveCalories}
|
||||||
size="small"
|
|
||||||
maxValue={5}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.statsContainer}>
|
<View style={styles.statsContainer}>
|
||||||
{nutritionStats.map((stat, index) => (
|
<View style={styles.statsBackground}>
|
||||||
|
{nutritionStats.map((stat) => (
|
||||||
<View key={stat.label} style={styles.statItem}>
|
<View key={stat.label} style={styles.statItem}>
|
||||||
<Text style={styles.statLabel}>{stat.label}</Text>
|
<Text style={styles.statLabel}>{stat.label}</Text>
|
||||||
<Text style={styles.statValue}>{stat.value}</Text>
|
<Text style={styles.statValue}>{stat.value}</Text>
|
||||||
@@ -144,6 +174,7 @@ export function NutritionRadarCard({
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* 卡路里计算区域 */}
|
{/* 卡路里计算区域 */}
|
||||||
<View style={styles.calorieSection}>
|
<View style={styles.calorieSection}>
|
||||||
@@ -246,20 +277,40 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
radarContainer: {
|
radarContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: 6,
|
justifyContent: 'center',
|
||||||
|
marginRight: 8,
|
||||||
|
width: 78, // Fixed width to match ring chart size
|
||||||
|
height: 78, // Fixed height to match ring chart size
|
||||||
},
|
},
|
||||||
statsContainer: {
|
statsContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
marginLeft: 4
|
||||||
|
},
|
||||||
|
statsBackground: {
|
||||||
|
backgroundColor: 'rgba(248, 250, 252, 0.8)', // 毛玻璃背景色
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 12,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginLeft: 4
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.06,
|
||||||
|
shadowRadius: 3,
|
||||||
|
elevation: 1,
|
||||||
|
// 添加边框增强毛玻璃效果
|
||||||
|
borderWidth: 0.5,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.8)',
|
||||||
},
|
},
|
||||||
statItem: {
|
statItem: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
width: '48%',
|
width: '48%',
|
||||||
marginBottom: 16,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
statDot: {
|
statDot: {
|
||||||
width: 8,
|
width: 8,
|
||||||
@@ -279,6 +330,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
// 卡路里相关样式
|
// 卡路里相关样式
|
||||||
calorieSection: {
|
calorieSection: {
|
||||||
|
marginTop: 6,
|
||||||
},
|
},
|
||||||
|
|
||||||
calorieTitleContainer: {
|
calorieTitleContainer: {
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export function NutritionRecordCard({
|
|||||||
{record.imageUrl ? (
|
{record.imageUrl ? (
|
||||||
<Image source={{ uri: record.imageUrl }} style={styles.foodImage} />
|
<Image source={{ uri: record.imageUrl }} style={styles.foodImage} />
|
||||||
) : (
|
) : (
|
||||||
<Ionicons name="restaurant" size={24} color={textSecondaryColor} />
|
<Ionicons name="restaurant" size={28} color={textSecondaryColor} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -172,7 +172,6 @@ export function NutritionRecordCard({
|
|||||||
<View style={styles.rightSection}>
|
<View style={styles.rightSection}>
|
||||||
{/* 热量显示 */}
|
{/* 热量显示 */}
|
||||||
<View style={styles.caloriesContainer}>
|
<View style={styles.caloriesContainer}>
|
||||||
<View style={styles.caloriesDot} />
|
|
||||||
<ThemedText style={[styles.caloriesText]}>
|
<ThemedText style={[styles.caloriesText]}>
|
||||||
{record.estimatedCalories ? `${Math.round(record.estimatedCalories)} kcal` : '- kcal'}
|
{record.estimatedCalories ? `${Math.round(record.estimatedCalories)} kcal` : '- kcal'}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
@@ -185,14 +184,6 @@ export function NutritionRecordCard({
|
|||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 更多操作按钮 */}
|
|
||||||
<TouchableOpacity
|
|
||||||
ref={popoverRef}
|
|
||||||
style={styles.moreButton}
|
|
||||||
onPress={() => setShowPopover(true)}
|
|
||||||
>
|
|
||||||
<Ionicons name="ellipsis-horizontal" size={16} color={textSecondaryColor} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -204,21 +195,22 @@ export function NutritionRecordCard({
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
marginBottom: 12,
|
marginBottom: 16,
|
||||||
// iOS 阴影效果 - 更自然的阴影
|
// iOS 阴影效果 - 更自然的阴影
|
||||||
shadowColor: '#000000',
|
shadowColor: '#000000',
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.08,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 4,
|
shadowRadius: 8,
|
||||||
// Android 阴影效果
|
// Android 阴影效果
|
||||||
elevation: 2,
|
elevation: 3,
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: 80,
|
minHeight: 100,
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
borderRadius: 12,
|
borderRadius: 16,
|
||||||
padding: 12,
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 14,
|
||||||
},
|
},
|
||||||
mainContent: {
|
mainContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -226,10 +218,10 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
foodImageContainer: {
|
foodImageContainer: {
|
||||||
width: 32,
|
width: 48,
|
||||||
height: 32,
|
height: 48,
|
||||||
borderRadius: 8,
|
borderRadius: 12,
|
||||||
marginRight: 12,
|
marginRight: 16,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
foodImage: {
|
foodImage: {
|
||||||
@@ -244,68 +236,64 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
foodInfoContainer: {
|
foodInfoContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'center',
|
||||||
|
gap: 4,
|
||||||
},
|
},
|
||||||
foodName: {
|
foodName: {
|
||||||
fontSize: 14,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#333333',
|
color: '#333333',
|
||||||
marginTop: 2,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
mealTime: {
|
mealTime: {
|
||||||
fontSize: 10,
|
fontSize: 12,
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
color: '#999999',
|
color: '#999999',
|
||||||
|
lineHeight: 16,
|
||||||
},
|
},
|
||||||
nutritionContainer: {
|
nutritionContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 20,
|
gap: 16,
|
||||||
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
nutritionItem: {
|
nutritionItem: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 2,
|
gap: 4,
|
||||||
},
|
},
|
||||||
nutritionIcon: {
|
nutritionIcon: {
|
||||||
fontSize: 12,
|
fontSize: 14,
|
||||||
},
|
},
|
||||||
nutritionValue: {
|
nutritionValue: {
|
||||||
fontSize: 11,
|
fontSize: 13,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
color: '#666666',
|
color: '#666666',
|
||||||
},
|
},
|
||||||
rightSection: {
|
rightSection: {
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'center',
|
||||||
height: 48,
|
gap: 8,
|
||||||
|
minHeight: 60,
|
||||||
},
|
},
|
||||||
caloriesContainer: {
|
caloriesContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
caloriesDot: {
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
borderRadius: 3,
|
|
||||||
backgroundColor: '#333333',
|
|
||||||
marginRight: 6,
|
|
||||||
},
|
},
|
||||||
caloriesText: {
|
caloriesText: {
|
||||||
fontSize: 12,
|
fontSize: 14,
|
||||||
color: '#473c3cff',
|
color: '#333333',
|
||||||
fontWeight: '500',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
mealTypeBadge: {
|
mealTypeBadge: {
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 10,
|
||||||
borderRadius: 10,
|
paddingVertical: 4,
|
||||||
marginBottom: 4,
|
borderRadius: 12,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||||
},
|
},
|
||||||
mealTypeText: {
|
mealTypeText: {
|
||||||
fontSize: 10,
|
fontSize: 12,
|
||||||
fontWeight: '500',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
moreButton: {
|
moreButton: {
|
||||||
padding: 2,
|
padding: 2,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export function WeightHistoryCard() {
|
|||||||
const height = interpolate(
|
const height = interpolate(
|
||||||
animationProgress.value,
|
animationProgress.value,
|
||||||
[0, 1],
|
[0, 1],
|
||||||
[40, 200], // 从摘要高度到图表高度
|
[80, 200], // 从摘要高度到图表高度,适应毛玻璃背景
|
||||||
Extrapolation.CLAMP
|
Extrapolation.CLAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -308,6 +308,7 @@ export function WeightHistoryCard() {
|
|||||||
<Animated.View style={[styles.animationContainer, containerAnimatedStyle]}>
|
<Animated.View style={[styles.animationContainer, containerAnimatedStyle]}>
|
||||||
{/* 默认信息显示 - 带动画 */}
|
{/* 默认信息显示 - 带动画 */}
|
||||||
<Animated.View style={[styles.summaryInfo, summaryAnimatedStyle]}>
|
<Animated.View style={[styles.summaryInfo, summaryAnimatedStyle]}>
|
||||||
|
<View style={styles.summaryBackground}>
|
||||||
<View style={styles.summaryRow}>
|
<View style={styles.summaryRow}>
|
||||||
<View style={styles.summaryItem}>
|
<View style={styles.summaryItem}>
|
||||||
<Text style={styles.summaryLabel}>当前体重</Text>
|
<Text style={styles.summaryLabel}>当前体重</Text>
|
||||||
@@ -344,6 +345,7 @@ export function WeightHistoryCard() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{/* 图表容器 - 带动画 */}
|
{/* 图表容器 - 带动画 */}
|
||||||
@@ -631,13 +633,29 @@ const styles = StyleSheet.create({
|
|||||||
animationContainer: {
|
animationContainer: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
minHeight: 50,
|
minHeight: 80, // 增加最小高度以容纳毛玻璃背景
|
||||||
},
|
},
|
||||||
summaryInfo: {
|
summaryInfo: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
},
|
},
|
||||||
|
summaryBackground: {
|
||||||
|
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)',
|
||||||
|
},
|
||||||
chartContainer: {
|
chartContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|||||||
Reference in New Issue
Block a user