Files
digital-pilates/components/BasalMetabolismCard.tsx

281 lines
8.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ROUTES } from '@/constants/Routes';
import { useAppSelector } from '@/hooks/redux';
import { selectUserAge, selectUserProfile } from '@/store/userSlice';
import { fetchBasalEnergyBurned } from '@/utils/health';
import dayjs from 'dayjs';
import { Image } from 'expo-image';
import { router } from 'expo-router';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface BasalMetabolismCardProps {
selectedDate?: Date;
style?: any;
}
export function BasalMetabolismCard({ selectedDate, style }: BasalMetabolismCardProps) {
const { t } = useTranslation();
const [basalMetabolism, setBasalMetabolism] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
// 获取用户基本信息
const userProfile = useAppSelector(selectUserProfile);
const userAge = useAppSelector(selectUserAge);
// 缓存和防抖相关
const cacheRef = useRef<Map<string, { data: number | null; timestamp: number }>>(new Map());
const loadingRef = useRef<Map<string, Promise<number | null>>>(new Map());
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存
// 使用 useMemo 缓存 BMR 计算,避免每次渲染重复计算
const bmrRange = useMemo(() => {
const { gender, weight, height } = userProfile;
// 检查是否有足够的信息来计算BMR
if (!gender || !weight || !height || !userAge) {
return null;
}
// 将体重和身高转换为数字
const weightNum = parseFloat(weight);
const heightNum = parseFloat(height);
if (isNaN(weightNum) || isNaN(heightNum) || weightNum <= 0 || heightNum <= 0 || userAge <= 0) {
return null;
}
// 使用Mifflin-St Jeor公式计算BMR
let bmr: number;
if (gender === 'male') {
bmr = 10 * weightNum + 6.25 * heightNum - 5 * userAge + 5;
} else {
bmr = 10 * weightNum + 6.25 * heightNum - 5 * userAge - 161;
}
// 计算正常范围±15%
const minBMR = Math.round(bmr * 0.85);
const maxBMR = Math.round(bmr * 1.15);
return { min: minBMR, max: maxBMR, base: Math.round(bmr) };
}, [userProfile.gender, userProfile.weight, userProfile.height, userAge]);
// 优化的数据获取函数,包含缓存和去重复请求
const fetchBasalMetabolismData = useCallback(async (date: Date): Promise<number | null> => {
const dateKey = dayjs(date).format('YYYY-MM-DD');
const now = Date.now();
// 检查缓存
const cached = cacheRef.current.get(dateKey);
if (cached && (now - cached.timestamp) < CACHE_DURATION) {
return cached.data;
}
// 检查是否已经在请求中(防止重复请求)
const existingRequest = loadingRef.current.get(dateKey);
if (existingRequest) {
return existingRequest;
}
// 创建新的请求
const request = (async () => {
try {
const options = {
startDate: dayjs(date).startOf('day').toDate().toISOString(),
endDate: dayjs(date).endOf('day').toDate().toISOString()
};
const basalEnergy = await fetchBasalEnergyBurned(options);
const result = basalEnergy || null;
// 更新缓存
cacheRef.current.set(dateKey, { data: result, timestamp: now });
return result;
} catch (error) {
console.error('BasalMetabolismCard: Failed to get basal metabolism data:', error);
return null;
} finally {
// 清理请求记录
loadingRef.current.delete(dateKey);
}
})();
// 记录请求
loadingRef.current.set(dateKey, request);
return request;
}, []);
// 获取基础代谢数据
useEffect(() => {
if (!selectedDate) return;
let isCancelled = false;
const loadData = async () => {
setLoading(true);
try {
const result = await fetchBasalMetabolismData(selectedDate);
if (!isCancelled) {
setBasalMetabolism(result);
}
} finally {
if (!isCancelled) {
setLoading(false);
}
}
};
loadData();
// 清理函数,防止组件卸载后的状态更新
return () => {
isCancelled = true;
};
}, [selectedDate, fetchBasalMetabolismData]);
// 使用 useMemo 优化状态描述计算
const status = useMemo(() => {
if (basalMetabolism === null || basalMetabolism === 0) {
return { text: t('statistics.components.metabolism.status.unknown'), color: '#9AA3AE' };
}
// 基于常见的基础代谢范围来判断状态
if (basalMetabolism >= 1800) {
return { text: t('statistics.components.metabolism.status.high'), color: '#10B981' };
} else if (basalMetabolism >= 1400) {
return { text: t('statistics.components.metabolism.status.normal'), color: '#3B82F6' };
} else if (basalMetabolism >= 1000) {
return { text: t('statistics.components.metabolism.status.low'), color: '#F59E0B' };
} else {
return { text: t('statistics.components.metabolism.status.veryLow'), color: '#EF4444' };
}
}, [basalMetabolism, t]);
return (
<>
<TouchableOpacity
style={[styles.container, style]}
onPress={() => router.push(ROUTES.BASAL_METABOLISM_DETAIL)}
activeOpacity={0.8}
>
{/* 头部区域 */}
<View style={styles.header}>
<View style={styles.leftSection}>
<Image
source={require('@/assets/images/icons/icon-fire.png')}
style={styles.titleIcon}
/>
<Text style={styles.title}>{t('statistics.components.metabolism.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}>
<Text style={styles.value}>
{loading ? t('statistics.components.metabolism.loading') : (basalMetabolism != null && basalMetabolism > 0 ? Math.round(basalMetabolism).toString() : '--')}
</Text>
<Text style={styles.unit}>{t('statistics.components.metabolism.unit')}</Text>
</View>
</TouchableOpacity>
</>
);
}
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',
},
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: 14,
color: '#0F172A',
fontWeight: '600',
fontFamily: 'AliBold',
},
titleIcon: {
width: 16,
height: 16,
marginRight: 4,
},
statusBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
},
statusText: {
fontSize: 11,
fontWeight: '600',
fontFamily: 'AliBold',
},
valueSection: {
flexDirection: 'row',
alignItems: 'center',
zIndex: 1,
},
value: {
fontSize: 16,
fontWeight: '600',
color: '#0F172A',
lineHeight: 28,
fontFamily: 'AliBold',
},
unit: {
fontSize: 12,
color: '#64748B',
marginLeft: 6,
fontFamily: 'AliRegular',
},
});