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, 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 { return new Promise((resolve) => { console.log('开始初始化HealthKit...'); resolve(true) 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 createSleepDateRange(date: Date): HealthDataOptions { return { startDate: dayjs(date).subtract(1, 'day').hour(18).minute(0).second(0).millisecond(0).toDate().toISOString(), // 前一天18:00开始 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; } // 健康数据获取函数 export async function fetchStepCount(date: Date): Promise { 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); }); }); } // 使用样本数据获取每小时步数 export async function fetchHourlyStepSamples(date: Date): Promise { 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); // 如果主方法失败,返回默认数据 resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 }))); return; } 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 fetchHourlyActiveCalories(date: Date): Promise { return new Promise(async (resolve) => { const startOfDay = dayjs(date).startOf('day'); // 初始化24小时数据 const hourlyData: HourlyActivityData[] = Array.from({ length: 24 }, (_, i) => ({ hour: i, calories: 0 })); try { // 为每个小时单独获取数据,确保精确性 const promises = Array.from({ length: 24 }, (_, hour) => { const hourStart = startOfDay.add(hour, 'hour'); const hourEnd = hourStart.add(1, 'hour'); const options = { startDate: hourStart.toDate().toISOString(), endDate: hourEnd.toDate().toISOString(), ascending: true, includeManuallyAdded: false }; return new Promise((resolveHour) => { AppleHealthKit.getActiveEnergyBurned(options, (err, res) => { if (err || !res || !Array.isArray(res)) { resolveHour(0); return; } const total = res.reduce((acc: number, sample: any) => { return acc + (sample?.value || 0); }, 0); resolveHour(Math.round(total)); }); }); }); const results = await Promise.all(promises); results.forEach((calories, hour) => { hourlyData[hour].calories = calories; }); logSuccess('每小时活动热量', hourlyData); resolve(hourlyData); } catch (error) { logError('每小时活动热量', error); resolve(hourlyData); } }); } // 获取每小时锻炼分钟数据 // 使用 AppleHealthKit.getAppleExerciseTime 获取锻炼样本数据 async function fetchHourlyExerciseMinutes(date: Date): Promise { return new Promise((resolve) => { const startOfDay = dayjs(date).startOf('day'); const endOfDay = dayjs(date).endOf('day'); const options = { startDate: startOfDay.toDate().toISOString(), endDate: endOfDay.toDate().toISOString(), ascending: true, includeManuallyAdded: false }; // 使用 getAppleExerciseTime 获取详细的锻炼样本数据 AppleHealthKit.getAppleExerciseTime(options, (err, res) => { if (err) { logError('每小时锻炼分钟', err); resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, minutes: 0 }))); return; } if (!res || !Array.isArray(res)) { logWarning('每小时锻炼分钟', '数据为空'); resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, minutes: 0 }))); return; } logSuccess('每小时锻炼分钟', res); // 初始化24小时数据 const hourlyData: HourlyExerciseData[] = Array.from({ length: 24 }, (_, i) => ({ hour: i, minutes: 0 })); // 将锻炼样本数据按小时分组统计 res.forEach((sample: any) => { if (sample && sample.startDate && sample.value !== undefined) { const hour = dayjs(sample.startDate).hour(); if (hour >= 0 && hour < 24) { hourlyData[hour].minutes += sample.value; } } }); // 四舍五入处理 hourlyData.forEach(data => { data.minutes = Math.round(data.minutes); }); resolve(hourlyData); }); }); } // 获取每小时站立小时数据 // 使用 AppleHealthKit.getAppleStandTime 获取站立样本数据 async function fetchHourlyStandHours(date: Date): Promise { return new Promise((resolve) => { const startOfDay = dayjs(date).startOf('day'); const endOfDay = dayjs(date).endOf('day'); const options = { startDate: startOfDay.toDate().toISOString(), endDate: endOfDay.toDate().toISOString() }; // 使用 getAppleStandTime 获取详细的站立样本数据 AppleHealthKit.getAppleStandTime(options, (err, res) => { if (err) { logError('每小时站立数据', err); resolve(Array.from({ length: 24 }, () => 0)); return; } if (!res || !Array.isArray(res)) { logWarning('每小时站立数据', '数据为空'); resolve(Array.from({ length: 24 }, () => 0)); return; } logSuccess('每小时站立数据', res); // 初始化24小时数据 const hourlyData: number[] = Array.from({ length: 24 }, () => 0); // 将站立样本数据按小时分组统计 res.forEach((sample: any) => { if (sample && sample.startDate && sample.value !== undefined) { const hour = dayjs(sample.startDate).hour(); if (hour >= 0 && hour < 24) { // 站立时间通常以分钟为单位,转换为小时(1表示该小时有站立,0表示没有) hourlyData[hour] = sample.value > 0 ? 1 : 0; } } }); resolve(hourlyData); }); }); } async function fetchActiveEnergyBurned(options: HealthDataOptions): Promise { 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 { 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 fetchHeartRateVariability(options: HealthDataOptions): Promise { return new Promise((resolve) => { console.log('=== 开始获取HRV数据 ==='); console.log('查询选项:', options); AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => { console.log('HRV API调用结果:', { err, res }); if (err) { logError('HRV数据', err); console.error('HRV获取错误详情:', err); return resolve(null); } if (!res || !Array.isArray(res) || res.length === 0) { logWarning('HRV', '为空或格式错误'); console.warn('HRV数据为空,原始响应:', res); return resolve(null); } resolve(Math.round(res[0].value * 1000)) }); }); } async function fetchActivitySummary(options: HealthDataOptions): Promise { return new Promise((resolve) => { AppleHealthKit.getActivitySummary( options, (err: string, 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 { 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 { 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)); }); }); } // 获取指定时间范围内的最大心率 export async function fetchMaximumHeartRate(options: HealthDataOptions): Promise { 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); } // 从所有心率样本中找出最大值 let maxHeartRate = 0; let validSamplesCount = 0; res.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 }); resolve(maxHeartRate); } else { logWarning('最大心率', '没有找到有效的样本数据'); resolve(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 { 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 { return fetchHealthDataForDate(dayjs().toDate()); } export async function fetchHRVForDate(date: Date): Promise { console.log('开始获取指定日期HRV数据...', date); const options = createDateRange(date); return fetchHeartRateVariability(options); } export async function fetchTodayHRV(): Promise { return fetchHRVForDate(dayjs().toDate()); } // 获取最近几小时内的实时HRV数据 export async function fetchRecentHRV(hoursBack: number = 2): Promise { 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 { 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) { 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 { 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(); }); }); } // 添加饮水记录到 HealthKit export async function saveWaterIntakeToHealthKit(amount: number, recordedAt?: string): Promise { return new Promise((resolve) => { // HealthKit 水分摄入量使用升(L)作为单位,需要将毫升转换为升 const waterOptions = { value: amount / 1000, // 将毫升转换为升 (ml -> L) startDate: recordedAt ? new Date(recordedAt).toISOString() : new Date().toISOString(), endDate: recordedAt ? new Date(recordedAt).toISOString() : new Date().toISOString(), }; AppleHealthKit.saveWater(waterOptions, (error: string, result) => { if (error) { console.error('添加饮水记录到 HealthKit 失败:', error); resolve(false); return; } console.log('成功添加饮水记录到 HealthKit:', { originalAmount: amount, convertedAmount: amount / 1000, recordedAt, result }); resolve(true); }); }); } // 获取 HealthKit 中的饮水记录 export async function getWaterIntakeFromHealthKit(options: HealthDataOptions): Promise { return new Promise((resolve) => { AppleHealthKit.getWaterSamples(options, (error: string, results: any[]) => { if (error) { console.error('获取 HealthKit 饮水记录失败:', error); resolve([]); return; } console.log('从 HealthKit 获取饮水记录:', results); resolve(results || []); }); }); } // 删除 HealthKit 中的饮水记录 // 注意: react-native-health 库可能不支持直接删除特定记录,这个功能可能需要手动实现或使用其他方法 export async function deleteWaterIntakeFromHealthKit(recordId: string, recordedAt: string): Promise { // 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 { return fetchHourlyActiveCalories(date); } // 导出每小时锻炼分钟数据获取函数 export async function fetchHourlyExerciseMinutesForDate(date: Date): Promise { return fetchHourlyExerciseMinutes(date); } // 导出每小时站立数据获取函数 export async function fetchHourlyStandHoursForDate(date: Date): Promise { const hourlyStandData = await fetchHourlyStandHours(date); return hourlyStandData.map((hasStood, hour) => ({ hour, hasStood })); } // 专门为活动圆环详情页获取精简的数据 export async function fetchActivityRingsForDate(date: Date): Promise { 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; } }