513 lines
16 KiB
TypeScript
513 lines
16 KiB
TypeScript
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;
|
||
}
|
||
|
||
await HealthKitManager.requestAuthorization()
|
||
// 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';
|
||
}
|
||
}; |