Files
digital-pilates/app/sleep-detail.tsx
richarjiang a84c026599 feat(ui): 更新应用品牌名称为 Out Live 并优化睡眠详情页默认数据展示
- 将 Sealife 更名为 Out Live(登录页、隐私弹窗)
- 睡眠详情页无数据时显示 "--" 替代固定默认值
- 移除睡眠阶段卡片中的质量标签与总览徽章
- 修复体重历史卡片依赖监听字段与跳转路由
- 调整喝水提醒后台任务时间范围为 8-21 点
- 标签栏按钮新增 activeOpacity=1 禁用点击透明度变化
2025-09-12 09:59:01 +08:00

951 lines
26 KiB
TypeScript
Raw Permalink 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 { ThemedView } from '@/components/ThemedView';
import {
fetchCompleteSleepData,
formatSleepTime,
formatTime,
getSleepStageColor,
SleepStage,
type CompleteSleepData
} from '@/utils/sleepHealthKit';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { router, useLocalSearchParams } 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';
// 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 { date: dateParam } = useLocalSearchParams<{ date?: string }>();
const [selectedDate] = useState(() => {
if (dateParam) {
return dayjs(dateParam).toDate();
}
return 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 (
<ThemedView style={styles.container}>
{/* 顶部导航 */}
<HeaderBar
title={`${dayjs(selectedDate).isSame(dayjs(), 'day') ? '今天' : dayjs(selectedDate).format('M月DD日')}`}
onBack={() => router.back()}
transparent={true}
variant="default"
/>
<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) : '--'}
</Text>
</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}%` : '--%'}
</Text>
</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: 0, percentage: 0, quality: 'good' as any },
{ stage: SleepStage.REM, duration: 0, percentage: 0, quality: 'good' as any },
{ stage: SleepStage.Core, duration: 0, percentage: 0, quality: 'good' as any },
{ stage: SleepStage.Deep, duration: 0, percentage: 0, 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>
);
})}
</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 })}
/>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
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',
},
});