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 { useColorScheme } from '@/hooks/useColorScheme'; import { useI18n } from '@/hooks/useI18n'; import { fetchCompleteSleepData, formatSleepTime, getSleepStageColor, SleepStage, type CompleteSleepData } from '@/utils/sleepHealthKit'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { router, useLocalSearchParams } from 'expo-router'; import React, { useCallback, useEffect, useState } from 'react'; import { ActivityIndicator, Dimensions, ScrollView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; const { width } = Dimensions.get('window'); const HERO_HEIGHT = width * 0.76; export default function SleepDetailScreen() { const { t } = useI18n(); const insets = useSafeAreaInsets(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; 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); const data = await fetchCompleteSleepData(selectedDate); setSleepData(data); } catch (error) { console.error('Failed to load sleep data:', error); } finally { setLoading(false); } }, [selectedDate]); useEffect(() => { loadSleepData(); }, [loadSleepData]); // 如果没有数据,使用默认数据结构 const displayData: CompleteSleepData = sleepData || { sleepScore: 0, totalSleepTime: 0, sleepQualityPercentage: 0, bedtime: '', wakeupTime: '', timeInBed: 0, sleepStages: [], rawSleepSamples: [], averageHeartRate: null, sleepHeartRateData: [], sleepEfficiency: 0, qualityDescription: t('sleepDetail.noData'), recommendation: t('sleepDetail.noDataRecommendation') }; const formatDateTitle = (date: Date) => { if (dayjs(date).isSame(dayjs(), 'day')) { return t('sleepDetail.today'); } return dayjs(date).format('M月D日'); }; const getScoreColor = (score: number) => { if (score >= 85) return '#10B981'; // Green if (score >= 70) return '#3B82F6'; // Blue if (score >= 60) return '#F59E0B'; // Yellow }; // 加载状态 if (loading) { return ( router.back()} withSafeTop transparent={false} /> {t('sleepDetail.loading')} ); } return ( {/* 顶部导航覆盖层 */} {isLiquidGlassAvailable() ? ( {}} activeOpacity={0.7} > ) : ( )} } /> {/* Hero 区域 */} {/* 头部文本块 */} {displayData.sleepScore} {formatDateTitle(selectedDate)}{t('sleepDetail.sleepScore')} {displayData.qualityDescription} {displayData.recommendation} {/* 核心数据卡片 */} {/* 睡眠时长 */} {t('sleepDetail.sleepDuration')} setInfoModal({ visible: true, title: t('sleepDetail.infoModalTitles.sleepTime'), type: 'sleep-time' })} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > {displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '--'} {/* 分割线 */} {/* 睡眠质量 */} {t('sleepDetail.sleepQuality')} setInfoModal({ visible: true, title: t('sleepDetail.infoModalTitles.sleepQuality'), type: 'sleep-quality' })} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > {displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '--%'} {/* 睡眠阶段时间轴 */} {t('sleepDetail.sleepStages')} setSleepStagesModal({ visible: true })}> {t('sleepDetail.learnMore')} {/* 睡眠阶段统计网格 */} {(() => { 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 }, existingStages.get(SleepStage.REM) || { stage: SleepStage.REM, duration: 0, percentage: 0 }, existingStages.get(SleepStage.Core) || { stage: SleepStage.Core, duration: 0, percentage: 0 }, existingStages.get(SleepStage.Deep) || { stage: SleepStage.Deep, duration: 0, percentage: 0 } ]; } else { stagesToDisplay = [ { stage: SleepStage.Awake, duration: 0, percentage: 0 }, { stage: SleepStage.REM, duration: 0, percentage: 0 }, { stage: SleepStage.Core, duration: 0, percentage: 0 }, { stage: SleepStage.Deep, duration: 0, percentage: 0 } ]; } return stagesToDisplay; })().map((stageData, index) => { const getStageName = (stage: SleepStage) => { switch (stage) { case SleepStage.Awake: return t('sleepDetail.awake'); case SleepStage.REM: return t('sleepDetail.rem'); case SleepStage.Core: return t('sleepDetail.core'); case SleepStage.Deep: return t('sleepDetail.deep'); default: return t('sleepDetail.unknown'); } }; const stageColor = getSleepStageColor(stageData.stage); return ( {getStageName(stageData.stage)} {formatSleepTime(stageData.duration)} {stageData.percentage}% ); })} {/* 原始数据列表 (如果有大量数据) */} {sleepData && sleepData.rawSleepSamples && sleepData.rawSleepSamples.length > 0 && ( {t('sleepDetail.rawData')} ({sleepData.rawSleepSamples.length}) {/* 这里可以考虑是否真的需要显示长列表,或者只显示摘要 */} {t('sleepDetail.rawDataDescription', { count: sleepData.rawSleepSamples.length })} )} {/* Modals */} {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: '#f3f4fb', }, safeArea: { flex: 1, }, headerOverlay: { position: 'absolute', left: 0, right: 0, top: 0, zIndex: 20, }, heroContainer: { height: HERO_HEIGHT, width: '100%', overflow: 'hidden', position: 'absolute', top: 0, }, heroImage: { width: '100%', height: '100%', }, scrollView: { flex: 1, }, scrollContent: { paddingBottom: 40, }, headerTextBlock: { paddingHorizontal: 24, marginTop: HERO_HEIGHT - 60, alignItems: 'center', }, scoreCircle: { alignItems: 'center', justifyContent: 'center', }, scoreValue: { fontSize: 48, fontWeight: '800', fontFamily: 'AliBold', lineHeight: 56, textShadowColor: 'rgba(255, 255, 255, 0.5)', textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 4, }, scoreLabel: { fontSize: 14, color: '#596095', marginTop: 4, fontFamily: 'AliRegular', letterSpacing: 0.5, }, summary: { marginTop: 16, fontSize: 18, fontWeight: '600', color: '#1c1f3a', textAlign: 'center', fontFamily: 'AliBold', lineHeight: 24, }, recommendation: { marginTop: 8, fontSize: 14, lineHeight: 20, color: '#7080b4', textAlign: 'center', fontFamily: 'AliRegular', paddingHorizontal: 10, }, detailCard: { marginTop: 28, marginHorizontal: 20, padding: 20, borderRadius: 28, backgroundColor: '#ffffff', shadowColor: 'rgba(30, 41, 59, 0.1)', shadowOpacity: 0.2, shadowRadius: 20, shadowOffset: { width: 0, height: 10 }, elevation: 8, }, detailRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, }, detailIconWrapper: { width: 46, height: 46, borderRadius: 23, backgroundColor: '#EEF0FF', alignItems: 'center', justifyContent: 'center', }, detailTextWrapper: { marginLeft: 16, flex: 1, }, detailHeader: { flexDirection: 'row', alignItems: 'center', gap: 6, }, detailLabel: { fontSize: 14, color: '#6f7ba7', fontFamily: 'AliRegular', }, detailValue: { fontSize: 20, fontWeight: '700', color: '#1c1f3a', marginTop: 4, fontFamily: 'AliBold', }, divider: { height: 1, backgroundColor: '#F0F2F9', marginVertical: 4, marginLeft: 62, // align with text }, sectionHeader: { marginTop: 32, marginHorizontal: 24, marginBottom: 16, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, sectionTitle: { fontSize: 18, fontWeight: '700', color: '#1c1f3a', fontFamily: 'AliBold', }, sectionAction: { fontSize: 13, fontWeight: '600', color: '#5F6BF0', fontFamily: 'AliBold', }, timelineCard: { marginHorizontal: 20, borderRadius: 24, backgroundColor: '#ffffff', paddingVertical: 8, paddingHorizontal: 4, shadowColor: 'rgba(30, 41, 59, 0.08)', shadowOpacity: 0.15, shadowRadius: 16, shadowOffset: { width: 0, height: 8 }, elevation: 4, overflow: 'hidden', }, timelineInner: { backgroundColor: 'transparent', shadowOpacity: 0, elevation: 0, padding: 0, marginBottom: 0, marginHorizontal: 0, }, stagesGridContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, paddingHorizontal: 20, marginTop: 20, }, stageCard: { width: (width - 52) / 2, // 20*2 margin + 12 gap backgroundColor: '#ffffff', borderRadius: 20, padding: 16, shadowColor: 'rgba(30, 41, 59, 0.06)', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 3, }, stageHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12, }, stageDot: { width: 8, height: 8, borderRadius: 4, marginRight: 8, }, stageTitle: { fontSize: 13, fontWeight: '600', fontFamily: 'AliBold', }, stageValue: { fontSize: 18, fontWeight: '700', color: '#1c1f3a', fontFamily: 'AliBold', marginBottom: 8, }, stageProgressBg: { height: 4, backgroundColor: '#F0F2F9', borderRadius: 2, marginBottom: 8, overflow: 'hidden', }, stageProgressFill: { height: '100%', borderRadius: 2, }, stagePercentage: { fontSize: 12, color: '#6f7ba7', fontFamily: 'AliRegular', }, rawSamplesCard: { marginTop: 24, marginHorizontal: 20, padding: 20, borderRadius: 24, backgroundColor: '#ffffff', opacity: 0.8, }, rawSamplesHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8, }, rawSamplesTitle: { fontSize: 15, fontWeight: '600', color: '#1c1f3a', fontFamily: 'AliBold', }, rawSamplesSubtitle: { fontSize: 12, color: '#6f7ba7', fontFamily: 'AliRegular', }, missingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32, }, missingText: { fontSize: 16, color: '#6f7ba7', marginTop: 16, fontFamily: 'AliRegular', }, headerButtons: { flexDirection: 'row', alignItems: 'center', gap: 12, }, iconButton: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', }, fallbackIconButton: { backgroundColor: 'rgba(255, 255, 255, 0.2)', borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.3)', }, });