feat: Refactor MoodCalendarScreen to use dayjs for date handling and improve calendar data generation
feat: Update FitnessRingsCard to navigate to fitness rings detail page on press feat: Modify NutritionRadarCard to enhance UI and add haptic feedback on actions feat: Add FITNESS_RINGS_DETAIL route for navigation fix: Adjust minimum fetch interval in BackgroundTaskManager for background tasks feat: Implement haptic feedback utility functions for better user experience feat: Extend health permissions to include Apple Exercise Time and Apple Stand Time feat: Add functions to fetch hourly activity, exercise, and stand data for improved health tracking feat: Enhance user preferences to manage fitness exercise minutes and active hours info dismissal
This commit is contained in:
242
utils/health.ts
242
utils/health.ts
@@ -20,6 +20,9 @@ const PERMISSIONS: HealthKitPermissions = {
|
||||
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: [
|
||||
// 支持体重写入
|
||||
@@ -35,6 +38,21 @@ export type HourlyStepData = {
|
||||
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
|
||||
@@ -192,8 +210,9 @@ async function fetchHourlyStepSamples(date: Date): Promise<HourlyStepData[]> {
|
||||
(err: any, res: any[]) => {
|
||||
if (err) {
|
||||
logError('每小时步数样本', err);
|
||||
// 如果主方法失败,尝试使用备用方法
|
||||
return null
|
||||
// 如果主方法失败,返回默认数据
|
||||
resolve(Array.from({ length: 24 }, (_, i) => ({ hour: i, steps: 0 })));
|
||||
return;
|
||||
}
|
||||
|
||||
logSuccess('每小时步数样本', res);
|
||||
@@ -225,6 +244,165 @@ async function fetchHourlyStepSamples(date: Date): Promise<HourlyStepData[]> {
|
||||
});
|
||||
}
|
||||
|
||||
// 获取每小时活动热量数据
|
||||
// 优化版本:使用更精确的时间间隔来获取每小时数据
|
||||
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) => {
|
||||
@@ -612,3 +790,63 @@ export async function getCurrentHourStandStatus(): Promise<{ hasStood: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// === 专门为健身圆环详情页提供的独立函数 ===
|
||||
|
||||
// 精简的活动圆环数据类型,只包含必要字段
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user