From 98176ee988a8afdf2fb6a7fc8d13de0aee6299ca Mon Sep 17 00:00:00 2001 From: richarjiang Date: Tue, 9 Sep 2025 23:16:54 +0800 Subject: [PATCH] Refactor iOS dependencies and update HealthKit integration - Removed NitroModules and ReactNativeHealthkit from Podfile.lock and package files. - Updated Info.plist to increment app version from 2 to 3. - Refactored background task manager to define background tasks within the class. - Added new utility file for sleep data management, including fetching sleep samples, calculating sleep statistics, and generating sleep quality scores. --- app.json | 5 +- app/sleep-detail.tsx | 498 ++------------------------- ios/Podfile.lock | 57 ---- ios/digitalpilates/Info.plist | 2 +- package-lock.json | 28 -- package.json | 4 +- services/backgroundTaskManager.ts | 25 +- utils/sleepHealthKit.ts | 537 ++++++++++++++++++++++++++++++ 8 files changed, 584 insertions(+), 572 deletions(-) create mode 100644 utils/sleepHealthKit.ts diff --git a/app.json b/app.json index 3c2a98a..c2b7d98 100644 --- a/app.json +++ b/app.json @@ -84,10 +84,7 @@ } ], [ - "expo-background-task", - { - "minimumInterval": 15 - } + "expo-background-task" ] ], "experiments": { diff --git a/app/sleep-detail.tsx b/app/sleep-detail.tsx index 29f49ae..c02e406 100644 --- a/app/sleep-detail.tsx +++ b/app/sleep-detail.tsx @@ -1,10 +1,12 @@ -import { Ionicons } from '@expo/vector-icons'; import { - AuthorizationRequestStatus, - queryCategorySamplesWithAnchor, - queryQuantitySamplesWithAnchor, - useHealthkitAuthorization -} from '@kingstinct/react-native-healthkit'; + fetchCompleteSleepData, + formatSleepTime, + formatTime, + getSleepStageColor, + SleepStage, + type CompleteSleepData +} from '@/utils/sleepHealthKit'; +import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { router } from 'expo-router'; @@ -18,425 +20,13 @@ import { View } from 'react-native'; -import { HeaderBar } from '@/components/ui/HeaderBar'; import { InfoModal, type SleepDetailData } from '@/components/sleep/InfoModal'; import { SleepStagesInfoModal } from '@/components/sleep/SleepStagesInfoModal'; +import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; -// 睡眠阶段枚举 -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; -}; - -// 睡眠详情数据类型从 InfoModal 组件导入,但需要扩展以包含其他字段 -type ExtendedSleepDetailData = SleepDetailData & { - sleepStages: SleepStageStats[]; - rawSleepSamples: SleepSample[]; - sleepHeartRateData: HeartRateData[]; -}; - -// 工具函数 -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: ExtendedSleepDetailData = { - 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 = ({ @@ -552,7 +142,7 @@ const SleepStageChart = ({ export default function SleepDetailScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; - const [sleepData, setSleepData] = useState(null); + const [sleepData, setSleepData] = useState(null); const [loading, setLoading] = useState(true); const [selectedDate] = useState(dayjs().toDate()); @@ -566,39 +156,26 @@ export default function SleepDetailScreen() { visible: false }); - // 使用 HealthKit 权限 hook - const [authorizationStatus, requestAuthorization] = useHealthkitAuthorization([ - 'HKCategoryTypeIdentifierSleepAnalysis', - 'HKQuantityTypeIdentifierHeartRate' - ]); - const loadSleepData = useCallback(async () => { try { setLoading(true); + console.log('开始加载睡眠数据...'); - // 如果需要请求权限,先请求权限 - if (authorizationStatus === AuthorizationRequestStatus.shouldRequest) { - console.log('请求 HealthKit 权限..., 当前状态:', authorizationStatus); - try { - await requestAuthorization(); - // 请求权限后,等待状态更新 - return; - } catch (permissionError) { - console.error('权限请求失败:', permissionError); - } - } - - // 如果权限不需要或已经处理,尝试获取数据 - console.log('尝试获取睡眠数据,权限状态:', authorizationStatus); - const data = await fetchSleepDetailData(selectedDate); + const data = await fetchCompleteSleepData(selectedDate); setSleepData(data); + if (data) { + console.log('睡眠数据加载成功,得分:', data.sleepScore); + } else { + console.log('未找到睡眠数据'); + } + } catch (error) { console.error('加载睡眠数据失败:', error); } finally { setLoading(false); } - }, [selectedDate, authorizationStatus, requestAuthorization]); + }, [selectedDate]); useEffect(() => { loadSleepData(); @@ -614,7 +191,7 @@ export default function SleepDetailScreen() { } // 如果没有数据,使用默认数据结构 - const displayData: ExtendedSleepDetailData = sleepData || { + const displayData: CompleteSleepData = sleepData || { sleepScore: 0, totalSleepTime: 0, sleepQualityPercentage: 0, @@ -622,7 +199,7 @@ export default function SleepDetailScreen() { wakeupTime: '', timeInBed: 0, sleepStages: [], - rawSleepSamples: [], // 添加空的原始睡眠样本数据 + rawSleepSamples: [], averageHeartRate: null, sleepHeartRateData: [], sleepEfficiency: 0, @@ -831,31 +408,18 @@ export default function SleepDetailScreen() { const duration = Math.round((new Date(sample.endDate).getTime() - new Date(sample.startDate).getTime()) / (1000 * 60)); // 获取睡眠阶段中文名称 - const getStageName = (value: string) => { + const getStageName = (value: SleepStage) => { switch (value) { - case 'HKCategoryValueSleepAnalysisInBed': return '在床上'; - case 'HKCategoryValueSleepAnalysisAwake': return '清醒'; - case 'HKCategoryValueSleepAnalysisAsleepCore': return '核心睡眠'; - case 'HKCategoryValueSleepAnalysisAsleepDeep': return '深度睡眠'; - case 'HKCategoryValueSleepAnalysisAsleepREM': return 'REM睡眠'; - case 'HKCategoryValueSleepAnalysisAsleepUnspecified': return '未指定睡眠'; + case SleepStage.InBed: return '在床上'; + case SleepStage.Awake: return '清醒'; + case SleepStage.Core: return '核心睡眠'; + case SleepStage.Deep: return '深度睡眠'; + case SleepStage.REM: return 'REM睡眠'; + case SleepStage.Asleep: return '未指定睡眠'; default: return value; } }; - // 获取状态颜色 - const getStageColor = (value: string) => { - switch (value) { - case 'HKCategoryValueSleepAnalysisInBed': return '#9CA3AF'; - case 'HKCategoryValueSleepAnalysisAwake': return '#F59E0B'; - case 'HKCategoryValueSleepAnalysisAsleepCore': return '#8B5CF6'; - case 'HKCategoryValueSleepAnalysisAsleepDeep': return '#3B82F6'; - case 'HKCategoryValueSleepAnalysisAsleepREM': return '#EC4899'; - case 'HKCategoryValueSleepAnalysisAsleepUnspecified': return '#6B7280'; - default: return '#6B7280'; - } - }; - return ( {/* 显示数据间隔 */} @@ -875,7 +439,7 @@ export default function SleepDetailScreen() { @@ -910,7 +474,7 @@ export default function SleepDetailScreen() { onClose={() => setInfoModal({ ...infoModal, visible: false })} title={infoModal.title} type={infoModal.type} - sleepData={displayData} + sleepData={displayData as SleepDetailData} /> )} @@ -1012,8 +576,6 @@ const styles = StyleSheet.create({ statCardIcon: { width: 20, height: 20, - borderRadius: 4, - backgroundColor: 'rgba(120, 120, 128, 0.08)', alignItems: 'center', justifyContent: 'center', alignSelf: 'center', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fdfd3bb..cb2afde 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -153,30 +153,6 @@ 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): @@ -1767,31 +1743,6 @@ 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 @@ -2078,7 +2029,6 @@ 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`) @@ -2146,7 +2096,6 @@ 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`)" @@ -2249,8 +2198,6 @@ 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: @@ -2381,8 +2328,6 @@ 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: @@ -2452,7 +2397,6 @@ SPEC CHECKSUMS: libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 lottie-ios: a881093fab623c467d3bce374367755c272bdd59 lottie-react-native: 4969af5ac8f2ed2c562d35b6d3d9fe7d34a1add1 - NitroModules: c99da4ad8dd1f8cf270770fbcd5d7659743bc010 PurchasesHybridCommon: 62f852419aae7041792217593998f7ac3f8b567d QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798 QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7 @@ -2522,7 +2466,6 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: f3e842e6cb5a825b6918a74a38402ba1409411f8 ReactCodegen: 272c9bc1a8a917bf557bd9d032a4b3e181c6abfe ReactCommon: 7eb76fcd5133313d8c6a138a5c7dd89f80f189d5 - ReactNativeHealthkit: 1b94bc11acc67035a878c3b9baedb6977b39a835 RevenueCat: eb2aa042789d9c99ad5172bd96e28b96286d6ada RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9 RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f diff --git a/ios/digitalpilates/Info.plist b/ios/digitalpilates/Info.plist index c2263cc..d5f6333 100644 --- a/ios/digitalpilates/Info.plist +++ b/ios/digitalpilates/Info.plist @@ -37,7 +37,7 @@ CFBundleVersion - 2 + 3 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/package-lock.json b/package-lock.json index edc25d3..bf32659 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.0.2", "dependencies": { "@expo/vector-icons": "^14.1.0", - "@kingstinct/react-native-healthkit": "^10.1.0", "@react-native-async-storage/async-storage": "^2.2.0", "@react-native-community/datetimepicker": "^8.4.4", "@react-native-masked-view/masked-view": "^0.3.2", @@ -56,7 +55,6 @@ "react-native-image-viewing": "^0.2.2", "react-native-markdown-display": "^7.0.2", "react-native-modal-datetime-picker": "^18.0.0", - "react-native-nitro-modules": "^0.29.3", "react-native-popover-view": "^6.1.0", "react-native-purchases": "^9.2.2", "react-native-reanimated": "~3.17.4", @@ -2730,21 +2728,6 @@ "react-native": "*" } }, - "node_modules/@kingstinct/react-native-healthkit": { - "version": "10.1.0", - "resolved": "https://mirrors.tencent.com/npm/@kingstinct/react-native-healthkit/-/react-native-healthkit-10.1.0.tgz", - "integrity": "sha512-p6f3Uf4p6GXs+8xIc5NHu8DPnNJC9kxGvI+4qmgGk5U24hVZBZFAwFT53jkQMoIHZIoQmtuXJDp8jMJ7WzeZ+Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kingstinct" - }, - "peerDependencies": { - "react": "*", - "react-native": "*", - "react-native-nitro-modules": "*" - } - }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -11749,17 +11732,6 @@ "react-native": ">=0.65.0" } }, - "node_modules/react-native-nitro-modules": { - "version": "0.29.3", - "resolved": "https://mirrors.tencent.com/npm/react-native-nitro-modules/-/react-native-nitro-modules-0.29.3.tgz", - "integrity": "sha512-gGaCueHKaZSw2rlrKrPgMZE6O6qvsnTJwNysJgk4ZEHMwnVe6Auk5hc4+sJPQLOVd6o+HMHdVhVQhZZv1u19Eg==", - "hasInstallScript": true, - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-popover-view": { "version": "6.1.0", "resolved": "https://mirrors.tencent.com/npm/react-native-popover-view/-/react-native-popover-view-6.1.0.tgz", diff --git a/package.json b/package.json index 5232473..33bfcd8 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ }, "dependencies": { "@expo/vector-icons": "^14.1.0", - "@kingstinct/react-native-healthkit": "^10.1.0", "@react-native-async-storage/async-storage": "^2.2.0", "@react-native-community/datetimepicker": "^8.4.4", "@react-native-masked-view/masked-view": "^0.3.2", @@ -60,7 +59,6 @@ "react-native-image-viewing": "^0.2.2", "react-native-markdown-display": "^7.0.2", "react-native-modal-datetime-picker": "^18.0.0", - "react-native-nitro-modules": "^0.29.3", "react-native-popover-view": "^6.1.0", "react-native-purchases": "^9.2.2", "react-native-reanimated": "~3.17.4", @@ -82,4 +80,4 @@ "typescript": "~5.8.3" }, "private": true -} +} \ No newline at end of file diff --git a/services/backgroundTaskManager.ts b/services/backgroundTaskManager.ts index e3aaae1..39d8026 100644 --- a/services/backgroundTaskManager.ts +++ b/services/backgroundTaskManager.ts @@ -8,17 +8,6 @@ import { TaskManagerTaskBody } from 'expo-task-manager'; const BACKGROUND_TASK_IDENTIFIER = 'background-task'; -// 定义后台任务 -TaskManager.defineTask(BACKGROUND_TASK_IDENTIFIER, async (body: TaskManagerTaskBody) => { - try { - console.log('[BackgroundTask] 后台任务执行'); - await executeBackgroundTasks(); - return BackgroundTask.BackgroundTaskResult.Success; - } catch (error) { - console.error('[BackgroundTask] 任务执行失败:', error); - return BackgroundTask.BackgroundTaskResult.Failed; - } -}); // 检查通知权限 async function checkNotificationPermissions(): Promise { @@ -201,6 +190,20 @@ export class BackgroundTaskManager { } try { + + // 定义后台任务 + TaskManager.defineTask(BACKGROUND_TASK_IDENTIFIER, async (body: TaskManagerTaskBody) => { + try { + console.log('[BackgroundTask] 后台任务执行'); + await executeBackgroundTasks(); + return BackgroundTask.BackgroundTaskResult.Success; + } catch (error) { + console.error('[BackgroundTask] 任务执行失败:', error); + return BackgroundTask.BackgroundTaskResult.Failed; + } + }); + + // 注册后台任务 const status = await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER, { minimumInterval: 15, diff --git a/utils/sleepHealthKit.ts b/utils/sleepHealthKit.ts new file mode 100644 index 0000000..c781a6f --- /dev/null +++ b/utils/sleepHealthKit.ts @@ -0,0 +1,537 @@ +import dayjs from 'dayjs'; +import HealthKit from 'react-native-health'; + +// 睡眠阶段枚举 +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; +}; + +// 完整睡眠详情数据 +export type CompleteSleepData = { + 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; +}; + +// HealthKit 权限配置 +const permissions = { + permissions: { + read: [ + HealthKit.Constants.Permissions.SleepAnalysis, + HealthKit.Constants.Permissions.HeartRate, + ], + write: [], // 我们只读取数据,不写入 + }, +}; + +/** + * 初始化 HealthKit 权限 + */ +export const initializeHealthKit = (): Promise => { + return new Promise((resolve, reject) => { + HealthKit.initHealthKit(permissions, (error: string) => { + if (error) { + console.error('[HealthKit] 权限初始化失败:', error); + reject(new Error(error)); + } else { + console.log('[HealthKit] 权限初始化成功'); + resolve(true); + } + }); + }); +}; + +/** + * 创建睡眠日期范围 (从前一天 18:00 到当天 12:00) + */ +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() + }; +}; + +/** + * 映射 HealthKit 睡眠值到自定义枚举 + * react-native-health 库返回的睡眠分析值是字符串: + * "INBED" = InBed (在床时间) + * "ASLEEP" = Asleep (睡着但未分阶段) + * "AWAKE" = Awake (醒来时间) + * "CORE" = Core (核心睡眠/浅度睡眠) + * "DEEP" = Deep (深度睡眠) + * "REM" = REM (快速眼动睡眠) + */ +const mapHealthKitSleepValue = (value: string): SleepStage => { + switch (value) { + case 'INBED': + return SleepStage.InBed; + case 'ASLEEP': + return SleepStage.Asleep; + case 'AWAKE': + return SleepStage.Awake; + case 'CORE': + return SleepStage.Core; + case 'DEEP': + return SleepStage.Deep; + case 'REM': + return SleepStage.REM; + default: + console.warn(`[Sleep] 未识别的睡眠阶段值: ${value}`); + return SleepStage.Asleep; + } +}; + +/** + * 获取睡眠样本数据 + */ +export const fetchSleepSamples = async (date: Date): Promise => { + try { + const dateRange = createSleepDateRange(date); + console.log('[Sleep] 查询睡眠数据范围:', { + startDate: dayjs(dateRange.startDate).format('YYYY-MM-DD HH:mm:ss'), + endDate: dayjs(dateRange.endDate).format('YYYY-MM-DD HH:mm:ss') + }); + + const options = { + startDate: dateRange.startDate.toISOString(), + endDate: dateRange.endDate.toISOString(), + }; + + return new Promise((resolve) => { + HealthKit.getSleepSamples(options, (error: string, results: any[]) => { + if (error) { + console.error('[Sleep] 获取睡眠数据失败:', error); + resolve([]); // 返回空数组而非拒绝,以便于处理 + return; + } + + if (!results || results.length === 0) { + console.warn('[Sleep] 未找到睡眠数据'); + resolve([]); + return; + } + + console.log('[Sleep] 获取到原始睡眠样本:', results.length, '条'); + + // 过滤并转换数据格式 + const sleepSamples: SleepSample[] = results + .filter(sample => { + const sampleStart = new Date(sample.startDate).getTime(); + const sampleEnd = new Date(sample.endDate).getTime(); + const rangeStart = dateRange.startDate.getTime(); + const rangeEnd = dateRange.endDate.getTime(); + + return (sampleStart >= rangeStart && sampleStart < rangeEnd) || + (sampleStart < rangeEnd && sampleEnd > rangeStart); + }) + .map(sample => { + console.log('[Sleep] 原始睡眠样本:', { + startDate: sample.startDate, + endDate: sample.endDate, + value: sample.value, + sourceName: sample.sourceName + }); + + return { + startDate: sample.startDate, + endDate: sample.endDate, + value: mapHealthKitSleepValue(sample.value), + sourceName: sample.sourceName, + sourceId: sample.sourceId || sample.uuid + }; + }); + + console.log('[Sleep] 过滤后的睡眠样本:', sleepSamples.length, '条'); + resolve(sleepSamples); + }); + }); + + } catch (error) { + console.error('[Sleep] 获取睡眠样本失败:', error); + return []; + } +}; + +/** + * 获取睡眠期间心率数据 + */ +export const fetchSleepHeartRateData = async (bedtime: string, wakeupTime: string): Promise => { + try { + const options = { + startDate: bedtime, + endDate: wakeupTime, + }; + + return new Promise((resolve) => { + HealthKit.getHeartRateSamples(options, (error: string, results: any[]) => { + if (error) { + console.error('[Sleep] 获取心率数据失败:', error); + resolve([]); + return; + } + + if (!results || results.length === 0) { + console.log('[Sleep] 未找到心率数据'); + resolve([]); + return; + } + + const heartRateData: HeartRateData[] = results + .filter(sample => { + const sampleTime = new Date(sample.startDate).getTime(); + const bedtimeMs = new Date(bedtime).getTime(); + const wakeupTimeMs = new Date(wakeupTime).getTime(); + return sampleTime >= bedtimeMs && sampleTime <= wakeupTimeMs; + }) + .map(sample => ({ + timestamp: sample.startDate, + value: Math.round(sample.value) + })) + .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); + + console.log('[Sleep] 获取到睡眠心率数据:', heartRateData.length, '个样本'); + resolve(heartRateData); + }); + }); + + } catch (error) { + console.error('[Sleep] 获取睡眠心率数据失败:', error); + return []; + } +}; + +/** + * 计算睡眠阶段统计 + */ +export const calculateSleepStageStats = (samples: SleepSample[]): SleepStageStats[] => { + console.log('[Sleep] 开始计算睡眠阶段统计,原始样本数:', samples.length); + + const stageMap = new Map(); + + // 统计各阶段持续时间 + samples.forEach(sample => { + const startTime = dayjs(sample.startDate); + const endTime = dayjs(sample.endDate); + const duration = endTime.diff(startTime, 'minute'); + + console.log(`[Sleep] 阶段: ${sample.value}, 持续时间: ${duration}分钟`); + + const currentDuration = stageMap.get(sample.value) || 0; + stageMap.set(sample.value, currentDuration + duration); + }); + + console.log('[Sleep] 阶段时间统计:', Array.from(stageMap.entries())); + + // 计算实际睡眠时间(排除在床时间,但包含醒来时间) + const actualSleepTime = Array.from(stageMap.entries()) + .filter(([stage]) => stage !== SleepStage.InBed) + .reduce((total, [, duration]) => total + duration, 0); + + console.log('[Sleep] 实际睡眠时间(包含醒来):', actualSleepTime, '分钟'); + + 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.Asleep: + // 未分阶段的睡眠时间,按中等质量处理 + quality = SleepQuality.Fair; + 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 + }); + + console.log(`[Sleep] 阶段统计: ${stage}, 时长: ${duration}分钟, 百分比: ${Math.round(percentage)}%, 质量: ${quality}`); + }); + + const sortedStats = stats.sort((a, b) => b.duration - a.duration); + console.log('[Sleep] 最终睡眠阶段统计:', sortedStats); + + return sortedStats; +}; + +/** + * 计算睡眠得分 + */ +export const calculateSleepScore = ( + sleepStages: SleepStageStats[], + sleepEfficiency: number, + totalSleepTime: number +): number => { + let score = 0; + + // 睡眠时长得分 (30%) + const idealSleepHours = 8 * 60; + 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)); +}; + +/** + * 获取睡眠质量描述和建议 + */ +export 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: '你的睡眠质量需要严重关注。建议咨询医生或睡眠专家,检查是否有睡眠障碍,同时改善睡眠环境和习惯,避免睡前刺激性活动。' + }; + } +}; + +/** + * 主函数:获取完整的睡眠详情数据 + */ +export const fetchCompleteSleepData = async (date: Date): Promise => { + try { + console.log('[Sleep] 开始获取完整睡眠数据...', dayjs(date).format('YYYY-MM-DD')); + + // 确保 HealthKit 已初始化 + await initializeHealthKit(); + + // 获取睡眠样本 + const sleepSamples = await fetchSleepSamples(date); + + if (sleepSamples.length === 0) { + console.warn('[Sleep] 没有找到睡眠数据'); + return null; + } + + // 计算入睡和起床时间 + const sortedSamples = sleepSamples.sort((a, b) => + new Date(a.startDate).getTime() - new Date(b.startDate).getTime() + ); + + const bedtime = sortedSamples[0].startDate; + const wakeupTime = sortedSamples[sortedSamples.length - 1].endDate; + + console.log('[Sleep] 计算睡眠时间范围:'); + console.log('- 入睡时间:', dayjs(bedtime).format('YYYY-MM-DD HH:mm:ss')); + console.log('- 起床时间:', dayjs(wakeupTime).format('YYYY-MM-DD HH:mm:ss')); + + // 计算在床时间 + 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('[Sleep] 在床时间:', timeInBed, '分钟'); + } else { + timeInBed = dayjs(wakeupTime).diff(dayjs(bedtime), 'minute'); + console.log('[Sleep] 使用睡眠时间作为在床时间:', timeInBed, '分钟'); + } + + // 计算睡眠阶段统计 + const sleepStages = calculateSleepStageStats(sleepSamples); + + // 计算总睡眠时间(排除在床时间和醒来时间) + const actualSleepStages = sleepStages.filter(stage => + stage.stage !== SleepStage.InBed && stage.stage !== SleepStage.Awake + ); + const totalSleepTime = actualSleepStages.reduce((total, stage) => total + stage.duration, 0); + + // 重新计算睡眠效率 + const sleepEfficiency = timeInBed > 0 ? Math.round((totalSleepTime / timeInBed) * 100) : 0; + + console.log('[Sleep] 睡眠效率计算:'); + console.log('- 总睡眠时间(不含醒来):', totalSleepTime, '分钟'); + console.log('- 在床时间:', timeInBed, '分钟'); + console.log('- 睡眠效率:', sleepEfficiency, '%'); + + // 获取睡眠期间心率数据 + 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('[Sleep] 睡眠数据处理完成:'); + console.log('- 总睡眠时间:', totalSleepTime, '分钟'); + console.log('- 睡眠效率:', sleepEfficiency, '%'); + console.log('- 睡眠得分:', sleepScore); + + return { + sleepScore, + totalSleepTime, + sleepQualityPercentage: sleepScore, + bedtime, + wakeupTime, + timeInBed, + sleepStages, + rawSleepSamples: sleepSamples, + averageHeartRate, + sleepHeartRateData, + sleepEfficiency, + qualityDescription: qualityInfo.description, + recommendation: qualityInfo.recommendation + }; + + } catch (error) { + console.error('[Sleep] 获取完整睡眠数据失败:', error); + return null; + } +}; + +/** + * 工具函数:格式化睡眠时间显示 + */ +export 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`; + } +}; + +/** + * 工具函数:格式化时间显示 + */ +export const formatTime = (dateString: string): string => { + return dayjs(dateString).format('HH:mm'); +}; + +/** + * 工具函数:获取睡眠阶段颜色 + */ +export 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'; + } +}; \ No newline at end of file