feat: 新增营养摄入分析卡片并优化相关页面
- 在统计页面中引入营养摄入分析卡片,展示用户的营养数据 - 更新探索页面,增加营养数据加载逻辑,确保用户体验一致性 - 移除不再使用的推荐文章逻辑,简化代码结构 - 更新路由常量,确保路径管理集中化 - 优化体重历史记录卡片,提升用户交互体验
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
// 如果出现错误,默认显示引导页面
|
||||
|
||||
Reference in New Issue
Block a user