Files
digital-pilates/utils/health.ts
2025-09-18 22:40:05 +08:00

700 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import dayjs from 'dayjs';
import { NativeModules } from 'react-native';
type HealthDataOptions = {
startDate: string;
endDate: string;
};
// React Native bridge to native HealthKitManager
const { HealthKitManager } = NativeModules;
// Interface for activity summary data from HealthKit
export interface HealthActivitySummary {
activeEnergyBurned: number;
activeEnergyBurnedGoal: number;
appleExerciseTime: number;
appleExerciseTimeGoal: number;
appleStandHours: number;
appleStandHoursGoal: number;
dateComponents: {
day: number;
month: number;
year: number;
};
}
// const PERMISSIONS: HealthKitPermissions = {
// permissions: {
// read: [
// AppleHealthKit.Constants.Permissions.StepCount,
// AppleHealthKit.Constants.Permissions.ActiveEnergyBurned,
// AppleHealthKit.Constants.Permissions.BasalEnergyBurned,
// AppleHealthKit.Constants.Permissions.SleepAnalysis,
// AppleHealthKit.Constants.Permissions.HeartRateVariability,
// AppleHealthKit.Constants.Permissions.ActivitySummary,
// AppleHealthKit.Constants.Permissions.OxygenSaturation,
// AppleHealthKit.Constants.Permissions.HeartRate,
// AppleHealthKit.Constants.Permissions.Water,
// // 添加 Apple Exercise Time 和 Apple Stand Time 权限
// AppleHealthKit.Constants.Permissions.AppleExerciseTime,
// AppleHealthKit.Constants.Permissions.AppleStandTime,
// ],
// write: [
// // 支持体重写入
// AppleHealthKit.Constants.Permissions.Weight,
// // 支持饮水量写入
// AppleHealthKit.Constants.Permissions.Water,
// ],
// },
// };
export type HourlyStepData = {
hour: number; // 0-23
steps: number;
};
export type HourlyActivityData = {
hour: number; // 0-23
calories: number; // 活动热量
};
export type HourlyExerciseData = {
hour: number; // 0-23
minutes: number; // 锻炼分钟数
};
export type HourlyStandData = {
hour: number; // 0-23
hasStood: number; // 1表示该小时有站立0表示没有
};
export type TodayHealthData = {
activeEnergyBurned: number; // kilocalories
basalEnergyBurned: number; // kilocalories - 基础代谢率
hrv: number | null; // 心率变异性 (ms)
// 健身圆环数据
activeCalories: number;
activeCaloriesGoal: number;
exerciseMinutes: number;
exerciseMinutesGoal: number;
standHours: number;
standHoursGoal: number;
// 新增血氧饱和度和心率数据
oxygenSaturation: number | null;
heartRate: number | null;
};
export async function ensureHealthPermissions(): Promise<boolean> {
try {
console.log('开始请求HealthKit权限...');
const result = await HealthKitManager.requestAuthorization();
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;
}
}
// 日期工具函数
function createDateRange(date: Date): HealthDataOptions {
return {
startDate: dayjs(date).startOf('day').toDate().toISOString(),
endDate: dayjs(date).endOf('day').toDate().toISOString()
};
}
// Note: createSleepDateRange and calculateSleepDuration functions removed as unused
// 通用错误处理
function logError(operation: string, error: any): void {
console.error(`获取${operation}失败:`, error);
}
function logWarning(operation: string, message: string): void {
console.warn(`${operation}数据${message}`);
}
function logSuccess(operation: string, data: any): void {
console.log(`${operation}数据:`, data);
}
// 数值验证和转换
function validateOxygenSaturation(value: any): number | null {
if (value === undefined || value === null) return null;
let numValue = Number(value);
// 如果值小于1可能是小数形式0.0-1.0),需要转换为百分比
if (numValue > 0 && numValue < 1) {
numValue = numValue * 100;
}
// 血氧饱和度通常在0-100之间验证数据有效性
if (numValue >= 0 && numValue <= 100) {
return Number(numValue.toFixed(1));
}
console.warn('血氧饱和度数据异常:', numValue);
return null;
}
function validateHeartRate(value: any): number | null {
if (value === undefined || value === null) return null;
const numValue = Number(value);
// 心率通常在30-200之间验证数据有效性
if (numValue >= 30 && numValue <= 200) {
return Math.round(numValue);
}
console.warn('心率数据异常:', numValue);
return null;
}
// 健康数据获取函数
export async function fetchStepCount(date: Date): Promise<number> {
try {
const options = createDateRange(date);
const result = await HealthKitManager.getStepCount(options);
if (result && result.totalValue !== undefined) {
logSuccess('步数', result);
return Math.round(result.totalValue);
} else {
logWarning('步数', '为空或格式错误');
return 0;
}
} catch (error) {
logError('步数', error);
return 0;
}
}
// 使用样本数据获取每小时步数
export async function fetchHourlyStepSamples(date: Date): Promise<HourlyStepData[]> {
try {
const options = createDateRange(date);
const result = await HealthKitManager.getDailyStepCountSamples(options);
if (result && result.data && Array.isArray(result.data)) {
logSuccess('每小时步数样本', result);
// 初始化24小时数据
const hourlyData: HourlyStepData[] = Array.from({ length: 24 }, (_, i) => ({
hour: i,
steps: 0
}));
// 将每小时的步数样本数据映射到对应的小时
result.data.forEach((sample: any) => {
if (sample && sample.hour !== undefined && sample.value !== undefined) {
const hour = sample.hour;
if (hour >= 0 && hour < 24) {
hourlyData[hour].steps = Math.round(sample.value);
}
}
});
return hourlyData;
} else {
logWarning('每小时步数', '为空或格式错误');
return Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 }));
}
} catch (error) {
logError('每小时步数样本', error);
return Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 }));
}
}
// 获取每小时活动热量数据(简化实现)
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 }));
} catch (error) {
logError('每小时活动热量', error);
return Array.from({ length: 24 }, (_, i) => ({ hour: i, calories: 0 }));
}
}
// 获取每小时锻炼分钟数据(简化实现)
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 }));
} catch (error) {
logError('每小时锻炼分钟', error);
return Array.from({ length: 24 }, (_, i) => ({ hour: i, minutes: 0 }));
}
}
// 获取每小时站立小时数据(简化实现)
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);
} catch (error) {
logError('每小时站立数据', error);
return Array.from({ length: 24 }, () => 0);
}
}
async function fetchActiveEnergyBurned(options: HealthDataOptions): Promise<number> {
try {
const result = await HealthKitManager.getActiveEnergyBurned(options);
if (result && result.totalValue !== undefined) {
logSuccess('消耗卡路里', result);
return result.totalValue;
} else {
logWarning('卡路里', '为空或格式错误');
return 0;
}
} catch (error) {
logError('消耗卡路里', error);
return 0;
}
}
async function fetchBasalEnergyBurned(options: HealthDataOptions): Promise<number> {
try {
const result = await HealthKitManager.getBasalEnergyBurned(options);
if (result && result.totalValue !== undefined) {
logSuccess('基础代谢', result);
return result.totalValue;
} else {
logWarning('基础代谢', '为空或格式错误');
return 0;
}
} catch (error) {
logError('基础代谢', error);
return 0;
}
}
async function fetchHeartRateVariability(options: HealthDataOptions): Promise<number | null> {
try {
console.log('=== 开始获取HRV数据 ===');
console.log('查询选项:', options);
const result = await HealthKitManager.getHeartRateVariabilitySamples(options);
console.log('HRV API调用结果:', result);
if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
const hrvValue = result.data[0].value;
logSuccess('HRV数据', result);
return Math.round(hrvValue); // Value already in ms from native
} else {
logWarning('HRV', '为空或格式错误');
console.warn('HRV数据为空原始响应:', result);
return null;
}
} catch (error) {
logError('HRV数据', error);
console.error('HRV获取错误详情:', error);
return null;
}
}
async function fetchActivitySummary(options: HealthDataOptions): Promise<HealthActivitySummary | null> {
try {
// 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;
// }
} catch (error) {
logError('ActivitySummary', error);
return null;
}
}
async function fetchOxygenSaturation(options: HealthDataOptions): Promise<number | null> {
try {
const result = await HealthKitManager.getOxygenSaturationSamples(options);
if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
logSuccess('血氧饱和度', result);
const latestOxygen = result.data[result.data.length - 1];
return validateOxygenSaturation(latestOxygen?.value);
} else {
logWarning('血氧饱和度', '为空或格式错误');
return null;
}
} catch (error) {
logError('血氧饱和度', error);
return null;
}
}
async function fetchHeartRate(options: HealthDataOptions): Promise<number | null> {
try {
const result = await HealthKitManager.getHeartRateSamples(options);
if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
logSuccess('心率', result);
const latestHeartRate = result.data[result.data.length - 1];
return validateHeartRate(latestHeartRate?.value);
} else {
logWarning('心率', '为空或格式错误');
return null;
}
} catch (error) {
logError('心率', error);
return null;
}
}
// 获取指定时间范围内的最大心率
export async function fetchMaximumHeartRate(options: HealthDataOptions): Promise<number | null> {
try {
// const result = await HealthKitManager.getHeartRateSamples(options);
// if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
// // 从所有心率样本中找出最大值
// let maxHeartRate = 0;
// let validSamplesCount = 0;
// result.data.forEach((sample: any) => {
// if (sample && sample.value !== undefined) {
// const heartRate = validateHeartRate(sample.value);
// if (heartRate !== null) {
// maxHeartRate = Math.max(maxHeartRate, heartRate);
// validSamplesCount++;
// }
// }
// });
// if (validSamplesCount > 0 && maxHeartRate > 0) {
// logSuccess('最大心率', { maxHeartRate, validSamplesCount });
// return maxHeartRate;
// } else {
// logWarning('最大心率', '没有找到有效的样本数据');
// return null;
// }
// } else {
// logWarning('最大心率', '为空或格式错误');
// return null;
// }
} catch (error) {
logError('最大心率', error);
return null;
}
}
// 默认健康数据
function getDefaultHealthData(): TodayHealthData {
return {
activeEnergyBurned: 0,
basalEnergyBurned: 0,
hrv: null,
activeCalories: 0,
activeCaloriesGoal: 350,
exerciseMinutes: 0,
exerciseMinutesGoal: 30,
standHours: 0,
standHoursGoal: 12,
oxygenSaturation: null,
heartRate: null,
};
}
export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthData> {
try {
console.log('开始获取指定日期健康数据...', date);
const options = createDateRange(date);
console.log('查询选项:', options);
// 并行获取所有健康数据
const [
activeEnergyBurned,
basalEnergyBurned,
hrv,
activitySummary,
oxygenSaturation,
heartRate
] = await Promise.all([
fetchActiveEnergyBurned(options),
fetchBasalEnergyBurned(options),
fetchHeartRateVariability(options),
fetchActivitySummary(options),
fetchOxygenSaturation(options),
fetchHeartRate(options)
]);
return {
activeEnergyBurned,
basalEnergyBurned,
hrv,
activeCalories: Math.round(activitySummary?.activeEnergyBurned || 0),
activeCaloriesGoal: Math.round(activitySummary?.activeEnergyBurnedGoal || 350),
exerciseMinutes: Math.round(activitySummary?.appleExerciseTime || 0),
exerciseMinutesGoal: Math.round(activitySummary?.appleExerciseTimeGoal || 30),
standHours: Math.round(activitySummary?.appleStandHours || 0),
standHoursGoal: Math.round(activitySummary?.appleStandHoursGoal || 12),
oxygenSaturation,
heartRate
};
} catch (error) {
console.error('获取指定日期健康数据失败:', error);
return getDefaultHealthData();
}
}
export async function fetchTodayHealthData(): Promise<TodayHealthData> {
return fetchHealthDataForDate(dayjs().toDate());
}
export async function fetchHRVForDate(date: Date): Promise<number | null> {
console.log('开始获取指定日期HRV数据...', date);
const options = createDateRange(date);
return fetchHeartRateVariability(options);
}
export async function fetchTodayHRV(): Promise<number | null> {
return fetchHRVForDate(dayjs().toDate());
}
// 获取最近几小时内的实时HRV数据
export async function fetchRecentHRV(hoursBack: number = 2): Promise<number | null> {
console.log(`开始获取最近${hoursBack}小时内的HRV数据...`);
const now = new Date();
const options = {
startDate: dayjs(now).subtract(hoursBack, 'hour').toDate().toISOString(),
endDate: now.toISOString()
};
return fetchHeartRateVariability(options);
}
// 测试HRV数据获取功能
export async function testHRVDataFetch(date: Date = dayjs().toDate()): Promise<void> {
console.log('=== 开始测试HRV数据获取 ===');
try {
// 首先确保权限
const hasPermission = await ensureHealthPermissions();
if (!hasPermission) {
console.error('没有健康数据权限无法测试HRV');
return;
}
console.log('权限检查通过开始获取HRV数据...');
// 测试不同时间范围的HRV数据
const options = createDateRange(date);
// 获取今日HRV
const todayHRV = await fetchHeartRateVariability(options);
console.log('今日HRV结果:', todayHRV);
// 获取最近2小时HRV
const recentHRV = await fetchRecentHRV(2);
console.log('最近2小时HRV结果:', recentHRV);
// 获取指定日期HRV
const dateHRV = await fetchHRVForDate(date);
console.log('指定日期HRV结果:', dateHRV);
console.log('=== HRV数据测试完成 ===');
} catch (error) {
console.error('HRV测试过程中出现错误:', error);
}
}
// 更新healthkit中的体重 (暂未实现)
export async function updateWeight(_weight: number) {
try {
// Note: Weight saving would need to be implemented in native module
console.log('体重保存到HealthKit暂未实现');
return true; // Return true for now to not break existing functionality
} catch (error) {
console.error('更新体重失败:', error);
return false;
}
}
export async function testOxygenSaturationData(date: Date = dayjs().toDate()): Promise<void> {
console.log('=== 开始测试血氧饱和度数据获取 ===');
const options = createDateRange(date);
try {
// const result = await HealthKitManager.getOxygenSaturationSamples(options);
// console.log('原始血氧饱和度数据:', result);
// if (!result || !result.data || !Array.isArray(result.data) || result.data.length === 0) {
// console.warn('血氧饱和度数据为空');
// return;
// }
// // 分析所有数据样本
// result.data.forEach((sample: any, index: number) => {
// console.log(`样本 ${index + 1}:`, {
// value: sample.value,
// valueType: typeof sample.value,
// startDate: sample.startDate,
// endDate: sample.endDate
// });
// });
// // 获取最新的血氧饱和度值并验证
// const latestOxygen = result.data[result.data.length - 1];
// if (latestOxygen?.value !== undefined && latestOxygen?.value !== null) {
// const processedValue = validateOxygenSaturation(latestOxygen.value);
// console.log('处理前的值:', latestOxygen.value);
// console.log('最终处理后的值:', processedValue);
// console.log('数据有效性检查:', processedValue !== null ? '有效' : '无效');
// }
// console.log('=== 血氧饱和度数据测试完成 ===');
} catch (error) {
console.error('获取血氧饱和度失败:', error);
}
}
// 添加饮水记录到 HealthKit (暂未实现)
export async function saveWaterIntakeToHealthKit(_amount: number, _recordedAt?: string): Promise<boolean> {
try {
// Note: Water intake saving would need to be implemented in native module
console.log('饮水记录保存到HealthKit暂未实现');
return true; // Return true for now to not break existing functionality
} catch (error) {
console.error('添加饮水记录到 HealthKit 失败:', error);
return false;
}
}
// 获取 HealthKit 中的饮水记录 (暂未实现)
export async function getWaterIntakeFromHealthKit(_options: HealthDataOptions): Promise<any[]> {
try {
// Note: Water intake fetching would need to be implemented in native module
console.log('从HealthKit获取饮水记录暂未实现');
return [];
} catch (error) {
console.error('获取 HealthKit 饮水记录失败:', error);
return [];
}
}
// 删除 HealthKit 中的饮水记录
// 注意: react-native-health 库可能不支持直接删除特定记录,这个功能可能需要手动实现或使用其他方法
export async function deleteWaterIntakeFromHealthKit(recordId: string, recordedAt: string): Promise<boolean> {
// HealthKit 通常不支持直接删除单条记录
// 这是一个占位函数,实际实现可能需要更复杂的逻辑
console.log('注意: HealthKit 通常不支持直接删除单条饮水记录');
console.log('记录信息:', { recordId, recordedAt });
// 返回 true 表示"成功"(但实际上可能没有真正删除)
return Promise.resolve(true);
}
// 获取当前小时的站立状态
export async function getCurrentHourStandStatus(): Promise<{ hasStood: boolean; standHours: number; standHoursGoal: number }> {
try {
const currentHour = new Date().getHours();
console.log(`检查当前小时 ${currentHour} 的站立状态...`);
// 获取今日健康数据
const todayHealthData = await fetchTodayHealthData();
return {
hasStood: todayHealthData.standHours > currentHour - 1, // 如果站立小时数大于当前小时-1说明当前小时已站立
standHours: todayHealthData.standHours,
standHoursGoal: todayHealthData.standHoursGoal
};
} catch (error) {
console.error('获取当前小时站立状态失败:', error);
return {
hasStood: true, // 默认认为已站立,避免过度提醒
standHours: 0,
standHoursGoal: 12
};
}
}
// === 专门为健身圆环详情页提供的独立函数 ===
// 精简的活动圆环数据类型,只包含必要字段
export type ActivityRingsData = {
// 活动圆环数据(来自 getActivitySummary
activeEnergyBurned: number; // activeEnergyBurned
activeEnergyBurnedGoal: number; // activeEnergyBurnedGoal
appleExerciseTime: number; // appleExerciseTime (分钟)
appleExerciseTimeGoal: number; // appleExerciseTimeGoal
appleStandHours: number; // appleStandHours
appleStandHoursGoal: number; // appleStandHoursGoal
};
// 导出每小时活动热量数据获取函数
export async function fetchHourlyActiveCaloriesForDate(date: Date): Promise<HourlyActivityData[]> {
return fetchHourlyActiveCalories(date);
}
// 导出每小时锻炼分钟数据获取函数
export async function fetchHourlyExerciseMinutesForDate(date: Date): Promise<HourlyExerciseData[]> {
return fetchHourlyExerciseMinutes(date);
}
// 导出每小时站立数据获取函数
export async function fetchHourlyStandHoursForDate(date: Date): Promise<HourlyStandData[]> {
const hourlyStandData = await fetchHourlyStandHours(date);
return hourlyStandData.map((hasStood, hour) => ({
hour,
hasStood
}));
}
// 专门为活动圆环详情页获取精简的数据
export async function fetchActivityRingsForDate(date: Date): Promise<ActivityRingsData | null> {
try {
console.log('获取活动圆环数据...', date);
const options = createDateRange(date);
const activitySummary = await fetchActivitySummary(options);
if (!activitySummary) {
console.warn('ActivitySummary 数据为空');
return null;
}
// 直接使用 getActivitySummary 返回的字段名,与文档保持一致
return {
activeEnergyBurned: Math.round(activitySummary.activeEnergyBurned || 0),
activeEnergyBurnedGoal: Math.round(activitySummary.activeEnergyBurnedGoal || 350),
appleExerciseTime: Math.round(activitySummary.appleExerciseTime || 0),
appleExerciseTimeGoal: Math.round(activitySummary.appleExerciseTimeGoal || 30),
appleStandHours: Math.round(activitySummary.appleStandHours || 0),
appleStandHoursGoal: Math.round(activitySummary.appleStandHoursGoal || 12),
};
} catch (error) {
console.error('获取活动圆环数据失败:', error);
return null;
}
}