feat: 支持营养圆环
This commit is contained in:
@@ -16,7 +16,7 @@ export default function TabLayout() {
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
initialRouteName="coach"
|
||||
initialRouteName="statistics"
|
||||
screenOptions={({ route }) => {
|
||||
const routeName = route.name;
|
||||
const isSelected = (routeName === 'explore' && pathname === ROUTES.TAB_EXPLORE) ||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||||
import { BasalMetabolismCard } from '@/components/BasalMetabolismCard';
|
||||
import { DateSelector } from '@/components/DateSelector';
|
||||
import { FitnessRingsCard } from '@/components/FitnessRingsCard';
|
||||
@@ -332,7 +331,7 @@ export default function ExploreScreen() {
|
||||
{/* 营养摄入雷达图卡片 */}
|
||||
<NutritionRadarCard
|
||||
nutritionSummary={nutritionSummary}
|
||||
burnedCalories={basalMetabolism || 0}
|
||||
burnedCalories={(basalMetabolism || 0) + (activeCalories || 0)}
|
||||
calorieDeficit={0}
|
||||
resetToken={animToken}
|
||||
onMealPress={(mealType: 'breakfast' | 'lunch' | 'dinner' | 'snack') => {
|
||||
@@ -355,28 +354,6 @@ export default function ExploreScreen() {
|
||||
/>
|
||||
</FloatingCard>
|
||||
|
||||
|
||||
<FloatingCard style={styles.masonryCard} delay={500}>
|
||||
<Text style={styles.cardTitle}>消耗卡路里</Text>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
marginTop: 20
|
||||
}}>
|
||||
{activeCalories != null ? (
|
||||
<AnimatedNumber
|
||||
value={activeCalories}
|
||||
resetToken={animToken}
|
||||
style={styles.caloriesValue}
|
||||
format={(v) => `${Math.round(v)}`}
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.caloriesValue}>——</Text>
|
||||
)}
|
||||
<Text style={styles.caloriesUnit}>千卡</Text>
|
||||
</View>
|
||||
</FloatingCard>
|
||||
|
||||
<FloatingCard style={styles.masonryCard}>
|
||||
<StepsCard
|
||||
stepCount={stepCount}
|
||||
@@ -392,6 +369,15 @@ export default function ExploreScreen() {
|
||||
hrvValue={hrvValue}
|
||||
/>
|
||||
</FloatingCard>
|
||||
|
||||
{/* 心率卡片 */}
|
||||
<FloatingCard style={styles.masonryCard} delay={2000}>
|
||||
<HeartRateCard
|
||||
resetToken={animToken}
|
||||
style={styles.basalMetabolismCardOverride}
|
||||
heartRate={heartRate}
|
||||
/>
|
||||
</FloatingCard>
|
||||
</View>
|
||||
|
||||
{/* 右列 */}
|
||||
@@ -439,14 +425,6 @@ export default function ExploreScreen() {
|
||||
/>
|
||||
</FloatingCard>
|
||||
|
||||
{/* 心率卡片 */}
|
||||
<FloatingCard style={styles.masonryCard} delay={2000}>
|
||||
<HeartRateCard
|
||||
resetToken={animToken}
|
||||
style={styles.basalMetabolismCardOverride}
|
||||
heartRate={heartRate}
|
||||
/>
|
||||
</FloatingCard>
|
||||
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function SplashScreen() {
|
||||
// router.replace('/onboarding');
|
||||
// }
|
||||
// setIsLoading(false);
|
||||
router.replace(ROUTES.TAB_COACH);
|
||||
router.replace(ROUTES.TAB_STATISTICS);
|
||||
} catch (error) {
|
||||
console.error('检查引导状态失败:', error);
|
||||
// 如果出现错误,默认显示引导页面
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { CalorieRingChart } from '@/components/CalorieRingChart';
|
||||
import { DateSelector } from '@/components/DateSelector';
|
||||
import { NutritionRecordCard } from '@/components/NutritionRecordCard';
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { DietRecord, deleteDietRecord, getDietRecords } from '@/services/dietRecords';
|
||||
import { selectHealthDataByDate } from '@/store/healthSlice';
|
||||
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
|
||||
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
@@ -24,12 +28,26 @@ type ViewMode = 'daily' | 'all';
|
||||
export default function NutritionRecordsScreen() {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// 日期相关状态 - 使用与统计页面相同的日期逻辑
|
||||
const days = getMonthDaysZh();
|
||||
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
|
||||
const monthTitle = getMonthTitleZh();
|
||||
|
||||
// 获取当前选中日期
|
||||
const getCurrentSelectedDate = () => {
|
||||
return days[selectedIndex]?.date?.toDate() ?? new Date();
|
||||
};
|
||||
|
||||
const currentSelectedDate = getCurrentSelectedDate();
|
||||
const currentSelectedDateString = dayjs(currentSelectedDate).format('YYYY-MM-DD');
|
||||
|
||||
// 从 Redux 获取数据
|
||||
const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString));
|
||||
const nutritionSummary = useAppSelector(selectNutritionSummaryByDate(currentSelectedDateString));
|
||||
const userProfile = useAppSelector((state) => state.user.profile);
|
||||
|
||||
// 视图模式:按天查看 vs 全部查看
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('daily');
|
||||
|
||||
@@ -40,41 +58,6 @@ export default function NutritionRecordsScreen() {
|
||||
const [hasMoreData, setHasMoreData] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
// 日期滚动相关
|
||||
const daysScrollRef = useRef<ScrollView | null>(null);
|
||||
const [scrollWidth, setScrollWidth] = useState(0);
|
||||
const DAY_PILL_WIDTH = 60; // 48px width + 12px marginRight = 60px total per item
|
||||
const DAY_PILL_SPACING = 0; // spacing is included in the width above
|
||||
|
||||
// 日期滚动控制
|
||||
const scrollToIndex = (index: number, animated = true) => {
|
||||
if (scrollWidth <= 0) return;
|
||||
|
||||
const itemOffset = index * DAY_PILL_WIDTH;
|
||||
const scrollViewCenterX = scrollWidth / 2;
|
||||
const itemCenterX = DAY_PILL_WIDTH / 2;
|
||||
const centerOffset = Math.max(0, itemOffset - scrollViewCenterX + itemCenterX);
|
||||
|
||||
daysScrollRef.current?.scrollTo({ x: centerOffset, animated });
|
||||
};
|
||||
|
||||
// 初始化时滚动到选中位置
|
||||
useEffect(() => {
|
||||
if (scrollWidth > 0) {
|
||||
// 延迟滚动以确保ScrollView已经完全渲染
|
||||
setTimeout(() => {
|
||||
scrollToIndex(selectedIndex, false);
|
||||
}, 100);
|
||||
}
|
||||
}, [scrollWidth]);
|
||||
|
||||
// 选中日期变化时滚动
|
||||
useEffect(() => {
|
||||
if (scrollWidth > 0) {
|
||||
scrollToIndex(selectedIndex, true);
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
// 加载记录数据
|
||||
const loadRecords = async (isRefresh = false, loadMore = false) => {
|
||||
try {
|
||||
@@ -126,10 +109,50 @@ export default function NutritionRecordsScreen() {
|
||||
loadRecords();
|
||||
}, [selectedIndex, viewMode]);
|
||||
|
||||
// 当选中日期变化时获取营养数据
|
||||
useEffect(() => {
|
||||
if (viewMode === 'daily') {
|
||||
dispatch(fetchDailyNutritionData(currentSelectedDate));
|
||||
}
|
||||
}, [selectedIndex, viewMode, currentSelectedDate, dispatch]);
|
||||
|
||||
const onRefresh = () => {
|
||||
loadRecords(true);
|
||||
};
|
||||
|
||||
// 计算营养目标
|
||||
const calculateNutritionGoals = () => {
|
||||
const weight = parseFloat(userProfile?.weight || '70'); // 默认70kg
|
||||
const height = parseFloat(userProfile?.height || '170'); // 默认170cm
|
||||
const age = userProfile?.birthDate ?
|
||||
dayjs().diff(dayjs(userProfile.birthDate), 'year') : 25; // 默认25岁
|
||||
const isWoman = userProfile?.gender === 'female';
|
||||
|
||||
// 基础代谢率计算(Mifflin-St Jeor Equation)
|
||||
let bmr;
|
||||
if (isWoman) {
|
||||
bmr = 10 * weight + 6.25 * height - 5 * age - 161;
|
||||
} else {
|
||||
bmr = 10 * weight + 6.25 * height - 5 * age + 5;
|
||||
}
|
||||
|
||||
// 总热量需求(假设轻度活动)
|
||||
const totalCalories = bmr * 1.375;
|
||||
|
||||
// 计算营养素目标
|
||||
const proteinGoal = weight * 1.6; // 1.6g/kg
|
||||
const fatGoal = totalCalories * 0.25 / 9; // 25%来自脂肪,9卡/克
|
||||
const carbsGoal = (totalCalories - proteinGoal * 4 - fatGoal * 9) / 4; // 剩余来自碳水
|
||||
|
||||
return {
|
||||
proteinGoal: Math.round(proteinGoal * 10) / 10,
|
||||
fatGoal: Math.round(fatGoal * 10) / 10,
|
||||
carbsGoal: Math.round(carbsGoal * 10) / 10,
|
||||
};
|
||||
};
|
||||
|
||||
const nutritionGoals = calculateNutritionGoals();
|
||||
|
||||
const loadMoreRecords = () => {
|
||||
if (hasMoreData && !loading && !refreshing) {
|
||||
loadRecords(false, true);
|
||||
@@ -254,6 +277,20 @@ export default function NutritionRecordsScreen() {
|
||||
{renderViewModeToggle()}
|
||||
{renderDateSelector()}
|
||||
|
||||
{/* Calorie Ring Chart */}
|
||||
<CalorieRingChart
|
||||
metabolism={healthData?.basalEnergyBurned || 1482}
|
||||
exercise={healthData?.activeEnergyBurned || 0}
|
||||
consumed={nutritionSummary?.totalCalories || 0}
|
||||
goal={userProfile?.dailyCaloriesGoal || 200}
|
||||
protein={nutritionSummary?.totalProtein || 0}
|
||||
fat={nutritionSummary?.totalFat || 0}
|
||||
carbs={nutritionSummary?.totalCarbohydrate || 0}
|
||||
proteinGoal={nutritionGoals.proteinGoal}
|
||||
fatGoal={nutritionGoals.fatGoal}
|
||||
carbsGoal={nutritionGoals.carbsGoal}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colorTokens.primary} />
|
||||
|
||||
Reference in New Issue
Block a user