import {
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';
import React, { useCallback, useEffect, useState } from 'react';
import {
ActivityIndicator,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import { InfoModal, type SleepDetailData } from '@/components/sleep/InfoModal';
import { SleepStagesInfoModal } from '@/components/sleep/SleepStagesInfoModal';
import { SleepStageTimeline } from '@/components/sleep/SleepStageTimeline';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
// 简化的睡眠阶段图表组件
const SleepStageChart = ({
sleepData,
onInfoPress
}: {
sleepData: SleepDetailData;
onInfoPress: () => void;
}) => {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
// 使用真实的睡眠阶段数据,如果没有则使用默认数据
const stages = sleepData.sleepStages.length > 0
? sleepData.sleepStages
.filter(stage => stage.percentage > 0) // 只显示有数据的阶段
.map(stage => ({
stage: stage.stage,
percentage: stage.percentage,
duration: stage.duration
}))
: [
{ stage: SleepStage.Awake, percentage: 1, duration: 3 },
{ stage: SleepStage.REM, percentage: 20, duration: 89 },
{ stage: SleepStage.Core, percentage: 67, duration: 295 },
{ stage: SleepStage.Deep, percentage: 12, duration: 51 }
];
return (
阶段分析
{/* 入睡时间和起床时间显示 */}
入睡时间
{sleepData.bedtime ? formatTime(sleepData.bedtime) : '--:--'}
起床时间
{sleepData.wakeupTime ? formatTime(sleepData.wakeupTime) : '--:--'}
{/* 简化的睡眠阶段条 */}
{stages.map((stageData, index) => {
const color = getSleepStageColor(stageData.stage);
// 确保最小宽度,避免清醒阶段等小比例的阶段完全不可见
const flexValue = Math.max(stageData.percentage || 1, 3);
return (
);
})}
{/* 图例 */}
清醒时间
快速眼动
核心睡眠
深度睡眠
);
};
// SleepGradeCard 组件现在在 InfoModal 组件内部
// SleepStagesInfoModal 组件现在从独立文件导入
// InfoModal 组件现在从独立文件导入
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
});
const loadSleepData = useCallback(async () => {
try {
setLoading(true);
console.log('开始加载睡眠数据...');
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]);
useEffect(() => {
loadSleepData();
}, [loadSleepData]);
if (loading) {
return (
加载睡眠数据中...
);
}
// 如果没有数据,使用默认数据结构
const displayData: CompleteSleepData = 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 })}
/> */}
{/* 苹果健康风格的睡眠阶段时间轴图表 */}
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 > 100 && (
原始睡眠数据 ({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: SleepStage) => {
switch (value) {
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;
}
};
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 as SleepDetailData}
/>
)}
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,
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 和 Grade Cards 样式已移动到独立组件中
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 样式已移动到独立组件中
// 睡眠时间标签样式
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',
},
});