feat: 新增基础代谢率功能及相关组件
- 在健康数据中引入基础代谢率的读取和展示,支持用户记录健身进度 - 更新统计页面,替换BMI卡片为基础代谢卡片,提升用户体验 - 优化健康数据获取逻辑,确保基础代谢数据的准确性 - 更新权限描述,明确应用对健康数据的访问需求
This commit is contained in:
3
app.json
3
app.json
@@ -47,7 +47,8 @@
|
|||||||
"react-native-health",
|
"react-native-health",
|
||||||
{
|
{
|
||||||
"enableHealthAPI": true,
|
"enableHealthAPI": true,
|
||||||
"healthSharePermission": "应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。"
|
"healthSharePermission": "应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。",
|
||||||
|
"healthUpdatePermission": "应用需要更新您的健康数据(体重信息)以记录您的健身进度。"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
import { AnimatedNumber } from '@/components/AnimatedNumber';
|
||||||
import { BMICard } from '@/components/BMICard';
|
import { BasalMetabolismCard } from '@/components/BasalMetabolismCard';
|
||||||
import { FitnessRingsCard } from '@/components/FitnessRingsCard';
|
import { FitnessRingsCard } from '@/components/FitnessRingsCard';
|
||||||
import { MoodCard } from '@/components/MoodCard';
|
import { MoodCard } from '@/components/MoodCard';
|
||||||
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
|
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
|
||||||
@@ -140,6 +140,8 @@ export default function ExploreScreen() {
|
|||||||
// HealthKit: 每次页面聚焦都拉取今日数据
|
// HealthKit: 每次页面聚焦都拉取今日数据
|
||||||
const [stepCount, setStepCount] = useState<number | null>(null);
|
const [stepCount, setStepCount] = useState<number | null>(null);
|
||||||
const [activeCalories, setActiveCalories] = 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);
|
const [sleepDuration, setSleepDuration] = useState<number | null>(null);
|
||||||
// HRV数据
|
// HRV数据
|
||||||
@@ -235,6 +237,7 @@ export default function ExploreScreen() {
|
|||||||
if (latestRequestKeyRef.current === requestKey) {
|
if (latestRequestKeyRef.current === requestKey) {
|
||||||
setStepCount(data.steps);
|
setStepCount(data.steps);
|
||||||
setActiveCalories(Math.round(data.activeEnergyBurned));
|
setActiveCalories(Math.round(data.activeEnergyBurned));
|
||||||
|
setBasalMetabolism(Math.round(data.basalEnergyBurned));
|
||||||
setSleepDuration(data.sleepDuration);
|
setSleepDuration(data.sleepDuration);
|
||||||
// 更新健身圆环数据
|
// 更新健身圆环数据
|
||||||
setFitnessRingsData({
|
setFitnessRingsData({
|
||||||
@@ -455,14 +458,6 @@ export default function ExploreScreen() {
|
|||||||
{/* 右列 */}
|
{/* 右列 */}
|
||||||
<View style={styles.masonryColumn}>
|
<View style={styles.masonryColumn}>
|
||||||
<FloatingCard style={styles.masonryCard} delay={250}>
|
<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
|
<FitnessRingsCard
|
||||||
activeCalories={fitnessRingsData.activeCalories}
|
activeCalories={fitnessRingsData.activeCalories}
|
||||||
activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
|
activeCaloriesGoal={fitnessRingsData.activeCaloriesGoal}
|
||||||
@@ -474,7 +469,7 @@ export default function ExploreScreen() {
|
|||||||
/>
|
/>
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
||||||
<FloatingCard style={[styles.masonryCard, styles.sleepCard]} delay={1250}>
|
<FloatingCard style={[styles.masonryCard, styles.sleepCard]} delay={750}>
|
||||||
<View style={styles.cardHeaderRow}>
|
<View style={styles.cardHeaderRow}>
|
||||||
<Text style={styles.cardTitle}>睡眠</Text>
|
<Text style={styles.cardTitle}>睡眠</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -487,6 +482,14 @@ export default function ExploreScreen() {
|
|||||||
)}
|
)}
|
||||||
</FloatingCard>
|
</FloatingCard>
|
||||||
|
|
||||||
|
{/* 基础代谢卡片 */}
|
||||||
|
<FloatingCard style={styles.masonryCard} delay={1250}>
|
||||||
|
<BasalMetabolismCard
|
||||||
|
value={basalMetabolism}
|
||||||
|
resetToken={animToken}
|
||||||
|
style={styles.basalMetabolismCardOverride}
|
||||||
|
/>
|
||||||
|
</FloatingCard>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -823,7 +826,7 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 12,
|
shadowRadius: 12,
|
||||||
elevation: 6,
|
elevation: 6,
|
||||||
},
|
},
|
||||||
bmiCardOverride: {
|
basalMetabolismCardOverride: {
|
||||||
margin: -16, // 抵消 masonryCard 的 padding
|
margin: -16, // 抵消 masonryCard 的 padding
|
||||||
borderRadius: 16,
|
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%' }]}>
|
<View style={[styles.progressBar, { width: '100%' }]}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['#10B981', '#FCD34D', '#EF4444']}
|
colors={['#EF4444', '#FCD34D', '#10B981']}
|
||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 0 }}
|
end={{ x: 1, y: 0 }}
|
||||||
style={styles.gradientBar}
|
style={styles.gradientBar}
|
||||||
@@ -114,9 +114,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
borderRadius: 16,
|
padding: 2,
|
||||||
padding: 14,
|
|
||||||
marginBottom: 12,
|
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: {
|
shadowOffset: {
|
||||||
width: 0,
|
width: 0,
|
||||||
@@ -127,16 +125,6 @@ const styles = StyleSheet.create({
|
|||||||
elevation: 3,
|
elevation: 3,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
minHeight: 110,
|
|
||||||
width: 140,
|
|
||||||
},
|
|
||||||
gradientBackground: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
borderRadius: 16,
|
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -2,13 +2,18 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { ROUTES } from '@/constants/Routes';
|
import { ROUTES } from '@/constants/Routes';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { fetchWeightHistory } from '@/store/userSlice';
|
import { fetchWeightHistory } from '@/store/userSlice';
|
||||||
|
import { BMI_CATEGORIES, canCalculateBMI, getBMIResult } from '@/utils/bmi';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Image,
|
Image,
|
||||||
|
Modal,
|
||||||
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@@ -37,14 +42,28 @@ export function WeightHistoryCard() {
|
|||||||
const weightHistory = useAppSelector((s) => s.user.weightHistory);
|
const weightHistory = useAppSelector((s) => s.user.weightHistory);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [showChart, setShowChart] = useState(false);
|
const [showChart, setShowChart] = useState(false);
|
||||||
|
const [showBMIModal, setShowBMIModal] = useState(false);
|
||||||
|
|
||||||
// 动画相关状态
|
// 动画相关状态
|
||||||
const animationProgress = useSharedValue(0);
|
const animationProgress = useSharedValue(0);
|
||||||
const [isAnimating, setIsAnimating] = useState(false);
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
|
||||||
const { pushIfAuthedElseLogin } = useAuthGuard();
|
const { pushIfAuthedElseLogin } = useAuthGuard();
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const themeColors = Colors[colorScheme ?? 'light'];
|
||||||
|
|
||||||
const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0;
|
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(() => {
|
useEffect(() => {
|
||||||
if (hasWeight) {
|
if (hasWeight) {
|
||||||
@@ -67,6 +86,14 @@ export function WeightHistoryCard() {
|
|||||||
pushIfAuthedElseLogin(ROUTES.TAB_COACH);
|
pushIfAuthedElseLogin(ROUTES.TAB_COACH);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleShowBMIModal = () => {
|
||||||
|
setShowBMIModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHideBMIModal = () => {
|
||||||
|
setShowBMIModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
// 切换图表显示状态的动画函数
|
// 切换图表显示状态的动画函数
|
||||||
const toggleChart = () => {
|
const toggleChart = () => {
|
||||||
if (isAnimating) return; // 防止动画期间重复触发
|
if (isAnimating) return; // 防止动画期间重复触发
|
||||||
@@ -314,9 +341,26 @@ export function WeightHistoryCard() {
|
|||||||
<View style={styles.summaryItem}>
|
<View style={styles.summaryItem}>
|
||||||
<Text style={styles.summaryLabel}>变化范围</Text>
|
<Text style={styles.summaryLabel}>变化范围</Text>
|
||||||
<Text style={styles.summaryValue}>
|
<Text style={styles.summaryValue}>
|
||||||
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
|
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</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>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
@@ -418,6 +462,107 @@ export function WeightHistoryCard() {
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
</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>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -546,20 +691,178 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
summaryRow: {
|
summaryRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-between',
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 8,
|
||||||
},
|
},
|
||||||
summaryItem: {
|
summaryItem: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
summaryLabel: {
|
summaryLabel: {
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
color: '#687076',
|
color: '#687076',
|
||||||
marginBottom: 4,
|
marginBottom: 3,
|
||||||
},
|
},
|
||||||
summaryValue: {
|
summaryValue: {
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: '#192126',
|
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>
|
<key>NSHealthShareUsageDescription</key>
|
||||||
<string>应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。</string>
|
<string>应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。</string>
|
||||||
<key>NSHealthUpdateUsageDescription</key>
|
<key>NSHealthUpdateUsageDescription</key>
|
||||||
<string>Allow $(PRODUCT_NAME) to update health info</string>
|
<string>应用需要更新您的健康数据(体重信息)以记录您的健身进度。</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>应用需要写入相册以保存拍摄的体态照片(可选)。</string>
|
<string>应用需要写入相册以保存拍摄的体态照片(可选)。</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const PERMISSIONS: HealthKitPermissions = {
|
|||||||
read: [
|
read: [
|
||||||
AppleHealthKit.Constants.Permissions.StepCount,
|
AppleHealthKit.Constants.Permissions.StepCount,
|
||||||
AppleHealthKit.Constants.Permissions.ActiveEnergyBurned,
|
AppleHealthKit.Constants.Permissions.ActiveEnergyBurned,
|
||||||
|
AppleHealthKit.Constants.Permissions.BasalEnergyBurned,
|
||||||
AppleHealthKit.Constants.Permissions.SleepAnalysis,
|
AppleHealthKit.Constants.Permissions.SleepAnalysis,
|
||||||
AppleHealthKit.Constants.Permissions.HeartRateVariability,
|
AppleHealthKit.Constants.Permissions.HeartRateVariability,
|
||||||
AppleHealthKit.Constants.Permissions.ActivitySummary,
|
AppleHealthKit.Constants.Permissions.ActivitySummary,
|
||||||
@@ -21,6 +22,7 @@ const PERMISSIONS: HealthKitPermissions = {
|
|||||||
export type TodayHealthData = {
|
export type TodayHealthData = {
|
||||||
steps: number;
|
steps: number;
|
||||||
activeEnergyBurned: number; // kilocalories
|
activeEnergyBurned: number; // kilocalories
|
||||||
|
basalEnergyBurned: number; // kilocalories - 基础代谢率
|
||||||
sleepDuration: number; // 睡眠时长(分钟)
|
sleepDuration: number; // 睡眠时长(分钟)
|
||||||
hrv: number | null; // 心率变异性 (ms)
|
hrv: number | null; // 心率变异性 (ms)
|
||||||
// 健身圆环数据
|
// 健身圆环数据
|
||||||
@@ -73,7 +75,7 @@ export async function fetchHealthDataForDate(date: Date): Promise<TodayHealthDat
|
|||||||
console.log('查询选项:', options);
|
console.log('查询选项:', options);
|
||||||
|
|
||||||
// 并行获取所有健康数据,包括ActivitySummary
|
// 并行获取所有健康数据,包括ActivitySummary
|
||||||
const [steps, calories, sleepDuration, hrv, activitySummary] = await Promise.all([
|
const [steps, calories, basalMetabolism, sleepDuration, hrv, activitySummary] = await Promise.all([
|
||||||
// 获取步数
|
// 获取步数
|
||||||
new Promise<number>((resolve) => {
|
new Promise<number>((resolve) => {
|
||||||
AppleHealthKit.getStepCount({
|
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) => {
|
new Promise<number>((resolve) => {
|
||||||
AppleHealthKit.getSleepSamples(options, (err, res) => {
|
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 {
|
return {
|
||||||
steps,
|
steps,
|
||||||
activeEnergyBurned: calories,
|
activeEnergyBurned: calories,
|
||||||
|
basalEnergyBurned: basalMetabolism,
|
||||||
sleepDuration,
|
sleepDuration,
|
||||||
hrv,
|
hrv,
|
||||||
// 健身圆环数据
|
// 健身圆环数据
|
||||||
|
|||||||
Reference in New Issue
Block a user