import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useI18n } from '@/hooks/useI18n'; import { formatTime, getSleepStageColor, SleepStage, type SleepSample } from '@/utils/sleepHealthKit'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useMemo } from 'react'; import { Dimensions, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop, Text as SvgText } from 'react-native-svg'; import { StyleProp, ViewStyle } from 'react-native'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); export type SleepStageTimelineProps = { sleepSamples: SleepSample[]; bedtime: string; wakeupTime: string; onInfoPress?: () => void; hideHeader?: boolean; style?: StyleProp; }; export const SleepStageTimeline = ({ sleepSamples, bedtime, wakeupTime, onInfoPress, hideHeader = false, style }: SleepStageTimelineProps) => { const { t } = useI18n(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; // 图表尺寸参数 - 更宽更现代的设计 const containerWidth = SCREEN_WIDTH - 64; // 留出左右边距 const chartPadding = 24; // 增加左右内边距,避免时间轴和标签被截断 const chartWidth = containerWidth - chartPadding * 2; const chartHeight = 140; // 增加高度以容纳更高的条形图 const timelineHeight = 48; // 更高的条形图 const timelineY = 16; const timeScaleY = timelineY + timelineHeight + 24; // 计算时间范围和刻度 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) + chartPadding; const width = Math.max(3, (duration / totalMinutes) * chartWidth); return { x, width, stage: sample.value, color: getSleepStageColor(sample.value) }; }); // 智能生成时间标签 const labels = []; const minLabelSpacing = 60; // 起始时间标签 labels.push({ time: startTime.format('HH:mm'), x: chartPadding }); const sleepDurationHours = totalMinutes / 60; let timeStepMinutes; if (sleepDurationHours <= 4) { timeStepMinutes = 60; } else if (sleepDurationHours <= 8) { timeStepMinutes = 120; } else { timeStepMinutes = 180; } 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 + chartPadding; const lastLabel = labels[labels.length - 1]; if (x - lastLabel.x >= minLabelSpacing && x <= containerWidth - chartPadding) { labels.push({ time: stepTime.format('HH:mm'), x: x }); } stepCount++; } // 结束时间标签 const endX = containerWidth - chartPadding; 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 ( {!hideHeader && ( {t('sleepDetail.sleepStages')} {onInfoPress && ( )} )} {t('sleepDetail.noData')} ); } return ( {/* 标题栏 */} {!hideHeader && ( {t('sleepDetail.sleepStages')} {onInfoPress && ( )} )} {/* 睡眠时间范围 - 更简洁的设计 */} {formatTime(bedtime)} {t('sleepDetail.infoModalTitles.sleepTime')} {formatTime(wakeupTime)} {t('sleepDetail.sleepDuration')} {/* SVG 图表 - iOS 健康风格 */} {/* 背景轨道 */} {/* 为每种睡眠阶段定义渐变 */} {/* 绘制睡眠阶段条形图 - 使用渐变和圆角 */} {timelineData.map((segment, index) => { const gradientId = segment.stage === SleepStage.Deep ? 'gradDeep' : segment.stage === SleepStage.Core ? 'gradCore' : segment.stage === SleepStage.REM || segment.stage === SleepStage.Asleep ? 'gradREM' : segment.stage === SleepStage.Awake ? 'gradAwake' : 'gradAsleep'; return ( ); })} {/* 绘制时间刻度标签 - 更细腻的设计 */} {timeLabels.map((label, index) => ( {/* 刻度线 */} {/* 时间标签 */} {label.time} ))} {/* 图例 - iOS 风格的标签 */} {t('sleepDetail.deep')} {t('sleepDetail.core')} {t('sleepDetail.rem')} {t('sleepDetail.awake')} ); }; const styles = StyleSheet.create({ container: { borderRadius: 16, paddingVertical: 20, paddingHorizontal: 16, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, title: { fontSize: 16, fontWeight: '600', fontFamily: 'AliBold', }, infoButton: { padding: 4, }, timeRange: { flexDirection: 'row', justifyContent: 'space-around', marginBottom: 28, paddingHorizontal: 20, }, timePoint: { alignItems: 'center', gap: 2, }, timeLabel: { fontSize: 11, fontWeight: '500', fontFamily: 'AliRegular', }, timeValue: { fontSize: 20, fontWeight: '700', fontFamily: 'AliBold', letterSpacing: -0.5, }, chartContainer: { alignItems: 'center', marginBottom: 20, position: 'relative', }, trackBackground: { position: 'absolute', top: 16, height: 48, backgroundColor: '#F0F2F9', borderRadius: 24, opacity: 0.5, }, emptyState: { alignItems: 'center', paddingVertical: 32, }, emptyText: { fontSize: 14, fontStyle: 'italic', fontFamily: 'AliRegular', }, legend: { flexDirection: 'row', justifyContent: 'center', flexWrap: 'wrap', gap: 16, paddingTop: 8, }, legendItem: { flexDirection: 'row', alignItems: 'center', gap: 6, }, legendPill: { width: 20, height: 10, borderRadius: 5, }, legendText: { fontSize: 12, fontWeight: '500', color: '#6B7280', fontFamily: 'AliRegular', }, });