feat(health): 完善HealthKit权限管理和数据获取系统

- 重构权限管理,新增SimpleEventEmitter实现状态监听
- 实现完整的健身圆环数据获取(活动热量、锻炼时间、站立小时)
- 优化组件状态管理,支持实时数据刷新和权限状态响应
- 新增useHealthPermissions Hook,简化权限状态管理
- 完善iOS原生代码,支持按小时统计健身数据
- 优化应用启动时权限初始化流程,避免启动弹窗

BREAKING CHANGE: FitnessRingsCard组件API变更,移除手动传参改为自动获取数据
This commit is contained in:
richarjiang
2025-09-19 14:16:11 +08:00
parent 184fb672b7
commit ccfccca7bc
11 changed files with 1044 additions and 360 deletions

View File

@@ -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;
}