From a7f5379d5ab5a81440b488a7aa63d5f93b06108a Mon Sep 17 00:00:00 2001 From: richarjiang Date: Tue, 9 Sep 2025 19:27:19 +0800 Subject: [PATCH] feat: Update Podfile.lock to include NitroModules and ReactNativeHealthkit dependencies fix: Adjust objectVersion in project.pbxproj and improve WaterWidget folder exception handling refactor: Remove sleepService.ts as part of code cleanup chore: Comment out HealthKit initialization in health.ts and clean up fetchSleepDuration function --- app/sleep-detail.tsx | 546 ++++++++++++++++--- ios/Podfile.lock | 57 ++ ios/digitalpilates.xcodeproj/project.pbxproj | 31 +- services/sleepService.ts | 508 ----------------- utils/health.ts | 45 +- 5 files changed, 583 insertions(+), 604 deletions(-) delete mode 100644 services/sleepService.ts diff --git a/app/sleep-detail.tsx b/app/sleep-detail.tsx index 8a18dcf..a66696d 100644 --- a/app/sleep-detail.tsx +++ b/app/sleep-detail.tsx @@ -1,8 +1,14 @@ import { Ionicons } from '@expo/vector-icons'; +import { + AuthorizationRequestStatus, + queryCategorySamplesWithAnchor, + queryQuantitySamplesWithAnchor, + useHealthkitAuthorization +} from '@kingstinct/react-native-healthkit'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { router } from 'expo-router'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { ActivityIndicator, Animated, @@ -18,17 +24,431 @@ import { import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; -import { - fetchSleepDetailForDate, - formatSleepTime, - formatTime, - getSleepStageColor, - SleepDetailData, - SleepStage -} from '@/services/sleepService'; -import { ensureHealthPermissions } from '@/utils/health'; +// 睡眠阶段枚举 +enum SleepStage { + InBed = 'INBED', + Asleep = 'ASLEEP', + Awake = 'AWAKE', + Core = 'CORE', + Deep = 'DEEP', + REM = 'REM' +} + +// 睡眠质量评级 +enum SleepQuality { + Poor = 'poor', + Fair = 'fair', + Good = 'good', + Excellent = 'excellent' +} + +// 睡眠样本数据类型 +type SleepSample = { + startDate: string; + endDate: string; + value: SleepStage; + sourceName?: string; + sourceId?: string; +}; + +// 睡眠阶段统计 +type SleepStageStats = { + stage: SleepStage; + duration: number; + percentage: number; + quality: SleepQuality; +}; + +// 心率数据类型 +type HeartRateData = { + timestamp: string; + value: number; +}; + +// 睡眠详情数据类型 +type SleepDetailData = { + sleepScore: number; + totalSleepTime: number; + sleepQualityPercentage: number; + bedtime: string; + wakeupTime: string; + timeInBed: number; + sleepStages: SleepStageStats[]; + rawSleepSamples: SleepSample[]; + averageHeartRate: number | null; + sleepHeartRateData: HeartRateData[]; + sleepEfficiency: number; + qualityDescription: string; + recommendation: string; +}; + +// 工具函数 +const formatSleepTime = (minutes: number): string => { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + if (hours > 0 && mins > 0) { + return `${hours}h ${mins}m`; + } else if (hours > 0) { + return `${hours}h`; + } else { + return `${mins}m`; + } +}; + +const formatTime = (dateString: string): string => { + return dayjs(dateString).format('HH:mm'); +}; + +const getSleepStageColor = (stage: SleepStage): string => { + switch (stage) { + case SleepStage.Deep: + return '#3B82F6'; + case SleepStage.Core: + return '#8B5CF6'; + case SleepStage.REM: + case SleepStage.Asleep: + return '#EC4899'; + case SleepStage.Awake: + return '#F59E0B'; + case SleepStage.InBed: + return '#6B7280'; + default: + return '#9CA3AF'; + } +}; + +// 创建睡眠日期范围 +const createSleepDateRange = (date: Date): { startDate: Date; endDate: Date } => { + return { + startDate: dayjs(date).subtract(1, 'day').hour(18).minute(0).second(0).millisecond(0).toDate(), + endDate: dayjs(date).hour(12).minute(0).second(0).millisecond(0).toDate() + }; +}; + +// 获取睡眠样本数据 +const fetchSleepSamples = async (date: Date): Promise => { + try { + const options = createSleepDateRange(date); + + // 使用 queryCategorySamplesWithAnchor 查询睡眠分析数据 + const { samples } = await queryCategorySamplesWithAnchor('HKCategoryTypeIdentifierSleepAnalysis', { + limit: 0, + filter: { + startDate: options.startDate, + endDate: options.endDate, + strictStartDate: true, + strictEndDate: true + } + }); + + if (!samples || !Array.isArray(samples)) { + console.warn('睡眠样本数据为空'); + return []; + } + + // 过滤指定日期范围内的样本 + const startTime = new Date(options.startDate).getTime(); + const endTime = new Date(options.endDate).getTime(); + + const filteredSamples = samples.filter(sample => { + const sampleStart = new Date(sample.startDate).getTime(); + const sampleEnd = new Date(sample.endDate).getTime(); + return (sampleStart >= startTime && sampleStart < endTime) || + (sampleStart < endTime && sampleEnd > startTime); + }); + + console.log(`过滤前样本数量: ${samples.length}, 过滤后样本数量: ${filteredSamples.length}`); + + // 转换数据格式 + const sleepSamples: SleepSample[] = filteredSamples.map(sample => { + let mappedValue: SleepStage; + + // HealthKit 睡眠分析值映射 + switch (sample.value) { + case 0: + mappedValue = SleepStage.InBed; + break; + case 1: + mappedValue = SleepStage.Asleep; + break; + case 2: + mappedValue = SleepStage.Awake; + break; + case 3: + mappedValue = SleepStage.Core; + break; + case 4: + mappedValue = SleepStage.Deep; + break; + case 5: + mappedValue = SleepStage.REM; + break; + default: + mappedValue = SleepStage.Asleep; + } + + return { + startDate: sample.startDate, + endDate: sample.endDate, + value: mappedValue, + sourceName: sample.sourceName, + sourceId: sample.uuid + }; + }); + + console.log('获取到睡眠样本:', sleepSamples.length); + return sleepSamples; + + } catch (error) { + console.error('获取睡眠样本失败:', error); + return []; + } +}; + +// 获取睡眠期间心率数据 +const fetchSleepHeartRateData = async (bedtime: string, wakeupTime: string): Promise => { + try { + const { samples } = await queryQuantitySamplesWithAnchor('HKQuantityTypeIdentifierHeartRate', { + limit: 0 + }); + + if (!samples || !Array.isArray(samples)) { + return []; + } + + const bedtimeMs = new Date(bedtime).getTime(); + const wakeupTimeMs = new Date(wakeupTime).getTime(); + + const sleepHeartRateData: HeartRateData[] = samples + .filter(sample => { + const sampleTime = new Date(sample.startDate).getTime(); + return sampleTime >= bedtimeMs && sampleTime <= wakeupTimeMs; + }) + .map(sample => ({ + timestamp: sample.startDate, + value: Math.round(sample.quantity) + })) + .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); + + console.log('获取到睡眠心率数据:', sleepHeartRateData.length, '个样本'); + return sleepHeartRateData; + + } catch (error) { + console.error('获取睡眠心率数据失败:', error); + return []; + } +}; + +// 计算睡眠阶段统计 +const calculateSleepStageStats = (samples: SleepSample[]): SleepStageStats[] => { + const stageMap = new Map(); + + samples.forEach(sample => { + const startTime = dayjs(sample.startDate); + const endTime = dayjs(sample.endDate); + const duration = endTime.diff(startTime, 'minute'); + + const currentDuration = stageMap.get(sample.value) || 0; + stageMap.set(sample.value, currentDuration + duration); + }); + + const actualSleepTime = Array.from(stageMap.entries()) + .reduce((total, [, duration]) => total + duration, 0); + + const stats: SleepStageStats[] = []; + + stageMap.forEach((duration, stage) => { + if (stage === SleepStage.InBed) return; + + const percentage = actualSleepTime > 0 ? (duration / actualSleepTime) * 100 : 0; + let quality: SleepQuality; + + switch (stage) { + case SleepStage.Deep: + quality = percentage >= 15 ? SleepQuality.Excellent : + percentage >= 10 ? SleepQuality.Good : + percentage >= 5 ? SleepQuality.Fair : SleepQuality.Poor; + break; + case SleepStage.REM: + quality = percentage >= 20 ? SleepQuality.Excellent : + percentage >= 15 ? SleepQuality.Good : + percentage >= 10 ? SleepQuality.Fair : SleepQuality.Poor; + break; + case SleepStage.Core: + quality = percentage >= 45 ? SleepQuality.Excellent : + percentage >= 35 ? SleepQuality.Good : + percentage >= 25 ? SleepQuality.Fair : SleepQuality.Poor; + break; + case SleepStage.Awake: + quality = percentage <= 5 ? SleepQuality.Excellent : + percentage <= 10 ? SleepQuality.Good : + percentage <= 15 ? SleepQuality.Fair : SleepQuality.Poor; + break; + default: + quality = SleepQuality.Fair; + } + + stats.push({ + stage, + duration, + percentage: Math.round(percentage), + quality + }); + }); + + return stats.sort((a, b) => b.duration - a.duration); +}; + +// 计算睡眠得分 +const calculateSleepScore = (sleepStages: SleepStageStats[], sleepEfficiency: number, totalSleepTime: number): number => { + let score = 0; + + const idealSleepHours = 8 * 60; + const sleepDurationScore = Math.min(30, (totalSleepTime / idealSleepHours) * 30); + score += sleepDurationScore; + + const efficiencyScore = (sleepEfficiency / 100) * 25; + score += efficiencyScore; + + const deepSleepStage = sleepStages.find(stage => stage.stage === SleepStage.Deep); + const deepSleepScore = deepSleepStage ? Math.min(25, (deepSleepStage.percentage / 20) * 25) : 0; + score += deepSleepScore; + + const remSleepStage = sleepStages.find(stage => stage.stage === SleepStage.REM); + const remSleepScore = remSleepStage ? Math.min(20, (remSleepStage.percentage / 25) * 20) : 0; + score += remSleepScore; + + return Math.round(Math.min(100, score)); +}; + +// 获取睡眠质量描述和建议 +const getSleepQualityInfo = (sleepScore: number): { description: string; recommendation: string } => { + if (sleepScore >= 85) { + return { + description: '你身心愉悦并且精力充沛', + recommendation: '恭喜你获得优质的睡眠!如果你感到精力充沛,可以考虑中等强度的运动,以维持健康的生活方式,并进一步减轻压力,以获得最佳睡眠。' + }; + } else if (sleepScore >= 70) { + return { + description: '睡眠质量良好,精神状态不错', + recommendation: '你的睡眠质量还不错,但还有改善空间。建议保持规律的睡眠时间,睡前避免使用电子设备,营造安静舒适的睡眠环境。' + }; + } else if (sleepScore >= 50) { + return { + description: '睡眠质量一般,可能影响日间表现', + recommendation: '你的睡眠需要改善。建议制定固定的睡前例行程序,限制咖啡因摄入,确保卧室温度适宜,考虑进行轻度运动来改善睡眠质量。' + }; + } else { + return { + description: '睡眠质量较差,建议重视睡眠健康', + recommendation: '你的睡眠质量需要严重关注。建议咨询医生或睡眠专家,检查是否有睡眠障碍,同时改善睡眠环境和习惯,避免睡前刺激性活动。' + }; + } +}; + +// 主函数:获取完整的睡眠详情数据 +const fetchSleepDetailData = async (date: Date): Promise => { + try { + console.log('开始获取睡眠详情数据...', date); + + const sleepSamples = await fetchSleepSamples(date); + + if (sleepSamples.length === 0) { + console.warn('没有找到睡眠数据'); + return null; + } + + let bedtime: string; + let wakeupTime: string; + + if (sleepSamples.length > 0) { + const sortedSamples = sleepSamples.sort((a, b) => + new Date(a.startDate).getTime() - new Date(b.startDate).getTime() + ); + + bedtime = sortedSamples[0].startDate; + wakeupTime = sortedSamples[sortedSamples.length - 1].endDate; + + console.log('计算入睡和起床时间:'); + console.log('- 入睡时间:', dayjs(bedtime).format('YYYY-MM-DD HH:mm:ss')); + console.log('- 起床时间:', dayjs(wakeupTime).format('YYYY-MM-DD HH:mm:ss')); + } else { + console.warn('没有找到睡眠样本数据'); + return null; + } + + // 计算在床时间 + let timeInBed: number; + const inBedSamples = sleepSamples.filter(sample => sample.value === SleepStage.InBed); + + if (inBedSamples.length > 0) { + const sortedInBedSamples = inBedSamples.sort((a, b) => + new Date(a.startDate).getTime() - new Date(b.startDate).getTime() + ); + + const inBedStart = sortedInBedSamples[0].startDate; + const inBedEnd = sortedInBedSamples[sortedInBedSamples.length - 1].endDate; + timeInBed = dayjs(inBedEnd).diff(dayjs(inBedStart), 'minute'); + + console.log('在床时间计算:'); + console.log('- 上床时间:', dayjs(inBedStart).format('YYYY-MM-DD HH:mm:ss')); + console.log('- 离床时间:', dayjs(inBedEnd).format('YYYY-MM-DD HH:mm:ss')); + console.log('- 在床时长:', timeInBed, '分钟'); + } else { + timeInBed = dayjs(wakeupTime).diff(dayjs(bedtime), 'minute'); + console.log('没有INBED数据,使用睡眠时间作为在床时间:', timeInBed, '分钟'); + } + + // 计算睡眠阶段统计 + const sleepStages = calculateSleepStageStats(sleepSamples); + const totalSleepTime = sleepStages.reduce((total, stage) => total + stage.duration, 0); + const sleepEfficiency = timeInBed > 0 ? Math.round((totalSleepTime / timeInBed) * 100) : 0; + + // 获取睡眠期间心率数据 + const sleepHeartRateData = await fetchSleepHeartRateData(bedtime, wakeupTime); + const averageHeartRate = sleepHeartRateData.length > 0 ? + Math.round(sleepHeartRateData.reduce((sum, data) => sum + data.value, 0) / sleepHeartRateData.length) : + null; + + // 计算睡眠得分 + const sleepScore = calculateSleepScore(sleepStages, sleepEfficiency, totalSleepTime); + const qualityInfo = getSleepQualityInfo(sleepScore); + + console.log('=== 睡眠数据处理结果 ==='); + console.log('时间范围:', dayjs(bedtime).format('HH:mm'), '-', dayjs(wakeupTime).format('HH:mm')); + console.log('在床时间:', timeInBed, '分钟'); + console.log('总睡眠时间:', totalSleepTime, '分钟'); + console.log('睡眠效率:', sleepEfficiency, '%'); + console.log('睡眠得分:', sleepScore); + console.log('========================'); + + const sleepDetailData: SleepDetailData = { + sleepScore, + totalSleepTime, + sleepQualityPercentage: sleepScore, + bedtime, + wakeupTime, + timeInBed, + sleepStages, + rawSleepSamples: sleepSamples, + averageHeartRate, + sleepHeartRateData, + sleepEfficiency, + qualityDescription: qualityInfo.description, + recommendation: qualityInfo.recommendation + }; + + console.log('睡眠详情数据获取完成,睡眠得分:', sleepScore); + return sleepDetailData; + + } catch (error) { + console.error('获取睡眠详情数据失败:', error); + return null; + } +}; + // 简化的睡眠阶段图表组件 const SleepStageChart = ({ sleepData, @@ -43,12 +463,12 @@ const SleepStageChart = ({ // 使用真实的睡眠阶段数据,如果没有则使用默认数据 const stages = sleepData.sleepStages.length > 0 ? sleepData.sleepStages - .filter(stage => stage.percentage > 0) // 只显示有数据的阶段 - .map(stage => ({ - stage: stage.stage, - percentage: stage.percentage, - duration: stage.duration - })) + .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 }, @@ -59,7 +479,7 @@ const SleepStageChart = ({ return ( - 睡眠阶段分析 + 阶段分析 - {sleepData.bedtime ? formatTime(sleepData.bedtime) : '23:15'} + {sleepData.bedtime ? formatTime(sleepData.bedtime) : '--:--'} @@ -83,7 +503,7 @@ const SleepStageChart = ({ 起床时间 - {sleepData.wakeupTime ? formatTime(sleepData.wakeupTime) : '06:52'} + {sleepData.wakeupTime ? formatTime(sleepData.wakeupTime) : '--:--'} @@ -92,6 +512,8 @@ const SleepStageChart = ({ {stages.map((stageData, index) => { const color = getSleepStageColor(stageData.stage); + // 确保最小宽度,避免清醒阶段等小比例的阶段完全不可见 + const flexValue = Math.max(stageData.percentage || 1, 3); return ( @@ -118,8 +540,6 @@ const SleepStageChart = ({ 快速眼动 - - 核心睡眠 @@ -495,23 +915,31 @@ export default function SleepDetailScreen() { visible: false }); - useEffect(() => { - loadSleepData(); - }, [selectedDate]); + // 使用 HealthKit 权限 hook + const [authorizationStatus, requestAuthorization] = useHealthkitAuthorization([ + 'HKCategoryTypeIdentifierSleepAnalysis', + 'HKQuantityTypeIdentifierHeartRate' + ]); - const loadSleepData = async () => { + const loadSleepData = useCallback(async () => { try { setLoading(true); - // 确保有健康权限 - const hasPermission = await ensureHealthPermissions(); - if (!hasPermission) { - console.warn('没有健康数据权限'); - return; + // 如果需要请求权限,先请求权限 + if (authorizationStatus === AuthorizationRequestStatus.shouldRequest) { + console.log('请求 HealthKit 权限..., 当前状态:', authorizationStatus); + try { + await requestAuthorization(); + // 请求权限后,等待状态更新 + return; + } catch (permissionError) { + console.error('权限请求失败:', permissionError); + } } - // 获取睡眠详情数据 - const data = await fetchSleepDetailForDate(selectedDate); + // 如果权限不需要或已经处理,尝试获取数据 + console.log('尝试获取睡眠数据,权限状态:', authorizationStatus); + const data = await fetchSleepDetailData(selectedDate); setSleepData(data); } catch (error) { @@ -519,7 +947,11 @@ export default function SleepDetailScreen() { } finally { setLoading(false); } - }; + }, [selectedDate, authorizationStatus, requestAuthorization]); + + useEffect(() => { + loadSleepData(); + }, [loadSleepData]); if (loading) { return ( @@ -535,8 +967,8 @@ export default function SleepDetailScreen() { sleepScore: 0, totalSleepTime: 0, sleepQualityPercentage: 0, - bedtime: new Date().toISOString(), - wakeupTime: new Date().toISOString(), + bedtime: '', + wakeupTime: '', timeInBed: 0, sleepStages: [], rawSleepSamples: [], // 添加空的原始睡眠样本数据 @@ -586,26 +1018,6 @@ export default function SleepDetailScreen() { {/* 建议文本 */} {displayData.recommendation} - {/* 调试信息 - 仅在开发模式下显示 */} - {__DEV__ && sleepData && sleepData.rawSleepSamples.length > 0 && ( - - - 调试信息 ({sleepData.rawSleepSamples.length} 个睡眠样本) - - - 原始睡眠样本类型: {[...new Set(sleepData.rawSleepSamples.map(s => s.value))].join(', ')} - - - 时间范围: {sleepData.rawSleepSamples.length > 0 ? - `${formatTime(sleepData.rawSleepSamples[0].startDate)} - ${formatTime(sleepData.rawSleepSamples[sleepData.rawSleepSamples.length - 1].endDate)}` : - '无数据'} - - - 在床时长: {displayData.timeInBed > 0 ? formatSleepTime(displayData.timeInBed) : '未知'} - - - )} - {/* 睡眠统计卡片 */} @@ -738,7 +1150,7 @@ export default function SleepDetailScreen() { {/* Raw Sleep Samples List - 显示所有原始睡眠数据 */} - {sleepData && sleepData.rawSleepSamples && sleepData.rawSleepSamples.length > 0 && ( + {sleepData && sleepData.rawSleepSamples && sleepData.rawSleepSamples.length > 1 && ( @@ -748,14 +1160,14 @@ export default function SleepDetailScreen() { 查看数据间隔和可能的gap - + {sleepData.rawSleepSamples.map((sample, index) => { // 计算与前一个样本的时间间隔 const prevSample = index > 0 ? sleepData.rawSleepSamples[index - 1] : null; let gapMinutes = 0; let hasGap = false; - + if (prevSample) { const prevEndTime = new Date(prevSample.endDate).getTime(); const currentStartTime = new Date(sample.startDate).getTime(); @@ -766,14 +1178,14 @@ export default function SleepDetailScreen() { const startTime = formatTime(sample.startDate); const endTime = formatTime(sample.endDate); const duration = Math.round((new Date(sample.endDate).getTime() - new Date(sample.startDate).getTime()) / (1000 * 60)); - + // 获取睡眠阶段中文名称 const getStageName = (value: string) => { switch (value) { case 'HKCategoryValueSleepAnalysisInBed': return '在床上'; case 'HKCategoryValueSleepAnalysisAwake': return '清醒'; case 'HKCategoryValueSleepAnalysisAsleepCore': return '核心睡眠'; - case 'HKCategoryValueSleepAnalysisAsleepDeep': return '深度睡眠'; + case 'HKCategoryValueSleepAnalysisAsleepDeep': return '深度睡眠'; case 'HKCategoryValueSleepAnalysisAsleepREM': return 'REM睡眠'; case 'HKCategoryValueSleepAnalysisAsleepUnspecified': return '未指定睡眠'; default: return value; @@ -804,16 +1216,16 @@ export default function SleepDetailScreen() { )} - + {/* 睡眠样本条目 */} - {getStageName(sample.value)} @@ -823,7 +1235,7 @@ export default function SleepDetailScreen() { {duration}分钟 - + {startTime} - {endTime} @@ -1302,7 +1714,7 @@ const styles = StyleSheet.create({ }, legendRow: { flexDirection: 'row', - justifyContent: 'space-between', + justifyContent: 'center', }, legendItem: { flexDirection: 'row', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cb2afde..fdfd3bb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -153,6 +153,30 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - NitroModules (0.29.3): + - DoubleConversion + - glog + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsc + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - PurchasesHybridCommon (16.2.2): - RevenueCat (= 5.34.0) - QCloudCore (6.5.1): @@ -1743,6 +1767,31 @@ PODS: - React-logger (= 0.79.5) - React-perflogger (= 0.79.5) - React-utils (= 0.79.5) + - ReactNativeHealthkit (10.1.0): + - DoubleConversion + - glog + - NitroModules + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsc + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - RevenueCat (5.34.0) - RNAppleHealthKit (1.7.0): - React @@ -2029,6 +2078,7 @@ DEPENDENCIES: - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - lottie-react-native (from `../node_modules/lottie-react-native`) + - NitroModules (from `../node_modules/react-native-nitro-modules`) - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) @@ -2096,6 +2146,7 @@ DEPENDENCIES: - ReactAppDependencyProvider (from `build/generated/ios`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "ReactNativeHealthkit (from `../node_modules/@kingstinct/react-native-healthkit`)" - RNAppleHealthKit (from `../node_modules/react-native-health`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)" @@ -2198,6 +2249,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" lottie-react-native: :path: "../node_modules/lottie-react-native" + NitroModules: + :path: "../node_modules/react-native-nitro-modules" RCT-Folly: :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: @@ -2328,6 +2381,8 @@ EXTERNAL SOURCES: :path: build/generated/ios ReactCommon: :path: "../node_modules/react-native/ReactCommon" + ReactNativeHealthkit: + :path: "../node_modules/@kingstinct/react-native-healthkit" RNAppleHealthKit: :path: "../node_modules/react-native-health" RNCAsyncStorage: @@ -2397,6 +2452,7 @@ SPEC CHECKSUMS: libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 lottie-ios: a881093fab623c467d3bce374367755c272bdd59 lottie-react-native: 4969af5ac8f2ed2c562d35b6d3d9fe7d34a1add1 + NitroModules: c99da4ad8dd1f8cf270770fbcd5d7659743bc010 PurchasesHybridCommon: 62f852419aae7041792217593998f7ac3f8b567d QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798 QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7 @@ -2466,6 +2522,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: f3e842e6cb5a825b6918a74a38402ba1409411f8 ReactCodegen: 272c9bc1a8a917bf557bd9d032a4b3e181c6abfe ReactCommon: 7eb76fcd5133313d8c6a138a5c7dd89f80f189d5 + ReactNativeHealthkit: 1b94bc11acc67035a878c3b9baedb6977b39a835 RevenueCat: eb2aa042789d9c99ad5172bd96e28b96286d6ada RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9 RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f diff --git a/ios/digitalpilates.xcodeproj/project.pbxproj b/ios/digitalpilates.xcodeproj/project.pbxproj index 3f3419e..58fa06f 100644 --- a/ios/digitalpilates.xcodeproj/project.pbxproj +++ b/ios/digitalpilates.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 70; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -64,7 +64,7 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 7996A1302E6FB82300371142 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 7996A1302E6FB82300371142 /* Exceptions for "WaterWidget" folder in "WaterWidgetExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, @@ -74,7 +74,18 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 7996A11C2E6FB82300371142 /* WaterWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (7996A1302E6FB82300371142 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WaterWidget; sourceTree = ""; }; + 7996A11C2E6FB82300371142 /* WaterWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 7996A1302E6FB82300371142 /* Exceptions for "WaterWidget" folder in "WaterWidgetExtension" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = WaterWidget; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -232,8 +243,6 @@ 7996A11C2E6FB82300371142 /* WaterWidget */, ); name = WaterWidgetExtension; - packageProductDependencies = ( - ); productName = WaterWidgetExtension; productReference = 7996A1172E6FB82300371142 /* WaterWidgetExtension.appex */; productType = "com.apple.product-type.app-extension"; @@ -556,6 +565,7 @@ MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.digitalpilates.WaterWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -601,6 +611,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.digitalpilates.WaterWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -666,7 +677,10 @@ LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -721,7 +735,10 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = false; diff --git a/services/sleepService.ts b/services/sleepService.ts deleted file mode 100644 index c56c624..0000000 --- a/services/sleepService.ts +++ /dev/null @@ -1,508 +0,0 @@ -import dayjs from 'dayjs'; -import AppleHealthKit from 'react-native-health'; - -// 睡眠阶段枚举(与 HealthKit 保持一致) -export enum SleepStage { - InBed = 'INBED', - Asleep = 'ASLEEP', - Awake = 'AWAKE', - Core = 'CORE', - Deep = 'DEEP', - REM = 'REM' -} - -// 睡眠质量评级 -export enum SleepQuality { - Poor = 'poor', // 差 - Fair = 'fair', // 一般 - Good = 'good', // 良好 - Excellent = 'excellent' // 优秀 -} - -// 睡眠样本数据类型 -export type SleepSample = { - startDate: string; - endDate: string; - value: SleepStage; - sourceName?: string; - sourceId?: string; -}; - -// 睡眠阶段统计 -export type SleepStageStats = { - stage: SleepStage; - duration: number; // 分钟 - percentage: number; // 百分比 - quality: SleepQuality; -}; - -// 心率数据类型 -export type HeartRateData = { - timestamp: string; - value: number; // BPM -}; - -// 睡眠详情数据类型 -export type SleepDetailData = { - // 基础睡眠信息 - sleepScore: number; // 睡眠得分 0-100 - totalSleepTime: number; // 总睡眠时间(分钟) - sleepQualityPercentage: number; // 睡眠质量百分比 - - // 睡眠时间信息 - bedtime: string; // 上床时间 - wakeupTime: string; // 起床时间 - timeInBed: number; // 在床时间(分钟) - - // 睡眠阶段统计 - sleepStages: SleepStageStats[]; - - // 原始睡眠样本数据(用于图表显示) - rawSleepSamples: SleepSample[]; - - // 心率数据 - averageHeartRate: number | null; // 平均心率 - sleepHeartRateData: HeartRateData[]; // 睡眠期间心率数据 - - // 睡眠效率 - sleepEfficiency: number; // 睡眠效率百分比 (总睡眠时间/在床时间) - - // 建议和评价 - qualityDescription: string; // 睡眠质量描述 - recommendation: string; // 睡眠建议 -}; - -// 日期范围工具函数 -function createSleepDateRange(date: Date): { startDate: string; endDate: string } { - // 睡眠数据通常跨越两天,从前一天18:00到当天12:00 - return { - startDate: dayjs(date).subtract(1, 'day').hour(18).minute(0).second(0).millisecond(0).toISOString(), - endDate: dayjs(date).hour(12).minute(0).second(0).millisecond(0).toISOString() - }; -} - -// 获取睡眠样本数据 -async function fetchSleepSamples(date: Date): Promise { - return new Promise((resolve) => { - const options = createSleepDateRange(date); - - AppleHealthKit.getSleepSamples(options, (err, results) => { - if (err) { - console.error('获取睡眠样本失败:', err); - resolve([]); - return; - } - - if (!results || !Array.isArray(results)) { - console.warn('睡眠样本数据为空'); - resolve([]); - return; - } - - // 添加详细日志,了解实际获取到的数据类型 - console.log('获取到睡眠样本:', results.length); - console.log('睡眠样本详情:', results.map(r => ({ - value: r.value, - start: r.startDate?.substring(11, 16), - end: r.endDate?.substring(11, 16), - duration: `${Math.round((new Date(r.endDate).getTime() - new Date(r.startDate).getTime()) / 60000)}min` - }))); - - // 检查可用的睡眠阶段类型 - const uniqueValues = [...new Set(results.map(r => r.value))]; - console.log('可用的睡眠阶段类型:', uniqueValues); - - resolve(results as unknown as SleepSample[]); - }); - }); -} - -// 获取睡眠期间心率数据 -async function fetchSleepHeartRateData(bedtime: string, wakeupTime: string): Promise { - return new Promise((resolve) => { - const options = { - startDate: bedtime, - endDate: wakeupTime, - ascending: true - }; - - AppleHealthKit.getHeartRateSamples(options, (err, results) => { - if (err) { - console.error('获取睡眠心率数据失败:', err); - resolve([]); - return; - } - - if (!results || !Array.isArray(results)) { - resolve([]); - return; - } - - const heartRateData: HeartRateData[] = results.map(sample => ({ - timestamp: sample.startDate, - value: Math.round(sample.value) - })); - - console.log('获取到睡眠心率数据:', heartRateData.length, '个样本'); - resolve(heartRateData); - }); - }); -} - -// 计算睡眠阶段统计 -function calculateSleepStageStats(samples: SleepSample[]): SleepStageStats[] { - const stageMap = new Map(); - - // 计算每个阶段的总时长 - samples.forEach(sample => { - const startTime = dayjs(sample.startDate); - const endTime = dayjs(sample.endDate); - const duration = endTime.diff(startTime, 'minute'); - - const currentDuration = stageMap.get(sample.value) || 0; - stageMap.set(sample.value, currentDuration + duration); - }); - - // 计算实际睡眠时间(包括所有睡眠阶段,排除在床时间) - const actualSleepTime = Array.from(stageMap.entries()) - .reduce((total, [, duration]) => total + duration, 0); - - // 生成统计数据,包含所有睡眠阶段(包括清醒时间) - const stats: SleepStageStats[] = []; - - stageMap.forEach((duration, stage) => { - // 只排除在床时间,保留清醒时间 - if (stage === SleepStage.InBed) return; - - const percentage = actualSleepTime > 0 ? (duration / actualSleepTime) * 100 : 0; - let quality: SleepQuality; - - // 根据睡眠阶段和比例判断质量 - switch (stage) { - case SleepStage.Deep: - quality = percentage >= 15 ? SleepQuality.Excellent : - percentage >= 10 ? SleepQuality.Good : - percentage >= 5 ? SleepQuality.Fair : SleepQuality.Poor; - break; - case SleepStage.REM: - quality = percentage >= 20 ? SleepQuality.Excellent : - percentage >= 15 ? SleepQuality.Good : - percentage >= 10 ? SleepQuality.Fair : SleepQuality.Poor; - break; - case SleepStage.Core: - quality = percentage >= 45 ? SleepQuality.Excellent : - percentage >= 35 ? SleepQuality.Good : - percentage >= 25 ? SleepQuality.Fair : SleepQuality.Poor; - break; - case SleepStage.Awake: - // 清醒时间越少越好 - quality = percentage <= 5 ? SleepQuality.Excellent : - percentage <= 10 ? SleepQuality.Good : - percentage <= 15 ? SleepQuality.Fair : SleepQuality.Poor; - break; - default: - quality = SleepQuality.Fair; - } - - stats.push({ - stage, - duration, - percentage: Math.round(percentage), - quality - }); - }); - - // 按持续时间排序 - return stats.sort((a, b) => b.duration - a.duration); -} - -// 计算睡眠得分 -function calculateSleepScore(sleepStages: SleepStageStats[], sleepEfficiency: number, totalSleepTime: number): number { - let score = 0; - - // 睡眠时长得分 (30分) - const idealSleepHours = 8 * 60; // 8小时 - const sleepDurationScore = Math.min(30, (totalSleepTime / idealSleepHours) * 30); - score += sleepDurationScore; - - // 睡眠效率得分 (25分) - const efficiencyScore = (sleepEfficiency / 100) * 25; - score += efficiencyScore; - - // 深度睡眠得分 (25分) - const deepSleepStage = sleepStages.find(stage => stage.stage === SleepStage.Deep); - const deepSleepScore = deepSleepStage ? Math.min(25, (deepSleepStage.percentage / 20) * 25) : 0; - score += deepSleepScore; - - // REM睡眠得分 (20分) - const remSleepStage = sleepStages.find(stage => stage.stage === SleepStage.REM); - const remSleepScore = remSleepStage ? Math.min(20, (remSleepStage.percentage / 25) * 20) : 0; - score += remSleepScore; - - return Math.round(Math.min(100, score)); -} - -// 获取睡眠质量描述和建议 -function getSleepQualityInfo(sleepScore: number): { description: string; recommendation: string } { - if (sleepScore >= 85) { - return { - description: '你身心愉悦并且精力充沛', - recommendation: '恭喜你获得优质的睡眠!如果你感到精力充沛,可以考虑中等强度的运动,以维持健康的生活方式,并进一步减轻压力,以获得最佳睡眠。' - }; - } else if (sleepScore >= 70) { - return { - description: '睡眠质量良好,精神状态不错', - recommendation: '你的睡眠质量还不错,但还有改善空间。建议保持规律的睡眠时间,睡前避免使用电子设备,营造安静舒适的睡眠环境。' - }; - } else if (sleepScore >= 50) { - return { - description: '睡眠质量一般,可能影响日间表现', - recommendation: '你的睡眠需要改善。建议制定固定的睡前例行程序,限制咖啡因摄入,确保卧室温度适宜,考虑进行轻度运动来改善睡眠质量。' - }; - } else { - return { - description: '睡眠质量较差,建议重视睡眠健康', - recommendation: '你的睡眠质量需要严重关注。建议咨询医生或睡眠专家,检查是否有睡眠障碍,同时改善睡眠环境和习惯,避免睡前刺激性活动。' - }; - } -} - -// 获取睡眠阶段中文名称 -export function getSleepStageDisplayName(stage: SleepStage): string { - switch (stage) { - case SleepStage.Deep: - return '深度'; - case SleepStage.Core: - return '核心'; - case SleepStage.REM: - return '快速眼动'; - case SleepStage.Asleep: - return '浅睡'; - case SleepStage.Awake: - return '清醒'; - case SleepStage.InBed: - return '在床'; - default: - return '未知'; - } -} - -// 获取睡眠质量颜色 -export function getSleepStageColor(stage: SleepStage): string { - switch (stage) { - case SleepStage.Deep: - return '#3B82F6'; // 蓝色 - case SleepStage.Core: - return '#8B5CF6'; // 紫色 - case SleepStage.REM: - case SleepStage.Asleep: - return '#EC4899'; // 粉色 - case SleepStage.Awake: - return '#F59E0B'; // 橙色 - case SleepStage.InBed: - return '#6B7280'; // 灰色 - default: - return '#9CA3AF'; - } -} - -// 主函数:获取完整的睡眠详情数据 -export async function fetchSleepDetailForDate(date: Date): Promise { - try { - console.log('开始获取睡眠详情数据...', date); - - // 获取睡眠样本数据 - const sleepSamples = await fetchSleepSamples(date); - - if (sleepSamples.length === 0) { - console.warn('没有找到睡眠数据'); - return null; - } - - // 找到入睡时间和起床时间 - // 使用所有样本数据来确定完整的睡眠周期(包含清醒时间) - let bedtime: string; - let wakeupTime: string; - - if (sleepSamples.length > 0) { - // 按开始时间排序 - const sortedSamples = sleepSamples.sort((a, b) => - new Date(a.startDate).getTime() - new Date(b.startDate).getTime() - ); - - // 入睡时间:第一个样本的开始时间(包含清醒时间,确保完整性) - bedtime = sortedSamples[0].startDate; - // 起床时间:最后一个样本的结束时间 - wakeupTime = sortedSamples[sortedSamples.length - 1].endDate; - - console.log('计算入睡和起床时间:'); - console.log('- 入睡时间:', dayjs(bedtime).format('YYYY-MM-DD HH:mm:ss')); - console.log('- 起床时间:', dayjs(wakeupTime).format('YYYY-MM-DD HH:mm:ss')); - } else { - console.warn('没有找到睡眠样本数据'); - return null; - } - - // 计算在床时间 - 使用 INBED 样本数据 - let timeInBed: number; - const inBedSamples = sleepSamples.filter(sample => sample.value === SleepStage.InBed); - - if (inBedSamples.length > 0) { - // 使用 INBED 样本计算在床时间 - const sortedInBedSamples = inBedSamples.sort((a, b) => - new Date(a.startDate).getTime() - new Date(b.startDate).getTime() - ); - - const inBedStart = sortedInBedSamples[0].startDate; - const inBedEnd = sortedInBedSamples[sortedInBedSamples.length - 1].endDate; - timeInBed = dayjs(inBedEnd).diff(dayjs(inBedStart), 'minute'); - - console.log('在床时间计算:'); - console.log('- 上床时间:', dayjs(inBedStart).format('YYYY-MM-DD HH:mm:ss')); - console.log('- 离床时间:', dayjs(inBedEnd).format('YYYY-MM-DD HH:mm:ss')); - console.log('- 在床时长:', timeInBed, '分钟'); - } else { - // 如果没有 INBED 数据,使用睡眠时间作为在床时间 - timeInBed = dayjs(wakeupTime).diff(dayjs(bedtime), 'minute'); - console.log('没有INBED数据,使用睡眠时间作为在床时间:', timeInBed, '分钟'); - } - - // 计算睡眠阶段统计 - const sleepStages = calculateSleepStageStats(sleepSamples); - - const totalSleepTime = sleepStages - .reduce((total, stage) => total + stage.duration, 0); - - // 计算睡眠效率 - const sleepEfficiency = timeInBed > 0 ? Math.round((totalSleepTime / timeInBed) * 100) : 0; - - // 获取睡眠期间心率数据 - const sleepHeartRateData = await fetchSleepHeartRateData(bedtime, wakeupTime); - - // 计算平均心率 - const averageHeartRate = sleepHeartRateData.length > 0 ? - Math.round(sleepHeartRateData.reduce((sum, data) => sum + data.value, 0) / sleepHeartRateData.length) : - null; - - // 计算睡眠得分 - const sleepScore = calculateSleepScore(sleepStages, sleepEfficiency, totalSleepTime); - - // 获取质量描述和建议 - const qualityInfo = getSleepQualityInfo(sleepScore); - - // 详细的调试信息 - console.log('=== 睡眠数据处理结果 ==='); - console.log('时间范围:', dayjs(bedtime).format('HH:mm'), '-', dayjs(wakeupTime).format('HH:mm')); - console.log('在床时间:', timeInBed, '分钟'); - console.log('总睡眠时间:', totalSleepTime, '分钟'); - console.log('睡眠效率:', sleepEfficiency, '%'); - console.log('睡眠阶段统计:'); - sleepStages.forEach(stage => { - console.log(` ${getSleepStageDisplayName(stage.stage)}: ${stage.duration}分钟 (${stage.percentage}%)`); - }); - - console.log('========================'); - - const sleepDetailData: SleepDetailData = { - sleepScore, - totalSleepTime, - sleepQualityPercentage: sleepScore, // 使用睡眠得分作为质量百分比 - bedtime, - wakeupTime, - timeInBed, - sleepStages, - rawSleepSamples: sleepSamples, // 保存原始睡眠样本数据 - averageHeartRate, - sleepHeartRateData, - sleepEfficiency, - qualityDescription: qualityInfo.description, - recommendation: qualityInfo.recommendation - }; - - console.log('睡眠详情数据获取完成,睡眠得分:', sleepScore); - return sleepDetailData; - - } catch (error) { - console.error('获取睡眠详情数据失败:', error); - return null; - } -} - -// 格式化睡眠时间显示 -export function formatSleepTime(minutes: number): string { - const hours = Math.floor(minutes / 60); - const mins = minutes % 60; - - if (hours > 0 && mins > 0) { - return `${hours}h ${mins}m`; - } else if (hours > 0) { - return `${hours}h`; - } else { - return `${mins}m`; - } -} - -// 格式化时间显示 (HH:MM) -export function formatTime(dateString: string): string { - return dayjs(dateString).format('HH:mm'); -} - -// 将睡眠样本数据转换为15分钟间隔的睡眠阶段数据 -export function convertSleepSamplesToIntervals(sleepSamples: SleepSample[], bedtime: string, wakeupTime: string): { time: string; stage: SleepStage }[] { - const data: { time: string; stage: SleepStage }[] = []; - - if (sleepSamples.length === 0) { - console.log('没有睡眠样本数据可用于图表显示'); - return []; - } - - // 过滤掉InBed阶段,只保留实际睡眠阶段 - const sleepOnlySamples = sleepSamples.filter(sample => - sample.value !== SleepStage.InBed - ); - - if (sleepOnlySamples.length === 0) { - console.log('只有InBed数据,没有详细睡眠阶段数据'); - return []; - } - - console.log('处理睡眠阶段数据 - 样本数量:', sleepOnlySamples.length); - console.log('时间范围:', formatTime(bedtime), '-', formatTime(wakeupTime)); - - const startTime = dayjs(bedtime); - const endTime = dayjs(wakeupTime); - let currentTime = startTime.clone(); - - // 创建一个映射,用于快速查找每个时间点的睡眠阶段 - while (currentTime.isBefore(endTime)) { - const currentTimestamp = currentTime.toDate().getTime(); - - // 找到当前时间点对应的睡眠阶段 - let currentStage = SleepStage.Awake; // 默认为清醒 - - for (const sample of sleepOnlySamples) { - const sampleStart = new Date(sample.startDate).getTime(); - const sampleEnd = new Date(sample.endDate).getTime(); - - // 如果当前时间在这个样本的时间范围内 - if (currentTimestamp >= sampleStart && currentTimestamp < sampleEnd) { - currentStage = sample.value; - break; - } - } - - const timeStr = currentTime.format('HH:mm'); - data.push({ time: timeStr, stage: currentStage }); - - // 移动到下一个15分钟间隔 - currentTime = currentTime.add(15, 'minute'); - } - - console.log('生成的睡眠阶段间隔数据点数量:', data.length); - console.log('阶段分布:', data.reduce((acc, curr) => { - acc[curr.stage] = (acc[curr.stage] || 0) + 1; - return acc; - }, {} as Record)); - - return data; -} \ No newline at end of file diff --git a/utils/health.ts b/utils/health.ts index 4b7eec1..bf2664d 100644 --- a/utils/health.ts +++ b/utils/health.ts @@ -77,21 +77,22 @@ export async function ensureHealthPermissions(): Promise { return new Promise((resolve) => { console.log('开始初始化HealthKit...'); - AppleHealthKit.initHealthKit(PERMISSIONS, (error) => { - if (error) { - console.error('HealthKit初始化失败:', error); - // 常见错误处理 - if (typeof error === 'string') { - if (error.includes('not available')) { - console.warn('HealthKit不可用 - 可能在模拟器上运行或非iOS设备'); - } - } - resolve(false); - return; - } - console.log('HealthKit初始化成功'); - resolve(true); - }); + resolve(true) + // AppleHealthKit.initHealthKit(PERMISSIONS, (error) => { + // if (error) { + // console.error('HealthKit初始化失败:', error); + // // 常见错误处理 + // if (typeof error === 'string') { + // if (error.includes('not available')) { + // console.warn('HealthKit不可用 - 可能在模拟器上运行或非iOS设备'); + // } + // } + // resolve(false); + // return; + // } + // console.log('HealthKit初始化成功'); + // resolve(true); + // }); }); } @@ -445,7 +446,7 @@ async function fetchSleepDuration(date: Date): Promise { return new Promise((resolve) => { // 使用睡眠专用的日期范围,包含前一天晚上的睡眠数据 const sleepOptions = createSleepDateRange(date); - + AppleHealthKit.getSleepSamples(sleepOptions, (err, res) => { if (err) { logError('睡眠数据', err); @@ -456,23 +457,23 @@ async function fetchSleepDuration(date: Date): Promise { 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') || + const isMainSleepPeriod = endDate.isSame(targetDate, 'day') || (startDate.isBefore(targetDate, 'day') && endDate.isAfter(targetDate.startOf('day'))); - + return isMainSleepPeriod; }); - + resolve(calculateSleepDuration(filteredSamples)); }); });