import { ThemedView } from '@/components/ThemedView'; import { fetchCompleteSleepData, formatSleepTime, formatTime, getSleepStageColor, SleepStage, type CompleteSleepData } from '@/utils/sleepHealthKit'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { router, useLocalSearchParams } 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'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; // SleepGradeCard 组件现在在 InfoModal 组件内部 // SleepStagesInfoModal 组件现在从独立文件导入 // InfoModal 组件现在从独立文件导入 export default function SleepDetailScreen() { const safeAreaTop = useSafeAreaTop() const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const [sleepData, setSleepData] = useState(null); const [loading, setLoading] = useState(true); // 从导航参数获取日期,如果没有则使用今天 const { date: dateParam } = useLocalSearchParams<{ date?: string }>(); const [selectedDate] = useState(() => { if (dateParam) { return dayjs(dateParam).toDate(); } return dayjs().toDate(); }); const [infoModal, setInfoModal] = useState<{ visible: boolean; title: string; type: 'sleep-time' | 'sleep-quality' | null }>({ visible: false, 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()} transparent={true} variant="default" /> {/* 睡眠得分圆形显示 */} {displayData.sleepScore} 睡眠得分 {/* 睡眠质量描述 */} {displayData.qualityDescription} {/* 建议文本 */} {displayData.recommendation} {/* 睡眠统计卡片 */} 睡眠时间 setInfoModal({ visible: true, title: '睡眠时间', type: 'sleep-time' })} > {displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '--'} 睡眠质量 setInfoModal({ visible: true, title: '睡眠质量', type: 'sleep-quality' })} > {displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '--%'} {/* 睡眠阶段图表 */} {/* 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: 0, percentage: 0, quality: 'good' as any }, { stage: SleepStage.REM, duration: 0, percentage: 0, quality: 'good' as any }, { stage: SleepStage.Core, duration: 0, percentage: 0, quality: 'good' as any }, { stage: SleepStage.Deep, duration: 0, percentage: 0, 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}% ); })} {/* 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, }, 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', }, });