feat(workout): 新增锻炼历史记录功能与健康数据集成

- 新增锻炼历史页面,展示最近一个月的锻炼记录详情
- 添加锻炼汇总卡片组件,在统计页面显示当日锻炼数据
- 集成HealthKit锻炼数据获取,支持多种运动类型和详细信息
- 完善锻炼数据处理工具,包含统计分析和格式化功能
- 优化后台任务,随机选择挑战发送鼓励通知
- 版本升级至1.0.16
This commit is contained in:
2025-10-02 22:13:59 +08:00
parent 303c36025b
commit 79ddd41a49
13 changed files with 1437 additions and 34 deletions

View File

@@ -5,8 +5,120 @@ import { SimpleEventEmitter } from './SimpleEventEmitter';
type HealthDataOptions = {
startDate: string;
endDate: string;
limit?: number;
};
// 锻炼数据类型定义
export interface WorkoutData {
id: string;
startDate: string;
endDate: string;
duration: number; // 秒
workoutActivityType: number;
workoutActivityTypeString: string;
totalEnergyBurned?: number; // 千卡
totalDistance?: number; // 米
averageHeartRate?: number;
source: {
name: string;
bundleIdentifier: string;
};
metadata: Record<string, any>;
}
// 锻炼记录查询选项
export interface WorkoutOptions extends HealthDataOptions {
limit?: number; // 默认10条
}
// 锻炼活动类型枚举
export enum WorkoutActivityType {
AmericanFootball = 1,
Archery = 2,
AustralianFootball = 3,
Badminton = 4,
Baseball = 5,
Basketball = 6,
Bowling = 7,
Boxing = 8,
Climbing = 9,
Cricket = 10,
CrossTraining = 11,
Curling = 12,
Cycling = 13,
Dance = 14,
DanceInspiredTraining = 15,
Elliptical = 16,
EquestrianSports = 17,
Fencing = 18,
Fishing = 19,
FunctionalStrengthTraining = 20,
Golf = 21,
Gymnastics = 22,
Handball = 23,
Hiking = 24,
Hockey = 25,
Hunting = 26,
Lacrosse = 27,
MartialArts = 28,
MindAndBody = 29,
MixedMetabolicCardioTraining = 30,
PaddleSports = 31,
Play = 32,
PreparationAndRecovery = 33,
Racquetball = 34,
Rowing = 35,
Rugby = 36,
Running = 37,
Sailing = 38,
SkatingSports = 39,
SnowSports = 40,
Soccer = 41,
Softball = 42,
Squash = 43,
StairClimbing = 44,
SurfingSports = 45,
Swimming = 46,
TableTennis = 47,
Tennis = 48,
TrackAndField = 49,
TraditionalStrengthTraining = 50,
Volleyball = 51,
Walking = 52,
WaterFitness = 53,
WaterPolo = 54,
WaterSports = 55,
Wrestling = 56,
Yoga = 57,
Barre = 58,
CoreTraining = 59,
CrossCountrySkiing = 60,
DownhillSkiing = 61,
Flexibility = 62,
HighIntensityIntervalTraining = 63,
JumpRope = 64,
Kickboxing = 65,
Pilates = 66,
Snowboarding = 67,
Stairs = 68,
StepTraining = 69,
WheelchairWalkPace = 70,
WheelchairRunPace = 71,
TaiChi = 72,
MixedCardio = 73,
HandCycling = 74,
DiscSports = 75,
FitnessGaming = 76,
CardioDance = 77,
SocialDance = 78,
Pickleball = 79,
Cooldown = 80,
SwimBikeRun = 82,
Transition = 83,
UnderwaterDiving = 84,
Other = 3000
}
// React Native bridge to native HealthKitManager
const { HealthKitManager } = NativeModules;
@@ -1317,6 +1429,314 @@ export async function fetchSmartHRVData(date: Date): Promise<HRVData | null> {
}
}
// === 锻炼记录相关方法 ===
// 获取最近锻炼记录
export async function fetchRecentWorkouts(options?: Partial<WorkoutOptions>): Promise<WorkoutData[]> {
try {
console.log('开始获取最近锻炼记录...', options);
// 设置默认选项
const defaultOptions: WorkoutOptions = {
startDate: dayjs().subtract(30, 'day').startOf('day').toISOString(),
endDate: dayjs().endOf('day').toISOString(),
limit: 10
};
const finalOptions = { ...defaultOptions, ...options };
const result = await HealthKitManager.getRecentWorkouts(finalOptions);
if (result && result.data && Array.isArray(result.data)) {
logSuccess('锻炼记录', result);
// 验证和处理返回的数据
const validatedWorkouts: WorkoutData[] = result.data
.filter((workout: any) => {
// 基本数据验证
return workout &&
workout.id &&
workout.startDate &&
workout.endDate &&
workout.duration !== undefined;
})
.map((workout: any) => ({
id: workout.id,
startDate: workout.startDate,
endDate: workout.endDate,
duration: workout.duration,
workoutActivityType: workout.workoutActivityType || 0,
workoutActivityTypeString: workout.workoutActivityTypeString || 'unknown',
totalEnergyBurned: workout.totalEnergyBurned,
totalDistance: workout.totalDistance,
averageHeartRate: workout.averageHeartRate,
source: {
name: workout.source?.name || 'Unknown',
bundleIdentifier: workout.source?.bundleIdentifier || ''
},
metadata: workout.metadata || {}
}));
console.log(`成功获取 ${validatedWorkouts.length} 条锻炼记录`);
return validatedWorkouts;
} else {
logWarning('锻炼记录', '为空或格式错误');
return [];
}
} catch (error) {
logError('锻炼记录', error);
return [];
}
}
// 获取指定日期范围内的锻炼记录
export async function fetchWorkoutsForDateRange(
startDate: Date,
endDate: Date,
limit: number = 10
): Promise<WorkoutData[]> {
const options: WorkoutOptions = {
startDate: dayjs(startDate).startOf('day').toISOString(),
endDate: dayjs(endDate).endOf('day').toISOString(),
limit
};
return fetchRecentWorkouts(options);
}
// 获取今日锻炼记录
export async function fetchTodayWorkouts(): Promise<WorkoutData[]> {
const today = dayjs();
return fetchWorkoutsForDateRange(today.toDate(), today.toDate(), 20);
}
// 获取本周锻炼记录
export async function fetchThisWeekWorkouts(): Promise<WorkoutData[]> {
const today = dayjs();
const startOfWeek = today.startOf('week');
return fetchWorkoutsForDateRange(startOfWeek.toDate(), today.toDate(), 50);
}
// 获取本月锻炼记录
export async function fetchThisMonthWorkouts(): Promise<WorkoutData[]> {
const today = dayjs();
const startOfMonth = today.startOf('month');
return fetchWorkoutsForDateRange(startOfMonth.toDate(), today.toDate(), 100);
}
// 根据锻炼类型筛选锻炼记录
export function filterWorkoutsByType(
workouts: WorkoutData[],
workoutType: WorkoutActivityType
): WorkoutData[] {
return workouts.filter(workout => workout.workoutActivityType === workoutType);
}
// 获取锻炼统计信息
export function getWorkoutStatistics(workouts: WorkoutData[]): {
totalWorkouts: number;
totalDuration: number; // 秒
totalEnergyBurned: number; // 千卡
totalDistance: number; // 米
averageDuration: number; // 秒
workoutTypes: Record<string, number>; // 各类型锻炼次数
} {
const stats = {
totalWorkouts: workouts.length,
totalDuration: 0,
totalEnergyBurned: 0,
totalDistance: 0,
averageDuration: 0,
workoutTypes: {} as Record<string, number>
};
workouts.forEach(workout => {
stats.totalDuration += workout.duration;
stats.totalEnergyBurned += workout.totalEnergyBurned || 0;
stats.totalDistance += workout.totalDistance || 0;
// 统计锻炼类型
const typeString = workout.workoutActivityTypeString;
stats.workoutTypes[typeString] = (stats.workoutTypes[typeString] || 0) + 1;
});
if (stats.totalWorkouts > 0) {
stats.averageDuration = Math.round(stats.totalDuration / stats.totalWorkouts);
}
return stats;
}
// 格式化锻炼持续时间
export function formatWorkoutDuration(durationInSeconds: number): string {
const hours = Math.floor(durationInSeconds / 3600);
const minutes = Math.floor((durationInSeconds % 3600) / 60);
const seconds = durationInSeconds % 60;
if (hours > 0) {
return `${hours}小时${minutes}分钟`;
} else if (minutes > 0) {
return `${minutes}分钟${seconds}`;
} else {
return `${seconds}`;
}
}
// 格式化锻炼距离
export function formatWorkoutDistance(distanceInMeters: number): string {
if (distanceInMeters >= 1000) {
return `${(distanceInMeters / 1000).toFixed(2)}公里`;
} else {
return `${Math.round(distanceInMeters)}`;
}
}
const WORKOUT_TYPE_LABELS: Record<string, string> = {
running: '跑步',
walking: '步行',
cycling: '骑行',
swimming: '游泳',
yoga: '瑜伽',
functionalstrengthtraining: '功能性力量训练',
traditionalstrengthtraining: '传统力量训练',
crosstraining: '交叉训练',
mixedcardio: '混合有氧',
highintensityintervaltraining: '高强度间歇训练',
flexibility: '柔韧性训练',
cooldown: '放松运动',
pilates: '普拉提',
dance: '舞蹈',
danceinspiredtraining: '舞蹈训练',
cardiodance: '有氧舞蹈',
socialdance: '社交舞',
swimbikerun: '铁人三项',
transition: '项目转换',
underwaterdiving: '水下潜水',
pickleball: '匹克球',
americanfootball: '美式橄榄球',
badminton: '羽毛球',
baseball: '棒球',
basketball: '篮球',
tennis: '网球',
tabletennis: '乒乓球',
functionalStrengthTraining: '功能性力量训练',
other: '其他运动',
};
function humanizeWorkoutTypeKey(raw: string | undefined): string {
if (!raw) {
return '其他运动';
}
const cleaned = raw
.replace(/^HKWorkoutActivityType/i, '')
.replace(/[_\-]+/g, ' ')
.trim();
if (!cleaned) {
return '其他运动';
}
const withSpaces = cleaned.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
const words = withSpaces
.split(/\s+/)
.filter(Boolean)
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
return words.join(' ');
}
// 获取锻炼类型的显示名称
export function getWorkoutTypeDisplayName(workoutType: WorkoutActivityType | string): string {
if (typeof workoutType === 'string') {
const normalized = workoutType.replace(/\s+/g, '').toLowerCase();
return WORKOUT_TYPE_LABELS[normalized] || humanizeWorkoutTypeKey(workoutType);
}
switch (workoutType) {
case WorkoutActivityType.Running:
return '跑步';
case WorkoutActivityType.Cycling:
return '骑行';
case WorkoutActivityType.Walking:
return '步行';
case WorkoutActivityType.Swimming:
return '游泳';
case WorkoutActivityType.Yoga:
return '瑜伽';
case WorkoutActivityType.FunctionalStrengthTraining:
return '功能性力量训练';
case WorkoutActivityType.TraditionalStrengthTraining:
return '传统力量训练';
case WorkoutActivityType.CrossTraining:
return '交叉训练';
case WorkoutActivityType.MixedCardio:
return '混合有氧';
case WorkoutActivityType.HighIntensityIntervalTraining:
return '高强度间歇训练';
case WorkoutActivityType.Flexibility:
return '柔韧性训练';
case WorkoutActivityType.Cooldown:
return '放松运动';
case WorkoutActivityType.Tennis:
return '网球';
case WorkoutActivityType.Other:
return '其他运动';
default:
return humanizeWorkoutTypeKey(WorkoutActivityType[workoutType]);
}
}
// 测试锻炼记录获取功能
export async function testWorkoutDataFetch(): Promise<void> {
console.log('=== 开始测试锻炼记录获取 ===');
try {
// 确保权限
const hasPermission = await ensureHealthPermissions();
if (!hasPermission) {
console.error('没有健康数据权限,无法测试锻炼记录');
return;
}
console.log('权限检查通过,开始获取锻炼记录...');
// 测试获取最近锻炼记录
console.log('--- 测试获取最近锻炼记录 ---');
const recentWorkouts = await fetchRecentWorkouts();
console.log(`获取到 ${recentWorkouts.length} 条最近锻炼记录`);
recentWorkouts.forEach((workout, index) => {
console.log(`锻炼 ${index + 1}:`, {
类型: getWorkoutTypeDisplayName(workout.workoutActivityTypeString),
持续时间: formatWorkoutDuration(workout.duration),
能量消耗: workout.totalEnergyBurned ? `${workout.totalEnergyBurned}千卡` : '无',
距离: workout.totalDistance ? formatWorkoutDistance(workout.totalDistance) : '无',
开始时间: workout.startDate,
数据来源: workout.source.name
});
});
// 测试统计功能
if (recentWorkouts.length > 0) {
console.log('--- 锻炼统计信息 ---');
const stats = getWorkoutStatistics(recentWorkouts);
console.log('统计结果:', {
总锻炼次数: stats.totalWorkouts,
总持续时间: formatWorkoutDuration(stats.totalDuration),
: `${stats.totalEnergyBurned}千卡`,
总距离: formatWorkoutDistance(stats.totalDistance),
平均持续时间: formatWorkoutDuration(stats.averageDuration),
锻炼类型分布: stats.workoutTypes
});
}
console.log('=== 锻炼记录测试完成 ===');
} catch (error) {
console.error('锻炼记录测试过程中出现错误:', error);
}
}
// 获取HRV数据并附带详细的状态信息
export async function fetchHRVWithStatus(date: Date): Promise<{
hrvData: HRVData | null;