feat(health): 完善HealthKit权限管理和数据获取系统

- 重构权限管理,新增SimpleEventEmitter实现状态监听
- 实现完整的健身圆环数据获取(活动热量、锻炼时间、站立小时)
- 优化组件状态管理,支持实时数据刷新和权限状态响应
- 新增useHealthPermissions Hook,简化权限状态管理
- 完善iOS原生代码,支持按小时统计健身数据
- 优化应用启动时权限初始化流程,避免启动弹窗

BREAKING CHANGE: FitnessRingsCard组件API变更,移除手动传参改为自动获取数据
This commit is contained in:
richarjiang
2025-09-19 14:16:11 +08:00
parent 184fb672b7
commit ccfccca7bc
11 changed files with 1044 additions and 360 deletions

View File

@@ -1,20 +1,14 @@
import React from 'react';
import React, { useState, useCallback, useRef } from 'react';
import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';
import { router } from 'expo-router';
import { useFocusEffect } from '@react-navigation/native';
import { CircularRing } from './CircularRing';
import { ROUTES } from '@/constants/Routes';
import { fetchActivityRingsForDate, ActivityRingsData } from '@/utils/health';
type FitnessRingsCardProps = {
style?: any;
// 活动卡路里数据
activeCalories?: number;
activeCaloriesGoal?: number;
// 锻炼分钟数据
exerciseMinutes?: number;
exerciseMinutesGoal?: number;
// 站立小时数据
standHours?: number;
standHoursGoal?: number;
selectedDate?: Date;
// 动画重置令牌
resetToken?: unknown;
};
@@ -24,14 +18,48 @@ type FitnessRingsCardProps = {
*/
export function FitnessRingsCard({
style,
activeCalories = 25,
activeCaloriesGoal = 350,
exerciseMinutes = 1,
exerciseMinutesGoal = 5,
standHours = 2,
standHoursGoal = 13,
selectedDate,
resetToken,
}: FitnessRingsCardProps) {
const [activityData, setActivityData] = useState<ActivityRingsData | null>(null);
const [loading, setLoading] = useState(false);
const loadingRef = useRef(false);
// 获取健身圆环数据 - 在页面聚焦、日期变化、从后台切换到前台时触发
useFocusEffect(
useCallback(() => {
const loadActivityData = async () => {
if (!selectedDate) return;
// 防止重复请求
if (loadingRef.current) return;
try {
loadingRef.current = true;
setLoading(true);
const data = await fetchActivityRingsForDate(selectedDate);
setActivityData(data);
} catch (error) {
console.error('FitnessRingsCard: 获取健身圆环数据失败:', error);
setActivityData(null);
} finally {
setLoading(false);
loadingRef.current = false;
}
};
loadActivityData();
}, [selectedDate])
);
// 使用获取到的数据或默认值
const activeCalories = activityData?.activeEnergyBurned ?? 0;
const activeCaloriesGoal = activityData?.activeEnergyBurnedGoal ?? 350;
const exerciseMinutes = activityData?.appleExerciseTime ?? 0;
const exerciseMinutesGoal = activityData?.appleExerciseTimeGoal ?? 30;
const standHours = activityData?.appleStandHours ?? 0;
const standHoursGoal = activityData?.appleStandHoursGoal ?? 12;
// 计算进度百分比
const caloriesProgress = Math.min(1, Math.max(0, activeCalories / activeCaloriesGoal));
const exerciseProgress = Math.min(1, Math.max(0, exerciseMinutes / exerciseMinutesGoal));
@@ -95,24 +123,42 @@ export function FitnessRingsCard({
<View style={styles.dataContainer}>
<View style={styles.dataRow}>
<Text style={styles.dataText}>
<Text style={styles.dataValue}>{Math.round(activeCalories)}</Text>
<Text style={styles.dataGoal}>/{activeCaloriesGoal}</Text>
{loading ? (
<Text style={styles.dataValue}>--</Text>
) : (
<>
<Text style={styles.dataValue}>{Math.round(activeCalories)}</Text>
<Text style={styles.dataGoal}>/{activeCaloriesGoal}</Text>
</>
)}
</Text>
<Text style={styles.dataUnit}></Text>
</View>
<View style={styles.dataRow}>
<Text style={styles.dataText}>
<Text style={styles.dataValue}>{Math.round(exerciseMinutes)}</Text>
<Text style={styles.dataGoal}>/{exerciseMinutesGoal}</Text>
{loading ? (
<Text style={styles.dataValue}>--</Text>
) : (
<>
<Text style={styles.dataValue}>{Math.round(exerciseMinutes)}</Text>
<Text style={styles.dataGoal}>/{exerciseMinutesGoal}</Text>
</>
)}
</Text>
<Text style={styles.dataUnit}></Text>
</View>
<View style={styles.dataRow}>
<Text style={styles.dataText}>
<Text style={styles.dataValue}>{Math.round(standHours)}</Text>
<Text style={styles.dataGoal}>/{standHoursGoal}</Text>
{loading ? (
<Text style={styles.dataValue}>--</Text>
) : (
<>
<Text style={styles.dataValue}>{Math.round(standHours)}</Text>
<Text style={styles.dataGoal}>/{standHoursGoal}</Text>
</>
)}
</Text>
<Text style={styles.dataUnit}></Text>
</View>