diff --git a/app/sleep-detail.tsx b/app/sleep-detail.tsx index 3833bc8..4cb16e4 100644 --- a/app/sleep-detail.tsx +++ b/app/sleep-detail.tsx @@ -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 ( - 🛏️ {sleepData.totalSleepTime > 0 ? formatTime(sleepData.bedtime) : '--:--'} + 🛏️ {startTime} ❤️ 平均心率: {sleepData.averageHeartRate || '--'} BPM - ☀️ {sleepData.totalSleepTime > 0 ? formatTime(sleepData.wakeupTime) : '--:--'} + ☀️ {endTime} - - {hourlyData.map((stage, index) => { - const barHeight = stage ? Math.random() * 0.6 + 0.4 : 0.1; // 随机高度模拟真实数据 - const color = stage ? getSleepStageColor(stage) : '#E5E7EB'; + {/* 分层睡眠阶段图表 */} + + {sleepDataPoints.map((dataPoint, index) => { + const blockWidth = chartWidth / sleepDataPoints.length - 1; + const yPosition = getStageYPosition(dataPoint.stage); + const color = getSleepStageColor(dataPoint.stage); return ( ); })} + + {/* 时间刻度 */} + + {startTime} + {endTime} + ); }; @@ -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 = ({ } ]}> - {icon} + 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(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} /> )} @@ -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', + }, }); \ No newline at end of file diff --git a/services/backgroundTaskManager.ts b/services/backgroundTaskManager.ts index 1606211..66074ce 100644 --- a/services/backgroundTaskManager.ts +++ b/services/backgroundTaskManager.ts @@ -163,10 +163,10 @@ async function executeBackgroundTasks(): Promise { } // 执行喝水提醒检查任务 - executeWaterReminderTask(); + await executeWaterReminderTask(); // 执行站立提醒检查任务 - executeStandReminderTask(); + await executeStandReminderTask(); console.log('后台任务执行完成'); } catch (error) { @@ -200,7 +200,9 @@ export class BackgroundTaskManager { try { // 注册后台任务 - const status = await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER); + const status = await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER, { + minimumInterval: 15, + }); console.log('[BackgroundTask] 配置状态:', status); diff --git a/services/sleepService.ts b/services/sleepService.ts index 3329412..8560808 100644 --- a/services/sleepService.ts +++ b/services/sleepService.ts @@ -57,6 +57,9 @@ export type SleepDetailData = { // 睡眠阶段统计 sleepStages: SleepStageStats[]; + // 原始睡眠样本数据(用于图表显示) + rawSleepSamples: SleepSample[]; + // 心率数据 averageHeartRate: number | null; // 平均心率 sleepHeartRateData: HeartRateData[]; // 睡眠期间心率数据 @@ -96,7 +99,19 @@ async function fetchSleepSamples(date: Date): Promise { return; } + // 添加详细日志,了解实际获取到的数据类型 console.log('获取到睡眠样本:', results.length); + console.log('睡眠样本详情:', results.map(r => ({ + value: r.value, + start: r.startDate?.substring(11, 16), + end: r.endDate?.substring(11, 16), + duration: `${Math.round((new Date(r.endDate).getTime() - new Date(r.startDate).getTime()) / 60000)}min` + }))); + + // 检查可用的睡眠阶段类型 + const uniqueValues = [...new Set(results.map(r => r.value))]; + console.log('可用的睡眠阶段类型:', uniqueValues); + resolve(results as unknown as SleepSample[]); }); }); @@ -274,7 +289,6 @@ export function getSleepStageColor(stage: SleepStage): string { case SleepStage.Core: return '#3B82F6'; // 蓝色 case SleepStage.REM: - return '#8B5CF6'; // 紫色 case SleepStage.Asleep: return '#06B6D4'; // 青色 case SleepStage.Awake: @@ -340,6 +354,7 @@ export async function fetchSleepDetailForDate(date: Date): Promise + sample.value !== SleepStage.InBed + ); + + if (sleepOnlySamples.length === 0) { + console.log('只有InBed数据,没有详细睡眠阶段数据'); + return []; + } + + console.log('处理睡眠阶段数据 - 样本数量:', sleepOnlySamples.length); + console.log('时间范围:', formatTime(bedtime), '-', formatTime(wakeupTime)); + + const startTime = dayjs(bedtime); + const endTime = dayjs(wakeupTime); + let currentTime = startTime.clone(); + + // 创建一个映射,用于快速查找每个时间点的睡眠阶段 + while (currentTime.isBefore(endTime)) { + const currentTimestamp = currentTime.toDate().getTime(); + + // 找到当前时间点对应的睡眠阶段 + let currentStage = SleepStage.Awake; // 默认为清醒 + + for (const sample of sleepOnlySamples) { + const sampleStart = new Date(sample.startDate).getTime(); + const sampleEnd = new Date(sample.endDate).getTime(); + + // 如果当前时间在这个样本的时间范围内 + if (currentTimestamp >= sampleStart && currentTimestamp < sampleEnd) { + currentStage = sample.value; + break; + } + } + + const timeStr = currentTime.format('HH:mm'); + data.push({ time: timeStr, stage: currentStage }); + + // 移动到下一个15分钟间隔 + currentTime = currentTime.add(15, 'minute'); + } + + console.log('生成的睡眠阶段间隔数据点数量:', data.length); + console.log('阶段分布:', data.reduce((acc, curr) => { + acc[curr.stage] = (acc[curr.stage] || 0) + 1; + return acc; + }, {} as Record)); + + return data; } \ No newline at end of file