Files
digital-pilates/components/BMICard.tsx
richarjiang f95401c1ce feat: 添加 BMI 计算和训练计划排课功能
- 新增 BMI 计算工具,支持用户输入体重和身高计算 BMI 值,并根据结果提供分类和建议
- 在训练计划中集成排课功能,允许用户选择和安排训练动作
- 更新个人信息页面,添加出生日期字段,支持用户完善个人资料
- 优化训练计划卡片样式,提升用户体验
- 更新相关依赖,确保项目兼容性和功能完整性
2025-08-15 10:45:37 +08:00

439 lines
11 KiB
TypeScript
Raw 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 {
BMI_CATEGORIES,
canCalculateBMI,
getBMIResult,
type BMIResult
} from '@/utils/bmi';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import React, { useState } from 'react';
import {
Modal,
Pressable,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
interface BMICardProps {
weight?: number;
height?: number;
style?: any;
}
export function BMICard({ weight, height, style }: BMICardProps) {
const router = useRouter();
const [showInfoModal, setShowInfoModal] = useState(false);
const canCalculate = canCalculateBMI(weight, height);
let bmiResult: BMIResult | null = null;
if (canCalculate && weight && height) {
try {
bmiResult = getBMIResult(weight, height);
} catch (error) {
console.warn('BMI 计算错误:', error);
}
}
const handleGoToProfile = () => {
router.push('/profile/edit');
};
const handleShowInfoModal = () => {
setShowInfoModal(true);
};
const handleHideInfoModal = () => {
setShowInfoModal(false);
};
const renderContent = () => {
if (!canCalculate) {
// 缺少数据的情况
return (
<View style={styles.incompleteContent}>
<View style={styles.cardHeader}>
<View style={styles.titleRow}>
<View style={styles.iconSquare}>
<Ionicons name="fitness-outline" size={18} color="#192126" />
</View>
<Text style={styles.cardTitle}>BMI </Text>
</View>
<TouchableOpacity
onPress={handleShowInfoModal}
style={styles.infoButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Ionicons name="information-circle-outline" size={20} color="#9AA3AE" />
</TouchableOpacity>
</View>
<View style={styles.missingDataContainer}>
<Ionicons name="alert-circle-outline" size={24} color="#F59E0B" />
<Text style={styles.missingDataText}>
{!weight && !height ? '请完善身高和体重信息' :
!weight ? '请完善体重信息' : '请完善身高信息'}
</Text>
</View>
<TouchableOpacity
onPress={handleGoToProfile}
style={styles.completeButton}
activeOpacity={0.8}
>
<Text style={styles.completeButtonText}></Text>
<Ionicons name="chevron-forward" size={16} color="#6B7280" />
</TouchableOpacity>
</View>
);
}
// 有完整数据的情况
return (
<View style={[styles.completeContent, { backgroundColor: bmiResult?.backgroundColor }]}>
<View style={styles.cardHeader}>
<View style={styles.titleRow}>
<View style={styles.iconSquare}>
<Ionicons name="fitness-outline" size={18} color="#192126" />
</View>
<Text style={styles.cardTitle}>BMI </Text>
</View>
<TouchableOpacity
onPress={handleShowInfoModal}
style={styles.infoButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Ionicons name="information-circle-outline" size={20} color="#9AA3AE" />
</TouchableOpacity>
</View>
<View style={styles.bmiValueContainer}>
<Text style={[styles.bmiValue, { color: bmiResult?.color }]}>
{bmiResult?.value}
</Text>
<View style={[styles.categoryBadge, { backgroundColor: bmiResult?.color + '20' }]}>
<Text style={[styles.categoryText, { color: bmiResult?.color }]}>
{bmiResult?.category.name}
</Text>
</View>
</View>
<Text style={[styles.bmiDescription, { color: bmiResult?.color }]}>
{bmiResult?.description}
</Text>
<Text style={styles.encouragementText}>
{bmiResult?.category.encouragement}
</Text>
</View>
);
};
return (
<>
<View style={[styles.card, style]}>
{renderContent()}
</View>
{/* BMI 信息弹窗 */}
<Modal
visible={showInfoModal}
transparent
animationType="fade"
onRequestClose={handleHideInfoModal}
>
<Pressable
style={styles.modalBackdrop}
onPress={handleHideInfoModal}
>
<Pressable style={styles.modalContainer} onPress={(e) => e.stopPropagation()}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>BMI </Text>
<TouchableOpacity
onPress={handleHideInfoModal}
style={styles.closeButton}
activeOpacity={0.7}
>
<Ionicons name="close" size={20} color="#6B7280" />
</TouchableOpacity>
</View>
<ScrollView
style={styles.modalBody}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 20 }}
>
<Text style={styles.modalDescription}>
BMI(kg) ÷ ²(m)
</Text>
<Text style={styles.sectionTitle}>BMI </Text>
{BMI_CATEGORIES.map((category, index) => {
const colors = index === 0 ? { bg: '#FFF4E6', text: '#8B7355' } :
index === 1 ? { bg: '#E8F5E8', text: '#2D5016' } :
index === 2 ? { bg: '#FEF3C7', text: '#B45309' } :
{ bg: '#FEE2E2', text: '#B91C1C' };
return (
<View key={index} style={[styles.categoryItem, { backgroundColor: colors.bg }]}>
<View style={styles.categoryHeader}>
<Text style={[styles.categoryName, { color: colors.text }]}>
{category.name}
</Text>
<Text style={[styles.categoryRange, { color: colors.text }]}>
{category.range}
</Text>
</View>
<Text style={styles.categoryAdvice}>
{category.advice}
</Text>
</View>
);
})}
<Text style={styles.disclaimer}>
* BMI
</Text>
</ScrollView>
</View>
</Pressable>
</Pressable>
</Modal>
</>
);
}
const styles = StyleSheet.create({
card: {
borderRadius: 22,
padding: 18,
marginBottom: 16,
overflow: 'hidden',
},
cardHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 12,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
},
iconSquare: {
width: 30,
height: 30,
borderRadius: 8,
backgroundColor: '#FFFFFF',
alignItems: 'center',
justifyContent: 'center',
marginRight: 10,
},
cardTitle: {
fontSize: 18,
fontWeight: '800',
color: '#192126',
},
infoButton: {
padding: 4,
},
// 缺少数据时的样式
incompleteContent: {
minHeight: 120,
backgroundColor: '#FFFFFF',
borderRadius: 22,
padding: 18,
margin: -18,
},
missingDataContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FEF3C7',
borderRadius: 12,
padding: 12,
marginBottom: 12,
},
missingDataText: {
fontSize: 14,
color: '#B45309',
fontWeight: '600',
marginLeft: 8,
flex: 1,
},
completeButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F3F4F6',
borderRadius: 12,
paddingVertical: 12,
paddingHorizontal: 16,
},
completeButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#6B7280',
marginRight: 4,
},
// 有完整数据时的样式
completeContent: {
minHeight: 120,
borderRadius: 22,
padding: 18,
margin: -18,
},
bmiValueContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
bmiValue: {
fontSize: 32,
fontWeight: '800',
marginRight: 12,
},
categoryBadge: {
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 12,
},
categoryText: {
fontSize: 14,
fontWeight: '700',
},
bmiDescription: {
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
},
encouragementText: {
fontSize: 13,
color: '#6B7280',
fontWeight: '500',
lineHeight: 18,
fontStyle: 'italic',
},
// 弹窗样式
modalBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.6)',
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
modalContainer: {
width: '90%',
maxHeight: '85%',
backgroundColor: '#FFFFFF',
borderRadius: 24,
overflow: 'hidden',
elevation: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.3,
shadowRadius: 20,
},
modalContent: {
maxHeight: '100%',
},
modalHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 24,
borderBottomWidth: 1,
borderBottomColor: '#F3F4F6',
backgroundColor: '#FAFAFA',
},
modalTitle: {
fontSize: 22,
fontWeight: '800',
color: '#111827',
letterSpacing: -0.5,
},
closeButton: {
padding: 8,
borderRadius: 20,
backgroundColor: '#F3F4F6',
},
modalBody: {
paddingHorizontal: 24,
paddingVertical: 20,
},
modalDescription: {
fontSize: 16,
color: '#4B5563',
lineHeight: 26,
marginBottom: 28,
textAlign: 'center',
backgroundColor: '#F8FAFC',
padding: 16,
borderRadius: 12,
borderLeftWidth: 4,
borderLeftColor: '#3B82F6',
},
sectionTitle: {
fontSize: 20,
fontWeight: '800',
color: '#111827',
marginBottom: 16,
letterSpacing: -0.3,
},
categoryItem: {
borderRadius: 16,
padding: 20,
marginBottom: 16,
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.05)',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
categoryHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
categoryName: {
fontSize: 18,
fontWeight: '800',
letterSpacing: -0.2,
},
categoryRange: {
fontSize: 15,
fontWeight: '700',
backgroundColor: 'rgba(255,255,255,0.8)',
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 20,
},
categoryAdvice: {
fontSize: 15,
color: '#374151',
lineHeight: 22,
fontWeight: '500',
},
disclaimer: {
fontSize: 13,
color: '#6B7280',
fontStyle: 'italic',
marginTop: 24,
textAlign: 'center',
backgroundColor: '#F9FAFB',
padding: 16,
borderRadius: 12,
lineHeight: 20,
},
});