feat: 优化营养记录和卡路里环图组件,增加毛玻璃背景和动画效果

This commit is contained in:
richarjiang
2025-09-04 11:28:31 +08:00
parent 4ae419754a
commit 5e00cb7788
6 changed files with 302 additions and 254 deletions

View File

@@ -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

View File

@@ -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 })}

View File

@@ -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,59 +123,58 @@ export function CalorieRingChart({
{/* 右侧数据展示 */} {/* 右侧数据展示 */}
<View style={styles.dataContainer}> <View style={styles.dataContainer}>
{/* 各项数值 */} <View style={styles.dataBackground}>
<View style={styles.dataItem}> {/* 左右两列布局 */}
<ThemedText style={[styles.dataLabel, { color: textSecondaryColor }]}></ThemedText> <View style={styles.dataColumns}>
<ThemedText style={[styles.dataValue, { color: textColor }]}> {/* 左列:卡路里数据 */}
{Math.round(metabolism)} <View style={styles.dataColumn}>
</ThemedText> <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 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> </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',
}, },
}); });

View File

@@ -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,21 +158,21 @@ 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}>
<View key={stat.label} style={styles.statItem}> {nutritionStats.map((stat) => (
<Text style={styles.statLabel}>{stat.label}</Text> <View key={stat.label} style={styles.statItem}>
<Text style={styles.statValue}>{stat.value}</Text> <Text style={styles.statLabel}>{stat.label}</Text>
</View> <Text style={styles.statValue}>{stat.value}</Text>
))} </View>
))}
</View>
</View> </View>
</View> </View>
@@ -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: {

View File

@@ -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,

View File

@@ -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,41 +308,43 @@ 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.summaryRow}> <View style={styles.summaryBackground}>
<View style={styles.summaryItem}> <View style={styles.summaryRow}>
<Text style={styles.summaryLabel}></Text>
<Text style={styles.summaryValue}>{userProfile.weight}kg</Text>
</View>
<View style={styles.summaryItem}>
<Text style={styles.summaryLabel}></Text>
<Text style={styles.summaryValue}>{sortedHistory.length}</Text>
</View>
<View style={styles.summaryItem}>
<Text style={styles.summaryLabel}></Text>
<Text style={styles.summaryValue}>
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}
</Text>
</View>
{bmiResult && (
<View style={styles.summaryItem}> <View style={styles.summaryItem}>
<Text style={styles.summaryLabel}>BMI</Text> <Text style={styles.summaryLabel}></Text>
<View style={styles.bmiValueContainer}> <Text style={styles.summaryValue}>{userProfile.weight}kg</Text>
<Text style={[styles.bmiValue, { color: bmiResult.color }]}>
{bmiResult.value}
</Text>
<TouchableOpacity
onPress={(e) => {
e.stopPropagation();
handleShowBMIModal();
}}
style={styles.bmiInfoButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Ionicons name="information-circle-outline" size={12} color="#9AA3AE" />
</TouchableOpacity>
</View>
</View> </View>
)} <View style={styles.summaryItem}>
<Text style={styles.summaryLabel}></Text>
<Text style={styles.summaryValue}>{sortedHistory.length}</Text>
</View>
<View style={styles.summaryItem}>
<Text style={styles.summaryLabel}></Text>
<Text style={styles.summaryValue}>
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}
</Text>
</View>
{bmiResult && (
<View style={styles.summaryItem}>
<Text style={styles.summaryLabel}>BMI</Text>
<View style={styles.bmiValueContainer}>
<Text style={[styles.bmiValue, { color: bmiResult.color }]}>
{bmiResult.value}
</Text>
<TouchableOpacity
onPress={(e) => {
e.stopPropagation();
handleShowBMIModal();
}}
style={styles.bmiInfoButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Ionicons name="information-circle-outline" size={12} color="#9AA3AE" />
</TouchableOpacity>
</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%',