feat: 添加 BMI 计算和训练计划排课功能
- 新增 BMI 计算工具,支持用户输入体重和身高计算 BMI 值,并根据结果提供分类和建议 - 在训练计划中集成排课功能,允许用户选择和安排训练动作 - 更新个人信息页面,添加出生日期字段,支持用户完善个人资料 - 优化训练计划卡片样式,提升用户体验 - 更新相关依赖,确保项目兼容性和功能完整性
This commit is contained in:
439
components/BMICard.tsx
Normal file
439
components/BMICard.tsx
Normal file
@@ -0,0 +1,439 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -39,15 +39,15 @@ export function PlanCard({ image, title, subtitle, level, progress }: PlanCardPr
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: '#F0F0F0',
|
||||
borderRadius: 28,
|
||||
padding: 20,
|
||||
marginBottom: 18,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 12,
|
||||
elevation: 3,
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 30,
|
||||
elevation: 10,
|
||||
},
|
||||
image: {
|
||||
width: 100,
|
||||
|
||||
Reference in New Issue
Block a user