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