- 在目标列表中添加左滑删除功能,用户可通过左滑手势显示删除按钮并确认删除目标 - 修改 GoalCard 组件,使用 Swipeable 组件包装卡片内容,支持删除操作 - 更新目标列表页面,集成删除目标的逻辑,确保与 Redux 状态管理一致 - 添加开发模式下的模拟数据,方便测试删除功能 - 更新相关文档,详细描述左滑删除功能的实现和使用方法
419 lines
14 KiB
TypeScript
419 lines
14 KiB
TypeScript
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();
|
||
});
|
||
});
|
||
}
|