Files
digital-pilates/utils/health.ts

519 lines
15 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 type { HealthActivitySummary, HealthKitPermissions } from 'react-native-health';
import AppleHealthKit from 'react-native-health';
type HealthDataOptions = {
startDate: string;
endDate: string;
};
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,
],
write: [
// 支持体重写入
AppleHealthKit.Constants.Permissions.Weight,
],
},
};
export type HourlyStepData = {
hour: number; // 0-23
steps: number;
};
export type TodayHealthData = {
steps: number;
activeEnergyBurned: number; // kilocalories
basalEnergyBurned: number; // kilocalories - 基础代谢率
sleepDuration: number; // 睡眠时长(分钟)
hrv: number | null; // 心率变异性 (ms)
// 健身圆环数据
activeCalories: number;
activeCaloriesGoal: number;
exerciseMinutes: number;
exerciseMinutesGoal: number;
standHours: number;
standHoursGoal: number;
// 新增血氧饱和度和心率数据
oxygenSaturation: number | null;
heartRate: number | null;
// 每小时步数数据
hourlySteps: HourlyStepData[];
};
export async function ensureHealthPermissions(): Promise<boolean> {
return new Promise((resolve) => {
console.log('开始初始化HealthKit...');
AppleHealthKit.initHealthKit(PERMISSIONS, (error) => {
if (error) {
console.error('HealthKit初始化失败:', error);
// 常见错误处理
if (typeof error === 'string') {
if (error.includes('not available')) {
console.warn('HealthKit不可用 - 可能在模拟器上运行或非iOS设备');
}
}
resolve(false);
return;
}
console.log('HealthKit初始化成功');
resolve(true);
});
});
}
// 日期工具函数
function createDateRange(date: Date): HealthDataOptions {
return {
startDate: dayjs(date).startOf('day').toDate().toISOString(),
endDate: dayjs(date).endOf('day').toDate().toISOString()
};
}
// 睡眠时长计算
function calculateSleepDuration(samples: any[]): number {
return samples.reduce((total, sample) => {
if (sample && sample.startDate && sample.endDate) {
const startTime = dayjs(sample.startDate).valueOf();
const endTime = dayjs(sample.endDate).valueOf();
const durationMinutes = (endTime - startTime) / (1000 * 60);
return total + durationMinutes;
}
return total;
}, 0);
}
// 通用错误处理
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;
}
// 健康数据获取函数
async function fetchStepCount(date: Date): Promise<number> {
return new Promise((resolve) => {
AppleHealthKit.getStepCount({
date: dayjs(date).toDate().toISOString()
}, (err, res) => {
if (err) {
logError('步数', err);
return resolve(0);
}
if (!res) {
logWarning('步数', '为空');
return resolve(0);
}
logSuccess('步数', res);
resolve(res.value || 0);
});
});
}
// 获取指定日期每小时步数数据 (已弃用,使用 fetchHourlyStepSamples 替代)
// 保留此函数以防后向兼容需求
async function fetchHourlyStepCount(date: Date): Promise<HourlyStepData[]> {
// 直接调用更准确的样本数据获取函数
return fetchHourlyStepSamples(date);
}
// 使用样本数据获取每小时步数
async function fetchHourlyStepSamples(date: Date): Promise<HourlyStepData[]> {
return new Promise((resolve) => {
const startOfDay = dayjs(date).startOf('day');
const endOfDay = dayjs(date).endOf('day');
// 使用正确的 getDailyStepCountSamples 方法,设置 period 为 60 分钟获取每小时数据
const options = {
startDate: startOfDay.toDate().toISOString(),
endDate: endOfDay.toDate().toISOString(),
ascending: false,
period: 60, // 60分钟为一个时间段获取每小时数据
includeManuallyAdded: false,
};
AppleHealthKit.getDailyStepCountSamples(
options,
(err: any, res: any[]) => {
if (err) {
logError('每小时步数样本', err);
// 如果主方法失败,尝试使用备用方法
return null
}
logSuccess('每小时步数样本', res);
// 初始化24小时数据
const hourlyData: HourlyStepData[] = Array.from({ length: 24 }, (_, i) => ({
hour: i,
steps: 0
}));
// 将每小时的步数样本数据映射到对应的小时
res.forEach((sample: any) => {
if (sample && sample.startDate && sample.value !== undefined) {
const hour = dayjs(sample.startDate).hour();
if (hour >= 0 && hour < 24) {
// 使用样本中的步数值,如果有 metadata优先使用 metadata 中的数据
const stepValue = sample.metadata && sample.metadata.length > 0
? sample.metadata.reduce((total: number, meta: any) => total + (meta.quantity || 0), 0)
: sample.value;
hourlyData[hour].steps = Math.round(stepValue);
}
}
});
resolve(hourlyData);
}
);
});
}
async function fetchActiveEnergyBurned(options: HealthDataOptions): Promise<number> {
return new Promise((resolve) => {
AppleHealthKit.getActiveEnergyBurned(options, (err, res) => {
if (err) {
logError('消耗卡路里', err);
return resolve(0);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('卡路里', '为空或格式错误');
return resolve(0);
}
logSuccess('卡路里', res);
const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
resolve(total);
});
});
}
async function fetchBasalEnergyBurned(options: HealthDataOptions): Promise<number> {
return new Promise((resolve) => {
AppleHealthKit.getBasalEnergyBurned(options, (err, res) => {
if (err) {
logError('基础代谢', err);
return resolve(0);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('基础代谢', '为空或格式错误');
return resolve(0);
}
logSuccess('基础代谢', res);
const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
resolve(total);
});
});
}
async function fetchSleepDuration(options: HealthDataOptions): Promise<number> {
return new Promise((resolve) => {
AppleHealthKit.getSleepSamples(options, (err, res) => {
if (err) {
logError('睡眠数据', err);
return resolve(0);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('睡眠', '为空或格式错误');
return resolve(0);
}
logSuccess('睡眠', res);
resolve(calculateSleepDuration(res));
});
});
}
async function fetchHeartRateVariability(options: HealthDataOptions): Promise<number | null> {
return new Promise((resolve) => {
AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => {
if (err) {
logError('HRV数据', err);
return resolve(null);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('HRV', '为空或格式错误');
return resolve(null);
}
logSuccess('HRV', res);
const latestHrv = res[res.length - 1];
if (latestHrv && latestHrv.value) {
resolve(Math.round(latestHrv.value * 1000));
} else {
resolve(null);
}
});
});
}
async function fetchActivitySummary(options: HealthDataOptions): Promise<HealthActivitySummary | null> {
return new Promise((resolve) => {
AppleHealthKit.getActivitySummary(
options,
(err: Object, results: HealthActivitySummary[]) => {
if (err) {
logError('ActivitySummary', err);
return resolve(null);
}
if (!results || results.length === 0) {
logWarning('ActivitySummary', '为空');
return resolve(null);
}
logSuccess('ActivitySummary', results[0]);
resolve(results[0]);
},
);
});
}
async function fetchOxygenSaturation(options: HealthDataOptions): Promise<number | null> {
return new Promise((resolve) => {
AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => {
if (err) {
logError('血氧饱和度', err);
return resolve(null);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('血氧饱和度', '为空或格式错误');
return resolve(null);
}
logSuccess('血氧饱和度', res);
const latestOxygen = res[res.length - 1];
return resolve(validateOxygenSaturation(latestOxygen?.value));
});
});
}
async function fetchHeartRate(options: HealthDataOptions): Promise<number | null> {
return new Promise((resolve) => {
AppleHealthKit.getHeartRateSamples(options, (err, res) => {
if (err) {
logError('心率', err);
return resolve(null);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('心率', '为空或格式错误');
return resolve(null);
}
logSuccess('心率', res);
const latestHeartRate = res[res.length - 1];
return resolve(validateHeartRate(latestHeartRate?.value));
});
});
}
// 默认健康数据
function getDefaultHealthData(): TodayHealthData {
return {
steps: 0,
activeEnergyBurned: 0,
basalEnergyBurned: 0,
sleepDuration: 0,
hrv: null,
activeCalories: 0,
activeCaloriesGoal: 350,
exerciseMinutes: 0,
exerciseMinutesGoal: 30,
standHours: 0,
standHoursGoal: 12,
oxygenSaturation: null,
heartRate: null,
hourlySteps: Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 }))
};
}
export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthData> {
try {
console.log('开始获取指定日期健康数据...', date);
const options = createDateRange(date);
console.log('查询选项:', options);
// 并行获取所有健康数据
const [
steps,
hourlySteps,
activeEnergyBurned,
basalEnergyBurned,
sleepDuration,
hrv,
activitySummary,
oxygenSaturation,
heartRate
] = await Promise.all([
fetchStepCount(date),
fetchHourlyStepSamples(date),
fetchActiveEnergyBurned(options),
fetchBasalEnergyBurned(options),
fetchSleepDuration(options),
fetchHeartRateVariability(options),
fetchActivitySummary(options),
fetchOxygenSaturation(options),
fetchHeartRate(options)
]);
console.log('指定日期健康数据获取完成:', {
steps,
hourlySteps,
activeEnergyBurned,
basalEnergyBurned,
sleepDuration,
hrv,
activitySummary,
oxygenSaturation,
heartRate
});
return {
steps,
hourlySteps,
activeEnergyBurned,
basalEnergyBurned,
sleepDuration,
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());
}
// 更新healthkit中的体重
export async function updateWeight(weight: number) {
return new Promise((resolve) => {
AppleHealthKit.saveWeight({
value: weight,
}, (err, res) => {
if (err) {
console.error('更新体重失败:', err);
return resolve(false);
}
console.log('体重更新成功:', res);
resolve(true);
});
});
}
export async function testOxygenSaturationData(date: Date = dayjs().toDate()): Promise<void> {
console.log('=== 开始测试血氧饱和度数据获取 ===');
const options = createDateRange(date);
return new Promise((resolve) => {
AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => {
if (err) {
console.error('获取血氧饱和度失败:', err);
resolve();
return;
}
console.log('原始血氧饱和度数据:', res);
if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('血氧饱和度数据为空');
resolve();
return;
}
// 分析所有数据样本
res.forEach((sample, index) => {
console.log(`样本 ${index + 1}:`, {
value: sample.value,
valueType: typeof sample.value,
startDate: sample.startDate,
endDate: sample.endDate
});
});
// 获取最新的血氧饱和度值并验证
const latestOxygen = res[res.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('=== 血氧饱和度数据测试完成 ===');
resolve();
});
});
}