Files
digital-pilates/utils/sleepHealthKit.ts
2025-09-18 09:51:37 +08:00

515 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 HealthKitManager, { HealthKitUtils } from './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;
};
// 完整睡眠详情数据
export type CompleteSleepData = {
sleepScore: number;
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;
};
/**
* 创建睡眠日期范围 (从前一天 18:00 到当天 12:00)
*/
const createSleepDateRange = (date: Date): { startDate: Date; endDate: Date } => {
return {
startDate: dayjs(date).subtract(1, 'day').hour(18).minute(0).second(0).millisecond(0).toDate(),
endDate: dayjs(date).hour(12).minute(0).second(0).millisecond(0).toDate()
};
};
/**
* 映射 HealthKit 睡眠值到自定义枚举
* react-native-health 库返回的睡眠分析值是字符串:
* "INBED" = InBed (在床时间)
* "ASLEEP" = Asleep (睡着但未分阶段)
* "AWAKE" = Awake (醒来时间)
* "CORE" = Core (核心睡眠/浅度睡眠)
* "DEEP" = Deep (深度睡眠)
* "REM" = REM (快速眼动睡眠)
*/
const mapHealthKitSleepValue = (value: string): SleepStage => {
switch (value) {
case 'INBED':
return SleepStage.InBed;
case 'ASLEEP':
return SleepStage.Asleep;
case 'AWAKE':
return SleepStage.Awake;
case 'CORE':
return SleepStage.Core;
case 'DEEP':
return SleepStage.Deep;
case 'REM':
return SleepStage.REM;
default:
console.warn(`[Sleep] 未识别的睡眠阶段值: ${value}`);
return SleepStage.Asleep;
}
};
/**
* 获取睡眠样本数据
*/
export const fetchSleepSamples = async (date: Date): Promise<SleepSample[]> => {
try {
const dateRange = createSleepDateRange(date);
console.log('[Sleep] 查询睡眠数据范围:', {
startDate: dayjs(dateRange.startDate).format('YYYY-MM-DD HH:mm:ss'),
endDate: dayjs(dateRange.endDate).format('YYYY-MM-DD HH:mm:ss')
});
const options = {
startDate: dateRange.startDate.toISOString(),
endDate: dateRange.endDate.toISOString(),
};
// return new Promise((resolve) => {
// HealthKitManager.(options, (error: string, results: any[]) => {
// if (error) {
// console.error('[Sleep] 获取睡眠数据失败:', error);
// resolve([]); // 返回空数组而非拒绝,以便于处理
// return;
// }
// if (!results || results.length === 0) {
// console.warn('[Sleep] 未找到睡眠数据');
// resolve([]);
// return;
// }
// console.log('[Sleep] 获取到原始睡眠样本:', results.length, '条');
// // 过滤并转换数据格式
// const sleepSamples: SleepSample[] = results
// .filter(sample => {
// const sampleStart = new Date(sample.startDate).getTime();
// const sampleEnd = new Date(sample.endDate).getTime();
// const rangeStart = dateRange.startDate.getTime();
// const rangeEnd = dateRange.endDate.getTime();
// return (sampleStart >= rangeStart && sampleStart < rangeEnd) ||
// (sampleStart < rangeEnd && sampleEnd > rangeStart);
// })
// .map(sample => {
// console.log('[Sleep] 原始睡眠样本:', {
// startDate: sample.startDate,
// endDate: sample.endDate,
// value: sample.value,
// sourceName: sample.sourceName
// });
// return {
// startDate: sample.startDate,
// endDate: sample.endDate,
// value: mapHealthKitSleepValue(sample.value),
// sourceName: sample.sourceName,
// sourceId: sample.sourceId || sample.uuid
// };
// });
// console.log('[Sleep] 过滤后的睡眠样本:', sleepSamples.length, '条');
// resolve(sleepSamples);
// });
// });
} catch (error) {
console.error('[Sleep] 获取睡眠样本失败:', error);
return [];
}
};
/**
* 获取睡眠期间心率数据
*/
export const fetchSleepHeartRateData = async (bedtime: string, wakeupTime: string): Promise<HeartRateData[]> => {
try {
const options = {
startDate: bedtime,
endDate: wakeupTime,
};
// return new Promise((resolve) => {
// HealthKit.getHeartRateSamples(options, (error: string, results: any[]) => {
// if (error) {
// console.error('[Sleep] 获取心率数据失败:', error);
// resolve([]);
// return;
// }
// if (!results || results.length === 0) {
// console.log('[Sleep] 未找到心率数据');
// resolve([]);
// return;
// }
// const heartRateData: HeartRateData[] = results
// .filter(sample => {
// const sampleTime = new Date(sample.startDate).getTime();
// const bedtimeMs = new Date(bedtime).getTime();
// const wakeupTimeMs = new Date(wakeupTime).getTime();
// return sampleTime >= bedtimeMs && sampleTime <= wakeupTimeMs;
// })
// .map(sample => ({
// timestamp: sample.startDate,
// value: Math.round(sample.value)
// }))
// .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
// console.log('[Sleep] 获取到睡眠心率数据:', heartRateData.length, '个样本');
// resolve(heartRateData);
// });
// });
} catch (error) {
console.error('[Sleep] 获取睡眠心率数据失败:', error);
return [];
}
};
/**
* 计算睡眠阶段统计
*/
export const calculateSleepStageStats = (samples: SleepSample[]): SleepStageStats[] => {
console.log('[Sleep] 开始计算睡眠阶段统计,原始样本数:', samples.length);
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');
console.log(`[Sleep] 阶段: ${sample.value}, 持续时间: ${duration}分钟`);
const currentDuration = stageMap.get(sample.value) || 0;
stageMap.set(sample.value, currentDuration + duration);
});
console.log('[Sleep] 阶段时间统计:', Array.from(stageMap.entries()));
// 计算实际睡眠时间(排除在床时间,但包含醒来时间)
const actualSleepTime = Array.from(stageMap.entries())
.filter(([stage]) => stage !== SleepStage.InBed)
.reduce((total, [, duration]) => total + duration, 0);
console.log('[Sleep] 实际睡眠时间(包含醒来):', actualSleepTime, '分钟');
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.Asleep:
// 未分阶段的睡眠时间,按中等质量处理
quality = SleepQuality.Fair;
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
});
console.log(`[Sleep] 阶段统计: ${stage}, 时长: ${duration}分钟, 百分比: ${Math.round(percentage)}%, 质量: ${quality}`);
});
const sortedStats = stats.sort((a, b) => b.duration - a.duration);
console.log('[Sleep] 最终睡眠阶段统计:', sortedStats);
return sortedStats;
};
/**
* 计算睡眠得分
*/
export const calculateSleepScore = (
sleepStages: SleepStageStats[],
sleepEfficiency: number,
totalSleepTime: number
): number => {
let score = 0;
// 睡眠时长得分 (30%)
const idealSleepHours = 8 * 60;
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));
};
/**
* 获取睡眠质量描述和建议
*/
export const 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 const fetchCompleteSleepData = async (date: Date): Promise<CompleteSleepData | null> => {
try {
console.log('[Sleep] 开始获取完整睡眠数据...', dayjs(date).format('YYYY-MM-DD'));
// 检查HealthKit是否可用
if (!HealthKitUtils.isAvailable()) {
console.log('HealthKit不可用可能运行在Android设备或模拟器上');
return null;
}
const res = await HealthKitManager.requestAuthorization()
const status = await HealthKitManager.getAuthorizationStatus()
console.log('status~~~', status);
// await ensureHealthPermissions()
// 获取睡眠样本
const sleepSamples = await fetchSleepSamples(date);
if (sleepSamples.length === 0) {
console.warn('[Sleep] 没有找到睡眠数据');
return null;
}
// 计算入睡和起床时间
const sortedSamples = sleepSamples.sort((a, b) =>
new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
);
const bedtime = sortedSamples[0].startDate;
const wakeupTime = sortedSamples[sortedSamples.length - 1].endDate;
console.log('[Sleep] 计算睡眠时间范围:');
console.log('- 入睡时间:', dayjs(bedtime).format('YYYY-MM-DD HH:mm:ss'));
console.log('- 起床时间:', dayjs(wakeupTime).format('YYYY-MM-DD HH:mm:ss'));
// 计算在床时间
let timeInBed: number;
const inBedSamples = sleepSamples.filter(sample => sample.value === SleepStage.InBed);
if (inBedSamples.length > 0) {
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('[Sleep] 在床时间:', timeInBed, '分钟');
} else {
timeInBed = dayjs(wakeupTime).diff(dayjs(bedtime), 'minute');
console.log('[Sleep] 使用睡眠时间作为在床时间:', timeInBed, '分钟');
}
// 计算睡眠阶段统计
const sleepStages = calculateSleepStageStats(sleepSamples);
// 计算总睡眠时间(排除在床时间和醒来时间)
const actualSleepStages = sleepStages.filter(stage =>
stage.stage !== SleepStage.InBed
);
const totalSleepTime = actualSleepStages.reduce((total, stage) => total + stage.duration, 0);
// 重新计算睡眠效率
const sleepEfficiency = timeInBed > 0 ? Math.round((totalSleepTime / timeInBed) * 100) : 0;
console.log('[Sleep] 睡眠效率计算:');
console.log('- 总睡眠时间(不含醒来):', totalSleepTime, '分钟');
console.log('- 在床时间:', timeInBed, '分钟');
console.log('- 睡眠效率:', sleepEfficiency, '%');
// 获取睡眠期间心率数据
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);
console.log('[Sleep] 睡眠数据处理完成:');
console.log('- 总睡眠时间:', totalSleepTime, '分钟');
console.log('- 睡眠效率:', sleepEfficiency, '%');
console.log('- 睡眠得分:', sleepScore);
return {
sleepScore,
totalSleepTime,
sleepQualityPercentage: sleepScore,
bedtime,
wakeupTime,
timeInBed,
sleepStages,
rawSleepSamples: sleepSamples,
averageHeartRate,
sleepHeartRateData,
sleepEfficiency,
qualityDescription: qualityInfo.description,
recommendation: qualityInfo.recommendation
};
} catch (error) {
console.error('[Sleep] 获取完整睡眠数据失败:', error);
return null;
}
};
/**
* 工具函数:格式化睡眠时间显示
*/
export const 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`;
}
};
/**
* 工具函数:格式化时间显示
*/
export const formatTime = (dateString: string): string => {
return dayjs(dateString).format('HH:mm');
};
/**
* 工具函数:获取睡眠阶段颜色
*/
export const 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';
}
};