feat: 更新睡眠详情页面,集成真实睡眠数据生成逻辑,优化睡眠阶段图表展示,添加睡眠样本数据处理功能,提升用户体验

This commit is contained in:
richarjiang
2025-09-08 19:26:02 +08:00
parent bf3304eb06
commit 1de4b9fe4c
3 changed files with 270 additions and 48 deletions

View File

@@ -25,6 +25,7 @@ import {
formatTime,
getSleepStageColor,
getSleepStageDisplayName,
convertSleepSamplesToIntervals,
SleepDetailData,
SleepStage
} from '@/services/sleepService';
@@ -81,65 +82,149 @@ const CircularProgress = ({
// 睡眠阶段图表组件
const SleepStageChart = ({ sleepData }: { sleepData: SleepDetailData }) => {
const chartWidth = width - 80;
const maxHeight = 120;
const chartHeight = 120;
const coreBaselineHeight = chartHeight * 0.6; // 核心睡眠作为基准线
const blockHeight = 20; // 每个睡眠阶段块的固定高度
// 生成24小时的睡眠阶段数据模拟数据实际应根据真实样本计算
const hourlyData = Array.from({ length: 24 }, (_, hour) => {
// 如果没有数据,显示空状态
// 使用真实的 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 null;
return { startTime: '--:--', endTime: '--:--' };
}
// 根据时间判断可能的睡眠状态,包括清醒时间段
if (hour >= 0 && hour <= 6) {
// 凌晨0-6点主要睡眠时间包含一些清醒时段
if (hour <= 1) return SleepStage.Core;
if (hour === 2) return SleepStage.Awake; // 添加清醒时间段
if (hour <= 4) return SleepStage.Deep;
if (hour === 5) return SleepStage.Awake; // 添加清醒时间段
return SleepStage.REM;
} else if (hour >= 22) {
// 晚上10点后开始入睡
if (hour === 23) return SleepStage.Awake; // 入睡前的清醒时间
return SleepStage.Core;
}
return null; // 白天清醒时间
});
return {
startTime: formatTime(sleepData.bedtime),
endTime: formatTime(sleepData.wakeupTime)
};
};
const { startTime, endTime } = getTimeLabels();
return (
<View style={styles.chartContainer}>
<View style={styles.chartHeader}>
<View style={styles.chartTimeLabel}>
<Text style={styles.chartTimeText}>🛏 {sleepData.totalSleepTime > 0 ? formatTime(sleepData.bedtime) : '--:--'}</Text>
<Text style={styles.chartTimeText}>🛏 {startTime}</Text>
</View>
<View style={styles.chartHeartRate}>
<Text style={styles.chartHeartRateText}> : {sleepData.averageHeartRate || '--'} BPM</Text>
</View>
<View style={styles.chartTimeLabel}>
<Text style={styles.chartTimeText}> {sleepData.totalSleepTime > 0 ? formatTime(sleepData.wakeupTime) : '--:--'}</Text>
<Text style={styles.chartTimeText}> {endTime}</Text>
</View>
</View>
<View style={styles.chartBars}>
{hourlyData.map((stage, index) => {
const barHeight = stage ? Math.random() * 0.6 + 0.4 : 0.1; // 随机高度模拟真实数据
const color = stage ? getSleepStageColor(stage) : '#E5E7EB';
{/* 分层睡眠阶段图表 */}
<View style={[styles.layeredChartContainer, { height: chartHeight }]}>
{sleepDataPoints.map((dataPoint, index) => {
const blockWidth = chartWidth / sleepDataPoints.length - 1;
const yPosition = getStageYPosition(dataPoint.stage);
const color = getSleepStageColor(dataPoint.stage);
return (
<View
key={index}
style={[
styles.chartBar,
styles.sleepBlock,
{
height: barHeight * maxHeight,
width: blockWidth,
height: blockHeight,
backgroundColor: color,
width: chartWidth / 24 - 2,
position: 'absolute',
left: index * (blockWidth),
top: yPosition,
}
]}
/>
);
})}
</View>
{/* 时间刻度 */}
<View style={styles.chartTimeScale}>
<Text style={styles.chartTimeScaleText}>{startTime}</Text>
<Text style={styles.chartTimeScaleText}>{endTime}</Text>
</View>
</View>
);
};
@@ -161,8 +246,8 @@ const SleepGradeCard = ({
const getGradeColor = (grade: string) => {
switch (grade) {
case '低': return { bg: '#FECACA', text: '#DC2626' };
case '正常': return { bg: '#D1FAE5', text: '#065F46' };
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 };
@@ -180,7 +265,7 @@ const SleepGradeCard = ({
}
]}>
<View style={styles.gradeCardLeft}>
<Text style={[styles.gradeIcon, { color: colors.text }]}>{icon}</Text>
<Ionicons name={icon as any} size={16} color={colors.text} />
<Text style={[
styles.gradeText,
{ color: isActive ? colors.text : colorTokens.textSecondary }
@@ -203,12 +288,14 @@ const InfoModal = ({
visible,
onClose,
title,
type
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];
@@ -244,18 +331,39 @@ const InfoModal = ({
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: '⚠️', grade: '低', range: '< 6h', isActive: false },
{ icon: '', grade: '正常', range: '6h - 7h or > 9h', isActive: false },
{ icon: '', grade: '良好', range: '7h - 8h', isActive: true },
{ icon: '', grade: '优秀', range: '8h - 9h', isActive: false },
{ 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: '⚠️', grade: '较差', range: '< 55%', isActive: false },
{ icon: '', grade: '一般', range: '55% - 69%', isActive: false },
{ icon: '', grade: '良好', range: '70% - 84%', isActive: false },
{ icon: '', grade: '优秀', range: '85% - 100%', isActive: true },
{ 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;
@@ -326,6 +434,7 @@ export default function SleepDetailScreen() {
const [sleepData, setSleepData] = useState<SleepDetailData | 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: '',
@@ -376,6 +485,7 @@ export default function SleepDetailScreen() {
wakeupTime: new Date().toISOString(),
timeInBed: 0,
sleepStages: [],
rawSleepSamples: [], // 添加空的原始睡眠样本数据
averageHeartRate: null,
sleepHeartRateData: [],
sleepEfficiency: 0,
@@ -576,6 +686,7 @@ export default function SleepDetailScreen() {
onClose={() => setInfoModal({ ...infoModal, visible: false })}
title={infoModal.title}
type={infoModal.type}
sleepData={displayData}
/>
)}
</View>
@@ -788,6 +899,31 @@ const styles = StyleSheet.create({
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,
@@ -939,9 +1075,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
gap: 8,
},
gradeIcon: {
fontSize: 16,
},
gradeText: {
fontSize: 16,
fontWeight: '600',
@@ -952,4 +1085,16 @@ const styles = StyleSheet.create({
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',
},
});