Files
digital-pilates/utils/health.ts
richarjiang 4f2bd76b8f feat: 实现目标列表左滑删除功能及相关组件
- 在目标列表中添加左滑删除功能,用户可通过左滑手势显示删除按钮并确认删除目标
- 修改 GoalCard 组件,使用 Swipeable 组件包装卡片内容,支持删除操作
- 更新目标列表页面,集成删除目标的逻辑,确保与 Redux 状态管理一致
- 添加开发模式下的模拟数据,方便测试删除功能
- 更新相关文档,详细描述左滑删除功能的实现和使用方法
2025-08-23 17:58:39 +08:00

419 lines
14 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';
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);
});
});
}
export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthData> {
console.log('开始获取指定日期健康数据...', date);
const start = dayjs(date).startOf('day').toDate();
const end = dayjs(date).endOf('day').toDate();
const options = {
startDate: start.toISOString(),
endDate: end.toISOString()
} as any;
const activitySummaryOptions = {
startDate: start.toISOString(),
endDate: end.toISOString()
};
console.log('查询选项:', options);
// 并行获取所有健康数据包括ActivitySummary、血氧饱和度和心率
const [steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary, oxygenSaturation, heartRate] = await Promise.all([
// 获取步数
new Promise<number>((resolve) => {
AppleHealthKit.getStepCount({
date: dayjs(date).toISOString()
}, (err, res) => {
if (err) {
console.error('获取步数失败:', err);
return resolve(0);
}
if (!res) {
console.warn('步数数据为空');
return resolve(0);
}
console.log('步数数据:', res);
resolve(res.value || 0);
});
}),
// 获取消耗卡路里
new Promise<number>((resolve) => {
AppleHealthKit.getActiveEnergyBurned(options, (err, res) => {
if (err) {
console.error('获取消耗卡路里失败:', err);
return resolve(0);
}
if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('卡路里数据为空或格式错误');
return resolve(0);
}
console.log('卡路里数据:', res);
// 求和该日内的所有记录(单位:千卡)
const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
resolve(total);
});
}),
// 获取基础代谢率
new Promise<number>((resolve) => {
AppleHealthKit.getBasalEnergyBurned(options, (err, res) => {
if (err) {
console.error('获取基础代谢失败:', err);
return resolve(0);
}
if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('基础代谢数据为空或格式错误');
return resolve(0);
}
console.log('基础代谢数据:', res);
// 求和该日内的所有记录(单位:千卡)
const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
resolve(total);
});
}),
// 获取睡眠时长
new Promise<number>((resolve) => {
AppleHealthKit.getSleepSamples(options, (err, res) => {
if (err) {
console.error('获取睡眠数据失败:', err);
return resolve(0);
}
if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('睡眠数据为空或格式错误');
return resolve(0);
}
console.log('睡眠数据:', res);
// 计算总睡眠时间(单位:分钟)
let totalSleepDuration = 0;
res.forEach((sample: any) => {
if (sample && sample.startDate && sample.endDate) {
const startTime = new Date(sample.startDate).getTime();
const endTime = new Date(sample.endDate).getTime();
const durationMinutes = (endTime - startTime) / (1000 * 60);
totalSleepDuration += durationMinutes;
}
});
resolve(totalSleepDuration);
});
}),
// 获取HRV数据
new Promise<number | null>((resolve) => {
AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => {
if (err) {
console.error('获取HRV数据失败:', err);
return resolve(null);
}
if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('HRV数据为空或格式错误');
return resolve(null);
}
console.log('HRV数据:', res);
// 获取最新的HRV值
const latestHrv = res[res.length - 1];
if (latestHrv && latestHrv.value) {
resolve(Math.round(latestHrv.value * 1000));
} else {
resolve(null);
}
});
}),
// 获取ActivitySummary数据健身圆环数据
new Promise<HealthActivitySummary | null>((resolve) => {
AppleHealthKit.getActivitySummary(
activitySummaryOptions,
(err: Object, results: HealthActivitySummary[]) => {
if (err) {
console.error('获取ActivitySummary失败:', err);
return resolve(null);
}
if (!results || results.length === 0) {
console.warn('ActivitySummary数据为空');
return resolve(null);
}
console.log('ActivitySummary数据:', results[0]);
resolve(results[0]);
},
);
}),
// 获取血氧饱和度数据
new Promise<number | null>((resolve) => {
AppleHealthKit.getOxygenSaturationSamples(options, (err, res) => {
if (err) {
console.error('获取血氧饱和度失败:', err);
return resolve(null);
}
if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('血氧饱和度数据为空或格式错误');
return resolve(null);
}
console.log('血氧饱和度数据:', res);
// 获取最新的血氧饱和度值
const latestOxygen = res[res.length - 1];
if (latestOxygen && latestOxygen.value !== undefined && latestOxygen.value !== null) {
let value = Number(latestOxygen.value);
// 检查数据格式如果值小于1可能是小数形式0.0-1.0),需要转换为百分比
if (value > 0 && value < 1) {
value = value * 100;
console.log('血氧饱和度数据从小数转换为百分比:', latestOxygen.value, '->', value);
}
// 血氧饱和度通常在0-100之间验证数据有效性
if (value >= 0 && value <= 100) {
resolve(Number(value.toFixed(1)));
} else {
console.warn('血氧饱和度数据异常:', value);
resolve(null);
}
} else {
resolve(null);
}
});
}),
// 获取心率数据
new Promise<number | null>((resolve) => {
AppleHealthKit.getHeartRateSamples(options, (err, res) => {
if (err) {
console.error('获取心率失败:', err);
return resolve(null);
}
if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('心率数据为空或格式错误');
return resolve(null);
}
console.log('心率数据:', res);
// 获取最新的心率值
const latestHeartRate = res[res.length - 1];
if (latestHeartRate && latestHeartRate.value !== undefined && latestHeartRate.value !== null) {
// 心率通常在30-200之间验证数据有效性
const value = Number(latestHeartRate.value);
if (value >= 30 && value <= 200) {
resolve(Math.round(value));
} else {
console.warn('心率数据异常:', value);
resolve(null);
}
} else {
resolve(null);
}
});
})
]);
console.log('指定日期健康数据获取完成:', { steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary, oxygenSaturation, heartRate });
return {
steps,
activeEnergyBurned: calories,
basalEnergyBurned: basalMetabolism,
sleepDuration,
hrv,
// 健身圆环数据
activeCalories: activitySummary?.activeEnergyBurned || 0,
activeCaloriesGoal: activitySummary?.activeEnergyBurnedGoal || 350,
exerciseMinutes: activitySummary?.appleExerciseTime || 0,
exerciseMinutesGoal: activitySummary?.appleExerciseTimeGoal || 30,
standHours: activitySummary?.appleStandHours || 0,
standHoursGoal: activitySummary?.appleStandHoursGoal || 12,
// 血氧饱和度和心率数据
oxygenSaturation,
heartRate
};
}
export async function fetchTodayHealthData(): Promise<TodayHealthData> {
return fetchHealthDataForDate(new Date());
}
// 新增专门获取HRV数据的函数
export async function fetchHRVForDate(date: Date): Promise<number | null> {
console.log('开始获取指定日期HRV数据...', date);
const start = new Date(date);
start.setHours(0, 0, 0, 0);
const end = new Date(date);
end.setHours(23, 59, 59, 999);
const options = {
startDate: start.toISOString(),
endDate: end.toISOString()
} as any;
return new Promise<number | null>((resolve) => {
AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => {
if (err) {
console.error('获取HRV数据失败:', err);
return resolve(null);
}
if (!res || !Array.isArray(res) || res.length === 0) {
console.warn('HRV数据为空或格式错误');
return resolve(null);
}
console.log('HRV数据:', res);
// 获取最新的HRV值
const latestHrv = res[res.length - 1];
if (latestHrv && latestHrv.value) {
resolve(latestHrv.value);
} else {
resolve(null);
}
});
});
}
// 新增获取今日HRV数据
export async function fetchTodayHRV(): Promise<number | null> {
return fetchHRVForDate(new Date());
}
// 更新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 = new Date()): Promise<void> {
console.log('=== 开始测试血氧饱和度数据获取 ===');
const start = dayjs(date).startOf('day').toDate();
const end = dayjs(date).endOf('day').toDate();
const options = {
startDate: start.toISOString(),
endDate: end.toISOString()
} as any;
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 && latestOxygen.value !== undefined && latestOxygen.value !== null) {
let value = Number(latestOxygen.value);
console.log('处理前的值:', latestOxygen.value);
console.log('转换为数字后的值:', value);
// 检查数据格式如果值小于1可能是小数形式0.0-1.0),需要转换为百分比
if (value > 0 && value < 1) {
const originalValue = value;
value = value * 100;
console.log('血氧饱和度数据从小数转换为百分比:', originalValue, '->', value);
}
console.log('最终处理后的值:', value);
console.log('数据有效性检查:', value >= 0 && value <= 100 ? '有效' : '无效');
}
console.log('=== 血氧饱和度数据测试完成 ===');
resolve();
});
});
}