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, { useCallback, useEffect, useState } from 'react'; import { ActivityIndicator, Animated, Modal, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; 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; }; // 睡眠详情数据类型 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, 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 ( ); })} {/* 图例 */} 清醒时间 快速眼动 核心睡眠 深度睡眠 ); }; // Sleep Grade Component 睡眠等级组件 const SleepGradeCard = ({ icon, grade, range, isActive = false }: { icon: string; grade: string; range: string; isActive?: boolean; }) => { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const getGradeColor = (grade: string) => { switch (grade) { case '低': case '较差': return { bg: '#FECACA', text: '#DC2626' }; case '正常': case '一般': return { bg: '#D1FAE5', text: '#065F46' }; case '良好': return { bg: '#D1FAE5', text: '#065F46' }; case '优秀': return { bg: '#FEF3C7', text: '#92400E' }; default: return { bg: colorTokens.pageBackgroundEmphasis, text: colorTokens.textSecondary }; } }; const colors = getGradeColor(grade); return ( {grade} {range} ); }; // Sleep Stages Info Modal 组件 const SleepStagesInfoModal = ({ visible, onClose }: { visible: boolean; onClose: () => void; }) => { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const slideAnim = useState(new Animated.Value(0))[0]; React.useEffect(() => { if (visible) { slideAnim.setValue(0); Animated.spring(slideAnim, { toValue: 1, useNativeDriver: true, tension: 100, friction: 8, }).start(); } else { Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, tension: 100, friction: 8, }).start(); } }, [visible]); const translateY = slideAnim.interpolate({ inputRange: [0, 1], outputRange: [300, 0], }); const opacity = slideAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], }); return ( 了解你的睡眠阶段 人们对睡眠阶段和睡眠质量有许多误解。有些人可能需要更多深度睡眠,其他人则不然。科学家和医生仍在探索不同睡眠阶段的作用及其对身体的影响。通过跟踪睡眠阶段并留意每天清晨的感受,你或许能深入了解自己的睡眠。 {/* 清醒时间 */} 清醒时间 一次睡眠期间,你可能会醒来几次。偶尔醒来很正常。可能你会立刻再次入睡,并不记得曾在夜间醒来。 {/* 快速动眼睡眠 */} 快速动眼睡眠 这一睡眠阶段可能对学习和记忆产生一定影响。在此阶段,你的肌肉最为放松,眼球也会快速左右移动。这也是你大多数梦境出现的阶段。 {/* 核心睡眠 */} 核心睡眠 这一阶段有时也称为浅睡期,与其他阶段一样重要。此阶段通常占据你每晚大部分的睡眠时间。对于认知至关重要的脑电波会在这一阶段产生。 {/* 深度睡眠 */} 深度睡眠 因为脑电波的特征,这一阶段也称为慢波睡眠。在此阶段,身体组织得到修复,并释放重要荷尔蒙。它通常出现在睡眠的前半段,且持续时间较长。深度睡眠期间,身体非常放松,因此相较于其他阶段,你可能更难在此阶段醒来。 ); }; // Info Modal 组件 const InfoModal = ({ visible, onClose, title, type, sleepData }: { visible: boolean; onClose: () => void; title: string; type: 'sleep-time' | 'sleep-quality'; sleepData: SleepDetailData; }) => { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const slideAnim = useState(new Animated.Value(0))[0]; React.useEffect(() => { if (visible) { // 重置动画值确保每次打开都有动画 slideAnim.setValue(0); Animated.spring(slideAnim, { toValue: 1, useNativeDriver: true, tension: 100, friction: 8, }).start(); } else { Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, tension: 100, friction: 8, }).start(); } }, [visible]); const translateY = slideAnim.interpolate({ inputRange: [0, 1], outputRange: [300, 0], }); const opacity = slideAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], }); // 根据实际睡眠时间计算等级 const getSleepTimeGrade = (totalSleepMinutes: number) => { const hours = totalSleepMinutes / 60; if (hours < 6) return 0; // 低 if ((hours >= 6 && hours < 7) || hours > 9) return 1; // 正常 if (hours >= 7 && hours < 8) return 2; // 良好 if (hours >= 8 && hours <= 9) return 3; // 优秀 return 1; // 默认正常 }; // 根据实际睡眠质量百分比计算等级 const getSleepQualityGrade = (qualityPercentage: number) => { if (qualityPercentage < 55) return 0; // 较差 if (qualityPercentage < 70) return 1; // 一般 if (qualityPercentage < 85) return 2; // 良好 return 3; // 优秀 }; const currentSleepTimeGrade = getSleepTimeGrade(sleepData.totalSleepTime || 443); // 默认7h23m const currentSleepQualityGrade = getSleepQualityGrade(sleepData.sleepQualityPercentage || 94); // 默认94% const sleepTimeGrades = [ { icon: 'alert-circle-outline', grade: '低', range: '< 6h', isActive: currentSleepTimeGrade === 0 }, { icon: 'checkmark-circle-outline', grade: '正常', range: '6h - 7h or > 9h', isActive: currentSleepTimeGrade === 1 }, { icon: 'checkmark-circle', grade: '良好', range: '7h - 8h', isActive: currentSleepTimeGrade === 2 }, { icon: 'star', grade: '优秀', range: '8h - 9h', isActive: currentSleepTimeGrade === 3 }, ]; const sleepQualityGrades = [ { icon: 'alert-circle-outline', grade: '较差', range: '< 55%', isActive: currentSleepQualityGrade === 0 }, { icon: 'checkmark-circle-outline', grade: '一般', range: '55% - 69%', isActive: currentSleepQualityGrade === 1 }, { icon: 'checkmark-circle', grade: '良好', range: '70% - 84%', isActive: currentSleepQualityGrade === 2 }, { icon: 'star', grade: '优秀', range: '85% - 100%', isActive: currentSleepQualityGrade === 3 }, ]; const currentGrades = type === 'sleep-time' ? sleepTimeGrades : sleepQualityGrades; const getDescription = () => { if (type === 'sleep-time') { return '睡眠最重要 - 它占据了你睡眠得分的一半以上。长时间的睡眠可以减少睡眠债务,但是规律的睡眠时间对于高质量的休息至关重要。'; } else { return '睡眠质量综合评估您的睡眠效率、深度睡眠时长、REM睡眠比例等多个指标。高质量的睡眠不仅仅取决于时长,还包括睡眠的连续性和各睡眠阶段的平衡。'; } }; return ( {title} {/* 等级卡片区域 */} {currentGrades.map((grade, index) => ( ))} {getDescription()} ); }; export default function SleepDetailScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const [sleepData, setSleepData] = useState(null); const [loading, setLoading] = useState(true); const [selectedDate] = useState(dayjs().toDate()); const [infoModal, setInfoModal] = useState<{ visible: boolean; title: string; type: 'sleep-time' | 'sleep-quality' | null }>({ visible: false, title: '', type: null }); const [sleepStagesModal, setSleepStagesModal] = useState({ visible: false }); // 使用 HealthKit 权限 hook const [authorizationStatus, requestAuthorization] = useHealthkitAuthorization([ 'HKCategoryTypeIdentifierSleepAnalysis', 'HKQuantityTypeIdentifierHeartRate' ]); const loadSleepData = useCallback(async () => { try { setLoading(true); // 如果需要请求权限,先请求权限 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); setSleepData(data); } catch (error) { console.error('加载睡眠数据失败:', error); } finally { setLoading(false); } }, [selectedDate, authorizationStatus, requestAuthorization]); useEffect(() => { loadSleepData(); }, [loadSleepData]); if (loading) { return ( 加载睡眠数据中... ); } // 如果没有数据,使用默认数据结构 const displayData: SleepDetailData = sleepData || { sleepScore: 0, totalSleepTime: 0, sleepQualityPercentage: 0, bedtime: '', wakeupTime: '', timeInBed: 0, sleepStages: [], rawSleepSamples: [], // 添加空的原始睡眠样本数据 averageHeartRate: null, sleepHeartRateData: [], sleepEfficiency: 0, qualityDescription: '暂无睡眠数据', recommendation: '请确保在真实iOS设备上运行并授权访问健康数据,或等待有睡眠数据后再查看。' }; return ( {/* 背景渐变 */} {/* 顶部导航 */} router.back()} withSafeTop={true} transparent={true} /> {/* 睡眠得分圆形显示 */} {displayData.sleepScore} 睡眠得分 {/* 睡眠质量描述 */} {displayData.qualityDescription} {/* 建议文本 */} {displayData.recommendation} {/* 睡眠统计卡片 */} 睡眠时间 setInfoModal({ visible: true, title: '睡眠时间', type: 'sleep-time' })} > {displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '7h 23m'} ✓ 良好 睡眠质量 setInfoModal({ visible: true, title: '睡眠质量', type: 'sleep-quality' })} > {displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '94%'} ★ 优秀 {/* 睡眠阶段图表 */} setSleepStagesModal({ visible: true })} /> {/* 睡眠阶段统计 - 2x2网格布局 */} {/* 使用真实数据或默认数据,确保包含所有4个阶段 */} {(() => { let stagesToDisplay; if (displayData.sleepStages.length > 0) { // 使用真实数据,确保所有阶段都存在 const existingStages = new Map(displayData.sleepStages.map(s => [s.stage, s])); stagesToDisplay = [ existingStages.get(SleepStage.Awake) || { stage: SleepStage.Awake, duration: 0, percentage: 0, quality: 'good' as any }, existingStages.get(SleepStage.REM) || { stage: SleepStage.REM, duration: 0, percentage: 0, quality: 'good' as any }, existingStages.get(SleepStage.Core) || { stage: SleepStage.Core, duration: 0, percentage: 0, quality: 'good' as any }, existingStages.get(SleepStage.Deep) || { stage: SleepStage.Deep, duration: 0, percentage: 0, quality: 'good' as any } ]; } else { // 使用默认数据 stagesToDisplay = [ { stage: SleepStage.Awake, duration: 3, percentage: 1, quality: 'good' as any }, { stage: SleepStage.REM, duration: 89, percentage: 20, quality: 'good' as any }, { stage: SleepStage.Core, duration: 295, percentage: 67, quality: 'good' as any }, { stage: SleepStage.Deep, duration: 51, percentage: 12, quality: 'poor' as any } ]; } return stagesToDisplay; })().map((stageData, index) => { const getStageName = (stage: SleepStage) => { switch (stage) { case SleepStage.Awake: return '清醒时间'; case SleepStage.REM: return '快速眼动'; case SleepStage.Core: return '核心睡眠'; case SleepStage.Deep: return '深度睡眠'; default: return '未知'; } }; const getQualityDisplay = (quality: any) => { switch (quality) { case 'excellent': return { text: '★ 优秀', color: '#10B981', bgColor: '#D1FAE5', progressColor: '#10B981', progressWidth: '100%' }; case 'good': return { text: '✓ 良好', color: '#065F46', bgColor: '#D1FAE5', progressColor: '#10B981', progressWidth: '85%' }; case 'fair': return { text: '○ 一般', color: '#92400E', bgColor: '#FEF3C7', progressColor: '#F59E0B', progressWidth: '65%' }; case 'poor': return { text: '⚠ 低', color: '#DC2626', bgColor: '#FECACA', progressColor: '#F59E0B', progressWidth: '45%' }; default: return { text: '✓ 正常', color: '#065F46', bgColor: '#D1FAE5', progressColor: '#10B981', progressWidth: '75%' }; } }; const qualityInfo = getQualityDisplay(stageData.quality); return ( {getStageName(stageData.stage)} {formatSleepTime(stageData.duration)} 占总体睡眠的 {stageData.percentage}% {qualityInfo.text} ); })} {/* Raw Sleep Samples List - 显示所有原始睡眠数据 */} {sleepData && sleepData.rawSleepSamples && sleepData.rawSleepSamples.length > 1 && ( 原始睡眠数据 ({sleepData.rawSleepSamples.length} 条记录) 查看数据间隔和可能的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(); gapMinutes = (currentStartTime - prevEndTime) / (1000 * 60); hasGap = gapMinutes > 1; // 大于1分钟视为有间隔 } 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 'HKCategoryValueSleepAnalysisAsleepREM': return 'REM睡眠'; case 'HKCategoryValueSleepAnalysisAsleepUnspecified': 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 ( {/* 显示数据间隔 */} {hasGap && ( 数据间隔: {Math.round(gapMinutes)}分钟 )} {/* 睡眠样本条目 */} {getStageName(sample.value)} {duration}分钟 {startTime} - {endTime} #{index + 1} ); })} )} {infoModal.type && ( setInfoModal({ ...infoModal, visible: false })} title={infoModal.title} type={infoModal.type} sleepData={displayData} /> )} setSleepStagesModal({ visible: false })} /> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#F8FAFC', }, gradientBackground: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, scrollView: { flex: 1, }, scrollContent: { paddingHorizontal: 20, paddingBottom: 40, }, scoreContainer: { alignItems: 'center', }, circularProgressContainer: { position: 'relative', alignItems: 'center', justifyContent: 'center', }, scoreTextContainer: { alignItems: 'center', justifyContent: 'center', }, scoreNumber: { fontSize: 48, fontWeight: '800', color: '#1F2937', lineHeight: 48, }, scoreLabel: { fontSize: 14, color: '#6B7280', marginTop: 4, }, qualityDescription: { fontSize: 18, fontWeight: '600', color: '#1F2937', textAlign: 'center', marginBottom: 16, lineHeight: 24, }, recommendationText: { fontSize: 14, color: '#6B7280', textAlign: 'center', lineHeight: 20, marginBottom: 32, paddingHorizontal: 16, }, statsContainer: { flexDirection: 'row', gap: 12, marginBottom: 32, paddingHorizontal: 4, }, newStatCard: { flex: 1, borderRadius: 20, padding: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.08, shadowRadius: 12, elevation: 4, borderWidth: 1, borderColor: 'rgba(0, 0, 0, 0.06)', }, statCardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8, }, statCardLeftGroup: { flexDirection: 'row', alignItems: 'center', gap: 8, }, statCardIcon: { width: 20, height: 20, borderRadius: 4, backgroundColor: 'rgba(120, 120, 128, 0.08)', alignItems: 'center', justifyContent: 'center', alignSelf: 'center', }, infoButton: { padding: 4, alignSelf: 'center', }, statCard: { flex: 1, backgroundColor: 'rgba(255, 255, 255, 0.9)', borderRadius: 16, padding: 16, alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, }, statIcon: { fontSize: 18, }, statLabel: { fontSize: 12, fontWeight: '500', letterSpacing: 0.2, alignSelf: 'center', }, newStatValue: { fontSize: 20, fontWeight: '600', marginBottom: 12, letterSpacing: -0.5, }, qualityBadge: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, alignSelf: 'flex-start', }, goodQualityBadge: { backgroundColor: '#D1FAE5', }, excellentQualityBadge: { backgroundColor: '#FEF3C7', }, qualityBadgeText: { fontSize: 12, fontWeight: '600', letterSpacing: 0.1, }, goodQualityText: { color: '#065F46', }, excellentQualityText: { color: '#92400E', }, statValue: { fontSize: 18, fontWeight: '700', color: '#1F2937', marginBottom: 4, }, statQuality: { fontSize: 12, color: '#10B981', fontWeight: '500', }, chartContainer: { backgroundColor: 'rgba(255, 255, 255, 0.9)', borderRadius: 16, padding: 16, marginBottom: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, }, chartHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, chartTimeLabel: { alignItems: 'center', }, chartTimeText: { fontSize: 12, color: '#6B7280', fontWeight: '500', }, chartHeartRate: { alignItems: 'center', }, chartHeartRateText: { fontSize: 12, color: '#EF4444', fontWeight: '600', }, chartBars: { flexDirection: 'row', alignItems: 'flex-end', height: 120, gap: 2, }, chartBar: { borderRadius: 2, minHeight: 8, }, chartTimeScale: { flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 4, marginTop: 8, }, chartTimeScaleText: { fontSize: 10, color: '#9CA3AF', textAlign: 'center', }, layeredChartContainer: { position: 'relative', marginVertical: 16, }, sleepBlock: { borderRadius: 2, borderWidth: 0.5, borderColor: 'rgba(255, 255, 255, 0.2)', }, baselineLine: { height: 1, backgroundColor: '#E5E7EB', position: 'absolute', }, stagesContainer: { backgroundColor: 'rgba(255, 255, 255, 0.9)', borderRadius: 16, padding: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, }, stageRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F3F4F6', }, stageInfo: { flexDirection: 'row', alignItems: 'center', flex: 1, }, stageColorDot: { width: 12, height: 12, borderRadius: 6, marginRight: 12, }, stageName: { fontSize: 14, color: '#374151', fontWeight: '500', }, stageStats: { alignItems: 'flex-end', }, stagePercentage: { fontSize: 16, fontWeight: '700', color: '#1F2937', }, stageDuration: { fontSize: 12, color: '#6B7280', marginTop: 2, }, stageQuality: { fontSize: 11, fontWeight: '600', marginTop: 2, }, loadingContainer: { justifyContent: 'center', alignItems: 'center', }, loadingText: { fontSize: 16, color: '#6B7280', marginTop: 16, }, errorText: { fontSize: 16, color: '#6B7280', marginBottom: 16, }, retryButton: { backgroundColor: Colors.light.primary, borderRadius: 8, paddingHorizontal: 24, paddingVertical: 12, }, retryButtonText: { color: '#FFFFFF', fontSize: 14, fontWeight: '600', }, noDataContainer: { alignItems: 'center', paddingVertical: 24, }, noDataText: { fontSize: 14, color: '#9CA3AF', fontStyle: 'italic', }, // Info Modal 样式 modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'flex-end', }, infoModalContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, paddingTop: 12, paddingHorizontal: 20, paddingBottom: 34, minHeight: 200, shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.1, shadowRadius: 16, elevation: 8, }, modalHandle: { width: 36, height: 4, backgroundColor: '#D1D5DB', borderRadius: 2, alignSelf: 'center', marginBottom: 20, }, infoModalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, infoModalTitle: { fontSize: 18, fontWeight: '700', letterSpacing: -0.3, }, infoModalCloseButton: { padding: 4, }, infoModalText: { fontSize: 15, lineHeight: 22, letterSpacing: -0.1, }, // Grade Cards 样式 gradesContainer: { marginBottom: 20, gap: 8, }, gradeCard: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, borderRadius: 12, borderWidth: 1, }, gradeCardLeft: { flexDirection: 'row', alignItems: 'center', gap: 8, }, gradeText: { fontSize: 16, fontWeight: '600', letterSpacing: -0.2, }, gradeRange: { fontSize: 16, fontWeight: '700', letterSpacing: -0.3, }, mockDataToggle: { paddingHorizontal: 12, paddingVertical: 6, backgroundColor: 'rgba(255, 255, 255, 0.2)', borderRadius: 16, borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.3)', }, mockDataToggleText: { fontSize: 12, fontWeight: '600', }, // 简化睡眠阶段图表样式 simplifiedChartContainer: { backgroundColor: 'rgba(255, 255, 255, 0.9)', borderRadius: 16, padding: 16, marginBottom: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, }, chartTitleContainer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, chartTitle: { fontSize: 16, fontWeight: '600', color: '#1F2937', }, chartInfoButton: { padding: 4, }, simplifiedChartBar: { flexDirection: 'row', height: 24, borderRadius: 12, overflow: 'hidden', marginBottom: 16, }, stageSegment: { height: '100%', }, chartLegend: { gap: 8, }, legendRow: { flexDirection: 'row', justifyContent: 'center', }, legendItem: { flexDirection: 'row', alignItems: 'center', flex: 1, }, legendDot: { width: 8, height: 8, borderRadius: 4, marginRight: 6, }, legendText: { fontSize: 12, color: '#6B7280', fontWeight: '500', }, // 睡眠阶段卡片网格样式 stagesGridContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, paddingHorizontal: 4, }, stageCard: { width: '48%', borderRadius: 20, padding: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.08, shadowRadius: 12, elevation: 4, borderWidth: 1, borderColor: 'rgba(0, 0, 0, 0.06)', }, stageCardTitle: { fontSize: 14, fontWeight: '500', marginBottom: 8, }, stageCardValue: { fontSize: 24, fontWeight: '700', lineHeight: 28, marginBottom: 4, }, stageCardPercentage: { fontSize: 12, marginBottom: 12, }, stageCardQuality: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, alignSelf: 'flex-start', }, normalQuality: { backgroundColor: '#D1FAE5', }, lowQuality: { backgroundColor: '#FECACA', }, stageCardQualityText: { fontSize: 12, fontWeight: '600', }, normalQualityText: { color: '#065F46', }, lowQualityText: { color: '#DC2626', }, stageCardProgress: { height: 6, backgroundColor: '#E5E7EB', borderRadius: 3, overflow: 'hidden', }, stageCardProgressBar: { height: '100%', borderRadius: 3, }, // Sleep Stages Modal 样式 sleepStagesModalContent: { borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '80%', shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.1, shadowRadius: 16, elevation: 8, }, sleepStagesModalInner: { flex: 1, paddingTop: 12, paddingHorizontal: 20, paddingBottom: 34, }, sleepStagesModalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20, }, sleepStagesModalTitle: { fontSize: 20, fontWeight: '700', letterSpacing: -0.4, }, sleepStagesScrollView: { flex: 1, }, sleepStagesScrollContent: { paddingBottom: 40, }, sleepStagesDescription: { fontSize: 15, lineHeight: 22, letterSpacing: -0.1, marginBottom: 24, }, sleepStageInfoCard: { marginBottom: 20, }, sleepStageInfoHeader: { paddingBottom: 12, marginBottom: 12, }, sleepStageInfoTitleContainer: { flexDirection: 'row', alignItems: 'center', gap: 12, }, sleepStageDot: { width: 12, height: 12, borderRadius: 6, }, sleepStageInfoTitle: { fontSize: 18, fontWeight: '600', letterSpacing: -0.2, }, sleepStageInfoContent: { fontSize: 15, lineHeight: 22, letterSpacing: -0.1, }, // 睡眠时间标签样式 sleepTimeLabels: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 16, }, sleepTimeLabel: { alignItems: 'center', }, sleepTimeText: { fontSize: 12, fontWeight: '500', marginBottom: 4, }, sleepTimeValue: { fontSize: 16, fontWeight: '700', letterSpacing: -0.2, }, // 调试信息样式 debugContainer: { marginHorizontal: 20, marginBottom: 20, padding: 16, borderRadius: 12, borderWidth: 1, borderColor: 'rgba(0, 0, 0, 0.1)', }, debugTitle: { fontSize: 14, fontWeight: '600', marginBottom: 8, }, debugText: { fontSize: 12, lineHeight: 16, marginBottom: 4, }, // Raw Sleep Samples List 样式 rawSamplesContainer: { borderRadius: 16, padding: 16, marginBottom: 24, marginHorizontal: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, }, rawSamplesHeader: { marginBottom: 16, }, rawSamplesTitle: { fontSize: 16, fontWeight: '600', marginBottom: 4, }, rawSamplesSubtitle: { fontSize: 12, fontWeight: '500', }, rawSamplesScrollView: { maxHeight: 400, // 限制高度,避免列表过长 }, rawSampleItem: { paddingVertical: 12, paddingHorizontal: 16, borderLeftWidth: 3, borderLeftColor: 'transparent', marginBottom: 8, borderRadius: 8, backgroundColor: 'rgba(248, 250, 252, 0.5)', }, sampleHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4, }, sampleLeft: { flexDirection: 'row', alignItems: 'center', flex: 1, }, stageDot: { width: 8, height: 8, borderRadius: 4, marginRight: 8, }, sampleStage: { fontSize: 14, fontWeight: '500', flex: 1, }, sampleDuration: { fontSize: 12, fontWeight: '600', }, sampleTimeRange: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, sampleTime: { fontSize: 12, }, sampleIndex: { fontSize: 10, fontWeight: '500', }, gapIndicator: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 8, paddingHorizontal: 12, marginVertical: 4, borderRadius: 8, gap: 6, }, gapText: { fontSize: 12, fontWeight: '600', }, });