将sleepHealthKit模块中的HealthKit权限初始化逻辑替换为使用health工具中的通用ensureHealthPermissions方法,移除重复的权限配置代码。同时更新后台任务标识符以保持一致性。
918 lines
29 KiB
TypeScript
918 lines
29 KiB
TypeScript
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 = {
|
||
steps: number;
|
||
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;
|
||
// 每小时步数数据
|
||
hourlySteps: HourlyStepData[];
|
||
};
|
||
|
||
export async function ensureHealthPermissions(): Promise<boolean> {
|
||
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;
|
||
}
|
||
|
||
// 健康数据获取函数
|
||
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);
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
// 使用样本数据获取每小时步数
|
||
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);
|
||
// 如果主方法失败,返回默认数据
|
||
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<HourlyActivityData[]> {
|
||
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<number>((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<HourlyExerciseData[]> {
|
||
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<number[]> {
|
||
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<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 fetchHeartRateVariability(options: HealthDataOptions): Promise<number | null> {
|
||
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);
|
||
}
|
||
|
||
logSuccess('HRV', res);
|
||
console.log('HRV数据样本数量:', res.length);
|
||
|
||
// 打印最新的几个样本用于调试
|
||
const latestSamples = res.slice(-3);
|
||
console.log('最新的HRV样本:', latestSamples.map(sample => ({
|
||
value: sample.value,
|
||
startDate: sample.startDate,
|
||
endDate: sample.endDate
|
||
})));
|
||
|
||
const latestHrv = res[res.length - 1];
|
||
if (latestHrv && latestHrv.value !== undefined && latestHrv.value !== null) {
|
||
// HealthKit 中的 HRV 数据已经是毫秒单位,无需转换
|
||
const hrvValue = Math.round(latestHrv.value);
|
||
console.log('最终HRV值:', hrvValue);
|
||
resolve(hrvValue);
|
||
} else {
|
||
console.warn('HRV样本值无效:', latestHrv);
|
||
resolve(null);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
async function fetchActivitySummary(options: HealthDataOptions): Promise<HealthActivitySummary | null> {
|
||
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<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));
|
||
});
|
||
});
|
||
}
|
||
|
||
// 获取指定时间范围内的最大心率
|
||
export async function fetchMaximumHeartRate(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);
|
||
}
|
||
|
||
// 从所有心率样本中找出最大值
|
||
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 {
|
||
steps: 0,
|
||
activeEnergyBurned: 0,
|
||
basalEnergyBurned: 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,
|
||
hrv,
|
||
activitySummary,
|
||
oxygenSaturation,
|
||
heartRate
|
||
] = await Promise.all([
|
||
fetchStepCount(date),
|
||
fetchHourlyStepSamples(date),
|
||
fetchActiveEnergyBurned(options),
|
||
fetchBasalEnergyBurned(options),
|
||
fetchHeartRateVariability(options),
|
||
fetchActivitySummary(options),
|
||
fetchOxygenSaturation(options),
|
||
fetchHeartRate(options)
|
||
]);
|
||
|
||
return {
|
||
steps,
|
||
hourlySteps,
|
||
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) {
|
||
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();
|
||
});
|
||
});
|
||
}
|
||
|
||
// 添加饮水记录到 HealthKit
|
||
export async function saveWaterIntakeToHealthKit(amount: number, recordedAt?: string): Promise<boolean> {
|
||
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<any[]> {
|
||
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<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;
|
||
}
|
||
}
|
||
|