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:
richarjiang
2025-09-05 15:32:34 +08:00
parent 460a7e4289
commit 83805a4b07
9 changed files with 1337 additions and 79 deletions

View File

@@ -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;
}
}