feat: 支持营养圆环
This commit is contained in:
@@ -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