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',
},
});