import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import { formatTime, getSleepStageColor, SleepStage, type SleepSample } from '@/utils/sleepHealthKit'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import React, { useMemo } from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Svg, { Rect, Text as SvgText } from 'react-native-svg'; export type SleepStageTimelineProps = { sleepSamples: SleepSample[]; bedtime: string; wakeupTime: string; onInfoPress?: () => void; }; export const SleepStageTimeline = ({ sleepSamples, bedtime, wakeupTime, onInfoPress }: SleepStageTimelineProps) => { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; // 图表尺寸参数 const chartWidth = 320; const chartHeight = 80; const timelineHeight = 32; const timelineY = 24; const timeScaleY = timelineY + timelineHeight + 16; // 计算时间范围和刻度 const { timelineData, timeLabels } = useMemo(() => { if (!bedtime || !wakeupTime || sleepSamples.length === 0) { return { timelineData: [], timeLabels: [] }; } const startTime = dayjs(bedtime); const endTime = dayjs(wakeupTime); const totalMinutes = endTime.diff(startTime, 'minute'); // 过滤相关睡眠阶段,排除InBed const relevantSamples = sleepSamples.filter(sample => sample.value !== SleepStage.InBed ); // 转换样本数据为时间轴数据 const data = relevantSamples.map(sample => { const sampleStart = dayjs(sample.startDate); const sampleEnd = dayjs(sample.endDate); const startOffset = sampleStart.diff(startTime, 'minute'); const duration = sampleEnd.diff(sampleStart, 'minute'); const x = Math.max(0, (startOffset / totalMinutes) * chartWidth); const width = Math.max(2, (duration / totalMinutes) * chartWidth); return { x, width, stage: sample.value, color: getSleepStageColor(sample.value) }; }); // 智能生成时间标签,避免重合 const labels = []; const minLabelSpacing = 50; // 最小标签间距(像素) // 总是显示起始时间 labels.push({ time: startTime.format('HH:mm'), x: 0 }); // 根据睡眠总时长动态调整时间间隔 const sleepDurationHours = totalMinutes / 60; let timeStepMinutes; if (sleepDurationHours <= 4) { timeStepMinutes = 60; // 1小时间隔 } else if (sleepDurationHours <= 8) { timeStepMinutes = 120; // 2小时间隔 } else { timeStepMinutes = 180; // 3小时间隔 } // 添加中间时间标签,确保不重合 let currentTime = startTime; let stepCount = 1; while (currentTime.add(timeStepMinutes * stepCount, 'minute').isBefore(endTime)) { const stepTime = currentTime.add(timeStepMinutes * stepCount, 'minute'); const x = (stepTime.diff(startTime, 'minute') / totalMinutes) * chartWidth; // 检查与前一个标签的间距 const lastLabel = labels[labels.length - 1]; if (x - lastLabel.x >= minLabelSpacing && x <= chartWidth - 30) { labels.push({ time: stepTime.format('HH:mm'), x: x }); } stepCount++; } // 总是显示结束时间,但要确保与前一个标签有足够间距 const endX = chartWidth - 25; const lastLabel = labels[labels.length - 1]; if (endX - lastLabel.x >= minLabelSpacing) { labels.push({ time: endTime.format('HH:mm'), x: endX }); } else { // 如果空间不够,替换最后一个标签为结束时间 labels[labels.length - 1] = { time: endTime.format('HH:mm'), x: endX }; } return { timelineData: data, timeLabels: labels }; }, [sleepSamples, bedtime, wakeupTime, chartWidth]); // 如果没有数据,显示空状态 if (timelineData.length === 0) { return ( 睡眠阶段图 {onInfoPress && ( )} 暂无睡眠阶段数据 ); } return ( {/* 标题栏 */} 睡眠阶段图 {onInfoPress && ( )} {/* 睡眠时间范围 */} 入睡 {formatTime(bedtime)} 起床 {formatTime(wakeupTime)} {/* SVG 图表 */} {/* 绘制睡眠阶段条形图 */} {timelineData.map((segment, index) => ( ))} {/* 绘制时间刻度标签 */} {timeLabels.map((label, index) => ( {/* 刻度线 */} {/* 时间标签 */} {label.time} ))} {/* 图例 */} 深度睡眠 核心睡眠 快速眼动 清醒时间 ); }; const styles = StyleSheet.create({ container: { borderRadius: 16, padding: 16, marginBottom: 24, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, marginHorizontal: 4, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, title: { fontSize: 16, fontWeight: '600', }, infoButton: { padding: 4, }, timeRange: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20, }, timePoint: { alignItems: 'center', }, timeLabel: { fontSize: 12, fontWeight: '500', marginBottom: 4, }, timeValue: { fontSize: 16, fontWeight: '700', letterSpacing: -0.2, }, chartContainer: { alignItems: 'center', marginBottom: 16, }, emptyState: { alignItems: 'center', paddingVertical: 32, }, emptyText: { fontSize: 14, fontStyle: 'italic', }, legend: { gap: 8, }, legendRow: { flexDirection: 'row', justifyContent: 'center', gap: 24, }, legendItem: { flexDirection: 'row', alignItems: 'center', gap: 6, }, legendDot: { width: 8, height: 8, borderRadius: 4, }, legendText: { fontSize: 12, fontWeight: '500', }, });