Files
digital-pilates/utils/health.ts
richarjiang bca6670390 Add Chinese translations for medication management and personal settings
- Introduced new translation files for medication, personal, and weight management in Chinese.
- Updated the main index file to include the new translation modules.
- Enhanced the medication type definitions to include 'ointment'.
- Refactored workout type labels to utilize i18n for better localization support.
- Improved sleep quality descriptions and recommendations with i18n integration.
2025-11-28 17:29:51 +08:00

2054 lines
63 KiB
TypeScript
Raw Permalink 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 { AppState, AppStateStatus, NativeModules } from 'react-native';
import i18n from '../i18n';
import { SimpleEventEmitter } from './SimpleEventEmitter';
type HealthDataOptions = {
startDate: string;
endDate: string;
limit?: number;
};
// 锻炼数据类型定义
export interface WorkoutData {
id: string;
startDate: string;
endDate: string;
duration: number; // 秒
workoutActivityType: number;
workoutActivityTypeString: string;
totalEnergyBurned?: number; // 千卡
totalDistance?: number; // 米
averageHeartRate?: number;
source: {
name: string;
bundleIdentifier: string;
};
metadata: Record<string, any>;
}
export interface HeartRateSample {
id: string;
startDate: string;
endDate: string;
value: number;
source?: {
name: string;
bundleIdentifier: string;
};
metadata?: Record<string, any>;
}
// 锻炼记录查询选项
export interface WorkoutOptions extends HealthDataOptions {
limit?: number; // 默认10条
}
// 锻炼活动类型枚举
export enum WorkoutActivityType {
AmericanFootball = 1,
Archery = 2,
AustralianFootball = 3,
Badminton = 4,
Baseball = 5,
Basketball = 6,
Bowling = 7,
Boxing = 8,
Climbing = 9,
Cricket = 10,
CrossTraining = 11,
Curling = 12,
Cycling = 13,
Dance = 14,
DanceInspiredTraining = 15,
Elliptical = 16,
EquestrianSports = 17,
Fencing = 18,
Fishing = 19,
FunctionalStrengthTraining = 20,
Golf = 21,
Gymnastics = 22,
Handball = 23,
Hiking = 24,
Hockey = 25,
Hunting = 26,
Lacrosse = 27,
MartialArts = 28,
MindAndBody = 29,
MixedMetabolicCardioTraining = 30,
PaddleSports = 31,
Play = 32,
PreparationAndRecovery = 33,
Racquetball = 34,
Rowing = 35,
Rugby = 36,
Running = 37,
Sailing = 38,
SkatingSports = 39,
SnowSports = 40,
Soccer = 41,
Softball = 42,
Squash = 43,
StairClimbing = 44,
SurfingSports = 45,
Swimming = 46,
TableTennis = 47,
Tennis = 48,
TrackAndField = 49,
TraditionalStrengthTraining = 50,
Volleyball = 51,
Walking = 52,
WaterFitness = 53,
WaterPolo = 54,
WaterSports = 55,
Wrestling = 56,
Yoga = 57,
Barre = 58,
CoreTraining = 59,
CrossCountrySkiing = 60,
DownhillSkiing = 61,
Flexibility = 62,
HighIntensityIntervalTraining = 63,
JumpRope = 64,
Kickboxing = 65,
Pilates = 66,
Snowboarding = 67,
Stairs = 68,
StepTraining = 69,
WheelchairWalkPace = 70,
WheelchairRunPace = 71,
TaiChi = 72,
MixedCardio = 73,
HandCycling = 74,
DiscSports = 75,
FitnessGaming = 76,
CardioDance = 77,
SocialDance = 78,
Pickleball = 79,
Cooldown = 80,
SwimBikeRun = 82,
Transition = 83,
UnderwaterDiving = 84,
Other = 3000
}
// 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;
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
// 健身圆环数据
activeCalories: number;
activeCaloriesGoal: number;
exerciseMinutes: number;
exerciseMinutesGoal: number;
standHours: number;
standHoursGoal: number;
heartRate: number | null;
};
// 更新:使用新的权限管理系统
export async function ensureHealthPermissions(): Promise<boolean> {
return await healthPermissionManager.requestPermission();
}
// 获取当前权限状态
export function getHealthPermissionStatus(): HealthPermissionStatus {
return healthPermissionManager.getPermissionStatus();
}
// 检查权限状态
export async function checkHealthPermissionStatus(forceCheck: boolean = false): Promise<HealthPermissionStatus> {
return await healthPermissionManager.checkPermissionStatus(forceCheck);
}
// 日期工具函数
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;
}
function validateHRVValue(value: any): number | null {
if (value === undefined || value === null) return null;
const numValue = Number(value);
// HRV SDNN 正常范围检查
// 正常范围: 18-76ms但允许更宽范围 5-150ms 以包含边缘情况
if (numValue >= 5 && numValue <= 150) {
// 保留1位小数的精度避免过度舍入
return Math.round(numValue * 10) / 10;
}
// 记录异常值用于调试
console.warn('HRV数据超出合理范围:', {
value: numValue,
expectedRange: '5-150ms',
normalRange: '18-76ms'
});
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);
// 优化:使用更高效的数据结构
const hourlyMap = new Map<number, number>();
// 优化:批量处理数据,减少重复验证
result.data.forEach((sample: any) => {
if (sample?.hour >= 0 && sample?.hour < 24 && sample?.value !== undefined) {
hourlyMap.set(sample.hour, Math.round(sample.value));
}
});
// 生成最终数组
const hourlyData: HourlyStepData[] = [];
for (let i = 0; i < 24; i++) {
hourlyData.push({
hour: i,
steps: hourlyMap.get(i) || 0
});
}
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 {
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[]> {
try {
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[]> {
try {
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);
}
}
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;
}
}
export 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<HRVData | 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) {
let selectedSample: any = null;
let bestQuality = -1;
console.log(`获取到 ${result.data.length} 个HRV样本`);
// 首先尝试使用最佳质量值
if (result.bestQualityValue && typeof result.bestQualityValue === 'number') {
const qualityValue = validateHRVValue(result.bestQualityValue);
if (qualityValue !== null) {
// 找到质量分数最高的样本
for (const sample of result.data) {
const sampleQuality = sample.qualityScore || 0;
const sampleValue = validateHRVValue(sample.value);
if (sampleValue !== null && sampleQuality > bestQuality) {
bestQuality = sampleQuality;
selectedSample = sample;
}
}
if (selectedSample) {
logSuccess('HRV数据最佳质量', {
value: qualityValue,
qualityScore: bestQuality,
totalSamples: result.data.length,
recordedAt: selectedSample.endDate
});
}
}
}
// 如果没有找到最佳质量样本,或者最佳质量值无效,重新评估所有样本
if (!selectedSample) {
console.log('重新评估所有样本以找到最佳数据...');
// 按质量分数、手动测量标志和时间排序
const sortedSamples = result.data.sort((a: any, b: any) => {
const qualityA = a.qualityScore || 0;
const qualityB = b.qualityScore || 0;
const isManualA = a.isManualMeasurement || false;
const isManualB = b.isManualMeasurement || false;
// 手动测量优先
if (isManualA && !isManualB) return -1;
if (!isManualA && isManualB) return 1;
// 质量分数优先
if (qualityA !== qualityB) return qualityB - qualityA;
// 时间优先(最新的优先)
const dateA = new Date(a.endDate || a.startDate).getTime();
const dateB = new Date(b.endDate || b.startDate).getTime();
return dateB - dateA;
});
// 选择第一个有效样本
for (const sample of sortedSamples) {
const sampleValue = validateHRVValue(sample.value);
if (sampleValue !== null) {
selectedSample = sample;
bestQuality = sample.qualityScore || 0;
console.log('选择最佳HRV样本:', {
value: sampleValue,
qualityScore: bestQuality,
isManual: sample.isManualMeasurement,
source: sample.source?.name,
recordedAt: sample.endDate
});
break;
}
}
}
// 构建完整的HRV数据对象
if (selectedSample) {
const validatedValue = validateHRVValue(selectedSample.value);
if (validatedValue !== null) {
const hrvData: HRVData = {
value: validatedValue,
recordedAt: selectedSample.startDate,
endDate: selectedSample.endDate,
source: {
name: selectedSample.source?.name || 'Unknown',
bundleIdentifier: selectedSample.source?.bundleIdentifier || ''
},
isManualMeasurement: selectedSample.isManualMeasurement || false,
qualityScore: selectedSample.qualityScore,
sampleId: selectedSample.id
};
logSuccess('HRV完整数据', hrvData);
return hrvData;
}
}
}
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;
}
}
export 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;
}
}
export async function fetchHeartRateSamplesForRange(
startDate: Date,
endDate: Date,
limit: number = 500
): Promise<HeartRateSample[]> {
try {
const options = {
startDate: dayjs(startDate).toISOString(),
endDate: dayjs(endDate).toISOString(),
limit,
};
const result = await HealthKitManager.getHeartRateSamples(options);
if (result && Array.isArray(result.data)) {
const samples: HeartRateSample[] = result.data
.filter((sample: any) => sample && typeof sample.value === 'number' && !Number.isNaN(sample.value))
.map((sample: any) => ({
id: sample.id,
startDate: sample.startDate,
endDate: sample.endDate,
value: Number(sample.value),
source: sample.source,
metadata: sample.metadata,
}));
logSuccess('锻炼心率采样', { count: samples.length, startDate: options.startDate, endDate: options.endDate });
return samples;
}
logWarning('锻炼心率采样', '为空或格式错误');
return [];
} catch (error) {
logError('锻炼心率采样', error);
return [];
}
}
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 {
// 暂未实现返回null
console.log('最大心率获取暂未实现');
return null;
// 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,
activeCalories: 0,
activeCaloriesGoal: 350,
exerciseMinutes: 0,
exerciseMinutesGoal: 30,
standHours: 0,
standHoursGoal: 12,
heartRate: null,
};
}
export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthData> {
try {
console.log('开始获取指定日期健康数据...', date);
const options = createDateRange(date);
console.log('查询选项:', options);
// 并行获取所有健康数据
const [
activeEnergyBurned,
activitySummary,
heartRate
] = await Promise.all([
fetchActiveEnergyBurned(options),
fetchActivitySummary(options),
fetchHeartRate(options)
]);
return {
activeEnergyBurned,
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),
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<HRVData | null> {
console.log('开始获取指定日期HRV数据...', date);
// 首先尝试获取指定日期的HRV数据
const options = createDateRange(date);
const hrvData = await fetchHeartRateVariability(options);
// 如果当天没有数据尝试获取最近7天内的最新数据
if (!hrvData) {
console.log('指定日期无HRV数据尝试获取最近7天内的数据...');
const endDate = new Date(date);
const startDate = new Date(date);
startDate.setDate(startDate.getDate() - 7); // 往前推7天
const recentOptions = {
startDate: startDate.toISOString(),
endDate: endDate.toISOString()
};
const recentHrvData = await fetchHeartRateVariability(recentOptions);
if (recentHrvData) {
console.log('获取到最近7天内的HRV数据:', recentHrvData);
return recentHrvData;
} else {
console.log('最近7天内也无HRV数据');
}
}
return hrvData;
}
export async function fetchTodayHRV(): Promise<HRVData | null> {
return fetchHRVForDate(dayjs().toDate());
}
// 获取最近几小时内的实时HRV数据
export async function fetchRecentHRV(hoursBack: number = 2): Promise<HRVData | 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带详细分析
console.log('--- 测试今日HRV ---');
const result = await HealthKitManager.getHeartRateVariabilitySamples(options);
console.log('原始HRV API响应:', result);
if (result && result.data && Array.isArray(result.data)) {
console.log(`获取到 ${result.data.length} 个HRV样本`);
// 分析数据质量
result.data.forEach((sample: any, index: number) => {
console.log(`样本 ${index + 1}:`, {
value: sample.value,
source: sample.source?.name,
bundleId: sample.source?.bundleIdentifier,
isManual: sample.isManualMeasurement,
qualityScore: sample.qualityScore,
startDate: sample.startDate,
endDate: sample.endDate
});
});
if (result.bestQualityValue !== undefined) {
console.log('最佳质量HRV值:', result.bestQualityValue);
}
}
// 使用优化后的方法获取HRV
const todayHRV = await fetchHeartRateVariability(options);
console.log('最终HRV结果:', todayHRV);
// 获取最近2小时HRV
console.log('--- 测试最近2小时HRV ---');
const recentHRV = await fetchRecentHRV(2);
console.log('最近2小时HRV结果:', recentHRV);
// 获取指定日期HRV
console.log('--- 测试指定日期HRV ---');
const dateHRV = await fetchHRVForDate(date);
console.log('指定日期HRV结果:', dateHRV);
// 提供数据解释
if (todayHRV) {
console.log('--- HRV数据解读 ---');
console.log(`HRV值: ${todayHRV.value}ms`);
console.log(`记录时间: ${todayHRV.recordedAt}`);
console.log(`数据来源: ${todayHRV.source.name}`);
console.log(`手动测量: ${todayHRV.isManualMeasurement ? '是' : '否'}`);
if (todayHRV.value >= 18 && todayHRV.value <= 76) {
console.log('✅ HRV值在正常范围内 (18-76ms)');
} else if (todayHRV.value < 18) {
console.log('⚠️ HRV值偏低可能表示压力或疲劳状态');
} else if (todayHRV.value > 76) {
console.log('📈 HRV值较高通常表示良好的恢复状态');
}
}
console.log('=== HRV数据测试完成 ===');
} catch (error) {
console.error('HRV测试过程中出现错误:', error);
}
}
// === 个人健康数据读取和写入方法 ===
/**
* 从 HealthKit 获取身高(单位:厘米)
*/
export async function fetchHeight(): Promise<number | null> {
try {
console.log('开始从 HealthKit 获取身高...');
const result = await HealthKitManager.getHeight();
if (result && typeof result.value === 'number') {
const heightInCm = Math.round(result.value);
console.log('成功获取身高:', heightInCm, 'cm');
return heightInCm;
} else {
console.log('未找到身高数据');
return null;
}
} catch (error) {
console.error('获取身高失败:', error);
return null;
}
}
/**
* 从 HealthKit 获取体重(单位:千克)
*/
export async function fetchWeight(): Promise<number | null> {
try {
console.log('开始从 HealthKit 获取体重...');
const result = await HealthKitManager.getWeight();
if (result && typeof result.value === 'number') {
const weightInKg = Math.round(result.value * 10) / 10; // 保留1位小数
console.log('成功获取体重:', weightInKg, 'kg');
return weightInKg;
} else {
console.log('未找到体重数据');
return null;
}
} catch (error) {
console.error('获取体重失败:', error);
return null;
}
}
/**
* 从 HealthKit 获取出生日期
*/
export async function fetchDateOfBirth(): Promise<string | null> {
try {
console.log('开始从 HealthKit 获取出生日期...');
const result = await HealthKitManager.getDateOfBirth();
if (result && typeof result.value === 'string') {
console.log('成功获取出生日期:', result.value);
return result.value;
} else {
console.log('未找到出生日期数据');
return null;
}
} catch (error) {
console.error('获取出生日期失败:', error);
return null;
}
}
/**
* 保存身高到 HealthKit
* @param heightInCm 身高(单位:厘米)
* @param unit 单位,默认为 'cm'(厘米),也支持 'in'(英寸)
*/
export async function saveHeight(heightInCm: number, unit: 'cm' | 'in' = 'cm'): Promise<boolean> {
try {
console.log('开始保存身高到 HealthKit...', { heightInCm, unit });
if (heightInCm <= 0) {
console.error('身高值无效:', heightInCm);
return false;
}
const options = {
value: heightInCm,
unit: unit
};
const result = await HealthKitManager.saveHeight(options);
if (result && result.success) {
console.log('身高保存成功');
return true;
} else {
console.error('身高保存失败:', result);
return false;
}
} catch (error) {
console.error('保存身高到 HealthKit 失败:', error);
return false;
}
}
/**
* 保存体重到 HealthKit
* @param weightInKg 体重(单位:千克)
*/
export async function saveWeight(weightInKg: number): Promise<boolean> {
try {
console.log('开始保存体重到 HealthKit...', { weightInKg });
if (weightInKg <= 0) {
console.error('体重值无效:', weightInKg);
return false;
}
const options = {
value: weightInKg
};
const result = await HealthKitManager.saveWeight(options);
if (result && result.success) {
console.log('体重保存成功');
return true;
} else {
console.error('体重保存失败:', result);
return false;
}
} catch (error) {
console.error('保存体重到 HealthKit 失败:', error);
return false;
}
}
// 保持向后兼容的旧函数名
export async function updateWeight(weight: number): Promise<boolean> {
return saveWeight(weight);
}
/**
* 批量获取个人健康数据(身高、体重、出生日期)
*/
export async function fetchPersonalHealthData(): Promise<{
height: number | null;
weight: number | null;
dateOfBirth: string | null;
}> {
try {
console.log('开始批量获取个人健康数据...');
const [height, weight, dateOfBirth] = await Promise.all([
fetchHeight(),
fetchWeight(),
fetchDateOfBirth()
]);
console.log('个人健康数据获取完成:', { height, weight, dateOfBirth });
return { height, weight, dateOfBirth };
} catch (error) {
console.error('批量获取个人健康数据失败:', error);
return { height: null, weight: null, dateOfBirth: null };
}
}
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 {
console.log('开始保存饮水记录到HealthKit...', { amount, recordedAt });
const options = {
amount: amount,
recordedAt: recordedAt || new Date().toISOString()
};
const result = await HealthKitManager.saveWaterIntakeToHealthKit(options);
if (result && result.success) {
console.log('饮水记录保存成功:', result);
return true;
} else {
console.error('饮水记录保存失败:', result);
return false;
}
} catch (error) {
console.error('添加饮水记录到 HealthKit 失败:', error);
return false;
}
}
// 添加蛋白质记录到 HealthKit
export async function saveProteinToHealthKit(grams: number, recordedAt?: string): Promise<boolean> {
try {
console.log('开始保存蛋白质记录到HealthKit...', { grams, recordedAt });
const options = {
amount: grams,
recordedAt: recordedAt || new Date().toISOString()
};
const result = await HealthKitManager.saveProteinToHealthKit(options);
if (result && result.success) {
console.log('蛋白质记录保存成功:', result);
return true;
} else {
console.error('蛋白质记录保存失败:', result);
return false;
}
} catch (error) {
console.error('添加蛋白质记录到 HealthKit 失败:', error);
return false;
}
}
// 添加脂肪记录到 HealthKit
export async function saveFatToHealthKit(grams: number, recordedAt?: string): Promise<boolean> {
try {
console.log('开始保存脂肪记录到HealthKit...', { grams, recordedAt });
const options = {
amount: grams,
recordedAt: recordedAt || new Date().toISOString()
};
const result = await HealthKitManager.saveFatToHealthKit(options);
if (result && result.success) {
console.log('脂肪记录保存成功:', result);
return true;
} else {
console.error('脂肪记录保存失败:', result);
return false;
}
} catch (error) {
console.error('添加脂肪记录到 HealthKit 失败:', error);
return false;
}
}
// 添加碳水化合物记录到 HealthKit
export async function saveCarbohydratesToHealthKit(grams: number, recordedAt?: string): Promise<boolean> {
try {
console.log('开始保存碳水化合物记录到HealthKit...', { grams, recordedAt });
const options = {
amount: grams,
recordedAt: recordedAt || new Date().toISOString()
};
const result = await HealthKitManager.saveCarbohydratesToHealthKit(options);
if (result && result.success) {
console.log('碳水化合物记录保存成功:', result);
return true;
} else {
console.error('碳水化合物记录保存失败:', result);
return false;
}
} catch (error) {
console.error('添加碳水化合物记录到 HealthKit 失败:', error);
return false;
}
}
// 批量保存营养数据到 HealthKit蛋白质、脂肪和碳水化合物
export async function saveNutritionToHealthKit(
nutrition: { proteinGrams?: number; fatGrams?: number; carbohydrateGrams?: number },
recordedAt?: string
): Promise<{ proteinSaved: boolean; fatSaved: boolean; carbohydrateSaved: boolean }> {
try {
console.log('开始批量保存营养数据到HealthKit...', { nutrition, recordedAt });
const results = await Promise.allSettled([
nutrition.proteinGrams && nutrition.proteinGrams > 0
? saveProteinToHealthKit(nutrition.proteinGrams, recordedAt)
: Promise.resolve(false),
nutrition.fatGrams && nutrition.fatGrams > 0
? saveFatToHealthKit(nutrition.fatGrams, recordedAt)
: Promise.resolve(false),
nutrition.carbohydrateGrams && nutrition.carbohydrateGrams > 0
? saveCarbohydratesToHealthKit(nutrition.carbohydrateGrams, recordedAt)
: Promise.resolve(false),
]);
const proteinSaved = results[0].status === 'fulfilled' ? results[0].value : false;
const fatSaved = results[1].status === 'fulfilled' ? results[1].value : false;
const carbohydrateSaved = results[2].status === 'fulfilled' ? results[2].value : false;
console.log('营养数据批量保存结果:', { proteinSaved, fatSaved, carbohydrateSaved });
return { proteinSaved, fatSaved, carbohydrateSaved };
} catch (error) {
console.error('批量保存营养数据到 HealthKit 失败:', error);
return { proteinSaved: false, fatSaved: false, carbohydrateSaved: false };
}
}
// 获取 HealthKit 中的饮水记录
export async function getWaterIntakeFromHealthKit(options: HealthDataOptions): Promise<any[]> {
try {
console.log('开始从HealthKit获取饮水记录...', options);
const result = await HealthKitManager.getWaterIntakeFromHealthKit(options);
if (result && result.data && Array.isArray(result.data)) {
console.log('成功获取饮水记录:', result);
return result.data;
} else {
console.log('饮水记录为空或格式错误:', result);
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;
}
}
// === 权限管理工具函数 ===
// 初始化健康权限管理(应在应用启动时调用)
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;
}
// HRV数据结构
export interface HRVData {
value: number;
recordedAt: string; // ISO string format
endDate: string; // ISO string format
source: {
name: string;
bundleIdentifier: string;
};
isManualMeasurement: boolean;
qualityScore?: number;
sampleId?: string;
}
// HRV数据质量分析和解读
export interface HRVAnalysis {
value: number;
quality: 'excellent' | 'good' | 'fair' | 'poor';
interpretation: string;
recommendations: string[];
dataSource: string;
isManualMeasurement: boolean;
recordedAt: string;
}
export function analyzeHRVData(hrvData: HRVData): HRVAnalysis {
const { value: hrvValue, source, isManualMeasurement, recordedAt } = hrvData;
const sourceName = source.name;
let quality: HRVAnalysis['quality'];
let interpretation: string;
let recommendations: string[] = [];
// 质量评估基于数值范围和数据来源
if (hrvValue >= 18 && hrvValue <= 76) {
if (isManualMeasurement) {
quality = 'excellent';
interpretation = 'HRV值在正常范围内且来自高质量测量';
} else {
quality = 'good';
interpretation = 'HRV值在正常范围内';
}
} else if (hrvValue >= 10 && hrvValue < 18) {
quality = 'fair';
interpretation = 'HRV值偏低可能表示压力、疲劳或恢复不足';
recommendations.push('考虑增加休息和恢复时间');
recommendations.push('评估近期的压力水平和睡眠质量');
} else if (hrvValue > 76 && hrvValue <= 100) {
quality = isManualMeasurement ? 'excellent' : 'good';
interpretation = 'HRV值较高通常表示良好的心血管健康和恢复状态';
recommendations.push('保持当前的生活方式和训练强度');
} else if (hrvValue < 10) {
quality = 'poor';
interpretation = 'HRV值异常低建议关注身体状态或数据准确性';
recommendations.push('建议使用手动测量(如呼吸应用)获得更准确的数据');
recommendations.push('如持续偏低,建议咨询医疗专业人士');
} else if (hrvValue > 100) {
quality = 'fair';
interpretation = 'HRV值异常高可能需要验证数据准确性';
recommendations.push('建议重复测量确认数据准确性');
} else {
quality = 'poor';
interpretation = 'HRV数据超出预期范围';
recommendations.push('建议使用标准化的测量方法');
}
// 根据数据来源添加建议
if (!isManualMeasurement) {
recommendations.push('推荐使用呼吸应用进行手动HRV测量以获得更准确的数据');
}
return {
value: hrvValue,
quality,
interpretation,
recommendations,
dataSource: sourceName,
isManualMeasurement,
recordedAt
};
}
// 获取HRV数据并提供分析
export async function fetchHRVWithAnalysis(date: Date): Promise<{ hrvData: HRVData | null; analysis: HRVAnalysis | null }> {
try {
const hrvData = await fetchHRVForDate(date);
if (hrvData) {
const analysis = analyzeHRVData(hrvData);
return { hrvData, analysis };
}
return { hrvData: null, analysis: null };
} catch (error) {
console.error('获取HRV分析数据失败:', error);
return { hrvData: null, analysis: null };
}
}
// 智能HRV数据获取 - 优先获取实时数据,如果没有则获取历史数据
export async function fetchSmartHRVData(date: Date): Promise<HRVData | null> {
console.log('开始智能HRV数据获取...', date);
try {
// 1. 首先尝试获取最近2小时的实时数据
console.log('1. 尝试获取最近2小时的实时HRV数据...');
const recentHRV = await fetchRecentHRV(2);
if (recentHRV) {
console.log('✅ 成功获取到实时HRV数据:', recentHRV);
// 检查数据是否足够新1小时内
const dataTime = new Date(recentHRV.recordedAt).getTime();
const now = Date.now();
const oneHour = 60 * 60 * 1000;
if (now - dataTime <= oneHour) {
console.log('✅ 实时数据足够新,直接使用');
return recentHRV;
} else {
console.log('⚠️ 实时数据较旧,继续寻找更好的数据');
}
}
// 2. 如果没有实时数据或数据太旧,尝试获取当天的数据
console.log('2. 尝试获取当天的HRV数据...');
const todayHRV = await fetchHRVForDate(date);
if (todayHRV) {
console.log('✅ 成功获取到当天HRV数据:', todayHRV);
return todayHRV;
}
// 3. 如果当天没有数据尝试获取最近3天的数据
console.log('3. 尝试获取最近3天的HRV数据...');
const endDate = new Date(date);
const startDate = new Date(date);
startDate.setDate(startDate.getDate() - 3);
const recentOptions = {
startDate: startDate.toISOString(),
endDate: endDate.toISOString()
};
const recentData = await fetchHeartRateVariability(recentOptions);
if (recentData) {
console.log('✅ 成功获取到最近3天的HRV数据:', recentData);
return recentData;
}
// 4. 如果仍然没有数据返回null
console.log('❌ 未找到任何HRV数据');
return null;
} catch (error) {
console.error('智能HRV数据获取失败:', error);
return null;
}
}
// === 锻炼记录相关方法 ===
// 获取最近锻炼记录
export async function fetchRecentWorkouts(options?: Partial<WorkoutOptions>): Promise<WorkoutData[]> {
try {
console.log('开始获取最近锻炼记录...', options);
// 设置默认选项
const defaultOptions: WorkoutOptions = {
startDate: dayjs().subtract(30, 'day').startOf('day').toISOString(),
endDate: dayjs().endOf('day').toISOString(),
limit: 10
};
const finalOptions = { ...defaultOptions, ...options };
const result = await HealthKitManager.getRecentWorkouts(finalOptions);
if (result && result.data && Array.isArray(result.data)) {
logSuccess('锻炼记录', result);
// 验证和处理返回的数据
const validatedWorkouts: WorkoutData[] = result.data
.filter((workout: any) => {
// 基本数据验证
return workout &&
workout.id &&
workout.startDate &&
workout.endDate &&
workout.duration !== undefined;
})
.map((workout: any) => ({
id: workout.id,
startDate: workout.startDate,
endDate: workout.endDate,
duration: workout.duration,
workoutActivityType: workout.workoutActivityType || 0,
workoutActivityTypeString: workout.workoutActivityTypeString || 'unknown',
totalEnergyBurned: workout.totalEnergyBurned,
totalDistance: workout.totalDistance,
averageHeartRate: workout.averageHeartRate,
source: {
name: workout.source?.name || 'Unknown',
bundleIdentifier: workout.source?.bundleIdentifier || ''
},
metadata: workout.metadata || {}
}));
console.log(`成功获取 ${validatedWorkouts.length} 条锻炼记录`);
return validatedWorkouts;
} else {
logWarning('锻炼记录', '为空或格式错误');
return [];
}
} catch (error) {
logError('锻炼记录', error);
return [];
}
}
// 获取指定日期范围内的锻炼记录
export async function fetchWorkoutsForDateRange(
startDate: Date,
endDate: Date,
limit: number = 10
): Promise<WorkoutData[]> {
const options: WorkoutOptions = {
startDate: dayjs(startDate).startOf('day').toISOString(),
endDate: dayjs(endDate).endOf('day').toISOString(),
limit
};
return fetchRecentWorkouts(options);
}
// 获取今日锻炼记录
export async function fetchTodayWorkouts(): Promise<WorkoutData[]> {
const today = dayjs();
return fetchWorkoutsForDateRange(today.toDate(), today.toDate(), 20);
}
// 获取本周锻炼记录
export async function fetchThisWeekWorkouts(): Promise<WorkoutData[]> {
const today = dayjs();
const startOfWeek = today.startOf('week');
return fetchWorkoutsForDateRange(startOfWeek.toDate(), today.toDate(), 50);
}
// 获取本月锻炼记录
export async function fetchThisMonthWorkouts(): Promise<WorkoutData[]> {
const today = dayjs();
const startOfMonth = today.startOf('month');
return fetchWorkoutsForDateRange(startOfMonth.toDate(), today.toDate(), 100);
}
// 根据锻炼类型筛选锻炼记录
export function filterWorkoutsByType(
workouts: WorkoutData[],
workoutType: WorkoutActivityType
): WorkoutData[] {
return workouts.filter(workout => workout.workoutActivityType === workoutType);
}
// 获取锻炼统计信息
export function getWorkoutStatistics(workouts: WorkoutData[]): {
totalWorkouts: number;
totalDuration: number; // 秒
totalEnergyBurned: number; // 千卡
totalDistance: number; // 米
averageDuration: number; // 秒
workoutTypes: Record<string, number>; // 各类型锻炼次数
} {
const stats = {
totalWorkouts: workouts.length,
totalDuration: 0,
totalEnergyBurned: 0,
totalDistance: 0,
averageDuration: 0,
workoutTypes: {} as Record<string, number>
};
workouts.forEach(workout => {
stats.totalDuration += workout.duration;
stats.totalEnergyBurned += workout.totalEnergyBurned || 0;
stats.totalDistance += workout.totalDistance || 0;
// 统计锻炼类型
const typeString = workout.workoutActivityTypeString;
stats.workoutTypes[typeString] = (stats.workoutTypes[typeString] || 0) + 1;
});
if (stats.totalWorkouts > 0) {
stats.averageDuration = Math.round(stats.totalDuration / stats.totalWorkouts);
}
return stats;
}
// 格式化锻炼持续时间
export function formatWorkoutDuration(durationInSeconds: number): string {
const hours = Math.floor(durationInSeconds / 3600);
const minutes = Math.floor((durationInSeconds % 3600) / 60);
const seconds = durationInSeconds % 60;
if (hours > 0) {
return `${hours}小时${minutes}分钟`;
} else if (minutes > 0) {
return `${minutes}分钟${seconds}`;
} else {
return `${seconds}`;
}
}
// 格式化锻炼距离
export function formatWorkoutDistance(distanceInMeters: number): string {
if (distanceInMeters >= 1000) {
return `${(distanceInMeters / 1000).toFixed(2)}公里`;
} else {
return `${Math.round(distanceInMeters)}`;
}
}
function humanizeWorkoutTypeKey(raw: string | undefined): string {
if (!raw) {
return i18n.t('workoutTypes.other');
}
const cleaned = raw
.replace(/^HKWorkoutActivityType/i, '')
.replace(/[_\-]+/g, ' ')
.trim();
if (!cleaned) {
return i18n.t('workoutTypes.other');
}
const withSpaces = cleaned.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
const words = withSpaces
.split(/\s+/)
.filter(Boolean)
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
return words.join(' ');
}
// 获取锻炼类型的显示名称
export function getWorkoutTypeDisplayName(workoutType: WorkoutActivityType | string): string {
if (typeof workoutType === 'string') {
const normalized = workoutType.replace(/\s+/g, '').toLowerCase();
// 尝试从翻译中获取
const translationKey = `workoutTypes.${normalized}`;
if (i18n.exists(translationKey)) {
return i18n.t(translationKey);
}
return humanizeWorkoutTypeKey(workoutType);
}
// 使用枚举值查找对应的字符串键
const enumKey = WorkoutActivityType[workoutType];
if (enumKey) {
const normalized = enumKey.toLowerCase();
const translationKey = `workoutTypes.${normalized}`;
if (i18n.exists(translationKey)) {
return i18n.t(translationKey);
}
}
return humanizeWorkoutTypeKey(WorkoutActivityType[workoutType]);
}
// 测试锻炼记录获取功能
export async function testWorkoutDataFetch(): Promise<void> {
console.log('=== 开始测试锻炼记录获取 ===');
try {
// 确保权限
const hasPermission = await ensureHealthPermissions();
if (!hasPermission) {
console.error('没有健康数据权限,无法测试锻炼记录');
return;
}
console.log('权限检查通过,开始获取锻炼记录...');
// 测试获取最近锻炼记录
console.log('--- 测试获取最近锻炼记录 ---');
const recentWorkouts = await fetchRecentWorkouts();
console.log(`获取到 ${recentWorkouts.length} 条最近锻炼记录`);
recentWorkouts.forEach((workout, index) => {
console.log(`锻炼 ${index + 1}:`, {
类型: getWorkoutTypeDisplayName(workout.workoutActivityTypeString),
持续时间: formatWorkoutDuration(workout.duration),
能量消耗: workout.totalEnergyBurned ? `${workout.totalEnergyBurned}千卡` : '无',
距离: workout.totalDistance ? formatWorkoutDistance(workout.totalDistance) : '无',
开始时间: workout.startDate,
数据来源: workout.source.name
});
});
// 测试统计功能
if (recentWorkouts.length > 0) {
console.log('--- 锻炼统计信息 ---');
const stats = getWorkoutStatistics(recentWorkouts);
console.log('统计结果:', {
总锻炼次数: stats.totalWorkouts,
总持续时间: formatWorkoutDuration(stats.totalDuration),
: `${stats.totalEnergyBurned}千卡`,
总距离: formatWorkoutDistance(stats.totalDistance),
平均持续时间: formatWorkoutDuration(stats.averageDuration),
锻炼类型分布: stats.workoutTypes
});
}
console.log('=== 锻炼记录测试完成 ===');
} catch (error) {
console.error('锻炼记录测试过程中出现错误:', error);
}
}
// 获取HRV数据并附带详细的状态信息
export async function fetchHRVWithStatus(date: Date): Promise<{
hrvData: HRVData | null;
status: 'realtime' | 'recent' | 'historical' | 'none';
message: string;
}> {
try {
const hrvData = await fetchSmartHRVData(date);
if (!hrvData) {
return {
hrvData: null,
status: 'none',
message: '未找到HRV数据'
};
}
const dataTime = new Date(hrvData.recordedAt).getTime();
const now = Date.now();
const oneHour = 60 * 60 * 1000;
const oneDay = 24 * 60 * 60 * 1000;
let status: 'realtime' | 'recent' | 'historical';
let message: string;
if (now - dataTime <= oneHour) {
status = 'realtime';
message = '实时HRV数据';
} else if (now - dataTime <= oneDay) {
status = 'recent';
message = '近期HRV数据';
} else {
status = 'historical';
message = '历史HRV数据';
}
return {
hrvData,
status,
message
};
} catch (error) {
console.error('获取HRV状态失败:', error);
return {
hrvData: null,
status: 'none',
message: '获取HRV数据失败'
};
}
}