feat: 支持步数卡片; 优化数据分析各类卡片样式

This commit is contained in:
richarjiang
2025-08-30 17:07:04 +08:00
parent 465d5350f3
commit 741688065d
9 changed files with 462 additions and 103 deletions

View File

@@ -4,9 +4,9 @@ import { DateSelector } from '@/components/DateSelector';
import { FitnessRingsCard } from '@/components/FitnessRingsCard';
import { MoodCard } from '@/components/MoodCard';
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
import { ProgressBar } from '@/components/ProgressBar';
import HeartRateCard from '@/components/statistic/HeartRateCard';
import OxygenSaturationCard from '@/components/statistic/OxygenSaturationCard';
import StepsCard from '@/components/StepsCard';
import { StressMeter } from '@/components/StressMeter';
import { WeightHistoryCard } from '@/components/weight/WeightHistoryCard';
import { Colors } from '@/constants/Colors';
@@ -19,13 +19,13 @@ import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/mo
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health';
import { getTestHealthData } from '@/utils/mockHealthData';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
Animated,
SafeAreaView,
ScrollView,
StyleSheet,
@@ -40,53 +40,28 @@ const FloatingCard = ({ children, delay = 0, style }: {
delay?: number;
style?: any;
}) => {
const floatAnim = useRef(new Animated.Value(0)).current;
// useEffect(() => {
// const startAnimation = () => {
// Animated.loop(
// Animated.sequence([
// Animated.timing(floatAnim, {
// toValue: 1,
// duration: 3000,
// delay: delay,
// useNativeDriver: true,
// }),
// Animated.timing(floatAnim, {
// toValue: 0,
// duration: 3000,
// useNativeDriver: true,
// }),
// ])
// ).start();
// };
// startAnimation();
// }, [floatAnim, delay]);
const translateY = floatAnim.interpolate({
inputRange: [0, 1],
outputRange: [-2, -6],
});
return (
<Animated.View
<View
style={[
style,
{
transform: [{ translateY }],
marginBottom: 8,
},
]}
>
{children}
</Animated.View>
</View>
);
};
export default function ExploreScreen() {
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
// 开发调试设置为true来使用mock数据
const useMockData = true; // 改为true来启用mock数据调试
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
// 使用 dayjs当月日期与默认选中"今天"
@@ -111,15 +86,24 @@ export default function ExploreScreen() {
// 从 Redux 获取指定日期的健康数据
const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString));
// 解构健康数据
const stepCount = healthData?.steps ?? null;
const activeCalories = healthData?.activeEnergyBurned ?? null;
const basalMetabolism = healthData?.basalEnergyBurned ?? null;
const sleepDuration = healthData?.sleepDuration ?? null;
const hrvValue = healthData?.hrv ?? 0;
const oxygenSaturation = healthData?.oxygenSaturation ?? null;
const heartRate = healthData?.heartRate ?? null;
const fitnessRingsData = healthData ? {
// 解构健康数据支持mock数据
const mockData = useMockData ? getTestHealthData('mock') : null;
const stepCount: number | null = useMockData ? (mockData?.steps ?? null) : (healthData?.steps ?? null);
const hourlySteps = useMockData ? (mockData?.hourlySteps ?? []) : (healthData?.hourlySteps ?? []);
const activeCalories = useMockData ? (mockData?.activeEnergyBurned ?? null) : (healthData?.activeEnergyBurned ?? null);
const basalMetabolism: number | null = useMockData ? (mockData?.basalEnergyBurned ?? null) : (healthData?.basalEnergyBurned ?? null);
const sleepDuration = useMockData ? (mockData?.sleepDuration ?? null) : (healthData?.sleepDuration ?? null);
const hrvValue = useMockData ? (mockData?.hrv ?? 0) : (healthData?.hrv ?? 0);
const oxygenSaturation = useMockData ? (mockData?.oxygenSaturation ?? null) : (healthData?.oxygenSaturation ?? null);
const heartRate = useMockData ? (mockData?.heartRate ?? null) : (healthData?.heartRate ?? null);
const fitnessRingsData = useMockData ? {
activeCalories: mockData?.activeCalories ?? 0,
activeCaloriesGoal: mockData?.activeCaloriesGoal ?? 350,
exerciseMinutes: mockData?.exerciseMinutes ?? 0,
exerciseMinutesGoal: mockData?.exerciseMinutesGoal ?? 30,
standHours: mockData?.standHours ?? 0,
standHoursGoal: mockData?.standHoursGoal ?? 12,
} : (healthData ? {
activeCalories: healthData.activeEnergyBurned,
activeCaloriesGoal: healthData.activeCaloriesGoal,
exerciseMinutes: healthData.exerciseMinutes,
@@ -133,7 +117,7 @@ export default function ExploreScreen() {
exerciseMinutesGoal: 30,
standHours: 0,
standHoursGoal: 12,
};
});
// HRV更新时间
const [hrvUpdateTime, setHrvUpdateTime] = useState<Date>(new Date());
@@ -357,38 +341,31 @@ export default function ExploreScreen() {
<FloatingCard style={styles.masonryCard} delay={500}>
<Text style={styles.cardTitle}></Text>
{activeCalories != null ? (
<AnimatedNumber
value={activeCalories}
resetToken={animToken}
style={styles.caloriesValue}
format={(v) => `${Math.round(v)} 千卡`}
/>
) : (
<Text style={styles.caloriesValue}></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} delay={1000}>
<View style={styles.cardHeaderRow}>
<Text style={styles.cardTitle}></Text>
</View>
{stepCount != null ? (
<AnimatedNumber
value={stepCount}
resetToken={animToken}
style={styles.stepsValue}
format={(v) => `${Math.round(v)}/${stepGoal}`}
/>
) : (
<Text style={styles.stepsValue}>/{stepGoal}</Text>
)}
<ProgressBar
progress={Math.min(1, Math.max(0, (stepCount ?? 0) / stepGoal))}
height={14}
trackColor="#FFEBCB"
fillColor="#FFC365"
showLabel={false}
<FloatingCard style={styles.masonryCard}>
<StepsCard
stepCount={stepCount}
stepGoal={stepGoal}
hourlySteps={hourlySteps}
style={styles.stepsCardOverride}
/>
</FloatingCard>
<FloatingCard style={styles.masonryCard} delay={0}>
@@ -414,7 +391,7 @@ export default function ExploreScreen() {
/>
</FloatingCard>
<FloatingCard style={styles.masonryCard} delay={750}>
<FloatingCard style={styles.masonryCard}>
<View style={styles.cardHeaderRow}>
<Text style={styles.cardTitle}></Text>
</View>
@@ -463,8 +440,6 @@ export default function ExploreScreen() {
}
const primary = Colors.light.primary;
const lightColors = Colors.light;
const darkColors = Colors.dark;
const styles = StyleSheet.create({
container: {
@@ -543,8 +518,15 @@ const styles = StyleSheet.create({
caloriesValue: {
color: '#192126',
fontSize: 18,
fontWeight: '800',
marginTop: 18,
lineHeight: 18,
fontWeight: '600',
textAlignVertical: 'bottom'
},
caloriesUnit: {
color: '#515558ff',
fontSize: 12,
marginLeft: 4,
lineHeight: 18,
},
trainingContent: {
marginTop: 8,
@@ -737,6 +719,11 @@ const styles = StyleSheet.create({
margin: -16, // 抵消 masonryCard 的 padding
borderRadius: 16,
},
stepsCardOverride: {
margin: -16, // 抵消 masonryCard 的 padding
borderRadius: 16,
height: '100%', // 填充整个masonryCard
},
compactStepsCard: {
minHeight: 100,
},