feat: 新增基础代谢率功能及相关组件
- 在健康数据中引入基础代谢率的读取和展示,支持用户记录健身进度 - 更新统计页面,替换BMI卡片为基础代谢卡片,提升用户体验 - 优化健康数据获取逻辑,确保基础代谢数据的准确性 - 更新权限描述,明确应用对健康数据的访问需求
This commit is contained in:
3
app.json
3
app.json
@@ -47,7 +47,8 @@
|
||||
"react-native-health",
|
||||
{
|
||||
"enableHealthAPI": true,
|
||||
"healthSharePermission": "应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。"
|
||||
"healthSharePermission": "应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。",
|
||||
"healthUpdatePermission": "应用需要更新您的健康数据(体重信息)以记录您的健身进度。"
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||||
import { BMICard } from '@/components/BMICard';
|
||||
import { BasalMetabolismCard } from '@/components/BasalMetabolismCard';
|
||||
import { FitnessRingsCard } from '@/components/FitnessRingsCard';
|
||||
import { MoodCard } from '@/components/MoodCard';
|
||||
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
|
||||
@@ -140,6 +140,8 @@ export default function ExploreScreen() {
|
||||
// HealthKit: 每次页面聚焦都拉取今日数据
|
||||
const [stepCount, setStepCount] = useState<number | null>(null);
|
||||
const [activeCalories, setActiveCalories] = useState<number | null>(null);
|
||||
// 基础代谢率(千卡)
|
||||
const [basalMetabolism, setBasalMetabolism] = useState<number | null>(null);
|
||||
// 睡眠时长(分钟)
|
||||
const [sleepDuration, setSleepDuration] = useState<number | null>(null);
|
||||
// HRV数据
|
||||
@@ -235,6 +237,7 @@ export default function ExploreScreen() {
|
||||
if (latestRequestKeyRef.current === requestKey) {
|
||||
setStepCount(data.steps);
|
||||
setActiveCalories(Math.round(data.activeEnergyBurned));
|
||||
setBasalMetabolism(Math.round(data.basalEnergyBurned));
|
||||
setSleepDuration(data.sleepDuration);
|
||||
// 更新健身圆环数据
|
||||
setFitnessRingsData({
|
||||
@@ -455,14 +458,6 @@ export default function ExploreScreen() {
|
||||
{/* 右列 */}
|
||||
<View style={styles.masonryColumn}>
|
||||
<FloatingCard style={styles.masonryCard} delay={250}>
|
||||
<BMICard
|
||||
weight={userProfile?.weight ? parseFloat(userProfile.weight) : undefined}
|
||||
height={userProfile?.height ? parseFloat(userProfile.height) : undefined}
|
||||
style={styles.bmiCardOverride}
|
||||
/>
|
||||
</FloatingCard>
|
||||
|
||||
<FloatingCard style={styles.masonryCard} delay={750}>
|
||||
<FitnessRingsCard
|
||||
activeCalories={fitnessRingsData.activeCalories}
|
||||
activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
|
||||
@@ -474,7 +469,7 @@ export default function ExploreScreen() {
|
||||
/>
|
||||
</FloatingCard>
|
||||
|
||||
<FloatingCard style={[styles.masonryCard, styles.sleepCard]} delay={1250}>
|
||||
<FloatingCard style={[styles.masonryCard, styles.sleepCard]} delay={750}>
|
||||
<View style={styles.cardHeaderRow}>
|
||||
<Text style={styles.cardTitle}>睡眠</Text>
|
||||
</View>
|
||||
@@ -487,6 +482,14 @@ export default function ExploreScreen() {
|
||||
)}
|
||||
</FloatingCard>
|
||||
|
||||
{/* 基础代谢卡片 */}
|
||||
<FloatingCard style={styles.masonryCard} delay={1250}>
|
||||
<BasalMetabolismCard
|
||||
value={basalMetabolism}
|
||||
resetToken={animToken}
|
||||
style={styles.basalMetabolismCardOverride}
|
||||
/>
|
||||
</FloatingCard>
|
||||
|
||||
</View>
|
||||
</View>
|
||||
@@ -823,7 +826,7 @@ const styles = StyleSheet.create({
|
||||
shadowRadius: 12,
|
||||
elevation: 6,
|
||||
},
|
||||
bmiCardOverride: {
|
||||
basalMetabolismCardOverride: {
|
||||
margin: -16, // 抵消 masonryCard 的 padding
|
||||
borderRadius: 16,
|
||||
},
|
||||
|
||||
188
components/BasalMetabolismCard.tsx
Normal file
188
components/BasalMetabolismCard.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
interface BasalMetabolismCardProps {
|
||||
value: number | null;
|
||||
resetToken?: number;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export function BasalMetabolismCard({ value, resetToken, style }: BasalMetabolismCardProps) {
|
||||
// 获取基础代谢状态描述
|
||||
const getMetabolismStatus = () => {
|
||||
if (value === null || value === 0) {
|
||||
return { text: '未知', color: '#9AA3AE' };
|
||||
}
|
||||
|
||||
// 基于常见的基础代谢范围来判断状态
|
||||
if (value >= 1800) {
|
||||
return { text: '高代谢', color: '#10B981' };
|
||||
} else if (value >= 1400) {
|
||||
return { text: '正常', color: '#3B82F6' };
|
||||
} else if (value >= 1000) {
|
||||
return { text: '偏低', color: '#F59E0B' };
|
||||
} else {
|
||||
return { text: '较低', color: '#EF4444' };
|
||||
}
|
||||
};
|
||||
|
||||
const status = getMetabolismStatus();
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
{/* 渐变背景 */}
|
||||
<LinearGradient
|
||||
colors={['#F0F9FF', '#E0F2FE']}
|
||||
style={styles.gradientBackground}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
/>
|
||||
|
||||
{/* 装饰性圆圈 */}
|
||||
<View style={styles.decorativeCircle1} />
|
||||
<View style={styles.decorativeCircle2} />
|
||||
|
||||
{/* 头部区域 */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.leftSection}>
|
||||
|
||||
<Text style={styles.title}>基础代谢</Text>
|
||||
</View>
|
||||
<View style={[styles.statusBadge, { backgroundColor: status.color + '20' }]}>
|
||||
<Text style={[styles.statusText, { color: status.color }]}>{status.text}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 数值显示区域 */}
|
||||
<View style={styles.valueSection}>
|
||||
{value != null && value > 0 ? (
|
||||
<AnimatedNumber
|
||||
value={value}
|
||||
resetToken={resetToken}
|
||||
style={styles.value}
|
||||
format={(v) => Math.round(v).toString()}
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.value}>--</Text>
|
||||
)}
|
||||
<Text style={styles.unit}>千卡/日</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 20,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 12,
|
||||
elevation: 4,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
gradientBackground: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
opacity: 0.6,
|
||||
},
|
||||
decorativeCircle1: {
|
||||
position: 'absolute',
|
||||
top: -20,
|
||||
right: -20,
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.1,
|
||||
},
|
||||
decorativeCircle2: {
|
||||
position: 'absolute',
|
||||
bottom: -15,
|
||||
left: -15,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#0EA5E9',
|
||||
opacity: 0.05,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16,
|
||||
zIndex: 1,
|
||||
},
|
||||
leftSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
iconContainer: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#FFFFFF',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 8,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 1,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
},
|
||||
fireIcon: {
|
||||
width: 14,
|
||||
height: 18,
|
||||
backgroundColor: '#EF4444',
|
||||
borderTopLeftRadius: 7,
|
||||
borderTopRightRadius: 7,
|
||||
borderBottomLeftRadius: 2,
|
||||
borderBottomRightRadius: 2,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#0F172A',
|
||||
},
|
||||
statusBadge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
},
|
||||
valueSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
zIndex: 1,
|
||||
},
|
||||
value: {
|
||||
fontSize: 24,
|
||||
fontWeight: '800',
|
||||
color: '#0F172A',
|
||||
lineHeight: 28,
|
||||
},
|
||||
unit: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: '#64748B',
|
||||
marginLeft: 6,
|
||||
},
|
||||
});
|
||||
@@ -83,7 +83,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
|
||||
{/* 渐变背景进度条 */}
|
||||
<View style={[styles.progressBar, { width: '100%' }]}>
|
||||
<LinearGradient
|
||||
colors={['#10B981', '#FCD34D', '#EF4444']}
|
||||
colors={['#EF4444', '#FCD34D', '#10B981']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.gradientBar}
|
||||
@@ -114,9 +114,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
padding: 14,
|
||||
marginBottom: 12,
|
||||
padding: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
@@ -127,16 +125,6 @@ const styles = StyleSheet.create({
|
||||
elevation: 3,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
minHeight: 110,
|
||||
width: 140,
|
||||
},
|
||||
gradientBackground: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
borderRadius: 16,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -2,13 +2,18 @@ import { Colors } from '@/constants/Colors';
|
||||
import { ROUTES } from '@/constants/Routes';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { fetchWeightHistory } from '@/store/userSlice';
|
||||
import { BMI_CATEGORIES, canCalculateBMI, getBMIResult } from '@/utils/bmi';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Dimensions,
|
||||
Image,
|
||||
Modal,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
@@ -37,14 +42,28 @@ export function WeightHistoryCard() {
|
||||
const weightHistory = useAppSelector((s) => s.user.weightHistory);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showChart, setShowChart] = useState(false);
|
||||
const [showBMIModal, setShowBMIModal] = useState(false);
|
||||
|
||||
// 动画相关状态
|
||||
const animationProgress = useSharedValue(0);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
const { pushIfAuthedElseLogin } = useAuthGuard();
|
||||
const colorScheme = useColorScheme();
|
||||
const themeColors = Colors[colorScheme ?? 'light'];
|
||||
|
||||
const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0;
|
||||
const hasHeight = userProfile?.height && parseFloat(userProfile.height) > 0;
|
||||
|
||||
// BMI 计算
|
||||
const canCalculate = canCalculateBMI(
|
||||
userProfile?.weight ? parseFloat(userProfile.weight) : undefined,
|
||||
userProfile?.height ? parseFloat(userProfile.height) : undefined
|
||||
);
|
||||
|
||||
const bmiResult = canCalculate && userProfile?.weight && userProfile?.height
|
||||
? getBMIResult(parseFloat(userProfile.weight), parseFloat(userProfile.height))
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasWeight) {
|
||||
@@ -67,6 +86,14 @@ export function WeightHistoryCard() {
|
||||
pushIfAuthedElseLogin(ROUTES.TAB_COACH);
|
||||
};
|
||||
|
||||
const handleShowBMIModal = () => {
|
||||
setShowBMIModal(true);
|
||||
};
|
||||
|
||||
const handleHideBMIModal = () => {
|
||||
setShowBMIModal(false);
|
||||
};
|
||||
|
||||
// 切换图表显示状态的动画函数
|
||||
const toggleChart = () => {
|
||||
if (isAnimating) return; // 防止动画期间重复触发
|
||||
@@ -314,9 +341,26 @@ export function WeightHistoryCard() {
|
||||
<View style={styles.summaryItem}>
|
||||
<Text style={styles.summaryLabel}>变化范围</Text>
|
||||
<Text style={styles.summaryValue}>
|
||||
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
|
||||
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}
|
||||
</Text>
|
||||
</View>
|
||||
{bmiResult && (
|
||||
<View style={styles.summaryItem}>
|
||||
<Text style={styles.summaryLabel}>BMI</Text>
|
||||
<View style={styles.bmiValueContainer}>
|
||||
<Text style={[styles.bmiValue, { color: bmiResult.color }]}>
|
||||
{bmiResult.value}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={handleShowBMIModal}
|
||||
style={styles.bmiInfoButton}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="information-circle-outline" size={12} color="#9AA3AE" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
@@ -418,6 +462,107 @@ export function WeightHistoryCard() {
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
{/* BMI 信息弹窗 */}
|
||||
<Modal
|
||||
visible={showBMIModal}
|
||||
animationType="slide"
|
||||
presentationStyle="pageSheet"
|
||||
onRequestClose={handleHideBMIModal}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[themeColors.backgroundGradientStart, themeColors.backgroundGradientEnd]}
|
||||
style={styles.bmiModalContainer}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
>
|
||||
<ScrollView style={styles.bmiModalContent} showsVerticalScrollIndicator={false}>
|
||||
{/* 标题 */}
|
||||
<Text style={styles.bmiModalTitle}>BMI 指数说明</Text>
|
||||
|
||||
{/* 介绍部分 */}
|
||||
<View style={styles.bmiModalIntroSection}>
|
||||
<Text style={styles.bmiModalDescription}>
|
||||
BMI(身体质量指数)是评估体重与身高关系的国际通用健康指标
|
||||
</Text>
|
||||
<View style={styles.bmiModalFormulaContainer}>
|
||||
<Text style={styles.bmiModalFormulaText}>
|
||||
计算公式:体重(kg) ÷ 身高²(m)
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* BMI 分类标准 */}
|
||||
<Text style={styles.bmiModalSectionTitle}>BMI 分类标准</Text>
|
||||
|
||||
<View style={styles.bmiModalStatsCard}>
|
||||
{BMI_CATEGORIES.map((category, index) => {
|
||||
const colors = [
|
||||
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 偏瘦
|
||||
{ bg: '#E8F5E8', text: Colors.light.accentGreenDark, border: Colors.light.accentGreen }, // 正常
|
||||
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 超重
|
||||
{ bg: '#FEE2E2', text: '#B91C1C', border: '#EF4444' } // 肥胖
|
||||
][index];
|
||||
|
||||
return (
|
||||
<View key={index} style={[styles.bmiModalStatItem, { backgroundColor: colors.bg, borderColor: colors.border }]}>
|
||||
<View style={styles.bmiModalStatHeader}>
|
||||
<Text style={[styles.bmiModalStatTitle, { color: colors.text }]}>
|
||||
{category.name}
|
||||
</Text>
|
||||
<Text style={[styles.bmiModalStatRange, { color: colors.text }]}>
|
||||
{category.range}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.bmiModalStatAdvice, { color: colors.text }]}>
|
||||
{category.advice}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* 健康建议 */}
|
||||
<Text style={styles.bmiModalSectionTitle}>健康建议</Text>
|
||||
<View style={styles.bmiModalHealthTips}>
|
||||
<View style={styles.bmiModalTipsItem}>
|
||||
<Ionicons name="nutrition-outline" size={20} color="#3B82F6" />
|
||||
<Text style={styles.bmiModalTipsText}>保持均衡饮食,控制热量摄入</Text>
|
||||
</View>
|
||||
<View style={styles.bmiModalTipsItem}>
|
||||
<Ionicons name="walk-outline" size={20} color="#3B82F6" />
|
||||
<Text style={styles.bmiModalTipsText}>每周至少150分钟中等强度运动</Text>
|
||||
</View>
|
||||
<View style={styles.bmiModalTipsItem}>
|
||||
<Ionicons name="moon-outline" size={20} color="#3B82F6" />
|
||||
<Text style={styles.bmiModalTipsText}>保证7-9小时充足睡眠</Text>
|
||||
</View>
|
||||
<View style={styles.bmiModalTipsItem}>
|
||||
<Ionicons name="calendar-outline" size={20} color="#3B82F6" />
|
||||
<Text style={styles.bmiModalTipsText}>定期监测体重变化,及时调整</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 免责声明 */}
|
||||
<View style={styles.bmiModalDisclaimer}>
|
||||
<Ionicons name="information-circle-outline" size={16} color="#6B7280" />
|
||||
<Text style={styles.bmiModalDisclaimerText}>
|
||||
BMI 仅供参考,不能反映肌肉量、骨密度等指标。如有健康疑问,请咨询专业医生。
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部继续按钮 */}
|
||||
<View style={styles.bmiModalBottomContainer}>
|
||||
<TouchableOpacity style={styles.bmiModalContinueButton} onPress={handleHideBMIModal}>
|
||||
<View style={styles.bmiModalButtonBackground}>
|
||||
<Text style={styles.bmiModalButtonText}>继续</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.bmiModalHomeIndicator} />
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -546,20 +691,178 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
summaryRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 0,
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
},
|
||||
summaryItem: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
summaryLabel: {
|
||||
fontSize: 12,
|
||||
fontSize: 11,
|
||||
color: '#687076',
|
||||
marginBottom: 4,
|
||||
marginBottom: 3,
|
||||
},
|
||||
summaryValue: {
|
||||
fontSize: 14,
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
color: '#192126',
|
||||
},
|
||||
|
||||
// BMI 相关样式
|
||||
bmiValueContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
},
|
||||
bmiValue: {
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
},
|
||||
bmiInfoButton: {
|
||||
padding: 0,
|
||||
},
|
||||
bmiStatusBadge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 8,
|
||||
},
|
||||
bmiStatusText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
},
|
||||
|
||||
// BMI 弹窗样式
|
||||
bmiModalContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
bmiModalContent: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
},
|
||||
bmiModalTitle: {
|
||||
fontSize: 28,
|
||||
fontWeight: '800',
|
||||
color: '#111827',
|
||||
textAlign: 'center',
|
||||
marginBottom: 24,
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
bmiModalIntroSection: {
|
||||
marginBottom: 32,
|
||||
},
|
||||
bmiModalDescription: {
|
||||
fontSize: 16,
|
||||
color: '#374151',
|
||||
lineHeight: 24,
|
||||
textAlign: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
bmiModalFormulaContainer: {
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
bmiModalFormulaText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
},
|
||||
bmiModalSectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#111827',
|
||||
marginBottom: 16,
|
||||
letterSpacing: -0.5,
|
||||
},
|
||||
bmiModalStatsCard: {
|
||||
marginBottom: 32,
|
||||
},
|
||||
bmiModalStatItem: {
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
borderWidth: 1,
|
||||
},
|
||||
bmiModalStatHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
bmiModalStatTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
bmiModalStatRange: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
bmiModalStatAdvice: {
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
},
|
||||
bmiModalHealthTips: {
|
||||
marginBottom: 32,
|
||||
},
|
||||
bmiModalTipsItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: '#F8FAFC',
|
||||
borderRadius: 12,
|
||||
},
|
||||
bmiModalTipsText: {
|
||||
fontSize: 14,
|
||||
color: '#374151',
|
||||
marginLeft: 12,
|
||||
flex: 1,
|
||||
lineHeight: 20,
|
||||
},
|
||||
bmiModalDisclaimer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: '#FEF3C7',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 20,
|
||||
},
|
||||
bmiModalDisclaimerText: {
|
||||
fontSize: 13,
|
||||
color: '#B45309',
|
||||
marginLeft: 8,
|
||||
flex: 1,
|
||||
lineHeight: 18,
|
||||
},
|
||||
bmiModalBottomContainer: {
|
||||
padding: 20,
|
||||
paddingBottom: 34,
|
||||
},
|
||||
bmiModalContinueButton: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
bmiModalButtonBackground: {
|
||||
backgroundColor: '#192126',
|
||||
borderRadius: 16,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
bmiModalButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
bmiModalHomeIndicator: {
|
||||
height: 5,
|
||||
backgroundColor: '#D1D5DB',
|
||||
borderRadius: 3,
|
||||
alignSelf: 'center',
|
||||
width: 36,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<key>NSHealthShareUsageDescription</key>
|
||||
<string>应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。</string>
|
||||
<key>NSHealthUpdateUsageDescription</key>
|
||||
<string>Allow $(PRODUCT_NAME) to update health info</string>
|
||||
<string>应用需要更新您的健康数据(体重信息)以记录您的健身进度。</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>应用需要写入相册以保存拍摄的体态照片(可选)。</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
|
||||
@@ -7,6 +7,7 @@ const PERMISSIONS: HealthKitPermissions = {
|
||||
read: [
|
||||
AppleHealthKit.Constants.Permissions.StepCount,
|
||||
AppleHealthKit.Constants.Permissions.ActiveEnergyBurned,
|
||||
AppleHealthKit.Constants.Permissions.BasalEnergyBurned,
|
||||
AppleHealthKit.Constants.Permissions.SleepAnalysis,
|
||||
AppleHealthKit.Constants.Permissions.HeartRateVariability,
|
||||
AppleHealthKit.Constants.Permissions.ActivitySummary,
|
||||
@@ -21,6 +22,7 @@ const PERMISSIONS: HealthKitPermissions = {
|
||||
export type TodayHealthData = {
|
||||
steps: number;
|
||||
activeEnergyBurned: number; // kilocalories
|
||||
basalEnergyBurned: number; // kilocalories - 基础代谢率
|
||||
sleepDuration: number; // 睡眠时长(分钟)
|
||||
hrv: number | null; // 心率变异性 (ms)
|
||||
// 健身圆环数据
|
||||
@@ -73,7 +75,7 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
||||
console.log('查询选项:', options);
|
||||
|
||||
// 并行获取所有健康数据,包括ActivitySummary
|
||||
const [steps, calories, sleepDuration, hrv, activitySummary] = await Promise.all([
|
||||
const [steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary] = await Promise.all([
|
||||
// 获取步数
|
||||
new Promise<number>((resolve) => {
|
||||
AppleHealthKit.getStepCount({
|
||||
@@ -110,6 +112,24 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
||||
});
|
||||
}),
|
||||
|
||||
// 获取基础代谢率
|
||||
new Promise<number>((resolve) => {
|
||||
AppleHealthKit.getBasalEnergyBurned(options, (err, res) => {
|
||||
if (err) {
|
||||
console.error('获取基础代谢失败:', err);
|
||||
return resolve(0);
|
||||
}
|
||||
if (!res || !Array.isArray(res) || res.length === 0) {
|
||||
console.warn('基础代谢数据为空或格式错误');
|
||||
return resolve(0);
|
||||
}
|
||||
console.log('基础代谢数据:', res);
|
||||
// 求和该日内的所有记录(单位:千卡)
|
||||
const total = res.reduce((acc: number, item: any) => acc + (item?.value || 0), 0);
|
||||
resolve(total);
|
||||
});
|
||||
}),
|
||||
|
||||
// 获取睡眠时长
|
||||
new Promise<number>((resolve) => {
|
||||
AppleHealthKit.getSleepSamples(options, (err, res) => {
|
||||
@@ -181,11 +201,12 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
||||
})
|
||||
]);
|
||||
|
||||
console.log('指定日期健康数据获取完成:', { steps, calories, sleepDuration, hrv, activitySummary });
|
||||
console.log('指定日期健康数据获取完成:', { steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary });
|
||||
|
||||
return {
|
||||
steps,
|
||||
activeEnergyBurned: calories,
|
||||
basalEnergyBurned: basalMetabolism,
|
||||
sleepDuration,
|
||||
hrv,
|
||||
// 健身圆环数据
|
||||
|
||||
Reference in New Issue
Block a user