From 62690ee3fc429eba9b3bdd2f967c16c8d5cee985 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 11 Sep 2025 09:08:51 +0800 Subject: [PATCH] =?UTF-8?q?refactor(sleep):=20=E9=87=8D=E6=9E=84=E7=9D=A1?= =?UTF-8?q?=E7=9C=A0=E6=95=B0=E6=8D=AE=E8=8E=B7=E5=8F=96=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=86=97=E4=BD=99=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E7=BB=84=E4=BB=B6=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从 healthSlice 和 health.ts 中移除 sleepDuration 字段及相关获取逻辑 - 将 SleepCard 改为按需异步获取睡眠数据,支持传入指定日期 - 睡眠详情页改为通过路由参数接收日期,支持查看历史记录 - 移除 statistics 页面对 sleepDuration 的直接依赖,统一由 SleepCard 管理 - 删除未使用的 SleepStageChart 组件,简化页面结构 --- app/(tabs)/statistics.tsx | 8 +- app/sleep-detail.tsx | 121 +++-------------------------- components/statistic/SleepCard.tsx | 38 ++++++--- store/healthSlice.ts | 3 +- utils/health.ts | 54 ------------- 5 files changed, 43 insertions(+), 181 deletions(-) diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index d050b08..a825f3e 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -98,7 +98,6 @@ export default function ExploreScreen() { 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 ?? null) : (healthData?.hrv ?? null); const oxygenSaturation = useMockData ? (mockData?.oxygenSaturation ?? null) : (healthData?.oxygenSaturation ?? null); @@ -293,7 +292,6 @@ export default function ExploreScreen() { steps: data.steps, activeCalories: data.activeEnergyBurned, basalEnergyBurned: data.basalEnergyBurned, - sleepDuration: data.sleepDuration, hrv: data.hrv, oxygenSaturation: data.oxygenSaturation, heartRate: data.heartRate, @@ -587,9 +585,9 @@ export default function ExploreScreen() { */} - pushIfAuthedElseLogin('/sleep-detail')} + pushIfAuthedElseLogin(`/sleep-detail?date=${dayjs(currentSelectedDate).format('YYYY-MM-DD')}`)} /> diff --git a/app/sleep-detail.tsx b/app/sleep-detail.tsx index f35f4c6..ca391f1 100644 --- a/app/sleep-detail.tsx +++ b/app/sleep-detail.tsx @@ -9,7 +9,7 @@ import { import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; -import { router } from 'expo-router'; +import { router, useLocalSearchParams } from 'expo-router'; import React, { useCallback, useEffect, useState } from 'react'; import { ActivityIndicator, @@ -27,113 +27,6 @@ import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; - - -// 简化的睡眠阶段图表组件 -const SleepStageChart = ({ - sleepData, - onInfoPress -}: { - sleepData: SleepDetailData; - onInfoPress: () => void; -}) => { - const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; - const colorTokens = Colors[theme]; - - // 使用真实的睡眠阶段数据,如果没有则使用默认数据 - const stages = sleepData.sleepStages.length > 0 - ? sleepData.sleepStages - .filter(stage => stage.percentage > 0) // 只显示有数据的阶段 - .map(stage => ({ - stage: stage.stage, - percentage: stage.percentage, - duration: stage.duration - })) - : [ - { stage: SleepStage.Awake, percentage: 1, duration: 3 }, - { stage: SleepStage.REM, percentage: 20, duration: 89 }, - { stage: SleepStage.Core, percentage: 67, duration: 295 }, - { stage: SleepStage.Deep, percentage: 12, duration: 51 } - ]; - - return ( - - - 阶段分析 - - - - - - {/* 入睡时间和起床时间显示 */} - - - - 入睡时间 - - - {sleepData.bedtime ? formatTime(sleepData.bedtime) : '--:--'} - - - - - 起床时间 - - - {sleepData.wakeupTime ? formatTime(sleepData.wakeupTime) : '--:--'} - - - - - {/* 简化的睡眠阶段条 */} - - {stages.map((stageData, index) => { - const color = getSleepStageColor(stageData.stage); - // 确保最小宽度,避免清醒阶段等小比例的阶段完全不可见 - const flexValue = Math.max(stageData.percentage || 1, 3); - return ( - - ); - })} - - - {/* 图例 */} - - - - - 清醒时间 - - - - 快速眼动 - - - - 核心睡眠 - - - - 深度睡眠 - - - - - ); -}; - // SleepGradeCard 组件现在在 InfoModal 组件内部 // SleepStagesInfoModal 组件现在从独立文件导入 @@ -145,7 +38,15 @@ export default function SleepDetailScreen() { const colorTokens = Colors[theme]; const [sleepData, setSleepData] = useState(null); const [loading, setLoading] = useState(true); - const [selectedDate] = useState(dayjs().toDate()); + + // 从导航参数获取日期,如果没有则使用今天 + const { date: dateParam } = useLocalSearchParams<{ date?: string }>(); + const [selectedDate] = useState(() => { + if (dateParam) { + return dayjs(dateParam).toDate(); + } + return dayjs().toDate(); + }); const [infoModal, setInfoModal] = useState<{ visible: boolean; title: string; type: 'sleep-time' | 'sleep-quality' | null }>({ visible: false, @@ -220,7 +121,7 @@ export default function SleepDetailScreen() { {/* 顶部导航 */} router.back()} withSafeTop={true} transparent={true} diff --git a/components/statistic/SleepCard.tsx b/components/statistic/SleepCard.tsx index 336c91d..517630a 100644 --- a/components/statistic/SleepCard.tsx +++ b/components/statistic/SleepCard.tsx @@ -1,22 +1,40 @@ -import React from 'react'; -import { StyleSheet, Text, View, TouchableOpacity } from 'react-native'; +import { fetchCompleteSleepData, formatSleepTime } from '@/utils/sleepHealthKit'; +import React, { useEffect, useState } from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; interface SleepCardProps { - sleepDuration?: number | null; + selectedDate?: Date; style?: object; onPress?: () => void; } const SleepCard: React.FC = ({ - sleepDuration, + selectedDate, style, onPress }) => { - const formatSleepDuration = (duration: number): string => { - const hours = Math.floor(duration / 60); - const minutes = Math.floor(duration % 60); - return `${hours}小时${minutes}分钟`; - }; + const [sleepDuration, setSleepDuration] = useState(null); + const [loading, setLoading] = useState(false); + + // 获取睡眠数据 + useEffect(() => { + const loadSleepData = async () => { + if (!selectedDate) return; + + try { + setLoading(true); + const data = await fetchCompleteSleepData(selectedDate); + setSleepDuration(data?.totalSleepTime || null); + } catch (error) { + console.error('SleepCard: 获取睡眠数据失败:', error); + setSleepDuration(null); + } finally { + setLoading(false); + } + }; + + loadSleepData(); + }, [selectedDate]); const CardContent = ( @@ -24,7 +42,7 @@ const SleepCard: React.FC = ({ 睡眠 - {sleepDuration != null ? formatSleepDuration(sleepDuration) : '——'} + {loading ? '加载中...' : (sleepDuration != null ? formatSleepTime(sleepDuration) : '--')} ); diff --git a/store/healthSlice.ts b/store/healthSlice.ts index 9bb36ce..51c4257 100644 --- a/store/healthSlice.ts +++ b/store/healthSlice.ts @@ -1,6 +1,6 @@ +import { HourlyStepData } from '@/utils/health'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { AppDispatch, RootState } from './index'; -import { HourlyStepData } from '@/utils/health'; // 健康数据类型定义 export interface FitnessRingsData { @@ -16,7 +16,6 @@ export interface HealthData { steps: number | null; activeCalories: number | null; basalEnergyBurned: number | null; - sleepDuration: number | null; hrv: number | null; oxygenSaturation: number | null; heartRate: number | null; diff --git a/utils/health.ts b/utils/health.ts index bf2664d..ce483ba 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -57,7 +57,6 @@ export type TodayHealthData = { steps: number; activeEnergyBurned: number; // kilocalories basalEnergyBurned: number; // kilocalories - 基础代谢率 - sleepDuration: number; // 睡眠时长(分钟) hrv: number | null; // 心率变异性 (ms) // 健身圆环数据 activeCalories: number; @@ -442,43 +441,6 @@ async function fetchBasalEnergyBurned(options: HealthDataOptions): Promise { - return new Promise((resolve) => { - // 使用睡眠专用的日期范围,包含前一天晚上的睡眠数据 - const sleepOptions = createSleepDateRange(date); - - AppleHealthKit.getSleepSamples(sleepOptions, (err, res) => { - if (err) { - logError('睡眠数据', err); - return resolve(0); - } - if (!res || !Array.isArray(res) || res.length === 0) { - logWarning('睡眠', '为空或格式错误'); - return resolve(0); - } - logSuccess('睡眠', res); - - // 过滤睡眠数据,只计算主睡眠时间段 - const filteredSamples = res.filter(sample => { - if (!sample || !sample.startDate || !sample.endDate) return false; - - const startDate = dayjs(sample.startDate); - const endDate = dayjs(sample.endDate); - const targetDate = dayjs(date); - - // 判断这个睡眠段是否属于当天的主睡眠 - // 睡眠段的结束时间应该在当天,或者睡眠段跨越了前一天晚上到当天早上 - const isMainSleepPeriod = endDate.isSame(targetDate, 'day') || - (startDate.isBefore(targetDate, 'day') && endDate.isAfter(targetDate.startOf('day'))); - - return isMainSleepPeriod; - }); - - resolve(calculateSleepDuration(filteredSamples)); - }); - }); -} - async function fetchHeartRateVariability(options: HealthDataOptions): Promise { return new Promise((resolve) => { console.log('=== 开始获取HRV数据 ==='); @@ -626,7 +588,6 @@ function getDefaultHealthData(): TodayHealthData { steps: 0, activeEnergyBurned: 0, basalEnergyBurned: 0, - sleepDuration: 0, hrv: null, activeCalories: 0, activeCaloriesGoal: 350, @@ -653,7 +614,6 @@ export async function fetchHealthDataForDate(date: Date): Promise