feat: 支持步数卡片; 优化数据分析各类卡片样式
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user