feat: 集成健康数据功能优化

- 在 app.json 中添加 react-native-health 的配置以启用健康 API
- 在 Explore 页面中重构健康数据加载逻辑,增加加载状态提示
- 更新健康数据获取函数,增强错误处理和日志输出
- 修改 iOS 权限设置,确保健康数据访问权限的正确配置
- 更新 Info.plist 中的健康数据使用说明
This commit is contained in:
richarjiang
2025-08-12 10:50:37 +08:00
parent e87dc87e1b
commit 456f0d93ce
6 changed files with 174 additions and 84 deletions

View File

@@ -51,23 +51,38 @@ export default function ExploreScreen() {
// HealthKit: 每次页面聚焦都拉取今日数据
const [stepCount, setStepCount] = useState<number | null>(null);
const [activeCalories, setActiveCalories] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(false);
const loadHealthData = async () => {
try {
console.log('=== 开始HealthKit初始化流程 ===');
setIsLoading(true);
const ok = await ensureHealthPermissions();
if (!ok) {
const errorMsg = '无法获取健康权限请确保在真实iOS设备上运行并授权应用访问健康数据';
console.warn(errorMsg);
return;
}
console.log('权限获取成功,开始获取健康数据...');
const data = await fetchTodayHealthData();
console.log('设置UI状态:', data);
setStepCount(data.steps);
setActiveCalories(Math.round(data.activeEnergyBurned));
console.log('=== HealthKit数据获取完成 ===');
} catch (error) {
console.error('HealthKit流程出现异常:', error);
} finally {
setIsLoading(false);
}
};
useFocusEffect(
React.useCallback(() => {
let isActive = true;
const run = async () => {
console.log('HealthKit init start');
const ok = await ensureHealthPermissions();
if (!ok) return;
const data = await fetchTodayHealthData();
if (!isActive) return;
setStepCount(data.steps);
setActiveCalories(Math.round(data.activeEnergyBurned));
};
run();
return () => {
isActive = false;
};
loadHealthData();
}, [])
);
@@ -113,6 +128,24 @@ export default function ExploreScreen() {
{/* 今日报告 标题 */}
<Text style={styles.sectionTitle}></Text>
{/* 健康数据错误提示 */}
{isLoading && (
<View style={styles.errorContainer}>
<Ionicons name="warning-outline" size={20} color="#E54D4D" />
<Text style={styles.errorText}>...</Text>
<TouchableOpacity
style={styles.retryButton}
onPress={loadHealthData} disabled={isLoading}
>
<Ionicons
name="refresh-outline"
size={16}
color={isLoading ? '#9AA3AE' : '#E54D4D'}
/>
</TouchableOpacity>
</View>
)}
{/* 指标行:左大卡(训练时间),右两小卡(消耗卡路里、步数) */}
<View style={styles.metricsRow}>
<View style={[styles.trainingCard, styles.metricsLeft]}>
@@ -127,7 +160,7 @@ export default function ExploreScreen() {
<View style={[styles.metricsRightCard, styles.caloriesCard, { minHeight: 88 }]}>
<Text style={styles.cardTitleSecondary}></Text>
<Text style={styles.caloriesValue}>
{activeCalories != null ? `${activeCalories} 千卡` : '——'}
{isLoading ? '加载中...' : activeCalories != null ? `${activeCalories} 千卡` : '——'}
</Text>
</View>
<View style={[styles.metricsRightCard, styles.stepsCard, { minHeight: 88 }]}>
@@ -135,7 +168,7 @@ export default function ExploreScreen() {
<View style={styles.iconSquare}><Ionicons name="footsteps-outline" size={18} color="#192126" /></View>
<Text style={styles.cardTitle}></Text>
</View>
<Text style={styles.stepsValue}>{stepCount != null ? `${stepCount}/2000` : '——/2000'}</Text>
<Text style={styles.stepsValue}>{isLoading ? '加载中.../2000' : stepCount != null ? `${stepCount}/2000` : '——/2000'}</Text>
<ProgressBar progress={Math.min(1, Math.max(0, (stepCount ?? 0) / 2000))} height={12} trackColor="#FFEBCB" fillColor="#FFC365" />
</View>
</View>
@@ -386,5 +419,24 @@ const styles = StyleSheet.create({
color: '#7A6A42',
fontWeight: '700',
marginBottom: 8,
}
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFE5E5',
borderRadius: 12,
padding: 12,
marginBottom: 16,
},
errorText: {
fontSize: 14,
color: '#E54D4D',
fontWeight: '600',
marginLeft: 8,
flex: 1,
},
retryButton: {
padding: 4,
marginLeft: 8,
},
});