diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx
index 674a65f..8201d4f 100644
--- a/app/(tabs)/statistics.tsx
+++ b/app/(tabs)/statistics.tsx
@@ -660,13 +660,7 @@ export default function ExploreScreen() {
/>
- {/* 饮水记录卡片 */}
-
-
-
+
+
+
+
+ 睡眠
+
+ {sleepDuration != null ? (
+
+ {Math.floor(sleepDuration / 60)}小时{Math.floor(sleepDuration % 60)}分钟
+
+ ) : (
+ ——
+ )}
+
{/* 右列 */}
@@ -699,20 +706,15 @@ export default function ExploreScreen() {
resetToken={animToken}
/>
-
-
-
- 睡眠
-
- {sleepDuration != null ? (
-
- {Math.floor(sleepDuration / 60)}小时{Math.floor(sleepDuration % 60)}分钟
-
- ) : (
- ——
- )}
+ {/* 饮水记录卡片 */}
+
+
+
{/* 基础代谢卡片 */}
- {loading ? (
-
-
-
- 加载中...
-
-
- ) : (
+ {(
renderRecord({ item, index })}
diff --git a/components/CalorieRingChart.tsx b/components/CalorieRingChart.tsx
index 22c28cb..273f12d 100644
--- a/components/CalorieRingChart.tsx
+++ b/components/CalorieRingChart.tsx
@@ -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({
还能吃
- {canEat.toFixed(1)}千卡
+ {Math.round(canEat)}千卡
{Math.round(progressPercentage)}%
@@ -126,59 +123,58 @@ export function CalorieRingChart({
{/* 右侧数据展示 */}
- {/* 各项数值 */}
-
- 代谢
-
- {Math.round(metabolism)}千卡
-
+
+ {/* 左右两列布局 */}
+
+ {/* 左列:卡路里数据 */}
+
+
+ 代谢
+
+ {Math.round(metabolism)}千卡
+
+
+
+
+ 运动
+
+ {Math.round(exercise)}千卡
+
+
+
+
+ 饮食
+
+ {Math.round(consumed)}千卡
+
+
+
+
+ {/* 右列:营养数据 */}
+
+
+ 蛋白质
+
+ {Math.round(protein)}g
+
+
+
+
+ 脂肪
+
+ {Math.round(fat)}g
+
+
+
+
+ 碳水
+
+ {Math.round(carbs)}g
+
+
+
+
-
-
- 运动
-
- {Math.round(exercise)}千卡
-
-
-
-
- 饮食
-
- {Math.round(consumed)}千卡
-
-
-
-
-
-
-
- {/* 底部营养素展示 */}
-
-
-
- 蛋白质
-
-
- {Math.round(protein)}/{Math.round(proteinGoal)}g
-
-
-
-
-
- 脂肪
-
-
- {Math.round(fat)}/{Math.round(fatGoal)}g
-
-
-
-
-
- 碳水化合物
-
-
- {Math.round(carbs)}/{Math.round(carbsGoal)}g
-
@@ -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,
},
});
\ No newline at end of file
diff --git a/components/NutritionRadarCard.tsx b/components/NutritionRadarCard.tsx
index dba1b31..a490e64 100644
--- a/components/NutritionRadarCard.tsx
+++ b/components/NutritionRadarCard.tsx
@@ -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 (
+
+
+
+
+ {Math.round(remainingCalories)}
+
+ 还能吃
+
+
+ );
+};
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({
-
- {nutritionStats.map((stat, index) => (
-
- {stat.label}
- {stat.value}
-
- ))}
+
+ {nutritionStats.map((stat) => (
+
+ {stat.label}
+ {stat.value}
+
+ ))}
+
@@ -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: {
diff --git a/components/NutritionRecordCard.tsx b/components/NutritionRecordCard.tsx
index e85ca94..a968544 100644
--- a/components/NutritionRecordCard.tsx
+++ b/components/NutritionRecordCard.tsx
@@ -139,7 +139,7 @@ export function NutritionRecordCard({
{record.imageUrl ? (
) : (
-
+
)}
@@ -172,7 +172,6 @@ export function NutritionRecordCard({
{/* 热量显示 */}
-
{record.estimatedCalories ? `${Math.round(record.estimatedCalories)} kcal` : '- kcal'}
@@ -185,14 +184,6 @@ export function NutritionRecordCard({
- {/* 更多操作按钮 */}
- setShowPopover(true)}
- >
-
-
@@ -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,
diff --git a/components/weight/WeightHistoryCard.tsx b/components/weight/WeightHistoryCard.tsx
index 12474eb..3dc98e6 100644
--- a/components/weight/WeightHistoryCard.tsx
+++ b/components/weight/WeightHistoryCard.tsx
@@ -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() {
{/* 默认信息显示 - 带动画 */}
-
-
- 当前体重
- {userProfile.weight}kg
-
-
- 记录天数
- {sortedHistory.length}天
-
-
- 变化范围
-
- {minWeight.toFixed(1)}-{maxWeight.toFixed(1)}
-
-
- {bmiResult && (
+
+
- BMI
-
-
- {bmiResult.value}
-
- {
- e.stopPropagation();
- handleShowBMIModal();
- }}
- style={styles.bmiInfoButton}
- hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
- >
-
-
-
+ 当前体重
+ {userProfile.weight}kg
- )}
+
+ 记录天数
+ {sortedHistory.length}天
+
+
+ 变化范围
+
+ {minWeight.toFixed(1)}-{maxWeight.toFixed(1)}
+
+
+ {bmiResult && (
+
+ BMI
+
+
+ {bmiResult.value}
+
+ {
+ e.stopPropagation();
+ handleShowBMIModal();
+ }}
+ style={styles.bmiInfoButton}
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+ >
+
+
+
+
+ )}
+
@@ -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%',