From 5e00cb77880bf401f3287598d2b49f2904f40ad1 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 4 Sep 2025 11:28:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=90=A5=E5=85=BB?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=92=8C=E5=8D=A1=E8=B7=AF=E9=87=8C=E7=8E=AF?= =?UTF-8?q?=E5=9B=BE=E7=BB=84=E4=BB=B6=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=AF=9B?= =?UTF-8?q?=E7=8E=BB=E7=92=83=E8=83=8C=E6=99=AF=E5=92=8C=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/statistics.tsx | 40 +++--- app/nutrition/records.tsx | 11 +- components/CalorieRingChart.tsx | 165 ++++++++++++----------- components/NutritionRadarCard.tsx | 166 ++++++++++++++++-------- components/NutritionRecordCard.tsx | 86 ++++++------ components/weight/WeightHistoryCard.tsx | 88 ++++++++----- 6 files changed, 302 insertions(+), 254 deletions(-) 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 ( + + + + 80 ? "#FF6B6B" : "#4ECDC4"} + strokeWidth={strokeWidth} + fill="none" + strokeDasharray={`${circumference}`} + strokeDashoffset={strokeDashoffset} + strokeLinecap="round" + transform={`rotate(-90 ${center} ${center})`} + /> + + + + {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%',