Files
digital-pilates/services/sleepService.ts
2025-09-09 14:26:16 +08:00

506 lines
17 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 dayjs from 'dayjs';
import AppleHealthKit from 'react-native-health';
// 睡眠阶段枚举(与 HealthKit 保持一致)
export enum SleepStage {
InBed = 'INBED',
Asleep = 'ASLEEP',
Awake = 'AWAKE',
Core = 'CORE',
Deep = 'DEEP',
REM = 'REM'
}
// 睡眠质量评级
export enum SleepQuality {
Poor = 'poor', // 差
Fair = 'fair', // 一般
Good = 'good', // 良好
Excellent = 'excellent' // 优秀
}
// 睡眠样本数据类型
export type SleepSample = {
startDate: string;
endDate: string;
value: SleepStage;
sourceName?: string;
sourceId?: string;
};
// 睡眠阶段统计
export type SleepStageStats = {
stage: SleepStage;
duration: number; // 分钟
percentage: number; // 百分比
quality: SleepQuality;
};
// 心率数据类型
export type HeartRateData = {
timestamp: string;
value: number; // BPM
};
// 睡眠详情数据类型
export type SleepDetailData = {
// 基础睡眠信息
sleepScore: number; // 睡眠得分 0-100
totalSleepTime: number; // 总睡眠时间(分钟)
sleepQualityPercentage: number; // 睡眠质量百分比
// 睡眠时间信息
bedtime: string; // 上床时间
wakeupTime: string; // 起床时间
timeInBed: number; // 在床时间(分钟)
// 睡眠阶段统计
sleepStages: SleepStageStats[];
// 原始睡眠样本数据(用于图表显示)
rawSleepSamples: SleepSample[];
// 心率数据
averageHeartRate: number | null; // 平均心率
sleepHeartRateData: HeartRateData[]; // 睡眠期间心率数据
// 睡眠效率
sleepEfficiency: number; // 睡眠效率百分比 (总睡眠时间/在床时间)
// 建议和评价
qualityDescription: string; // 睡眠质量描述
recommendation: string; // 睡眠建议
};
// 日期范围工具函数
function createSleepDateRange(date: Date): { startDate: string; endDate: string } {
// 睡眠数据通常跨越两天从前一天18:00到当天12:00
return {
startDate: dayjs(date).subtract(1, 'day').hour(18).minute(0).second(0).millisecond(0).toISOString(),
endDate: dayjs(date).hour(12).minute(0).second(0).millisecond(0).toISOString()
};
}
// 获取睡眠样本数据
async function fetchSleepSamples(date: Date): Promise<SleepSample[]> {
return new Promise((resolve) => {
const options = createSleepDateRange(date);
AppleHealthKit.getSleepSamples(options, (err, results) => {
if (err) {
console.error('获取睡眠样本失败:', err);
resolve([]);
return;
}
if (!results || !Array.isArray(results)) {
console.warn('睡眠样本数据为空');
resolve([]);
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[]);
});
});
}
// 获取睡眠期间心率数据
async function fetchSleepHeartRateData(bedtime: string, wakeupTime: string): Promise<HeartRateData[]> {
return new Promise((resolve) => {
const options = {
startDate: bedtime,
endDate: wakeupTime,
ascending: true
};
AppleHealthKit.getHeartRateSamples(options, (err, results) => {
if (err) {
console.error('获取睡眠心率数据失败:', err);
resolve([]);
return;
}
if (!results || !Array.isArray(results)) {
resolve([]);
return;
}
const heartRateData: HeartRateData[] = results.map(sample => ({
timestamp: sample.startDate,
value: Math.round(sample.value)
}));
console.log('获取到睡眠心率数据:', heartRateData.length, '个样本');
resolve(heartRateData);
});
});
}
// 计算睡眠阶段统计
function calculateSleepStageStats(samples: SleepSample[]): SleepStageStats[] {
const stageMap = new Map<SleepStage, number>();
// 计算每个阶段的总时长
samples.forEach(sample => {
const startTime = dayjs(sample.startDate);
const endTime = dayjs(sample.endDate);
const duration = endTime.diff(startTime, 'minute');
const currentDuration = stageMap.get(sample.value) || 0;
stageMap.set(sample.value, currentDuration + duration);
});
// 计算实际睡眠时间(包括所有睡眠阶段,排除在床时间)
const actualSleepTime = Array.from(stageMap.entries())
.reduce((total, [, duration]) => total + duration, 0);
// 生成统计数据,包含所有睡眠阶段(包括清醒时间)
const stats: SleepStageStats[] = [];
stageMap.forEach((duration, stage) => {
// 只排除在床时间,保留清醒时间
if (stage === SleepStage.InBed) return;
const percentage = actualSleepTime > 0 ? (duration / actualSleepTime) * 100 : 0;
let quality: SleepQuality;
// 根据睡眠阶段和比例判断质量
switch (stage) {
case SleepStage.Deep:
quality = percentage >= 15 ? SleepQuality.Excellent :
percentage >= 10 ? SleepQuality.Good :
percentage >= 5 ? SleepQuality.Fair : SleepQuality.Poor;
break;
case SleepStage.REM:
quality = percentage >= 20 ? SleepQuality.Excellent :
percentage >= 15 ? SleepQuality.Good :
percentage >= 10 ? SleepQuality.Fair : SleepQuality.Poor;
break;
case SleepStage.Core:
quality = percentage >= 45 ? SleepQuality.Excellent :
percentage >= 35 ? SleepQuality.Good :
percentage >= 25 ? SleepQuality.Fair : SleepQuality.Poor;
break;
case SleepStage.Awake:
// 清醒时间越少越好
quality = percentage <= 5 ? SleepQuality.Excellent :
percentage <= 10 ? SleepQuality.Good :
percentage <= 15 ? SleepQuality.Fair : SleepQuality.Poor;
break;
default:
quality = SleepQuality.Fair;
}
stats.push({
stage,
duration,
percentage: Math.round(percentage),
quality
});
});
// 按持续时间排序
return stats.sort((a, b) => b.duration - a.duration);
}
// 计算睡眠得分
function calculateSleepScore(sleepStages: SleepStageStats[], sleepEfficiency: number, totalSleepTime: number): number {
let score = 0;
// 睡眠时长得分 (30分)
const idealSleepHours = 8 * 60; // 8小时
const sleepDurationScore = Math.min(30, (totalSleepTime / idealSleepHours) * 30);
score += sleepDurationScore;
// 睡眠效率得分 (25分)
const efficiencyScore = (sleepEfficiency / 100) * 25;
score += efficiencyScore;
// 深度睡眠得分 (25分)
const deepSleepStage = sleepStages.find(stage => stage.stage === SleepStage.Deep);
const deepSleepScore = deepSleepStage ? Math.min(25, (deepSleepStage.percentage / 20) * 25) : 0;
score += deepSleepScore;
// REM睡眠得分 (20分)
const remSleepStage = sleepStages.find(stage => stage.stage === SleepStage.REM);
const remSleepScore = remSleepStage ? Math.min(20, (remSleepStage.percentage / 25) * 20) : 0;
score += remSleepScore;
return Math.round(Math.min(100, score));
}
// 获取睡眠质量描述和建议
function getSleepQualityInfo(sleepScore: number): { description: string; recommendation: string } {
if (sleepScore >= 85) {
return {
description: '你身心愉悦并且精力充沛',
recommendation: '恭喜你获得优质的睡眠!如果你感到精力充沛,可以考虑中等强度的运动,以维持健康的生活方式,并进一步减轻压力,以获得最佳睡眠。'
};
} else if (sleepScore >= 70) {
return {
description: '睡眠质量良好,精神状态不错',
recommendation: '你的睡眠质量还不错,但还有改善空间。建议保持规律的睡眠时间,睡前避免使用电子设备,营造安静舒适的睡眠环境。'
};
} else if (sleepScore >= 50) {
return {
description: '睡眠质量一般,可能影响日间表现',
recommendation: '你的睡眠需要改善。建议制定固定的睡前例行程序,限制咖啡因摄入,确保卧室温度适宜,考虑进行轻度运动来改善睡眠质量。'
};
} else {
return {
description: '睡眠质量较差,建议重视睡眠健康',
recommendation: '你的睡眠质量需要严重关注。建议咨询医生或睡眠专家,检查是否有睡眠障碍,同时改善睡眠环境和习惯,避免睡前刺激性活动。'
};
}
}
// 获取睡眠阶段中文名称
export function getSleepStageDisplayName(stage: SleepStage): string {
switch (stage) {
case SleepStage.Deep:
return '深度';
case SleepStage.Core:
return '核心';
case SleepStage.REM:
return '快速眼动';
case SleepStage.Asleep:
return '浅睡';
case SleepStage.Awake:
return '清醒';
case SleepStage.InBed:
return '在床';
default:
return '未知';
}
}
// 获取睡眠质量颜色
export function getSleepStageColor(stage: SleepStage): string {
switch (stage) {
case SleepStage.Deep:
return '#3B82F6'; // 蓝色
case SleepStage.Core:
return '#8B5CF6'; // 紫色
case SleepStage.REM:
case SleepStage.Asleep:
return '#EC4899'; // 粉色
case SleepStage.Awake:
return '#F59E0B'; // 橙色
case SleepStage.InBed:
return '#6B7280'; // 灰色
default:
return '#9CA3AF';
}
}
// 主函数:获取完整的睡眠详情数据
export async function fetchSleepDetailForDate(date: Date): Promise<SleepDetailData | null> {
try {
console.log('开始获取睡眠详情数据...', date);
// 获取睡眠样本数据
const sleepSamples = await fetchSleepSamples(date);
if (sleepSamples.length === 0) {
console.warn('没有找到睡眠数据');
return null;
}
// 找到入睡时间和起床时间
// 过滤出实际睡眠阶段(排除在床时间和清醒时间)
const actualSleepSamples = sleepSamples.filter(sample =>
sample.value !== SleepStage.InBed && sample.value !== SleepStage.Awake
);
// 入睡时间:第一个实际睡眠阶段的开始时间
// 起床时间:最后一个实际睡眠阶段的结束时间
let bedtime: string;
let wakeupTime: string;
if (actualSleepSamples.length > 0) {
// 按开始时间排序
const sortedSleepSamples = actualSleepSamples.sort((a, b) =>
new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
);
bedtime = sortedSleepSamples[0].startDate;
wakeupTime = sortedSleepSamples[sortedSleepSamples.length - 1].endDate;
console.log('计算入睡和起床时间:');
console.log('- 入睡时间:', dayjs(bedtime).format('YYYY-MM-DD HH:mm:ss'));
console.log('- 起床时间:', dayjs(wakeupTime).format('YYYY-MM-DD HH:mm:ss'));
} else {
// 如果没有实际睡眠数据,回退到使用所有样本数据
console.warn('没有找到实际睡眠阶段数据,使用所有样本数据');
const sortedAllSamples = sleepSamples.sort((a, b) =>
new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
);
bedtime = sortedAllSamples[0].startDate;
wakeupTime = sortedAllSamples[sortedAllSamples.length - 1].endDate;
}
// 计算在床时间 - 使用 INBED 样本数据
let timeInBed: number;
const inBedSamples = sleepSamples.filter(sample => sample.value === SleepStage.InBed);
if (inBedSamples.length > 0) {
// 使用 INBED 样本计算在床时间
const sortedInBedSamples = inBedSamples.sort((a, b) =>
new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
);
const inBedStart = sortedInBedSamples[0].startDate;
const inBedEnd = sortedInBedSamples[sortedInBedSamples.length - 1].endDate;
timeInBed = dayjs(inBedEnd).diff(dayjs(inBedStart), 'minute');
console.log('在床时间计算:');
console.log('- 上床时间:', dayjs(inBedStart).format('YYYY-MM-DD HH:mm:ss'));
console.log('- 离床时间:', dayjs(inBedEnd).format('YYYY-MM-DD HH:mm:ss'));
console.log('- 在床时长:', timeInBed, '分钟');
} else {
// 如果没有 INBED 数据,使用睡眠时间作为在床时间
timeInBed = dayjs(wakeupTime).diff(dayjs(bedtime), 'minute');
console.log('没有INBED数据使用睡眠时间作为在床时间:', timeInBed, '分钟');
}
// 计算睡眠阶段统计
const sleepStages = calculateSleepStageStats(sleepSamples);
// 计算总睡眠时间
const totalSleepTime = sleepStages
.reduce((total, stage) => total + stage.duration, 0);
// 计算睡眠效率
const sleepEfficiency = timeInBed > 0 ? Math.round((totalSleepTime / timeInBed) * 100) : 0;
// 获取睡眠期间心率数据
const sleepHeartRateData = await fetchSleepHeartRateData(bedtime, wakeupTime);
// 计算平均心率
const averageHeartRate = sleepHeartRateData.length > 0 ?
Math.round(sleepHeartRateData.reduce((sum, data) => sum + data.value, 0) / sleepHeartRateData.length) :
null;
// 计算睡眠得分
const sleepScore = calculateSleepScore(sleepStages, sleepEfficiency, totalSleepTime);
// 获取质量描述和建议
const qualityInfo = getSleepQualityInfo(sleepScore);
const sleepDetailData: SleepDetailData = {
sleepScore,
totalSleepTime,
sleepQualityPercentage: sleepScore, // 使用睡眠得分作为质量百分比
bedtime,
wakeupTime,
timeInBed,
sleepStages,
rawSleepSamples: sleepSamples, // 保存原始睡眠样本数据
averageHeartRate,
sleepHeartRateData,
sleepEfficiency,
qualityDescription: qualityInfo.description,
recommendation: qualityInfo.recommendation
};
console.log('睡眠详情数据获取完成:', sleepDetailData);
return sleepDetailData;
} catch (error) {
console.error('获取睡眠详情数据失败:', error);
return null;
}
}
// 格式化睡眠时间显示
export function formatSleepTime(minutes: number): string {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours > 0 && mins > 0) {
return `${hours}h ${mins}m`;
} else if (hours > 0) {
return `${hours}h`;
} else {
return `${mins}m`;
}
}
// 格式化时间显示 (HH:MM)
export function formatTime(dateString: string): string {
return dayjs(dateString).format('HH:mm');
}
// 将睡眠样本数据转换为15分钟间隔的睡眠阶段数据
export function convertSleepSamplesToIntervals(sleepSamples: SleepSample[], bedtime: string, wakeupTime: string): { time: string; stage: SleepStage }[] {
const data: { time: string; stage: SleepStage }[] = [];
if (sleepSamples.length === 0) {
console.log('没有睡眠样本数据可用于图表显示');
return [];
}
// 过滤掉InBed阶段只保留实际睡眠阶段
const sleepOnlySamples = sleepSamples.filter(sample =>
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<string, number>));
return data;
}