为目标页面、营养记录、食物添加等功能添加登录状态检查和引导界面,确保用户在未登录状态下能够获得清晰的登录提示和指引。 - 在目标页面添加精美的未登录引导界面,包含渐变背景和登录按钮 - 为食物记录相关组件添加登录状态检查,未登录时自动跳转登录页面 - 重构血氧饱和度卡片为独立数据获取,移除对外部数据依赖 - 移除个人页面的实验性SwiftUI组件,统一使用原生TouchableOpacity - 清理统计页面和营养记录页面的冗余代码和未使用变量
541 lines
15 KiB
TypeScript
541 lines
15 KiB
TypeScript
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 } from '@/utils/bmi';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import { Image } from 'expo-image';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
Dimensions,
|
||
Modal,
|
||
ScrollView,
|
||
StyleSheet,
|
||
Text,
|
||
TouchableOpacity,
|
||
View
|
||
} from 'react-native';
|
||
import Svg, { Circle, Path } from 'react-native-svg';
|
||
|
||
const { width: screenWidth } = Dimensions.get('window');
|
||
const CARD_WIDTH = screenWidth - 40; // 减去左右边距
|
||
const CHART_WIDTH = CARD_WIDTH - 36; // 减去卡片内边距
|
||
const CHART_HEIGHT = 60;
|
||
const PADDING = 10;
|
||
|
||
|
||
export function WeightHistoryCard() {
|
||
const dispatch = useAppDispatch();
|
||
const userProfile = useAppSelector((s) => s.user.profile);
|
||
const weightHistory = useAppSelector((s) => s.user.weightHistory);
|
||
|
||
const [showBMIModal, setShowBMIModal] = useState(false);
|
||
|
||
|
||
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
|
||
const colorScheme = useColorScheme();
|
||
const themeColors = Colors[colorScheme ?? 'light'];
|
||
|
||
const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0;
|
||
|
||
|
||
useEffect(() => {
|
||
if (isLoggedIn) {
|
||
loadWeightHistory();
|
||
}
|
||
}, [userProfile?.weight, isLoggedIn]);
|
||
|
||
const loadWeightHistory = async () => {
|
||
try {
|
||
await dispatch(fetchWeightHistory() as any).unwrap();
|
||
} catch (error) {
|
||
console.error('加载体重历史失败:', error);
|
||
}
|
||
};
|
||
|
||
const navigateToCoach = () => {
|
||
pushIfAuthedElseLogin(ROUTES.WEIGHT_RECORDS);
|
||
};
|
||
|
||
const handleHideBMIModal = () => {
|
||
setShowBMIModal(false);
|
||
};
|
||
|
||
const navigateToWeightRecords = () => {
|
||
pushIfAuthedElseLogin(ROUTES.WEIGHT_RECORDS);
|
||
};
|
||
|
||
|
||
// 处理体重历史数据
|
||
const sortedHistory = [...weightHistory]
|
||
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||
.slice(-7); // 只显示最近7条记录
|
||
|
||
// return (
|
||
// <TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
|
||
// <View style={styles.cardHeader}>
|
||
// <Text style={styles.cardTitle}>体重记录</Text>
|
||
// </View>
|
||
|
||
// <View style={styles.emptyContent}>
|
||
// <Text style={styles.emptyDescription}>
|
||
// 暂无体重记录,点击下方按钮开始记录
|
||
// </Text>
|
||
// <TouchableOpacity
|
||
// style={styles.recordButton}
|
||
// onPress={(e) => {
|
||
// e.stopPropagation();
|
||
// navigateToCoach();
|
||
// }}
|
||
// activeOpacity={0.8}
|
||
// >
|
||
// <Ionicons name="add" size={18} color="#FFFFFF" />
|
||
// <Text style={styles.recordButtonText}>记录体重</Text>
|
||
// </TouchableOpacity>
|
||
// </View>
|
||
// </TouchableOpacity>
|
||
// );
|
||
// }
|
||
|
||
// 生成图表数据
|
||
const weights = sortedHistory.map(item => parseFloat(item.weight));
|
||
const minWeight = Math.min(...weights);
|
||
const maxWeight = Math.max(...weights);
|
||
const weightRange = maxWeight - minWeight || 1;
|
||
|
||
const points = sortedHistory.map((item, index) => {
|
||
const x = PADDING + (index / Math.max(sortedHistory.length - 1, 1)) * (CHART_WIDTH - 2 * PADDING);
|
||
const normalizedWeight = (parseFloat(item.weight) - minWeight) / weightRange;
|
||
// 减少顶部边距,压缩留白
|
||
const y = PADDING + 8 + (1 - normalizedWeight) * (CHART_HEIGHT - 2 * PADDING - 16);
|
||
return { x, y, weight: item.weight, date: item.createdAt };
|
||
});
|
||
|
||
// 生成路径
|
||
const pathData = points.map((point, index) => {
|
||
if (index === 0) return `M ${point.x} ${point.y}`;
|
||
return `L ${point.x} ${point.y}`;
|
||
}).join(' ');
|
||
|
||
// 如果只有一个数据点,显示为水平线
|
||
const singlePointPath = points.length === 1 ?
|
||
`M ${PADDING} ${points[0].y} L ${CHART_WIDTH - PADDING} ${points[0].y}` :
|
||
pathData;
|
||
|
||
return (
|
||
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
|
||
<View style={styles.cardHeader}>
|
||
<Image
|
||
source={require('@/assets/images/icons/icon-weight.png')}
|
||
style={styles.iconSquare}
|
||
/>
|
||
<Text style={styles.cardTitle}>体重记录</Text>
|
||
<TouchableOpacity
|
||
style={styles.addButton}
|
||
onPress={(e) => {
|
||
e.stopPropagation();
|
||
navigateToCoach();
|
||
}}
|
||
activeOpacity={0.8}
|
||
>
|
||
<Ionicons name="add" size={18} color={Colors.light.primary} />
|
||
</TouchableOpacity>
|
||
</View>
|
||
|
||
{/* 默认显示图表 */}
|
||
{sortedHistory.length > 0 && (
|
||
<View style={styles.chartContainer}>
|
||
<Svg width={CHART_WIDTH} height={CHART_HEIGHT + 15}>
|
||
{/* 背景网格线 */}
|
||
|
||
{/* 更抽象的折线 - 减小线宽和显示的细节 */}
|
||
<Path
|
||
d={singlePointPath}
|
||
stroke={Colors.light.accentGreen}
|
||
strokeWidth={2}
|
||
fill="none"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
opacity={0.8}
|
||
/>
|
||
|
||
{/* 简化的数据点 - 更小更精致 */}
|
||
{points.map((point, index) => {
|
||
const isLastPoint = index === points.length - 1;
|
||
|
||
return (
|
||
<React.Fragment key={index}>
|
||
<Circle
|
||
cx={point.x}
|
||
cy={point.y}
|
||
r={isLastPoint ? 3 : 2}
|
||
fill={Colors.light.accentGreen}
|
||
opacity={0.9}
|
||
/>
|
||
</React.Fragment>
|
||
);
|
||
})}
|
||
|
||
|
||
</Svg>
|
||
|
||
{/* 精简的图表信息 */}
|
||
<View style={styles.chartInfo}>
|
||
<View style={styles.infoItem}>
|
||
<Text style={styles.infoLabel}>{userProfile.weight}kg</Text>
|
||
</View>
|
||
<View style={styles.infoItem}>
|
||
<Text style={styles.infoLabel}>{sortedHistory.length}天</Text>
|
||
</View>
|
||
<View style={styles.infoItem}>
|
||
<Text style={styles.infoLabel}>
|
||
{minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
</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.accentGreen, 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>
|
||
</TouchableOpacity>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
card: {
|
||
backgroundColor: '#FFFFFF',
|
||
borderRadius: 22,
|
||
padding: 16,
|
||
marginBottom: 4,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 8,
|
||
elevation: 3,
|
||
marginTop: 16
|
||
},
|
||
cardHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
},
|
||
iconSquare: {
|
||
width: 14,
|
||
height: 14,
|
||
borderRadius: 8,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginRight: 4,
|
||
},
|
||
cardTitle: {
|
||
fontSize: 14,
|
||
color: '#192126',
|
||
flex: 1,
|
||
fontWeight: '600'
|
||
},
|
||
headerButtons: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
},
|
||
chartToggleButton: {
|
||
width: 28,
|
||
height: 28,
|
||
borderRadius: 14,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
addButton: {
|
||
width: 28,
|
||
height: 28,
|
||
borderRadius: 14,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
emptyContent: {
|
||
alignItems: 'center',
|
||
},
|
||
emptyTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginBottom: 6,
|
||
},
|
||
emptyDescription: {
|
||
fontSize: 14,
|
||
color: '#687076',
|
||
textAlign: 'center',
|
||
marginBottom: 16,
|
||
lineHeight: 20,
|
||
},
|
||
recordButton: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
backgroundColor: Colors.light.accentGreen,
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 10,
|
||
borderRadius: 20,
|
||
gap: 6,
|
||
},
|
||
recordButtonText: {
|
||
color: '#192126',
|
||
fontSize: 14,
|
||
fontWeight: '700',
|
||
},
|
||
chartContainer: {
|
||
width: '100%',
|
||
alignItems: 'center',
|
||
marginTop: 12,
|
||
},
|
||
chartInfo: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'space-around',
|
||
width: '100%',
|
||
},
|
||
infoItem: {
|
||
alignItems: 'center',
|
||
},
|
||
infoLabel: {
|
||
fontSize: 11,
|
||
color: '#687076',
|
||
fontWeight: '500',
|
||
},
|
||
infoValue: {
|
||
fontSize: 14,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
},
|
||
|
||
// 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,
|
||
},
|
||
});
|