Files
digital-pilates/utils/health.ts
richarjiang 3f89023447 feat: 更新 CLAUDE.md 文件及多个组件以优化用户体验和功能
- 更新 CLAUDE.md 文件,重构架构部分,增加认证和数据层的描述
- 在 GoalsScreen 中新增目标模板选择功能,支持用户选择和创建目标
- 在 CreateGoalModal 中添加初始数据支持,优化目标创建体验
- 新增 GoalTemplateModal 组件,提供目标模板选择界面
- 更新 NotificationHelpers,支持构建深度链接以便于导航
- 在 CoachScreen 中处理路由参数,增强用户交互体验
- 更新多个组件的样式和逻辑,提升整体用户体验
- 删除不再使用的中文回复规则文档
2025-08-26 15:04:04 +08:00

447 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import dayjs from 'dayjs';
import type { HealthActivitySummary, HealthKitPermissions } from 'react-native-health';
import AppleHealthKit from 'react-native-health';
type HealthDataOptions = {
startDate: string;
endDate: string;
};
const PERMISSIONS: HealthKitPermissions = {
permissions: {
read: [
AppleHealthKit.Constants.Permissions.StepCount,
AppleHealthKit.Constants.Permissions.ActiveEnergyBurned,
AppleHealthKit.Constants.Permissions.BasalEnergyBurned,
AppleHealthKit.Constants.Permissions.SleepAnalysis,
AppleHealthKit.Constants.Permissions.HeartRateVariability,
AppleHealthKit.Constants.Permissions.ActivitySummary,
AppleHealthKit.Constants.Permissions.OxygenSaturation,
AppleHealthKit.Constants.Permissions.HeartRate,
],
write: [
// 支持体重写入
AppleHealthKit.Constants.Permissions.Weight,
],
},
};
export type TodayHealthData = {
steps: number;
activeEnergyBurned: number; // kilocalories
basalEnergyBurned: number; // kilocalories - 基础代谢率
sleepDuration: number; // 睡眠时长(分钟)
hrv: number | null; // 心率变异性 (ms)
// 健身圆环数据
activeCalories: number;
activeCaloriesGoal: number;
exerciseMinutes: number;
exerciseMinutesGoal: number;
standHours: number;
standHoursGoal: number;
// 新增血氧饱和度和心率数据
oxygenSaturation: number | null;
heartRate: number | null;
};
export async function ensureHealthPermissions(): Promise<boolean> {
return new Promise((resolve) => {
console.log('开始初始化HealthKit...');
AppleHealthKit.initHealthKit(PERMISSIONS, (error) => {
if (error) {
console.error('HealthKit初始化失败:', error);
// 常见错误处理
if (typeof error === 'string') {
if (error.includes('not available')) {
console.warn('HealthKit不可用 - 可能在模拟器上运行或非iOS设备');
}
}
resolve(false);
return;
}
console.log('HealthKit初始化成功');
resolve(true);
});
});
}
// 日期工具函数
function createDateRange(date: Date): HealthDataOptions {
return {
startDate: dayjs(date).startOf('day').toDate().toISOString(),
endDate: dayjs(date).endOf('day').toDate().toISOString()
};
}
// 睡眠时长计算
function calculateSleepDuration(samples: any[]): number {
return samples.reduce((total, sample) => {
if (sample && sample.startDate && sample.endDate) {
const startTime = dayjs(sample.startDate).valueOf();
const endTime = dayjs(sample.endDate).valueOf();
const durationMinutes = (endTime - startTime) / (1000 * 60);
return total + durationMinutes;
}
return total;
}, 0);
}
// 通用错误处理
function logError(operation: string, error: any): void {
console.error(`获取${operation}失败:`, error);
}
function logWarning(operation: string, message: string): void {
console.warn(`${operation}数据${message}`);
}
function logSuccess(operation: string, data: any): void {
console.log(`${operation}数据:`, data);
}
// 数值验证和转换
function validateOxygenSaturation(value: any): number | null {
if (value === undefined || value === null) return null;
let numValue = Number(value);
// 如果值小于1可能是小数形式0.0-1.0),需要转换为百分比
if (numValue > 0 && numValue < 1) {
numValue = numValue * 100;
}
// 血氧饱和度通常在0-100之间验证数据有效性
if (numValue >= 0 && numValue <= 100) {
return Number(numValue.toFixed(1));
}
console.warn('血氧饱和度数据异常:', numValue);
return null;
}
function validateHeartRate(value: any): number | null {
if (value === undefined || value === null) return null;
const numValue = Number(value);
// 心率通常在30-200之间验证数据有效性
if (numValue >= 30 && numValue <= 200) {
return Math.round(numValue);
}
console.warn('心率数据异常:', numValue);
return null;
}
// 健康数据获取函数
async function fetchStepCount(date: Date): Promise<number> {
return new Promise((resolve) => {
AppleHealthKit.getStepCount({
date: dayjs(date).toDate().toISOString()
}, (err, res) => {
if (err) {
logError('步数', err);
return resolve(0);
}
if (!res) {
logWarning('步数', '为空');
return resolve(0);
}
logSuccess('步数', res);
resolve(res.value || 0);
});
});
}
async function fetchActiveEnergyBurned(options: HealthDataOptions): Promise<number> {
return new Promise((resolve) => {
AppleHealthKit.getActiveEnergyBurned(options, (err, res) => {
if (err) {
logError('消耗卡路里', err);
return resolve(0);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('卡路里', '为空或格式错误');
return resolve(0);
}
logSuccess('卡路里', res);
const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
resolve(total);
});
});
}
async function fetchBasalEnergyBurned(options: HealthDataOptions): Promise<number> {
return new Promise((resolve) => {
AppleHealthKit.getBasalEnergyBurned(options, (err, res) => {
if (err) {
logError('基础代谢', err);
return resolve(0);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('基础代谢', '为空或格式错误');
return resolve(0);
}
logSuccess('基础代谢', res);
const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
resolve(total);
});
});
}
async function fetchSleepDuration(options: HealthDataOptions): Promise<number> {
return new Promise((resolve) => {
AppleHealthKit.getSleepSamples(options, (err, res) => {
if (err) {
logError('睡眠数据', err);
return resolve(0);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('睡眠', '为空或格式错误');
return resolve(0);
}
logSuccess('睡眠', res);
resolve(calculateSleepDuration(res));
});
});
}
async function fetchHeartRateVariability(options: HealthDataOptions): Promise<number | null> {
return new Promise((resolve) => {
AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => {
if (err) {
logError('HRV数据', err);
return resolve(null);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('HRV', '为空或格式错误');
return resolve(null);
}
logSuccess('HRV', res);
const latestHrv = res[res.length - 1];
if (latestHrv && latestHrv.value) {
resolve(Math.round(latestHrv.value * 1000));
} else {
resolve(null);
}
});
});
}
async function fetchActivitySummary(options: HealthDataOptions): Promise<HealthActivitySummary | null> {
return new Promise((resolve) => {
AppleHealthKit.getActivitySummary(
options,
(err: Object, results: HealthActivitySummary[]) => {
if (err) {
logError('ActivitySummary', err);
return resolve(null);
}
if (!results || results.length === 0) {
logWarning('ActivitySummary', '为空');
return resolve(null);
}
logSuccess('ActivitySummary', results[0]);
resolve(results[0]);
},
);
});
}
async function fetchOxygenSaturation(options: HealthDataOptions): Promise<number | null> {
return new Promise((resolve) => {
AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => {
if (err) {
logError('血氧饱和度', err);
return resolve(null);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('血氧饱和度', '为空或格式错误');
return resolve(null);
}
logSuccess('血氧饱和度', res);
const latestOxygen = res[res.length - 1];
return resolve(validateOxygenSaturation(latestOxygen?.value));
});
});
}
async function fetchHeartRate(options: HealthDataOptions): Promise<number | null> {
return new Promise((resolve) => {
AppleHealthKit.getHeartRateSamples(options, (err, res) => {
if (err) {
logError('心率', err);
return resolve(null);
}
if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('心率', '为空或格式错误');
return resolve(null);
}
logSuccess('心率', res);
const latestHeartRate = res[res.length - 1];
return resolve(validateHeartRate(latestHeartRate?.value));
});
});
}
// 默认健康数据
function getDefaultHealthData(): TodayHealthData {
return {
steps: 0,
activeEnergyBurned: 0,
basalEnergyBurned: 0,
sleepDuration: 0,
hrv: null,
activeCalories: 0,
activeCaloriesGoal: 350,
exerciseMinutes: 0,
exerciseMinutesGoal: 30,
standHours: 0,
standHoursGoal: 12,
oxygenSaturation: null,
heartRate: null
};
}
export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthData> {
try {
console.log('开始获取指定日期健康数据...', date);
const options = createDateRange(date);
console.log('查询选项:', options);
// 并行获取所有健康数据
const [
steps,
activeEnergyBurned,
basalEnergyBurned,
sleepDuration,
hrv,
activitySummary,
oxygenSaturation,
heartRate
] = await Promise.all([
fetchStepCount(date),
fetchActiveEnergyBurned(options),
fetchBasalEnergyBurned(options),
fetchSleepDuration(options),
fetchHeartRateVariability(options),
fetchActivitySummary(options),
fetchOxygenSaturation(options),
fetchHeartRate(options)
]);
console.log('指定日期健康数据获取完成:', {
steps,
activeEnergyBurned,
basalEnergyBurned,
sleepDuration,
hrv,
activitySummary,
oxygenSaturation,
heartRate
});
return {
steps,
activeEnergyBurned,
basalEnergyBurned,
sleepDuration,
hrv,
activeCalories: Math.round(activitySummary?.activeEnergyBurned || 0),
activeCaloriesGoal: Math.round(activitySummary?.activeEnergyBurnedGoal || 350),
exerciseMinutes: Math.round(activitySummary?.appleExerciseTime || 0),
exerciseMinutesGoal: Math.round(activitySummary?.appleExerciseTimeGoal || 30),
standHours: Math.round(activitySummary?.appleStandHours || 0),
standHoursGoal: Math.round(activitySummary?.appleStandHoursGoal || 12),
oxygenSaturation,
heartRate
};
} catch (error) {
console.error('获取指定日期健康数据失败:', error);
return getDefaultHealthData();
}
}
export async function fetchTodayHealthData(): Promise<TodayHealthData> {
return fetchHealthDataForDate(dayjs().toDate());
}
export async function fetchHRVForDate(date: Date): Promise<number | null> {
console.log('开始获取指定日期HRV数据...', date);
const options = createDateRange(date);
return fetchHeartRateVariability(options);
}
export async function fetchTodayHRV(): Promise<number | null> {
return fetchHRVForDate(dayjs().toDate());
}
// 更新healthkit中的体重
export async function updateWeight(weight: number) {
return new Promise((resolve) => {
AppleHealthKit.saveWeight({
value: weight,
}, (err, res) => {
if (err) {
console.error('更新体重失败:', err);
return resolve(false);
}
console.log('体重更新成功:', res);
resolve(true);
});
});
}
export async function testOxygenSaturationData(date: Date = dayjs().toDate()): Promise<void> {
console.log('=== 开始测试血氧饱和度数据获取 ===');
const options = createDateRange(date);
return new Promise((resolve) => {
AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => {
if (err) {
console.error('获取血氧饱和度失败:', err);
resolve();
return;
}
console.log('原始血氧饱和度数据:', res);
if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('血氧饱和度数据为空');
resolve();
return;
}
// 分析所有数据样本
res.forEach((sample, index) => {
console.log(`样本 ${index + 1}:`, {
value: sample.value,
valueType: typeof sample.value,
startDate: sample.startDate,
endDate: sample.endDate
});
});
// 获取最新的血氧饱和度值并验证
const latestOxygen = res[res.length - 1];
if (latestOxygen?.value !== undefined && latestOxygen?.value !== null) {
const processedValue = validateOxygenSaturation(latestOxygen.value);
console.log('处理前的值:', latestOxygen.value);
console.log('最终处理后的值:', processedValue);
console.log('数据有效性检查:', processedValue !== null ? '有效' : '无效');
}
console.log('=== 血氧饱和度数据测试完成 ===');
resolve();
});
});
}