feat: 支持营养圆环

This commit is contained in:
richarjiang
2025-08-31 16:30:08 +08:00
parent 4bb0576d92
commit fe634ba258
8 changed files with 400 additions and 158 deletions

View File

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