feat: 新增营养摄入分析卡片并优化相关页面

- 在统计页面中引入营养摄入分析卡片,展示用户的营养数据
- 更新探索页面,增加营养数据加载逻辑,确保用户体验一致性
- 移除不再使用的推荐文章逻辑,简化代码结构
- 更新路由常量,确保路径管理集中化
- 优化体重历史记录卡片,提升用户交互体验
This commit is contained in:
richarjiang
2025-08-19 10:01:26 +08:00
parent c7d7255312
commit 9aa0a692a8
7 changed files with 452 additions and 201 deletions

View File

@@ -6,17 +6,16 @@ import { ThemedView } from '@/components/ThemedView';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { listRecommendedArticles } from '@/services/articles';
import { fetchRecommendations, RecommendationType } from '@/services/recommendations';
import { loadPlans } from '@/store/trainingPlanSlice';
// Removed WorkoutCard import since we no longer use the horizontal carousel
import { QUERY_PARAMS, ROUTE_PARAMS, ROUTES } from '@/constants/Routes';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { TrainingPlan } from '@/services/trainingPlanApi';
import { useRouter } from 'expo-router';
import React from 'react';
import { Animated, Image, PanResponder, Pressable, SafeAreaView, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ROUTES, QUERY_PARAMS, ROUTE_PARAMS } from '@/constants/Routes';
// 移除旧的“热门活动”滑动数据,改为固定的“热点功能”卡片
@@ -103,42 +102,7 @@ export default function HomeScreen() {
readCount: number;
};
// 打底数据(接口不可用时)
const getFallbackItems = React.useCallback((): RecommendItem[] => {
return [
{
type: 'plan',
key: 'today-workout',
image:
'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg',
title: '今日训练',
subtitle: '完成一次普拉提训练,记录你的坚持',
level: '初学者',
onPress: () => pushIfAuthedElseLogin(ROUTES.WORKOUT_TODAY),
},
{
type: 'plan',
key: 'assess',
image:
'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg',
title: '体态评估',
subtitle: '评估你的体态,制定训练计划',
level: '初学者',
onPress: () => router.push(ROUTES.AI_POSTURE_ASSESSMENT),
},
...listRecommendedArticles().map((a) => ({
type: 'article' as const,
key: `article-${a.id}`,
id: a.id,
title: a.title,
coverImage: a.coverImage,
publishedAt: a.publishedAt,
readCount: a.readCount,
})),
];
}, [router, pushIfAuthedElseLogin]);
const [items, setItems] = React.useState<RecommendItem[]>(() => getFallbackItems());
const [items, setItems] = React.useState<RecommendItem[]>();
// 加载训练计划数据
React.useEffect(() => {
@@ -191,15 +155,15 @@ export default function HomeScreen() {
}
}
// 若接口返回空,也回退到打底
setItems(mapped.length > 0 ? mapped : getFallbackItems());
setItems(mapped.length > 0 ? mapped : []);
} catch (e) {
console.error('fetchRecommendations error', e);
setItems(getFallbackItems());
setItems([]);
}
}
load();
return () => { canceled = true; };
}, [isLoggedIn, pushIfAuthedElseLogin, getFallbackItems]);
}, [isLoggedIn, pushIfAuthedElseLogin]);
// 处理点击训练计划卡片跳转到锻炼tab
const handlePlanCardPress = () => {
@@ -335,7 +299,7 @@ export default function HomeScreen() {
<ThemedText style={styles.sectionTitle}></ThemedText>
<View style={styles.planList}>
{items.map((item) => {
{items?.map((item) => {
if (item.type === 'article') {
return (
<ArticleCard

View File

@@ -1,18 +1,21 @@
import { AnimatedNumber } from '@/components/AnimatedNumber';
import { BMICard } from '@/components/BMICard';
import { CircularRing } from '@/components/CircularRing';
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
import { ProgressBar } from '@/components/ProgressBar';
import { WeightHistoryCard } from '@/components/WeightHistoryCard';
import { Colors } from '@/constants/Colors';
import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useColorScheme } from '@/hooks/useColorScheme';
import { calculateNutritionSummary, getDietRecords, NutritionSummary } from '@/services/dietRecords';
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health';
import { Ionicons } from '@expo/vector-icons';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useFocusEffect } from '@react-navigation/native';
import { useRouter } from 'expo-router';
import dayjs from 'dayjs';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
SafeAreaView,
@@ -25,12 +28,13 @@ import {
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function ExploreScreen() {
const router = useRouter();
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
const userProfile = useAppSelector((s) => s.user.profile);
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
// 使用 dayjs当月日期与默认选中“今天”
const days = getMonthDaysZh();
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
@@ -67,12 +71,17 @@ export default function ExploreScreen() {
const [isLoading, setIsLoading] = useState(false);
// 用于触发动画重置的 token当日期或数据变化时更新
const [animToken, setAnimToken] = useState(0);
const [trainingProgress, setTrainingProgress] = useState(0.8); // 暂定静态80%
const [trainingProgress, setTrainingProgress] = useState(0); // 暂定静态80%
// 营养数据状态
const [nutritionSummary, setNutritionSummary] = useState<NutritionSummary | null>(null);
const [isNutritionLoading, setIsNutritionLoading] = useState(false);
// 记录最近一次请求的“日期键”,避免旧请求覆盖新结果
const latestRequestKeyRef = useRef<string | null>(null);
const getDateKey = (d: Date) => `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
const getDateKey = (d: Date) => `${dayjs(d).year()}-${dayjs(d).month() + 1}-${dayjs(d).date()}`;
const loadHealthData = async (targetDate?: Date) => {
try {
@@ -112,11 +121,44 @@ export default function ExploreScreen() {
}
};
// 加载营养数据
const loadNutritionData = async (targetDate?: Date) => {
try {
setIsNutritionLoading(true);
// 若未显式传入日期,按当前选中索引推导日期
const derivedDate = targetDate ?? days[selectedIndex]?.date?.toDate() ?? new Date();
console.log('加载营养数据...', derivedDate);
const data = await getDietRecords({
startDate: dayjs(derivedDate).startOf('day').toISOString(),
endDate: dayjs(derivedDate).endOf('day').toISOString(),
});
if (data.records.length > 0) {
const summary = calculateNutritionSummary(data.records);
setNutritionSummary(summary);
} else {
setNutritionSummary(null);
}
console.log('营养数据加载完成:', data);
} catch (error) {
console.error('营养数据加载失败:', error);
setNutritionSummary(null);
} finally {
setIsNutritionLoading(false);
}
};
useFocusEffect(
React.useCallback(() => {
// 聚焦时按当前选中的日期加载,避免与用户手动选择的日期不一致
loadHealthData();
}, [selectedIndex])
if (isLoggedIn) {
loadNutritionData();
}
}, [])
);
// 日期点击时,加载对应日期数据
@@ -126,6 +168,9 @@ export default function ExploreScreen() {
const target = days[index]?.date?.toDate();
if (target) {
loadHealthData(target);
if (isLoggedIn) {
loadNutritionData(target);
}
}
};
@@ -138,10 +183,10 @@ export default function ExploreScreen() {
contentContainerStyle={{ paddingBottom: bottomPadding }}
showsVerticalScrollIndicator={false}
>
{/* 体重历史记录卡片 */}
<Text style={styles.sectionTitle}></Text>
{/* 体重历史记录卡片 */}
<Text style={styles.sectionTitle}></Text>
<WeightHistoryCard />
{/* 标题与日期选择 */}
<Text style={styles.monthTitle}>{monthTitle}</Text>
<ScrollView
@@ -169,6 +214,12 @@ export default function ExploreScreen() {
})}
</ScrollView>
{/* 营养摄入雷达图卡片 */}
<NutritionRadarCard
nutritionSummary={nutritionSummary}
isLoading={isNutritionLoading}
/>
{/* 指标行:左大卡(训练时间),右两小卡(消耗卡路里、步数) */}
<View style={styles.metricsRow}>
<View style={[styles.trainingCard, styles.metricsLeft]}>
@@ -230,7 +281,8 @@ export default function ExploreScreen() {
height={userProfile?.height ? parseFloat(userProfile.height) : undefined}
/>
</ScrollView>
</SafeAreaView>
</View>

View File

@@ -1,4 +1,5 @@
import { ThemedView } from '@/components/ThemedView';
import { ROUTES } from '@/constants/Routes';
import { useThemeColor } from '@/hooks/useThemeColor';
import { router } from 'expo-router';
import React, { useEffect, useState } from 'react';
@@ -25,7 +26,7 @@ export default function SplashScreen() {
// router.replace('/onboarding');
// }
// setIsLoading(false);
router.replace('/(tabs)');
router.replace(ROUTES.TAB_COACH);
} catch (error) {
console.error('检查引导状态失败:', error);
// 如果出现错误,默认显示引导页面