feat: Update Podfile.lock to include NitroModules and ReactNativeHealthkit dependencies
fix: Adjust objectVersion in project.pbxproj and improve WaterWidget folder exception handling refactor: Remove sleepService.ts as part of code cleanup chore: Comment out HealthKit initialization in health.ts and clean up fetchSleepDuration function
This commit is contained in:
@@ -1,508 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
// 找到入睡时间和起床时间
|
||||
// 使用所有样本数据来确定完整的睡眠周期(包含清醒时间)
|
||||
let bedtime: string;
|
||||
let wakeupTime: string;
|
||||
|
||||
if (sleepSamples.length > 0) {
|
||||
// 按开始时间排序
|
||||
const sortedSamples = sleepSamples.sort((a, b) =>
|
||||
new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
|
||||
);
|
||||
|
||||
// 入睡时间:第一个样本的开始时间(包含清醒时间,确保完整性)
|
||||
bedtime = sortedSamples[0].startDate;
|
||||
// 起床时间:最后一个样本的结束时间
|
||||
wakeupTime = sortedSamples[sortedSamples.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('没有找到睡眠样本数据');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算在床时间 - 使用 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);
|
||||
|
||||
// 详细的调试信息
|
||||
console.log('=== 睡眠数据处理结果 ===');
|
||||
console.log('时间范围:', dayjs(bedtime).format('HH:mm'), '-', dayjs(wakeupTime).format('HH:mm'));
|
||||
console.log('在床时间:', timeInBed, '分钟');
|
||||
console.log('总睡眠时间:', totalSleepTime, '分钟');
|
||||
console.log('睡眠效率:', sleepEfficiency, '%');
|
||||
console.log('睡眠阶段统计:');
|
||||
sleepStages.forEach(stage => {
|
||||
console.log(` ${getSleepStageDisplayName(stage.stage)}: ${stage.duration}分钟 (${stage.percentage}%)`);
|
||||
});
|
||||
|
||||
console.log('========================');
|
||||
|
||||
const sleepDetailData: SleepDetailData = {
|
||||
sleepScore,
|
||||
totalSleepTime,
|
||||
sleepQualityPercentage: sleepScore, // 使用睡眠得分作为质量百分比
|
||||
bedtime,
|
||||
wakeupTime,
|
||||
timeInBed,
|
||||
sleepStages,
|
||||
rawSleepSamples: sleepSamples, // 保存原始睡眠样本数据
|
||||
averageHeartRate,
|
||||
sleepHeartRateData,
|
||||
sleepEfficiency,
|
||||
qualityDescription: qualityInfo.description,
|
||||
recommendation: qualityInfo.recommendation
|
||||
};
|
||||
|
||||
console.log('睡眠详情数据获取完成,睡眠得分:', sleepScore);
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user