diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx
index ee4568b..e068ecc 100644
--- a/app/(tabs)/statistics.tsx
+++ b/app/(tabs)/statistics.tsx
@@ -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 (
-
{children}
-
+
);
};
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(new Date());
@@ -357,38 +341,31 @@ export default function ExploreScreen() {
消耗卡路里
- {activeCalories != null ? (
- `${Math.round(v)} 千卡`}
- />
- ) : (
- ——
- )}
+
+ {activeCalories != null ? (
+ `${Math.round(v)}`}
+ />
+ ) : (
+ ——
+ )}
+ 千卡
+
-
-
- 步数
-
- {stepCount != null ? (
- `${Math.round(v)}/${stepGoal}`}
- />
- ) : (
- ——/{stepGoal}
- )}
-
+
@@ -414,7 +391,7 @@ export default function ExploreScreen() {
/>
-
+
睡眠
@@ -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,
},
diff --git a/components/MoodCard.tsx b/components/MoodCard.tsx
index 2f17185..7562270 100644
--- a/components/MoodCard.tsx
+++ b/components/MoodCard.tsx
@@ -15,8 +15,6 @@ export function MoodCard({ moodCheckin, onPress, isLoading = false }: MoodCardPr
return (
心情
-
-
{isLoading ? (
加载中...
@@ -52,7 +50,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
- marginTop: 22,
+ marginTop: 12,
},
moodPreviewText: {
fontSize: 14,
diff --git a/components/StepsCard.tsx b/components/StepsCard.tsx
new file mode 100644
index 0000000..4683591
--- /dev/null
+++ b/components/StepsCard.tsx
@@ -0,0 +1,152 @@
+import React, { useMemo } from 'react';
+import {
+ StyleSheet,
+ Text,
+ View,
+ ViewStyle
+} from 'react-native';
+
+import { HourlyStepData } from '@/utils/health';
+// 使用原生View来替代SVG,避免导入问题
+// import Svg, { Rect } from 'react-native-svg';
+
+interface StepsCardProps {
+ stepCount: number | null;
+ stepGoal: number;
+ hourlySteps: HourlyStepData[];
+ style?: ViewStyle;
+}
+
+const StepsCard: React.FC = ({
+ stepCount,
+ stepGoal,
+ hourlySteps,
+ style
+}) => {
+ // 计算柱状图数据
+ const chartData = useMemo(() => {
+ if (!hourlySteps || hourlySteps.length === 0) {
+ return Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0, height: 0 }));
+ }
+
+ // 找到最大步数用于计算高度比例
+ const maxSteps = Math.max(...hourlySteps.map(data => data.steps), 1);
+ const maxHeight = 20; // 柱状图最大高度(缩小一半)
+
+ return hourlySteps.map(data => ({
+ ...data,
+ height: maxSteps > 0 ? (data.steps / maxSteps) * maxHeight : 0
+ }));
+ }, [hourlySteps]);
+
+ // 获取当前小时
+ const currentHour = new Date().getHours();
+
+ return (
+
+ {/* 标题和步数显示 */}
+
+ 步数
+
+
+ {/* 柱状图 */}
+
+
+
+ {chartData.map((data, index) => {
+ // 判断是否是当前小时或者有活动的小时
+ const isActive = data.steps > 0;
+ const isCurrent = index <= currentHour;
+
+ return (
+
+ );
+ })}
+
+
+
+
+ {/* 步数和目标显示 */}
+
+
+ {stepCount !== null ? stepCount.toLocaleString() : '——'}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'space-between',
+ borderRadius: 20,
+ padding: 16,
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 4,
+ },
+ shadowOpacity: 0.08,
+ shadowRadius: 20,
+ elevation: 8,
+ },
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: '700',
+ color: '#192126',
+ },
+ footprintIcons: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ },
+ chartContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ },
+ chartWrapper: {
+ width: '100%',
+ alignItems: 'center',
+ },
+ chartArea: {
+ flexDirection: 'row',
+ alignItems: 'flex-end',
+ height: 20,
+ width: '100%',
+ maxWidth: 240,
+ justifyContent: 'space-between',
+ paddingHorizontal: 4,
+ },
+ chartBar: {
+ width: 4,
+ borderRadius: 1,
+ alignSelf: 'flex-end',
+ },
+ statsContainer: {
+ alignItems: 'flex-start',
+ marginTop: 6
+ },
+ stepCount: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: '#192126',
+ },
+
+});
+
+export default StepsCard;
\ No newline at end of file
diff --git a/components/StressMeter.tsx b/components/StressMeter.tsx
index d7bd12a..ff4575b 100644
--- a/components/StressMeter.tsx
+++ b/components/StressMeter.tsx
@@ -154,10 +154,11 @@ const styles = StyleSheet.create({
marginBottom: 12,
},
value: {
- fontSize: 24,
- fontWeight: '800',
+ fontSize: 20,
+ fontWeight: '600',
color: '#192126',
- lineHeight: 32,
+ lineHeight: 20,
+ marginTop: 2,
},
unit: {
fontSize: 12,
@@ -166,10 +167,10 @@ const styles = StyleSheet.create({
marginLeft: 4,
},
progressContainer: {
- height: 16,
+ height: 6,
},
progressTrack: {
- height: 8,
+ height: 6,
borderRadius: 4,
position: 'relative',
overflow: 'visible',
@@ -185,9 +186,9 @@ const styles = StyleSheet.create({
},
indicator: {
position: 'absolute',
- top: -4,
- width: 16,
- height: 16,
+ top: -2,
+ width: 10,
+ height: 10,
borderRadius: 8,
backgroundColor: '#FFFFFF',
shadowColor: '#000',
diff --git a/components/statistic/HealthDataCard.tsx b/components/statistic/HealthDataCard.tsx
index 81d2615..510b7af 100644
--- a/components/statistic/HealthDataCard.tsx
+++ b/components/statistic/HealthDataCard.tsx
@@ -34,7 +34,6 @@ const HealthDataCard: React.FC = ({
const styles = StyleSheet.create({
card: {
- borderRadius: 16,
shadowColor: '#000',
paddingHorizontal: 16,
shadowOffset: {
@@ -48,11 +47,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
},
- iconContainer: {
- marginRight: 16,
- alignItems: 'center',
- justifyContent: 'center',
- },
content: {
flex: 1,
justifyContent: 'center',
@@ -60,7 +54,7 @@ const styles = StyleSheet.create({
title: {
fontSize: 14,
color: '#192126',
- marginBottom: 4,
+ marginBottom: 14,
fontWeight: '800',
},
valueContainer: {
@@ -68,8 +62,8 @@ const styles = StyleSheet.create({
alignItems: 'flex-end',
},
value: {
- fontSize: 24,
- fontWeight: '800',
+ fontSize: 20,
+ fontWeight: '600',
color: '#192126',
},
unit: {
diff --git a/components/statistic/OxygenSaturationCard.tsx b/components/statistic/OxygenSaturationCard.tsx
index fbc55e3..57b1028 100644
--- a/components/statistic/OxygenSaturationCard.tsx
+++ b/components/statistic/OxygenSaturationCard.tsx
@@ -1,4 +1,3 @@
-import { Ionicons } from '@expo/vector-icons';
import React from 'react';
import { StyleSheet } from 'react-native';
import HealthDataCard from './HealthDataCard';
@@ -14,10 +13,6 @@ const OxygenSaturationCard: React.FC = ({
style,
oxygenSaturation
}) => {
- const oxygenIcon = (
-
- );
-
return (
{
@@ -155,6 +162,88 @@ async function fetchStepCount(date: Date): Promise {
});
}
+// 获取指定日期每小时步数数据
+async function fetchHourlyStepCount(date: Date): Promise {
+ return new Promise((resolve) => {
+ const startOfDay = dayjs(date).startOf('day');
+ const endOfDay = dayjs(date).endOf('day');
+
+ AppleHealthKit.getStepCount({
+ startDate: startOfDay.toDate().toISOString(),
+ endDate: endOfDay.toDate().toISOString(),
+ includeManuallyAdded: false,
+ }, (err, res) => {
+ if (err) {
+ logError('每小时步数', err);
+ return resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 })));
+ }
+
+ if (!res || !Array.isArray(res) || res.length === 0) {
+ logWarning('每小时步数', '为空');
+ return resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 })));
+ }
+
+ logSuccess('每小时步数', res);
+
+ // 初始化24小时数据
+ const hourlyData: HourlyStepData[] = Array.from({ length: 24 }, (_, i) => ({
+ hour: i,
+ steps: 0
+ }));
+
+ // 如果返回的是累计数据,我们需要获取样本数据
+ resolve(hourlyData);
+ });
+ });
+}
+
+// 使用样本数据获取每小时步数
+async function fetchHourlyStepSamples(date: Date): Promise {
+ return new Promise((resolve) => {
+ const startOfDay = dayjs(date).startOf('day');
+ const endOfDay = dayjs(date).endOf('day');
+
+ AppleHealthKit.getSamples(
+ {
+ startDate: startOfDay.toDate().toISOString(),
+ endDate: endOfDay.toDate().toISOString(),
+ type: 'StepCount',
+ },
+ (err: any, res: any[]) => {
+ if (err) {
+ logError('每小时步数样本', err);
+ return resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 })));
+ }
+
+ if (!res || !Array.isArray(res) || res.length === 0) {
+ logWarning('每小时步数样本', '为空');
+ return resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 })));
+ }
+
+ logSuccess('每小时步数样本', res);
+
+ // 初始化24小时数据
+ const hourlyData: HourlyStepData[] = Array.from({ length: 24 }, (_, i) => ({
+ hour: i,
+ steps: 0
+ }));
+
+ // 将样本数据按小时分组并累加
+ res.forEach((sample: any) => {
+ if (sample && sample.startDate && sample.value) {
+ const hour = dayjs(sample.startDate).hour();
+ if (hour >= 0 && hour < 24) {
+ hourlyData[hour].steps += Math.round(sample.value);
+ }
+ }
+ });
+
+ resolve(hourlyData);
+ }
+ );
+ });
+}
+
async function fetchActiveEnergyBurned(options: HealthDataOptions): Promise {
return new Promise((resolve) => {
AppleHealthKit.getActiveEnergyBurned(options, (err, res) => {
@@ -304,7 +393,8 @@ function getDefaultHealthData(): TodayHealthData {
standHours: 0,
standHoursGoal: 12,
oxygenSaturation: null,
- heartRate: null
+ heartRate: null,
+ hourlySteps: Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 }))
};
}
@@ -318,6 +408,7 @@ export async function fetchHealthDataForDate(date: Date): Promise {
+ return Array.from({ length: 24 }, (_, hour) => {
+ let steps = 0;
+
+ // 模拟真实的活动模式
+ if (hour >= 6 && hour <= 22) {
+ if (hour >= 7 && hour <= 9) {
+ // 早晨高峰期
+ steps = Math.floor(Math.random() * 600) + 200;
+ } else if (hour >= 12 && hour <= 13) {
+ // 午餐时间
+ steps = Math.floor(Math.random() * 400) + 300;
+ } else if (hour >= 17 && hour <= 19) {
+ // 晚间活跃期
+ steps = Math.floor(Math.random() * 500) + 250;
+ } else if (hour >= 6 && hour <= 22) {
+ // 白天正常活动
+ steps = Math.floor(Math.random() * 300) + 50;
+ }
+ } else {
+ // 夜间很少活动
+ steps = Math.floor(Math.random() * 50);
+ }
+
+ return { hour, steps };
+ });
+};
+
+// 不同活动模式的预设数据
+export const activityPatterns = {
+ // 久坐办公族
+ sedentary: Array.from({ length: 24 }, (_, hour) => ({
+ hour,
+ steps: hour >= 7 && hour <= 18 ? Math.floor(Math.random() * 200) + 50 :
+ hour >= 19 && hour <= 21 ? Math.floor(Math.random() * 300) + 100 :
+ Math.floor(Math.random() * 20)
+ })),
+
+ // 活跃用户
+ active: Array.from({ length: 24 }, (_, hour) => ({
+ hour,
+ steps: hour >= 6 && hour <= 8 ? Math.floor(Math.random() * 800) + 400 :
+ hour >= 12 && hour <= 13 ? Math.floor(Math.random() * 600) + 300 :
+ hour >= 17 && hour <= 20 ? Math.floor(Math.random() * 900) + 500 :
+ hour >= 9 && hour <= 16 ? Math.floor(Math.random() * 400) + 100 :
+ Math.floor(Math.random() * 50)
+ })),
+
+ // 健身爱好者
+ fitness: Array.from({ length: 24 }, (_, hour) => ({
+ hour,
+ steps: hour === 6 ? Math.floor(Math.random() * 1200) + 800 : // 晨跑
+ hour === 18 ? Math.floor(Math.random() * 1000) + 600 : // 晚间锻炼
+ hour >= 7 && hour <= 17 ? Math.floor(Math.random() * 300) + 100 :
+ Math.floor(Math.random() * 50)
+ })),
+};
+
+// 用于快速切换测试数据的函数
+export const getTestHealthData = (pattern: 'mock' | 'random' | 'sedentary' | 'active' | 'fitness' = 'mock'): TodayHealthData => {
+ let hourlySteps: HourlyStepData[];
+
+ switch (pattern) {
+ case 'random':
+ hourlySteps = generateRandomHourlySteps();
+ break;
+ case 'sedentary':
+ hourlySteps = activityPatterns.sedentary;
+ break;
+ case 'active':
+ hourlySteps = activityPatterns.active;
+ break;
+ case 'fitness':
+ hourlySteps = activityPatterns.fitness;
+ break;
+ default:
+ hourlySteps = mockHourlySteps;
+ }
+
+ const totalSteps = hourlySteps.reduce((sum, data) => sum + data.steps, 0);
+
+ return {
+ ...mockHealthData,
+ steps: totalSteps,
+ hourlySteps,
+ };
+};
\ No newline at end of file