import { Colors } from '@/constants/Colors'; import { ROUTES } from '@/constants/Routes'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { fetchWeightHistory } from '@/store/userSlice'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import React, { useEffect, useState } from 'react'; import { Dimensions, Image, 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 animationProgress = useSharedValue(0); const [isAnimating, setIsAnimating] = useState(false); const { pushIfAuthedElseLogin } = useAuthGuard(); const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0; 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 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 (isLoading) { return ( 体重记录 加载中... ); } // 如果没有体重数据,显示引导卡片 if (!hasWeight) { return ( 体重记录 开始记录你的体重变化 记录体重变化,追踪你的健康进展 记录 ); } // 处理体重历史数据 const sortedHistory = [...weightHistory] .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) .slice(-7); // 只显示最近7条记录 if (sortedHistory.length === 0) { return ( 体重记录 暂无体重记录,点击下方按钮开始记录 记录体重 ); } // 生成图表数据 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 ( 体重记录 {/* 动画容器 */} {sortedHistory.length > 0 && ( {/* 默认信息显示 - 带动画 */} 当前体重 {userProfile.weight}kg 记录天数 {sortedHistory.length}天 变化范围 {minWeight.toFixed(1)}-{maxWeight.toFixed(1)}kg {/* 图表容器 - 带动画 */} {/* 背景网格线 */} {[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')} )} )} ); } const styles = StyleSheet.create({ card: { backgroundColor: '#FFFFFF', borderRadius: 22, padding: 18, marginBottom: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, }, cardHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 16, }, iconSquare: { width: 30, height: 30, borderRadius: 8, alignItems: 'center', justifyContent: 'center', marginRight: 2, }, cardTitle: { fontSize: 14, fontWeight: '800', 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%', }, chartContainer: { position: 'absolute', width: '100%', alignItems: 'center', minHeight: 100, }, chartInfo: { flexDirection: 'row', justifyContent: 'space-around', width: '100%', paddingTop: 16, borderTopWidth: 1, borderTopColor: '#F0F0F0', }, 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-around', marginBottom: 0, }, summaryItem: { alignItems: 'center', }, summaryLabel: { fontSize: 12, color: '#687076', marginBottom: 4, }, summaryValue: { fontSize: 14, fontWeight: '700', color: '#192126', }, });