import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { router } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, Animated, Dimensions, Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Svg, { Circle } from 'react-native-svg'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import { fetchSleepDetailForDate, formatSleepTime, formatTime, getSleepStageColor, getSleepStageDisplayName, convertSleepSamplesToIntervals, SleepDetailData, SleepStage } from '@/services/sleepService'; import { ensureHealthPermissions } from '@/utils/health'; const { width } = Dimensions.get('window'); // 圆形进度条组件 const CircularProgress = ({ size, strokeWidth, progress, color, backgroundColor = '#E5E7EB' }: { size: number; strokeWidth: number; progress: number; // 0-100 color: string; backgroundColor?: string; }) => { const radius = (size - strokeWidth) / 2; const circumference = radius * 2 * Math.PI; const strokeDasharray = circumference; const strokeDashoffset = circumference - (progress / 100) * circumference; return ( {/* 背景圆环 */} {/* 进度圆环 */} ); }; // 睡眠阶段图表组件 const SleepStageChart = ({ sleepData }: { sleepData: SleepDetailData }) => { const chartWidth = width - 80; const chartHeight = 120; const coreBaselineHeight = chartHeight * 0.6; // 核心睡眠作为基准线 const blockHeight = 20; // 每个睡眠阶段块的固定高度 // 使用真实的 HealthKit 睡眠数据 const generateRealSleepData = () => { // 如果没有睡眠数据,返回空数组 if (sleepData.totalSleepTime === 0 || !sleepData.rawSleepSamples || sleepData.rawSleepSamples.length === 0) { console.log('没有可用的睡眠数据用于图表显示'); return []; } console.log('使用真实 HealthKit 睡眠数据生成图表,样本数量:', sleepData.rawSleepSamples.length); // 使用新的转换函数,将睡眠样本转换为15分钟间隔数据 const intervalData = convertSleepSamplesToIntervals( sleepData.rawSleepSamples, sleepData.bedtime, sleepData.wakeupTime ); if (intervalData.length === 0) { console.log('无法生成睡眠阶段间隔数据 - 可能只有基本的InBed/Asleep数据'); // 如果没有详细的睡眠阶段数据,生成基本的模拟数据作为回退 return generateFallbackSleepData(); } return intervalData; }; // 回退方案:当没有详细睡眠阶段数据时使用 const generateFallbackSleepData = () => { console.log('使用回退睡眠数据 - 用户可能没有Apple Watch或详细睡眠追踪'); const data: { time: string; stage: SleepStage }[] = []; const bedtime = new Date(sleepData.bedtime); const wakeupTime = new Date(sleepData.wakeupTime); let currentTime = new Date(bedtime); // 基于典型睡眠模式生成合理的睡眠阶段分布 while (currentTime < wakeupTime) { const timeStr = `${String(currentTime.getHours()).padStart(2, '0')}:${String(currentTime.getMinutes()).padStart(2, '0')}`; const sleepDuration = wakeupTime.getTime() - bedtime.getTime(); const currentProgress = (currentTime.getTime() - bedtime.getTime()) / sleepDuration; let stage: SleepStage; if (currentProgress < 0.15 || currentProgress > 0.85) { stage = Math.random() < 0.6 ? SleepStage.Core : SleepStage.Awake; } else if (currentProgress < 0.4) { stage = Math.random() < 0.7 ? SleepStage.Deep : SleepStage.Core; } else if (currentProgress < 0.7) { const rand = Math.random(); stage = rand < 0.6 ? SleepStage.Core : (rand < 0.9 ? SleepStage.REM : SleepStage.Awake); } else { const rand = Math.random(); stage = rand < 0.5 ? SleepStage.REM : (rand < 0.9 ? SleepStage.Core : SleepStage.Awake); } data.push({ time: timeStr, stage }); currentTime.setMinutes(currentTime.getMinutes() + 15); } return data; }; const sleepDataPoints = generateRealSleepData(); // 获取睡眠阶段在Y轴上的位置 const getStageYPosition = (stage: SleepStage) => { switch (stage) { case SleepStage.Awake: return coreBaselineHeight - blockHeight * 2; // 最上方 case SleepStage.REM: return coreBaselineHeight - blockHeight; // 上方 case SleepStage.Core: return coreBaselineHeight; // 基准线 case SleepStage.Deep: return coreBaselineHeight + blockHeight; // 下方 default: return coreBaselineHeight; } }; // 获取时间标签 const getTimeLabels = () => { if (sleepData.totalSleepTime === 0) { return { startTime: '--:--', endTime: '--:--' }; } return { startTime: formatTime(sleepData.bedtime), endTime: formatTime(sleepData.wakeupTime) }; }; const { startTime, endTime } = getTimeLabels(); return ( 🛏️ {startTime} ❤️ 平均心率: {sleepData.averageHeartRate || '--'} BPM ☀️ {endTime} {/* 分层睡眠阶段图表 */} {sleepDataPoints.map((dataPoint, index) => { const blockWidth = chartWidth / sleepDataPoints.length - 1; const yPosition = getStageYPosition(dataPoint.stage); const color = getSleepStageColor(dataPoint.stage); return ( ); })} {/* 时间刻度 */} {startTime} {endTime} ); }; // 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} ); }; // 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 }); useEffect(() => { loadSleepData(); }, [selectedDate]); const loadSleepData = async () => { try { setLoading(true); // 确保有健康权限 const hasPermission = await ensureHealthPermissions(); if (!hasPermission) { console.warn('没有健康数据权限'); return; } // 获取睡眠详情数据 const data = await fetchSleepDetailForDate(selectedDate); setSleepData(data); } catch (error) { console.error('加载睡眠数据失败:', error); } finally { setLoading(false); } }; if (loading) { return ( 加载睡眠数据中... ); } // 如果没有数据,使用默认数据结构 const displayData: SleepDetailData = sleepData || { sleepScore: 0, totalSleepTime: 0, sleepQualityPercentage: 0, bedtime: new Date().toISOString(), wakeupTime: new Date().toISOString(), 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%'} ★ 优秀 {/* 睡眠阶段图表 */} {/* 睡眠阶段统计 */} {displayData.sleepStages.length > 0 ? displayData.sleepStages.map((stage, index) => ( {getSleepStageDisplayName(stage.stage)} {stage.percentage}% {formatSleepTime(stage.duration)} {stage.quality === 'excellent' ? '优秀' : stage.quality === 'good' ? '良好' : stage.quality === 'fair' ? '一般' : '偏低'} )) : ( /* 当没有真实数据时,显示包含清醒时间的模拟数据 */ <> {/* 深度睡眠 */} {getSleepStageDisplayName(SleepStage.Deep)} 28% 2h 04m 良好 {/* REM睡眠 */} {getSleepStageDisplayName(SleepStage.REM)} 22% 1h 37m 优秀 {/* 核心睡眠 */} {getSleepStageDisplayName(SleepStage.Core)} 38% 2h 48m 良好 {/* 清醒时间 */} {getSleepStageDisplayName(SleepStage.Awake)} 12% 54m 正常 )} {infoModal.type && ( setInfoModal({ ...infoModal, visible: false })} title={infoModal.title} type={infoModal.type} sleepData={displayData} /> )} ); } 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', marginVertical: 20, }, circularProgressContainer: { position: 'relative', alignItems: 'center', justifyContent: 'center', }, scoreTextContainer: { position: 'absolute', 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', }, });