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 [isLoading, setIsLoading] = useState(false);
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;
const hasHeight = userProfile?.height && parseFloat(userProfile.height) > 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 {
setIsLoading(true);
await dispatch(fetchWeightHistory() as any);
} catch (error) {
console.error('加载体重历史失败:', error);
} finally {
setIsLoading(false);
}
};
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],
[40, 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 }}
>
)}
{/* 图表容器 - 带动画 */}
{/* 图表底部信息 */}
当前体重
{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: 40,
},
summaryInfo: {
position: 'absolute',
width: '100%',
marginTop: 10,
},
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: 11,
color: '#687076',
marginBottom: 3,
},
summaryValue: {
fontSize: 13,
fontWeight: '700',
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,
},
});