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, canCalculateBMI, getBMIResult } from '@/utils/bmi'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useEffect, useState } from 'react'; import { Dimensions, Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import Animated, { Extrapolation, interpolate, runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; import Svg, { Circle, Line, Path, Text as SvgText } from 'react-native-svg'; const { width: screenWidth } = Dimensions.get('window'); const CARD_WIDTH = screenWidth - 40; // 减去左右边距 const CHART_WIDTH = CARD_WIDTH - 36; // 减去卡片内边距 const CHART_HEIGHT = 100; const PADDING = 10; export function WeightHistoryCard() { const dispatch = useAppDispatch(); const userProfile = useAppSelector((s) => s.user.profile); const weightHistory = useAppSelector((s) => s.user.weightHistory); const [showChart, setShowChart] = useState(false); const [showBMIModal, setShowBMIModal] = useState(false); // 动画相关状态 const animationProgress = useSharedValue(0); const [isAnimating, setIsAnimating] = useState(false); const { pushIfAuthedElseLogin } = useAuthGuard(); const colorScheme = useColorScheme(); const themeColors = Colors[colorScheme ?? 'light']; const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 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(() => { if (hasWeight) { loadWeightHistory(); } }, [hasWeight]); const loadWeightHistory = async () => { try { await dispatch(fetchWeightHistory() as any); } catch (error) { console.error('加载体重历史失败:', error); } }; const navigateToCoach = () => { pushIfAuthedElseLogin(ROUTES.TAB_COACH); }; const handleShowBMIModal = () => { setShowBMIModal(true); }; const handleHideBMIModal = () => { setShowBMIModal(false); }; // 切换图表显示状态的动画函数 const navigateToWeightRecords = () => { pushIfAuthedElseLogin(ROUTES.WEIGHT_RECORDS); }; const toggleChart = () => { if (isAnimating) return; // 防止动画期间重复触发 setIsAnimating(true); const newShowChart = !showChart; setShowChart(newShowChart); animationProgress.value = withTiming( newShowChart ? 1 : 0, { duration: 350, }, (finished) => { if (finished) { runOnJS(setIsAnimating)(false); } } ); }; // 动画容器的高度动画 const containerAnimatedStyle = useAnimatedStyle(() => { // 只有在展开状态时才应用固定高度 if (animationProgress.value === 0) { return {}; } const height = interpolate( animationProgress.value, [0, 1], [80, 200], // 从摘要高度到图表高度,适应毛玻璃背景 Extrapolation.CLAMP ); return { height, }; }); // 摘要信息的动画样式 const summaryAnimatedStyle = useAnimatedStyle(() => { const opacity = interpolate( animationProgress.value, [0, 0.4, 1], [1, 0.2, 0], Extrapolation.CLAMP ); const scale = interpolate( animationProgress.value, [0, 1], [1, 0.9], Extrapolation.CLAMP ); const translateY = interpolate( animationProgress.value, [0, 1], [0, -20], Extrapolation.CLAMP ); return { opacity, transform: [{ scale }, { translateY }], }; }); // 图表容器的动画样式 const chartAnimatedStyle = useAnimatedStyle(() => { const opacity = interpolate( animationProgress.value, [0, 0.6, 1], [0, 0.2, 1], Extrapolation.CLAMP ); const scale = interpolate( animationProgress.value, [0, 1], [0.9, 1], Extrapolation.CLAMP ); const translateY = interpolate( animationProgress.value, [0, 1], [20, 0], Extrapolation.CLAMP ); return { opacity, transform: [{ scale }, { translateY }], }; }); // 如果没有体重数据,显示引导卡片 if (!hasWeight) { return ( 体重记录 开始记录你的体重变化 记录体重变化,追踪你的健康进展 { e.stopPropagation(); navigateToCoach(); }} activeOpacity={0.8} > 记录 ); } // 处理体重历史数据 const sortedHistory = [...weightHistory] .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) .slice(-7); // 只显示最近7条记录 if (sortedHistory.length === 0) { return ( 体重记录 暂无体重记录,点击下方按钮开始记录 { e.stopPropagation(); navigateToCoach(); }} activeOpacity={0.8} > 记录体重 ); } // 生成图表数据 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 + 15 + (1 - normalizedWeight) * (CHART_HEIGHT - 2 * PADDING - 30); 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 ( 体重记录 { e.stopPropagation(); toggleChart(); }} activeOpacity={0.8} > {/* 动画容器 */} {sortedHistory.length > 0 && ( {/* 默认信息显示 - 带动画 */} 当前体重 {userProfile.weight}kg 记录天数 {sortedHistory.length}天 变化范围 {minWeight.toFixed(1)}-{maxWeight.toFixed(1)} {bmiResult && ( BMI {bmiResult.value} { e.stopPropagation(); handleShowBMIModal(); }} style={styles.bmiInfoButton} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > )} {/* 图表容器 - 带动画 */} {/* 背景网格线 */} {[0, 1, 2, 3, 4].map(i => ( ))} {/* 折线 */} {/* 数据点和标签 */} {points.map((point, index) => { const isLastPoint = index === points.length - 1; const isFirstPoint = index === 0; const showLabel = isFirstPoint || isLastPoint || points.length <= 3 || points.length === 1; return ( {/* 体重标签 - 只在关键点显示 */} {showLabel && ( <> {point.weight} )} ); })} {/* 图表底部信息 */} 当前体重 {userProfile.weight}kg 记录天数 {sortedHistory.length}天 变化范围 {minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg {/* 最近记录时间 */} {sortedHistory.length > 0 && ( 最近记录:{dayjs(sortedHistory[sortedHistory.length - 1].createdAt).format('MM/DD HH:mm')} )} )} {/* BMI 信息弹窗 */} {/* 标题 */} BMI 指数说明 {/* 介绍部分 */} BMI(身体质量指数)是评估体重与身高关系的国际通用健康指标 计算公式:体重(kg) ÷ 身高²(m) {/* BMI 分类标准 */} BMI 分类标准 {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 ( {category.name} {category.range} {category.advice} ); })} {/* 健康建议 */} 健康建议 保持均衡饮食,控制热量摄入 每周至少150分钟中等强度运动 保证7-9小时充足睡眠 定期监测体重变化,及时调整 {/* 免责声明 */} BMI 仅供参考,不能反映肌肉量、骨密度等指标。如有健康疑问,请咨询专业医生。 {/* 底部继续按钮 */} 继续 ); } 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: 30, height: 30, borderRadius: 8, alignItems: 'center', justifyContent: 'center', marginRight: 2, }, cardTitle: { fontSize: 14, color: '#192126', flex: 1, }, 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', }, animationContainer: { position: 'relative', overflow: 'hidden', minHeight: 80, // 增加最小高度以容纳毛玻璃背景 }, summaryInfo: { position: 'absolute', width: '100%', marginTop: 8, }, summaryBackground: { backgroundColor: 'rgba(248, 250, 252, 0.8)', // 毛玻璃背景色 borderRadius: 12, padding: 12, shadowColor: '#000', shadowOffset: { width: 0, height: 1, }, shadowOpacity: 0.06, shadowRadius: 3, elevation: 1, // 添加边框增强毛玻璃效果 borderWidth: 0.5, borderColor: 'rgba(255, 255, 255, 0.8)', }, chartContainer: { position: 'absolute', width: '100%', alignItems: 'center', minHeight: 100, }, chartInfo: { flexDirection: 'row', justifyContent: 'space-around', width: '100%', }, infoItem: { alignItems: 'center', }, infoLabel: { fontSize: 12, color: '#687076', marginBottom: 4, }, infoValue: { fontSize: 14, fontWeight: '700', color: '#192126', }, lastRecordText: { fontSize: 12, color: '#687076', textAlign: 'center', marginTop: 4, }, summaryRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 0, flexWrap: 'wrap', gap: 8, }, summaryItem: { alignItems: 'center', flex: 1, minWidth: 0, }, summaryLabel: { fontSize: 12, color: '#687076', marginBottom: 3, }, summaryValue: { fontSize: 14, marginTop: 2, fontWeight: '600', 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, }, });