import { useWaterDataByDate } from '@/hooks/useWaterData'; import { getQuickWaterAmount } from '@/utils/userPreferences'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; import * as Haptics from 'expo-haptics'; import { useRouter } from 'expo-router'; import LottieView from 'lottie-react-native'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Animated, StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native'; import { AnimatedNumber } from './AnimatedNumber'; interface WaterIntakeCardProps { style?: ViewStyle; selectedDate?: string; // 新增:选中的日期,格式为 YYYY-MM-DD } const WaterIntakeCard: React.FC = ({ style, selectedDate }) => { const router = useRouter(); const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord } = useWaterDataByDate(selectedDate); const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载 // 计算当前饮水量和目标 const currentIntake = waterStats?.totalAmount || 0; const targetIntake = dailyWaterGoal || 2000; const animationRef = useRef(null); // 为每个时间点创建独立的动画值 const animatedValues = useMemo(() => Array.from({ length: 24 }, () => new Animated.Value(0)) , []); // 计算柱状图数据 const chartData = useMemo(() => { if (!waterRecords || waterRecords.length === 0) { return Array.from({ length: 24 }, (_, i) => ({ hour: i, amount: 0, height: 0 })); } // 按小时分组数据 const hourlyData: { hour: number; amount: number }[] = Array.from({ length: 24 }, (_, i) => ({ hour: i, amount: 0, })); waterRecords.forEach(record => { // 优先使用 recordedAt,如果没有则使用 createdAt const dateTime = record.recordedAt || record.createdAt; const hour = dayjs(dateTime).hour(); if (hour >= 0 && hour < 24) { hourlyData[hour].amount += record.amount; } }); // 找到最大饮水量用于计算高度比例 const maxAmount = Math.max(...hourlyData.map(data => data.amount), 1); const maxHeight = 20; // 柱状图最大高度 return hourlyData.map(data => ({ hour: data.hour, amount: data.amount, height: maxAmount > 0 ? (data.amount / maxAmount) * maxHeight : 0 })); }, [waterRecords]); // 判断是否是今天 const isToday = selectedDate === dayjs().format('YYYY-MM-DD') || !selectedDate; // 加载用户偏好的快速添加饮水默认值 useFocusEffect( useCallback(() => { const loadQuickWaterAmount = async () => { try { const amount = await getQuickWaterAmount(); setQuickWaterAmount(amount); } catch (error) { console.error('加载快速添加饮水默认值失败:', error); // 保持默认值 250ml } }; loadQuickWaterAmount(); }, []) ); // 触发柱体动画 useEffect(() => { if (chartData && chartData.length > 0) { // 重置所有动画值 animatedValues.forEach(animValue => animValue.setValue(0)); // 找出所有有饮水记录的柱体索引 const activeBarIndices = chartData .map((data, index) => ({ data, index })) .filter(item => item.data.amount > 0) .map(item => item.index); // 依次执行动画,每个柱体间隔100ms activeBarIndices.forEach((barIndex, sequenceIndex) => { setTimeout(() => { Animated.spring(animatedValues[barIndex], { toValue: 1, tension: 150, friction: 8, useNativeDriver: false, }).start(); }, sequenceIndex * 100); // 每个柱体延迟100ms }); } }, [chartData, animatedValues]); // 处理添加喝水 - 右上角按钮直接添加 const handleQuickAddWater = async () => { // 触发震动反馈 if (process.env.EXPO_OS === 'ios') { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); } animationRef.current?.play(); // 使用用户配置的快速添加饮水量 const waterAmount = quickWaterAmount; // 如果有选中日期,则为该日期添加记录;否则为今天添加记录 const recordedAt = selectedDate ? dayjs(selectedDate).toISOString() : dayjs().toISOString(); await addWaterRecord(waterAmount, recordedAt); }; // 处理卡片点击 - 跳转到饮水设置页面 const handleCardPress = () => { // 触发震动反馈 if (process.env.EXPO_OS === 'ios') { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } // 跳转到饮水设置页面,传递选中的日期参数 router.push({ pathname: '/water-settings', params: selectedDate ? { selectedDate } : undefined }); }; return ( {/* 标题和加号按钮 */} 喝水 {isToday && ( + {quickWaterAmount}ml )} {/* 柱状图 */} {chartData.map((data, index) => { // 判断是否有活动的小时 const isActive = data.amount > 0; // 动画变换:高度从0到目标高度 const animatedHeight = animatedValues[index].interpolate({ inputRange: [0, 1], outputRange: [0, data.height], }); // 动画变换:透明度从0到1(保持柱体在动画过程中可见) const animatedOpacity = animatedValues[index].interpolate({ inputRange: [0, 0.1, 1], outputRange: [0, 1, 1], }); return ( {/* 背景柱体 - 始终显示,使用蓝色系的淡色 */} {/* 数据柱体 - 只有当有数据时才显示并执行动画 */} {isActive && ( )} ); })} {/* 饮水量显示 */} {currentIntake !== null ? ( `${Math.round(value)}ml`} resetToken={selectedDate} /> ) : ( —— )} / {targetIntake}ml ); }; const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'space-between', borderRadius: 20, padding: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.08, shadowRadius: 20, elevation: 8, backgroundColor: '#FFFFFF', }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4, minHeight: 22, }, title: { fontSize: 14, color: '#192126', fontWeight: '500', }, addButton: { borderRadius: 16, backgroundColor: '#E1E7FF', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 6, paddingVertical: 5, }, addButtonText: { fontSize: 10, color: '#6366F1', fontWeight: '700', lineHeight: 10, }, chartContainer: { flex: 1, justifyContent: 'center', }, chartWrapper: { width: '100%', alignItems: 'center', }, chartArea: { flexDirection: 'row', alignItems: 'flex-end', height: 20, width: '100%', maxWidth: 240, justifyContent: 'space-between', paddingHorizontal: 4, }, barContainer: { width: 4, height: 20, alignItems: 'center', justifyContent: 'flex-end', position: 'relative', }, chartBar: { width: 4, borderRadius: 1, position: 'absolute', bottom: 0, }, statsContainer: { flexDirection: 'row', alignItems: 'baseline', marginTop: 6, }, currentIntake: { fontSize: 14, fontWeight: '600', color: '#192126', }, targetIntake: { fontSize: 12, color: '#6B7280', marginLeft: 4, }, }); export default WaterIntakeCard;