Files
digital-pilates/app/sleep-detail.tsx

1076 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
fetchCompleteSleepData,
formatSleepTime,
formatTime,
getSleepStageColor,
SleepStage,
type CompleteSleepData
} from '@/utils/sleepHealthKit';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import { router } from 'expo-router';
import React, { useCallback, useEffect, useState } from 'react';
import {
ActivityIndicator,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
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 { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
// 简化的睡眠阶段图表组件
const SleepStageChart = ({
sleepData,
onInfoPress
}: {
sleepData: SleepDetailData;
onInfoPress: () => void;
}) => {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
// 使用真实的睡眠阶段数据,如果没有则使用默认数据
const stages = sleepData.sleepStages.length > 0
? sleepData.sleepStages
.filter(stage => stage.percentage > 0) // 只显示有数据的阶段
.map(stage => ({
stage: stage.stage,
percentage: stage.percentage,
duration: stage.duration
}))
: [
{ stage: SleepStage.Awake, percentage: 1, duration: 3 },
{ stage: SleepStage.REM, percentage: 20, duration: 89 },
{ stage: SleepStage.Core, percentage: 67, duration: 295 },
{ stage: SleepStage.Deep, percentage: 12, duration: 51 }
];
return (
<View style={styles.simplifiedChartContainer}>
<View style={styles.chartTitleContainer}>
<Text style={styles.chartTitle}></Text>
<TouchableOpacity
style={styles.chartInfoButton}
onPress={onInfoPress}
>
<Ionicons name="help-circle-outline" size={20} color="#6B7280" />
</TouchableOpacity>
</View>
{/* 入睡时间和起床时间显示 */}
<View style={styles.sleepTimeLabels}>
<View style={styles.sleepTimeLabel}>
<Text style={[styles.sleepTimeText, { color: colorTokens.textSecondary }]}>
</Text>
<Text style={[styles.sleepTimeValue, { color: colorTokens.text }]}>
{sleepData.bedtime ? formatTime(sleepData.bedtime) : '--:--'}
</Text>
</View>
<View style={styles.sleepTimeLabel}>
<Text style={[styles.sleepTimeText, { color: colorTokens.textSecondary }]}>
</Text>
<Text style={[styles.sleepTimeValue, { color: colorTokens.text }]}>
{sleepData.wakeupTime ? formatTime(sleepData.wakeupTime) : '--:--'}
</Text>
</View>
</View>
{/* 简化的睡眠阶段条 */}
<View style={styles.simplifiedChartBar}>
{stages.map((stageData, index) => {
const color = getSleepStageColor(stageData.stage);
// 确保最小宽度,避免清醒阶段等小比例的阶段完全不可见
const flexValue = Math.max(stageData.percentage || 1, 3);
return (
<View
key={index}
style={[
styles.stageSegment,
{
backgroundColor: color,
flex: flexValue,
}
]}
/>
);
})}
</View>
{/* 图例 */}
<View style={styles.chartLegend}>
<View style={styles.legendRow}>
<View style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.Awake) }]} />
<Text style={styles.legendText}></Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.REM) }]} />
<Text style={styles.legendText}></Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.Core) }]} />
<Text style={styles.legendText}></Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendDot, { backgroundColor: getSleepStageColor(SleepStage.Deep) }]} />
<Text style={styles.legendText}></Text>
</View>
</View>
</View>
</View>
);
};
// SleepGradeCard 组件现在在 InfoModal 组件内部
// SleepStagesInfoModal 组件现在从独立文件导入
// InfoModal 组件现在从独立文件导入
export default function SleepDetailScreen() {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const [sleepData, setSleepData] = useState<CompleteSleepData | null>(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
});
const [sleepStagesModal, setSleepStagesModal] = useState({
visible: false
});
const loadSleepData = useCallback(async () => {
try {
setLoading(true);
console.log('开始加载睡眠数据...');
const data = await fetchCompleteSleepData(selectedDate);
setSleepData(data);
if (data) {
console.log('睡眠数据加载成功,得分:', data.sleepScore);
} else {
console.log('未找到睡眠数据');
}
} catch (error) {
console.error('加载睡眠数据失败:', error);
} finally {
setLoading(false);
}
}, [selectedDate]);
useEffect(() => {
loadSleepData();
}, [loadSleepData]);
if (loading) {
return (
<View style={[styles.container, styles.loadingContainer]}>
<ActivityIndicator size="large" color={Colors.light.primary} />
<Text style={styles.loadingText}>...</Text>
</View>
);
}
// 如果没有数据,使用默认数据结构
const displayData: CompleteSleepData = sleepData || {
sleepScore: 0,
totalSleepTime: 0,
sleepQualityPercentage: 0,
bedtime: '',
wakeupTime: '',
timeInBed: 0,
sleepStages: [],
rawSleepSamples: [],
averageHeartRate: null,
sleepHeartRateData: [],
sleepEfficiency: 0,
qualityDescription: '暂无睡眠数据',
recommendation: '请确保在真实iOS设备上运行并授权访问健康数据或等待有睡眠数据后再查看。'
};
return (
<View style={styles.container}>
{/* 背景渐变 */}
<LinearGradient
colors={['#f0f4ff', '#e6f2ff', '#ffffff']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
{/* 顶部导航 */}
<HeaderBar
title={`今天, ${dayjs(selectedDate).format('M月DD日')}`}
onBack={() => router.back()}
withSafeTop={true}
transparent={true}
/>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* 睡眠得分圆形显示 */}
<View style={styles.scoreContainer}>
<View style={styles.scoreTextContainer}>
<Text style={styles.scoreNumber}>{displayData.sleepScore}</Text>
<Text style={styles.scoreLabel}></Text>
</View>
</View>
{/* 睡眠质量描述 */}
<Text style={styles.qualityDescription}>{displayData.qualityDescription}</Text>
{/* 建议文本 */}
<Text style={styles.recommendationText}>{displayData.recommendation}</Text>
{/* 睡眠统计卡片 */}
<View style={styles.statsContainer}>
<View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}>
<View style={styles.statCardHeader}>
<View style={styles.statCardLeftGroup}>
<View style={styles.statCardIcon}>
<Ionicons name="moon-outline" size={18} color="#6B7280" />
</View>
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}></Text>
</View>
<TouchableOpacity
style={styles.infoButton}
onPress={() => setInfoModal({
visible: true,
title: '睡眠时间',
type: 'sleep-time'
})}
>
<Ionicons name="information-circle-outline" size={18} color={colorTokens.textMuted} />
</TouchableOpacity>
</View>
<Text style={[styles.newStatValue, { color: colorTokens.text }]}>
{displayData.totalSleepTime > 0 ? formatSleepTime(displayData.totalSleepTime) : '7h 23m'}
</Text>
<View style={[styles.qualityBadge, styles.goodQualityBadge]}>
<Text style={[styles.qualityBadgeText, styles.goodQualityText]}> </Text>
</View>
</View>
<View style={[styles.newStatCard, { backgroundColor: colorTokens.background }]}>
<View style={styles.statCardHeader}>
<View style={styles.statCardLeftGroup}>
<View style={styles.statCardIcon}>
<Ionicons name="star-outline" size={18} color="#6B7280" />
</View>
<Text style={[styles.statLabel, { color: colorTokens.textSecondary }]}></Text>
</View>
<TouchableOpacity
style={styles.infoButton}
onPress={() => setInfoModal({
visible: true,
title: '睡眠质量',
type: 'sleep-quality'
})}
>
<Ionicons name="information-circle-outline" size={16} color={colorTokens.textMuted} />
</TouchableOpacity>
</View>
<Text style={[styles.newStatValue, { color: colorTokens.text }]}>
{displayData.sleepQualityPercentage > 0 ? `${displayData.sleepQualityPercentage}%` : '94%'}
</Text>
<View style={[styles.qualityBadge, styles.excellentQualityBadge]}>
<Text style={[styles.qualityBadgeText, styles.excellentQualityText]}> </Text>
</View>
</View>
</View>
{/* 睡眠阶段图表 */}
{/* <SleepStageChart
sleepData={displayData}
onInfoPress={() => setSleepStagesModal({ visible: true })}
/> */}
{/* 苹果健康风格的睡眠阶段时间轴图表 */}
<SleepStageTimeline
sleepSamples={displayData.rawSleepSamples}
bedtime={displayData.bedtime}
wakeupTime={displayData.wakeupTime}
onInfoPress={() => setSleepStagesModal({ visible: true })}
/>
{/* 睡眠阶段统计 - 2x2网格布局 */}
<View style={styles.stagesGridContainer}>
{/* 使用真实数据或默认数据确保包含所有4个阶段 */}
{(() => {
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, quality: 'good' as any },
existingStages.get(SleepStage.REM) || { stage: SleepStage.REM, duration: 0, percentage: 0, quality: 'good' as any },
existingStages.get(SleepStage.Core) || { stage: SleepStage.Core, duration: 0, percentage: 0, quality: 'good' as any },
existingStages.get(SleepStage.Deep) || { stage: SleepStage.Deep, duration: 0, percentage: 0, quality: 'good' as any }
];
} else {
// 使用默认数据
stagesToDisplay = [
{ stage: SleepStage.Awake, duration: 3, percentage: 1, quality: 'good' as any },
{ stage: SleepStage.REM, duration: 89, percentage: 20, quality: 'good' as any },
{ stage: SleepStage.Core, duration: 295, percentage: 67, quality: 'good' as any },
{ stage: SleepStage.Deep, duration: 51, percentage: 12, quality: 'poor' as any }
];
}
return stagesToDisplay;
})().map((stageData, index) => {
const getStageName = (stage: SleepStage) => {
switch (stage) {
case SleepStage.Awake: return '清醒时间';
case SleepStage.REM: return '快速眼动';
case SleepStage.Core: return '核心睡眠';
case SleepStage.Deep: return '深度睡眠';
default: return '未知';
}
};
const getQualityDisplay = (quality: any) => {
switch (quality) {
case 'excellent': return { text: '★ 优秀', color: '#10B981', bgColor: '#D1FAE5', progressColor: '#10B981', progressWidth: '100%' };
case 'good': return { text: '✓ 良好', color: '#065F46', bgColor: '#D1FAE5', progressColor: '#10B981', progressWidth: '85%' };
case 'fair': return { text: '○ 一般', color: '#92400E', bgColor: '#FEF3C7', progressColor: '#F59E0B', progressWidth: '65%' };
case 'poor': return { text: '⚠ 低', color: '#DC2626', bgColor: '#FECACA', progressColor: '#F59E0B', progressWidth: '45%' };
default: return { text: '✓ 正常', color: '#065F46', bgColor: '#D1FAE5', progressColor: '#10B981', progressWidth: '75%' };
}
};
const qualityInfo = getQualityDisplay(stageData.quality);
return (
<View key={index} style={[styles.stageCard, { backgroundColor: colorTokens.background }]}>
<Text style={[styles.stageCardTitle, { color: getSleepStageColor(stageData.stage) }]}>
{getStageName(stageData.stage)}
</Text>
<Text style={[styles.stageCardValue, { color: colorTokens.text }]}>
{formatSleepTime(stageData.duration)}
</Text>
<Text style={[styles.stageCardPercentage, { color: colorTokens.textSecondary }]}>
{stageData.percentage}%
</Text>
<View style={[styles.stageCardQuality, { backgroundColor: qualityInfo.bgColor }]}>
<Text style={[styles.stageCardQualityText, { color: qualityInfo.color }]}>
{qualityInfo.text}
</Text>
</View>
</View>
);
})}
</View>
{/* Raw Sleep Samples List - 显示所有原始睡眠数据 */}
{sleepData && sleepData.rawSleepSamples && sleepData.rawSleepSamples.length > 100 && (
<View style={[styles.rawSamplesContainer, { backgroundColor: colorTokens.background }]}>
<View style={styles.rawSamplesHeader}>
<Text style={[styles.rawSamplesTitle, { color: colorTokens.text }]}>
({sleepData.rawSleepSamples.length} )
</Text>
<Text style={[styles.rawSamplesSubtitle, { color: colorTokens.textSecondary }]}>
gap
</Text>
</View>
<ScrollView style={styles.rawSamplesScrollView} nestedScrollEnabled={true}>
{sleepData.rawSleepSamples.map((sample, index) => {
// 计算与前一个样本的时间间隔
const prevSample = index > 0 ? sleepData.rawSleepSamples[index - 1] : null;
let gapMinutes = 0;
let hasGap = false;
if (prevSample) {
const prevEndTime = new Date(prevSample.endDate).getTime();
const currentStartTime = new Date(sample.startDate).getTime();
gapMinutes = (currentStartTime - prevEndTime) / (1000 * 60);
hasGap = gapMinutes > 1; // 大于1分钟视为有间隔
}
const startTime = formatTime(sample.startDate);
const endTime = formatTime(sample.endDate);
const duration = Math.round((new Date(sample.endDate).getTime() - new Date(sample.startDate).getTime()) / (1000 * 60));
// 获取睡眠阶段中文名称
const getStageName = (value: SleepStage) => {
switch (value) {
case SleepStage.InBed: return '在床上';
case SleepStage.Awake: return '清醒';
case SleepStage.Core: return '核心睡眠';
case SleepStage.Deep: return '深度睡眠';
case SleepStage.REM: return 'REM睡眠';
case SleepStage.Asleep: return '未指定睡眠';
default: return value;
}
};
return (
<View key={index}>
{/* 显示数据间隔 */}
{hasGap && (
<View style={[styles.gapIndicator, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
<Ionicons name="alert-circle-outline" size={14} color="#F59E0B" />
<Text style={[styles.gapText, { color: '#F59E0B' }]}>
: {Math.round(gapMinutes)}
</Text>
</View>
)}
{/* 睡眠样本条目 */}
<View style={[styles.rawSampleItem, { borderColor: colorTokens.border }]}>
<View style={styles.sampleHeader}>
<View style={styles.sampleLeft}>
<View
style={[
styles.stageDot,
{ backgroundColor: getSleepStageColor(sample.value) }
]}
/>
<Text style={[styles.sampleStage, { color: colorTokens.text }]}>
{getStageName(sample.value)}
</Text>
</View>
<Text style={[styles.sampleDuration, { color: colorTokens.textSecondary }]}>
{duration}
</Text>
</View>
<View style={styles.sampleTimeRange}>
<Text style={[styles.sampleTime, { color: colorTokens.textSecondary }]}>
{startTime} - {endTime}
</Text>
<Text style={[styles.sampleIndex, { color: colorTokens.textMuted }]}>
#{index + 1}
</Text>
</View>
</View>
</View>
);
})}
</ScrollView>
</View>
)}
</ScrollView>
{infoModal.type && (
<InfoModal
visible={infoModal.visible}
onClose={() => setInfoModal({ ...infoModal, visible: false })}
title={infoModal.title}
type={infoModal.type}
sleepData={displayData as SleepDetailData}
/>
)}
<SleepStagesInfoModal
visible={sleepStagesModal.visible}
onClose={() => setSleepStagesModal({ visible: false })}
/>
</View>
);
}
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',
},
circularProgressContainer: {
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
},
scoreTextContainer: {
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,
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 和 Grade Cards 样式已移动到独立组件中
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',
},
// 简化睡眠阶段图表样式
simplifiedChartContainer: {
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,
},
chartTitleContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
chartTitle: {
fontSize: 16,
fontWeight: '600',
color: '#1F2937',
},
chartInfoButton: {
padding: 4,
},
simplifiedChartBar: {
flexDirection: 'row',
height: 24,
borderRadius: 12,
overflow: 'hidden',
marginBottom: 16,
},
stageSegment: {
height: '100%',
},
chartLegend: {
gap: 8,
},
legendRow: {
flexDirection: 'row',
justifyContent: 'center',
},
legendItem: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
legendDot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
legendText: {
fontSize: 12,
color: '#6B7280',
fontWeight: '500',
},
// 睡眠阶段卡片网格样式
stagesGridContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
paddingHorizontal: 4,
},
stageCard: {
width: '48%',
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)',
},
stageCardTitle: {
fontSize: 14,
fontWeight: '500',
marginBottom: 8,
},
stageCardValue: {
fontSize: 24,
fontWeight: '700',
lineHeight: 28,
marginBottom: 4,
},
stageCardPercentage: {
fontSize: 12,
marginBottom: 12,
},
stageCardQuality: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 8,
alignSelf: 'flex-start',
},
normalQuality: {
backgroundColor: '#D1FAE5',
},
lowQuality: {
backgroundColor: '#FECACA',
},
stageCardQualityText: {
fontSize: 12,
fontWeight: '600',
},
normalQualityText: {
color: '#065F46',
},
lowQualityText: {
color: '#DC2626',
},
stageCardProgress: {
height: 6,
backgroundColor: '#E5E7EB',
borderRadius: 3,
overflow: 'hidden',
},
stageCardProgressBar: {
height: '100%',
borderRadius: 3,
},
// Sleep Stages Modal 样式已移动到独立组件中
// 睡眠时间标签样式
sleepTimeLabels: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16,
},
sleepTimeLabel: {
alignItems: 'center',
},
sleepTimeText: {
fontSize: 12,
fontWeight: '500',
marginBottom: 4,
},
sleepTimeValue: {
fontSize: 16,
fontWeight: '700',
letterSpacing: -0.2,
},
// 调试信息样式
debugContainer: {
marginHorizontal: 20,
marginBottom: 20,
padding: 16,
borderRadius: 12,
borderWidth: 1,
borderColor: 'rgba(0, 0, 0, 0.1)',
},
debugTitle: {
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
},
debugText: {
fontSize: 12,
lineHeight: 16,
marginBottom: 4,
},
// Raw Sleep Samples List 样式
rawSamplesContainer: {
borderRadius: 16,
padding: 16,
marginBottom: 24,
marginHorizontal: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 3,
},
rawSamplesHeader: {
marginBottom: 16,
},
rawSamplesTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
rawSamplesSubtitle: {
fontSize: 12,
fontWeight: '500',
},
rawSamplesScrollView: {
maxHeight: 400, // 限制高度,避免列表过长
},
rawSampleItem: {
paddingVertical: 12,
paddingHorizontal: 16,
borderLeftWidth: 3,
borderLeftColor: 'transparent',
marginBottom: 8,
borderRadius: 8,
backgroundColor: 'rgba(248, 250, 252, 0.5)',
},
sampleHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
sampleLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
stageDot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 8,
},
sampleStage: {
fontSize: 14,
fontWeight: '500',
flex: 1,
},
sampleDuration: {
fontSize: 12,
fontWeight: '600',
},
sampleTimeRange: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
sampleTime: {
fontSize: 12,
},
sampleIndex: {
fontSize: 10,
fontWeight: '500',
},
gapIndicator: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 8,
paddingHorizontal: 12,
marginVertical: 4,
borderRadius: 8,
gap: 6,
},
gapText: {
fontSize: 12,
fontWeight: '600',
},
});