feat: 添加睡眠详情页面,集成睡眠数据获取功能,优化健康数据权限管理,更新相关组件以支持睡眠统计和展示

This commit is contained in:
richarjiang
2025-09-08 09:54:33 +08:00
parent df7f04808e
commit e91283fe4e
14 changed files with 1186 additions and 261 deletions

View File

@@ -5,17 +5,11 @@ import * as BackgroundTask from 'expo-background-task';
import * as TaskManager from 'expo-task-manager';
import { TaskManagerTaskBody } from 'expo-task-manager';
/**
* 后台任务标识符
*/
export const BACKGROUND_TASK_IDS = {
WATER_REMINDER: 'water-reminder-task',
STAND_REMINDER: 'stand-reminder-task',
HEALTH_REMINDERS: 'background-health-reminders',
} as const;
const BACKGROUND_TASK_IDENTIFIER = 'background-task';
// 定义后台任务
TaskManager.defineTask(BACKGROUND_TASK_IDS.HEALTH_REMINDERS, async (body: TaskManagerTaskBody) => {
TaskManager.defineTask(BACKGROUND_TASK_IDENTIFIER, async (body: TaskManagerTaskBody) => {
try {
console.log('[BackgroundTask] 后台任务执行');
await executeBackgroundTasks();
@@ -209,9 +203,7 @@ export class BackgroundTaskManager {
try {
// 注册后台任务
const status = await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDS.HEALTH_REMINDERS, {
minimumInterval: 15, // 15分钟
});
const status = await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER);
console.log('[BackgroundTask] 配置状态:', status);
@@ -226,26 +218,13 @@ export class BackgroundTaskManager {
/**
* 启动后台任务
*/
async start(): Promise<void> {
try {
await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDS.HEALTH_REMINDERS, {
minimumInterval: 15,
});
console.log('后台任务已启动');
} catch (error) {
console.error('启动后台任务失败:', error);
}
}
/**
* 停止后台任务
*/
async stop(): Promise<void> {
try {
await BackgroundTask.unregisterTaskAsync(BACKGROUND_TASK_IDS.HEALTH_REMINDERS);
await BackgroundTask.unregisterTaskAsync(BACKGROUND_TASK_IDENTIFIER);
console.log('后台任务已停止');
} catch (error) {
console.error('停止后台任务失败:', error);
@@ -281,6 +260,11 @@ export class BackgroundTaskManager {
}
}
async triggerTaskForTesting(): Promise<void> {
await BackgroundTask.triggerTaskWorkerForTestingAsync();
}
/**
* 测试后台任务
*/
@@ -296,42 +280,6 @@ export class BackgroundTaskManager {
}
}
/**
* 注册喝水提醒后台任务
*/
async registerWaterReminderTask(): Promise<void> {
console.log('注册喝水提醒后台任务...');
try {
// 检查是否已经初始化
if (!this.isInitialized) {
await this.initialize();
}
// 启动后台任务
await this.start();
console.log('喝水提醒后台任务注册成功');
} catch (error) {
console.error('注册喝水提醒后台任务失败:', error);
throw error;
}
}
/**
* 取消喝水提醒后台任务
*/
async unregisterWaterReminderTask(): Promise<void> {
console.log('取消喝水提醒后台任务...');
try {
await this.stop();
console.log('喝水提醒后台任务已取消');
} catch (error) {
console.error('取消喝水提醒后台任务失败:', error);
throw error;
}
}
/**
* 获取最后一次后台检查时间
@@ -345,73 +293,6 @@ export class BackgroundTaskManager {
return null;
}
}
/**
* 注册站立提醒后台任务
*/
async registerStandReminderTask(): Promise<void> {
console.log('注册站立提醒后台任务...');
try {
// 检查是否已经初始化
if (!this.isInitialized) {
await this.initialize();
}
// 启动后台任务
await this.start();
console.log('站立提醒后台任务注册成功');
} catch (error) {
console.error('注册站立提醒后台任务失败:', error);
throw error;
}
}
/**
* 取消站立提醒后台任务
*/
async unregisterStandReminderTask(): Promise<void> {
console.log('取消站立提醒后台任务...');
try {
// 取消所有相关通知
await StandReminderHelpers.cancelStandReminders();
console.log('站立提醒后台任务已取消');
} catch (error) {
console.error('取消站立提醒后台任务失败:', error);
throw error;
}
}
/**
* 获取最后一次站立检查时间
*/
async getLastStandCheckTime(): Promise<number | null> {
try {
const lastCheck = await AsyncStorage.getItem('@last_background_stand_check');
return lastCheck ? parseInt(lastCheck) : null;
} catch (error) {
console.error('获取最后站立检查时间失败:', error);
return null;
}
}
/**
* 测试站立提醒任务
*/
async testStandReminderTask(): Promise<void> {
console.log('开始测试站立提醒后台任务...');
try {
// 手动触发站立提醒任务执行
await executeStandReminderTask();
console.log('站立提醒后台任务测试完成');
} catch (error) {
console.error('站立提醒后台任务测试失败:', error);
}
}
}
/**

376
services/sleepService.ts Normal file
View File

@@ -0,0 +1,376 @@
import dayjs from 'dayjs';
import AppleHealthKit, { HealthKitPermissions } 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[];
// 心率数据
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);
resolve(results 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 totalSleepTime = Array.from(stageMap.entries())
.filter(([stage]) => stage !== SleepStage.InBed && stage !== SleepStage.Awake)
.reduce((total, [, duration]) => total + duration, 0);
// 生成统计数据
const stats: SleepStageStats[] = [];
stageMap.forEach((duration, stage) => {
if (stage === SleepStage.InBed || stage === SleepStage.Awake) return;
const percentage = totalSleepTime > 0 ? (duration / totalSleepTime) * 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;
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 '#1E40AF'; // 深蓝色
case SleepStage.Core:
return '#3B82F6'; // 蓝色
case SleepStage.REM:
return '#8B5CF6'; // 紫色
case SleepStage.Asleep:
return '#06B6D4'; // 青色
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 inBedSamples = sleepSamples.filter(sample => sample.value === SleepStage.InBed);
const bedtime = inBedSamples.length > 0 ? inBedSamples[0].startDate : sleepSamples[0].startDate;
const wakeupTime = inBedSamples.length > 0 ?
inBedSamples[inBedSamples.length - 1].endDate :
sleepSamples[sleepSamples.length - 1].endDate;
// 计算在床时间
const timeInBed = dayjs(wakeupTime).diff(dayjs(bedtime), 'minute');
// 计算睡眠阶段统计
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,
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');
}