feat(health): 完善HealthKit权限管理和数据获取系统
- 重构权限管理,新增SimpleEventEmitter实现状态监听 - 实现完整的健身圆环数据获取(活动热量、锻炼时间、站立小时) - 优化组件状态管理,支持实时数据刷新和权限状态响应 - 新增useHealthPermissions Hook,简化权限状态管理 - 完善iOS原生代码,支持按小时统计健身数据 - 优化应用启动时权限初始化流程,避免启动弹窗 BREAKING CHANGE: FitnessRingsCard组件API变更,移除手动传参改为自动获取数据
This commit is contained in:
109
utils/SimpleEventEmitter.ts
Normal file
109
utils/SimpleEventEmitter.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* React Native兼容的EventEmitter实现
|
||||
*
|
||||
* 提供与Node.js EventEmitter相似的API,但专门为React Native环境设计
|
||||
* 避免了对Node.js内置模块的依赖
|
||||
*/
|
||||
export class SimpleEventEmitter {
|
||||
private listeners: { [event: string]: ((...args: any[]) => void)[] } = {};
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
* @param event 事件名称
|
||||
* @param listener 监听器函数
|
||||
*/
|
||||
on(event: string, listener: (...args: any[]) => void): void {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event].push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听器
|
||||
* @param event 事件名称
|
||||
* @param listener 要移除的监听器函数
|
||||
*/
|
||||
off(event: string, listener: (...args: any[]) => void): void {
|
||||
if (!this.listeners[event]) return;
|
||||
|
||||
const index = this.listeners[event].indexOf(listener);
|
||||
if (index > -1) {
|
||||
this.listeners[event].splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
* @param event 事件名称
|
||||
* @param args 传递给监听器的参数
|
||||
*/
|
||||
emit(event: string, ...args: any[]): void {
|
||||
if (!this.listeners[event]) return;
|
||||
|
||||
// 复制监听器数组,避免在执行过程中数组被修改
|
||||
const listeners = [...this.listeners[event]];
|
||||
|
||||
listeners.forEach(listener => {
|
||||
try {
|
||||
listener(...args);
|
||||
} catch (error) {
|
||||
console.error(`Error in event listener for event "${event}":`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一次性事件监听器
|
||||
* @param event 事件名称
|
||||
* @param listener 监听器函数(只会执行一次)
|
||||
*/
|
||||
once(event: string, listener: (...args: any[]) => void): void {
|
||||
const onceWrapper = (...args: any[]) => {
|
||||
this.off(event, onceWrapper);
|
||||
listener(...args);
|
||||
};
|
||||
|
||||
this.on(event, onceWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定事件的所有监听器
|
||||
* @param event 事件名称(可选,如果未提供则移除所有事件的监听器)
|
||||
*/
|
||||
removeAllListeners(event?: string): void {
|
||||
if (event) {
|
||||
delete this.listeners[event];
|
||||
} else {
|
||||
this.listeners = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定事件的监听器数量
|
||||
* @param event 事件名称
|
||||
* @returns 监听器数量
|
||||
*/
|
||||
listenerCount(event: string): number {
|
||||
return this.listeners[event]?.length || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定事件的所有监听器
|
||||
* @param event 事件名称
|
||||
* @returns 监听器数组的副本
|
||||
*/
|
||||
listeners(event: string): ((...args: any[]) => void)[] {
|
||||
return this.listeners[event] ? [...this.listeners[event]] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有事件名称
|
||||
* @returns 事件名称数组
|
||||
*/
|
||||
eventNames(): string[] {
|
||||
return Object.keys(this.listeners);
|
||||
}
|
||||
}
|
||||
|
||||
export default SimpleEventEmitter;
|
||||
350
utils/health.ts
350
utils/health.ts
@@ -1,5 +1,6 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { NativeModules } from 'react-native';
|
||||
import { AppState, AppStateStatus, NativeModules } from 'react-native';
|
||||
import { SimpleEventEmitter } from './SimpleEventEmitter';
|
||||
|
||||
type HealthDataOptions = {
|
||||
startDate: string;
|
||||
@@ -9,6 +10,140 @@ type HealthDataOptions = {
|
||||
// React Native bridge to native HealthKitManager
|
||||
const { HealthKitManager } = NativeModules;
|
||||
|
||||
// HealthKit权限状态枚举
|
||||
export enum HealthPermissionStatus {
|
||||
Unknown = 'unknown',
|
||||
Authorized = 'authorized',
|
||||
Denied = 'denied',
|
||||
NotDetermined = 'notDetermined'
|
||||
}
|
||||
|
||||
// 权限状态管理类
|
||||
class HealthPermissionManager extends SimpleEventEmitter {
|
||||
private permissionStatus: HealthPermissionStatus = HealthPermissionStatus.Unknown;
|
||||
private isChecking: boolean = false;
|
||||
private lastCheckTime: number = 0;
|
||||
private checkInterval: number = 5000; // 5秒检查间隔,避免频繁检查
|
||||
private appStateSubscription: any = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.setupAppStateListener();
|
||||
}
|
||||
|
||||
// 设置应用状态监听
|
||||
private setupAppStateListener() {
|
||||
this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange.bind(this));
|
||||
}
|
||||
|
||||
// 处理应用状态变化
|
||||
private handleAppStateChange(nextAppState: AppStateStatus) {
|
||||
if (nextAppState === 'active') {
|
||||
// 应用回到前台时检查权限状态
|
||||
console.log('应用回到前台,检查HealthKit权限状态...');
|
||||
this.checkPermissionStatus(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前权限状态
|
||||
public getPermissionStatus(): HealthPermissionStatus {
|
||||
return this.permissionStatus;
|
||||
}
|
||||
|
||||
// 设置权限状态
|
||||
private setPermissionStatus(status: HealthPermissionStatus, shouldEmit: boolean = true) {
|
||||
const oldStatus = this.permissionStatus;
|
||||
this.permissionStatus = status;
|
||||
|
||||
if (shouldEmit && oldStatus !== status) {
|
||||
console.log(`HealthKit权限状态变化: ${oldStatus} -> ${status}`);
|
||||
this.emit('permissionStatusChanged', status, oldStatus);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查权限状态(通过尝试读取数据来间接判断)
|
||||
public async checkPermissionStatus(forceCheck: boolean = false): Promise<HealthPermissionStatus> {
|
||||
const now = Date.now();
|
||||
|
||||
// 避免频繁检查
|
||||
if (!forceCheck && this.isChecking) {
|
||||
return this.permissionStatus;
|
||||
}
|
||||
|
||||
if (!forceCheck && (now - this.lastCheckTime) < this.checkInterval) {
|
||||
return this.permissionStatus;
|
||||
}
|
||||
|
||||
this.isChecking = true;
|
||||
this.lastCheckTime = now;
|
||||
|
||||
try {
|
||||
// 尝试获取简单的步数数据来检测权限
|
||||
const today = new Date();
|
||||
const options = {
|
||||
startDate: dayjs(today).startOf('day').toDate().toISOString(),
|
||||
endDate: dayjs(today).endOf('day').toDate().toISOString()
|
||||
};
|
||||
|
||||
const result = await HealthKitManager.getStepCount(options);
|
||||
|
||||
if (result && result.totalValue !== undefined) {
|
||||
// 能够获取数据,说明有权限
|
||||
this.setPermissionStatus(HealthPermissionStatus.Authorized);
|
||||
} else if (result && result.error) {
|
||||
// 有错误返回,可能是权限被拒绝
|
||||
this.setPermissionStatus(HealthPermissionStatus.Denied);
|
||||
} else {
|
||||
// 其他情况
|
||||
this.setPermissionStatus(HealthPermissionStatus.Unknown);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('HealthKit权限检查失败,可能是权限被拒绝:', error);
|
||||
this.setPermissionStatus(HealthPermissionStatus.Denied);
|
||||
} finally {
|
||||
this.isChecking = false;
|
||||
}
|
||||
|
||||
return this.permissionStatus;
|
||||
}
|
||||
|
||||
// 请求权限
|
||||
public async requestPermission(): Promise<boolean> {
|
||||
try {
|
||||
console.log('开始请求HealthKit权限...');
|
||||
const result = await HealthKitManager.requestAuthorization();
|
||||
|
||||
if (result && result.success) {
|
||||
console.log('HealthKit权限请求成功');
|
||||
this.setPermissionStatus(HealthPermissionStatus.Authorized);
|
||||
|
||||
// 权限获取成功后触发数据刷新事件
|
||||
this.emit('permissionGranted');
|
||||
return true;
|
||||
} else {
|
||||
console.error('HealthKit权限请求失败');
|
||||
this.setPermissionStatus(HealthPermissionStatus.Denied);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('HealthKit权限请求出现异常:', error);
|
||||
this.setPermissionStatus(HealthPermissionStatus.Denied);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理资源
|
||||
public destroy() {
|
||||
if (this.appStateSubscription) {
|
||||
this.appStateSubscription.remove();
|
||||
}
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 全局权限管理实例
|
||||
export const healthPermissionManager = new HealthPermissionManager();
|
||||
|
||||
// Interface for activity summary data from HealthKit
|
||||
export interface HealthActivitySummary {
|
||||
activeEnergyBurned: number;
|
||||
@@ -86,23 +221,19 @@ export type TodayHealthData = {
|
||||
heartRate: number | null;
|
||||
};
|
||||
|
||||
// 更新:使用新的权限管理系统
|
||||
export async function ensureHealthPermissions(): Promise<boolean> {
|
||||
try {
|
||||
console.log('开始请求HealthKit权限...');
|
||||
const result = await HealthKitManager.requestAuthorization();
|
||||
return await healthPermissionManager.requestPermission();
|
||||
}
|
||||
|
||||
if (result && result.success) {
|
||||
console.log('HealthKit权限请求成功');
|
||||
console.log('权限状态:', result.permissions);
|
||||
return true;
|
||||
} else {
|
||||
console.error('HealthKit权限请求失败');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('HealthKit权限请求出现异常:', error);
|
||||
return false;
|
||||
}
|
||||
// 获取当前权限状态
|
||||
export function getHealthPermissionStatus(): HealthPermissionStatus {
|
||||
return healthPermissionManager.getPermissionStatus();
|
||||
}
|
||||
|
||||
// 检查权限状态
|
||||
export async function checkHealthPermissionStatus(forceCheck: boolean = false): Promise<HealthPermissionStatus> {
|
||||
return await healthPermissionManager.checkPermissionStatus(forceCheck);
|
||||
}
|
||||
|
||||
// 日期工具函数
|
||||
@@ -218,36 +349,105 @@ export async function fetchHourlyStepSamples(date: Date): Promise<HourlyStepData
|
||||
}
|
||||
}
|
||||
|
||||
// 获取每小时活动热量数据(简化实现)
|
||||
async function fetchHourlyActiveCalories(_date: Date): Promise<HourlyActivityData[]> {
|
||||
// 获取每小时活动热量数据
|
||||
async function fetchHourlyActiveCalories(date: Date): Promise<HourlyActivityData[]> {
|
||||
try {
|
||||
// For now, return default data as hourly data is complex and not critical for basic fitness rings
|
||||
console.log('每小时活动热量获取暂未实现,返回默认数据');
|
||||
return Array.from({ length: 24 }, (_, i) => ({ hour: i, calories: 0 }));
|
||||
const options = createDateRange(date);
|
||||
const result = await HealthKitManager.getHourlyActiveEnergyBurned(options);
|
||||
|
||||
if (result && result.data && Array.isArray(result.data)) {
|
||||
logSuccess('每小时活动热量', result);
|
||||
|
||||
// 初始化24小时数据
|
||||
const hourlyData: HourlyActivityData[] = Array.from({ length: 24 }, (_, i) => ({
|
||||
hour: i,
|
||||
calories: 0
|
||||
}));
|
||||
|
||||
// 将API返回的数据映射到对应的小时
|
||||
result.data.forEach((sample: any) => {
|
||||
if (sample && sample.hour !== undefined && sample.value !== undefined) {
|
||||
const hour = sample.hour;
|
||||
if (hour >= 0 && hour < 24) {
|
||||
hourlyData[hour].calories = Math.round(sample.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return hourlyData;
|
||||
} else {
|
||||
logWarning('每小时活动热量', '为空或格式错误');
|
||||
return Array.from({ length: 24 }, (_, i) => ({ hour: i, calories: 0 }));
|
||||
}
|
||||
} catch (error) {
|
||||
logError('每小时活动热量', error);
|
||||
return Array.from({ length: 24 }, (_, i) => ({ hour: i, calories: 0 }));
|
||||
}
|
||||
}
|
||||
|
||||
// 获取每小时锻炼分钟数据(简化实现)
|
||||
async function fetchHourlyExerciseMinutes(_date: Date): Promise<HourlyExerciseData[]> {
|
||||
// 获取每小时锻炼分钟数据
|
||||
async function fetchHourlyExerciseMinutes(date: Date): Promise<HourlyExerciseData[]> {
|
||||
try {
|
||||
// For now, return default data as hourly data is complex and not critical for basic fitness rings
|
||||
console.log('每小时锻炼分钟获取暂未实现,返回默认数据');
|
||||
return Array.from({ length: 24 }, (_, i) => ({ hour: i, minutes: 0 }));
|
||||
const options = createDateRange(date);
|
||||
const result = await HealthKitManager.getHourlyExerciseTime(options);
|
||||
|
||||
if (result && result.data && Array.isArray(result.data)) {
|
||||
logSuccess('每小时锻炼分钟', result);
|
||||
|
||||
// 初始化24小时数据
|
||||
const hourlyData: HourlyExerciseData[] = Array.from({ length: 24 }, (_, i) => ({
|
||||
hour: i,
|
||||
minutes: 0
|
||||
}));
|
||||
|
||||
// 将API返回的数据映射到对应的小时
|
||||
result.data.forEach((sample: any) => {
|
||||
if (sample && sample.hour !== undefined && sample.value !== undefined) {
|
||||
const hour = sample.hour;
|
||||
if (hour >= 0 && hour < 24) {
|
||||
hourlyData[hour].minutes = Math.round(sample.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return hourlyData;
|
||||
} else {
|
||||
logWarning('每小时锻炼分钟', '为空或格式错误');
|
||||
return Array.from({ length: 24 }, (_, i) => ({ hour: i, minutes: 0 }));
|
||||
}
|
||||
} catch (error) {
|
||||
logError('每小时锻炼分钟', error);
|
||||
return Array.from({ length: 24 }, (_, i) => ({ hour: i, minutes: 0 }));
|
||||
}
|
||||
}
|
||||
|
||||
// 获取每小时站立小时数据(简化实现)
|
||||
async function fetchHourlyStandHours(_date: Date): Promise<number[]> {
|
||||
// 获取每小时站立小时数据
|
||||
async function fetchHourlyStandHours(date: Date): Promise<number[]> {
|
||||
try {
|
||||
// For now, return default data as hourly data is complex and not critical for basic fitness rings
|
||||
console.log('每小时站立数据获取暂未实现,返回默认数据');
|
||||
return Array.from({ length: 24 }, () => 0);
|
||||
const options = createDateRange(date);
|
||||
const result = await HealthKitManager.getHourlyStandHours(options);
|
||||
|
||||
if (result && result.data && Array.isArray(result.data)) {
|
||||
logSuccess('每小时站立数据', result);
|
||||
|
||||
// 初始化24小时数据
|
||||
const hourlyData: number[] = Array.from({ length: 24 }, () => 0);
|
||||
|
||||
// 将API返回的数据映射到对应的小时
|
||||
result.data.forEach((sample: any) => {
|
||||
if (sample && sample.hour !== undefined && sample.value !== undefined) {
|
||||
const hour = sample.hour;
|
||||
if (hour >= 0 && hour < 24) {
|
||||
hourlyData[hour] = sample.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return hourlyData;
|
||||
} else {
|
||||
logWarning('每小时站立数据', '为空或格式错误');
|
||||
return Array.from({ length: 24 }, () => 0);
|
||||
}
|
||||
} catch (error) {
|
||||
logError('每小时站立数据', error);
|
||||
return Array.from({ length: 24 }, () => 0);
|
||||
@@ -314,15 +514,15 @@ async function fetchHeartRateVariability(options: HealthDataOptions): Promise<nu
|
||||
|
||||
async function fetchActivitySummary(options: HealthDataOptions): Promise<HealthActivitySummary | null> {
|
||||
try {
|
||||
// const result = await HealthKitManager.getActivitySummary(options);
|
||||
const result = await HealthKitManager.getActivitySummary(options);
|
||||
|
||||
// if (result && Array.isArray(result) && result.length > 0) {
|
||||
// logSuccess('ActivitySummary', result[0]);
|
||||
// return result[0];
|
||||
// } else {
|
||||
// logWarning('ActivitySummary', '为空');
|
||||
// return null;
|
||||
// }
|
||||
if (result && Array.isArray(result) && result.length > 0) {
|
||||
logSuccess('ActivitySummary', result[0]);
|
||||
return result[0];
|
||||
} else {
|
||||
logWarning('ActivitySummary', '为空');
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
logError('ActivitySummary', error);
|
||||
return null;
|
||||
@@ -366,8 +566,12 @@ async function fetchHeartRate(options: HealthDataOptions): Promise<number | null
|
||||
}
|
||||
|
||||
// 获取指定时间范围内的最大心率
|
||||
export async function fetchMaximumHeartRate(options: HealthDataOptions): Promise<number | null> {
|
||||
export async function fetchMaximumHeartRate(_options: HealthDataOptions): Promise<number | null> {
|
||||
try {
|
||||
// 暂未实现,返回null
|
||||
console.log('最大心率获取暂未实现');
|
||||
return null;
|
||||
|
||||
// const result = await HealthKitManager.getHeartRateSamples(options);
|
||||
|
||||
// if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
|
||||
@@ -536,10 +740,10 @@ export async function updateWeight(_weight: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function testOxygenSaturationData(date: Date = dayjs().toDate()): Promise<void> {
|
||||
export async function testOxygenSaturationData(_date: Date = dayjs().toDate()): Promise<void> {
|
||||
console.log('=== 开始测试血氧饱和度数据获取 ===');
|
||||
|
||||
const options = createDateRange(date);
|
||||
// const options = createDateRange(date);
|
||||
|
||||
try {
|
||||
// const result = await HealthKitManager.getOxygenSaturationSamples(options);
|
||||
@@ -697,3 +901,65 @@ export async function fetchActivityRingsForDate(date: Date): Promise<ActivityRin
|
||||
}
|
||||
}
|
||||
|
||||
// === 权限管理工具函数 ===
|
||||
|
||||
// 初始化健康权限管理(应在应用启动时调用)
|
||||
export function initializeHealthPermissions() {
|
||||
console.log('初始化HealthKit权限管理系统...');
|
||||
|
||||
// 延迟检查权限状态,避免应用启动时的性能影响
|
||||
setTimeout(() => {
|
||||
healthPermissionManager.checkPermissionStatus(true);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 监听权限状态变化(用于组件外部使用)
|
||||
export function addHealthPermissionListener(
|
||||
event: 'permissionStatusChanged' | 'permissionGranted',
|
||||
listener: (...args: any[]) => void
|
||||
) {
|
||||
healthPermissionManager.on(event, listener);
|
||||
}
|
||||
|
||||
// 移除权限状态监听器
|
||||
export function removeHealthPermissionListener(
|
||||
event: 'permissionStatusChanged' | 'permissionGranted',
|
||||
listener: (...args: any[]) => void
|
||||
) {
|
||||
healthPermissionManager.off(event, listener);
|
||||
}
|
||||
|
||||
// 清理权限管理资源(应在应用退出时调用)
|
||||
export function cleanupHealthPermissions() {
|
||||
console.log('清理HealthKit权限管理资源...');
|
||||
healthPermissionManager.destroy();
|
||||
}
|
||||
|
||||
// 获取权限状态的可读文本
|
||||
export function getPermissionStatusText(status: HealthPermissionStatus): string {
|
||||
switch (status) {
|
||||
case HealthPermissionStatus.Authorized:
|
||||
return '已授权';
|
||||
case HealthPermissionStatus.Denied:
|
||||
return '已拒绝';
|
||||
case HealthPermissionStatus.NotDetermined:
|
||||
return '未确定';
|
||||
case HealthPermissionStatus.Unknown:
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要显示权限请求UI
|
||||
export function shouldShowPermissionRequest(): boolean {
|
||||
const status = healthPermissionManager.getPermissionStatus();
|
||||
return status === HealthPermissionStatus.NotDetermined ||
|
||||
status === HealthPermissionStatus.Unknown;
|
||||
}
|
||||
|
||||
// 检查是否权限被用户拒绝
|
||||
export function isPermissionDenied(): boolean {
|
||||
const status = healthPermissionManager.getPermissionStatus();
|
||||
return status === HealthPermissionStatus.Denied;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
/**
|
||||
* HealthKit Native Module Usage Example
|
||||
* 展示如何使用HealthKit native module的示例代码
|
||||
*/
|
||||
|
||||
import HealthKitManager, { HealthKitUtils, SleepDataSample } from './healthKit';
|
||||
|
||||
export class HealthKitService {
|
||||
|
||||
/**
|
||||
* 初始化HealthKit并请求权限
|
||||
*/
|
||||
static async initializeHealthKit(): Promise<boolean> {
|
||||
try {
|
||||
// 检查HealthKit是否可用
|
||||
if (!HealthKitUtils.isAvailable()) {
|
||||
console.log('HealthKit不可用,可能运行在Android设备或模拟器上');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 请求授权
|
||||
const result = await HealthKitManager.requestAuthorization();
|
||||
|
||||
if (result.success) {
|
||||
console.log('HealthKit授权成功');
|
||||
console.log('权限状态:', result.permissions);
|
||||
|
||||
// 检查睡眠数据权限
|
||||
const sleepPermission = result.permissions['HKCategoryTypeIdentifierSleepAnalysis'];
|
||||
if (sleepPermission === 'authorized') {
|
||||
console.log('睡眠数据访问权限已获得');
|
||||
return true;
|
||||
} else {
|
||||
console.log('睡眠数据访问权限未获得:', sleepPermission);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.log('HealthKit授权失败');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('HealthKit初始化失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近的睡眠数据
|
||||
*/
|
||||
static async getRecentSleepData(days: number = 7): Promise<SleepDataSample[]> {
|
||||
try {
|
||||
const endDate = new Date();
|
||||
const startDate = new Date();
|
||||
startDate.setDate(endDate.getDate() - days);
|
||||
|
||||
const result = await HealthKitManager.getSleepData({
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
limit: 100
|
||||
});
|
||||
|
||||
console.log(`获取到 ${result.count} 条睡眠记录`);
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error('获取睡眠数据失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析睡眠质量
|
||||
*/
|
||||
static async analyzeSleepQuality(days: number = 7): Promise<any> {
|
||||
try {
|
||||
const sleepData = await this.getRecentSleepData(days);
|
||||
|
||||
if (sleepData.length === 0) {
|
||||
return {
|
||||
error: '没有找到睡眠数据',
|
||||
hasData: false
|
||||
};
|
||||
}
|
||||
|
||||
// 按日期分组
|
||||
const groupedData = HealthKitUtils.groupSamplesByDate(sleepData);
|
||||
const dates = Object.keys(groupedData).sort().reverse(); // 最新的日期在前
|
||||
|
||||
const analysis = dates.slice(0, days).map(date => {
|
||||
const daySamples = groupedData[date];
|
||||
const totalSleepDuration = HealthKitUtils.getTotalSleepDuration(daySamples, new Date(date));
|
||||
const qualityMetrics = HealthKitUtils.getSleepQualityMetrics(daySamples);
|
||||
|
||||
return {
|
||||
date,
|
||||
totalSleepDuration,
|
||||
totalSleepFormatted: HealthKitUtils.formatDuration(totalSleepDuration),
|
||||
qualityMetrics,
|
||||
samplesCount: daySamples.length
|
||||
};
|
||||
});
|
||||
|
||||
// 计算平均值
|
||||
const validDays = analysis.filter(day => day.totalSleepDuration > 0);
|
||||
const averageSleepDuration = validDays.length > 0
|
||||
? validDays.reduce((sum, day) => sum + day.totalSleepDuration, 0) / validDays.length
|
||||
: 0;
|
||||
|
||||
return {
|
||||
hasData: true,
|
||||
days: analysis,
|
||||
summary: {
|
||||
averageSleepDuration,
|
||||
averageSleepFormatted: HealthKitUtils.formatDuration(averageSleepDuration),
|
||||
daysWithData: validDays.length,
|
||||
totalDaysAnalyzed: days
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('睡眠质量分析失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
hasData: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取昨晚的睡眠数据
|
||||
*/
|
||||
static async getLastNightSleep(): Promise<any> {
|
||||
try {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
|
||||
// 设置查询范围:昨天下午6点到今天上午12点
|
||||
const startDate = new Date(yesterday);
|
||||
startDate.setHours(18, 0, 0, 0);
|
||||
|
||||
const endDate = new Date(today);
|
||||
endDate.setHours(12, 0, 0, 0);
|
||||
|
||||
const result = await HealthKitManager.getSleepData({
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
limit: 50
|
||||
});
|
||||
|
||||
if (result.data.length === 0) {
|
||||
return {
|
||||
hasData: false,
|
||||
message: '未找到昨晚的睡眠数据'
|
||||
};
|
||||
}
|
||||
|
||||
const sleepSamples = result.data.filter(sample =>
|
||||
['asleep', 'core', 'deep', 'rem'].includes(sample.categoryType)
|
||||
);
|
||||
|
||||
if (sleepSamples.length === 0) {
|
||||
return {
|
||||
hasData: false,
|
||||
message: '未找到有效的睡眠阶段数据'
|
||||
};
|
||||
}
|
||||
|
||||
// 找到睡眠的开始和结束时间
|
||||
const sleepStart = new Date(Math.min(...sleepSamples.map(s => new Date(s.startDate).getTime())));
|
||||
const sleepEnd = new Date(Math.max(...sleepSamples.map(s => new Date(s.endDate).getTime())));
|
||||
const totalDuration = sleepSamples.reduce((sum, s) => sum + s.duration, 0);
|
||||
|
||||
const qualityMetrics = HealthKitUtils.getSleepQualityMetrics(sleepSamples);
|
||||
|
||||
return {
|
||||
hasData: true,
|
||||
sleepStart: sleepStart.toISOString(),
|
||||
sleepEnd: sleepEnd.toISOString(),
|
||||
totalDuration,
|
||||
totalDurationFormatted: HealthKitUtils.formatDuration(totalDuration),
|
||||
qualityMetrics,
|
||||
samples: sleepSamples,
|
||||
bedTime: sleepStart.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
|
||||
wakeTime: sleepEnd.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取昨晚睡眠数据失败:', error);
|
||||
return {
|
||||
hasData: false,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
export const useHealthKitExample = async () => {
|
||||
console.log('=== HealthKit 使用示例 ===');
|
||||
|
||||
// 1. 初始化和授权
|
||||
const initialized = await HealthKitService.initializeHealthKit();
|
||||
if (!initialized) {
|
||||
console.log('HealthKit初始化失败,无法继续');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取昨晚的睡眠数据
|
||||
console.log('\n--- 昨晚睡眠数据 ---');
|
||||
const lastNightSleep = await HealthKitService.getLastNightSleep();
|
||||
if (lastNightSleep.hasData) {
|
||||
console.log(`睡眠时间: ${lastNightSleep.bedTime} - ${lastNightSleep.wakeTime}`);
|
||||
console.log(`睡眠时长: ${lastNightSleep.totalDurationFormatted}`);
|
||||
if (lastNightSleep.qualityMetrics) {
|
||||
console.log(`深睡眠: ${lastNightSleep.qualityMetrics.deepSleepPercentage.toFixed(1)}%`);
|
||||
console.log(`REM睡眠: ${lastNightSleep.qualityMetrics.remSleepPercentage.toFixed(1)}%`);
|
||||
}
|
||||
} else {
|
||||
console.log(lastNightSleep.message || '未找到睡眠数据');
|
||||
}
|
||||
|
||||
// 3. 分析最近一周的睡眠质量
|
||||
console.log('\n--- 最近一周睡眠分析 ---');
|
||||
const weeklyAnalysis = await HealthKitService.analyzeSleepQuality(7);
|
||||
if (weeklyAnalysis.hasData) {
|
||||
console.log(`平均睡眠时长: ${weeklyAnalysis.summary.averageSleepFormatted}`);
|
||||
console.log(`有数据的天数: ${weeklyAnalysis.summary.daysWithData}/${weeklyAnalysis.summary.totalDaysAnalyzed}`);
|
||||
|
||||
console.log('\n每日睡眠详情:');
|
||||
weeklyAnalysis.days.forEach((day: any) => {
|
||||
if (day.totalSleepDuration > 0) {
|
||||
console.log(`${day.date}: ${day.totalSleepFormatted}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(weeklyAnalysis.error || '睡眠分析失败');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user