import { useWaterDataByDate } from '@/hooks/useWaterData'; import dayjs from 'dayjs'; import React, { useEffect, useMemo, useState } from 'react'; import { Animated, StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native'; import AddWaterModal from './AddWaterModal'; interface WaterIntakeCardProps { style?: ViewStyle; selectedDate?: string; // 新增:选中的日期,格式为 YYYY-MM-DD } const WaterIntakeCard: React.FC = ({ style, selectedDate }) => { const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord } = useWaterDataByDate(selectedDate); const [isModalVisible, setIsModalVisible] = useState(false); // 计算当前饮水量和目标 const currentIntake = waterStats?.totalAmount || 0; const targetIntake = dailyWaterGoal || 2000; // 为每个时间点创建独立的动画值 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; const currentHour = isToday ? new Date().getHours() : -1; // 如果不是今天,设为-1表示没有当前小时 // 触发柱体动画 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 () => { // 默认添加250ml水 const waterAmount = 250; // 如果有选中日期,则为该日期添加记录;否则为今天添加记录 const recordedAt = selectedDate ? dayjs(selectedDate).toISOString() : dayjs().toISOString(); await addWaterRecord(waterAmount, recordedAt); }; // 处理卡片点击 - 打开配置饮水弹窗 const handleCardPress = () => { setIsModalVisible(true); }; // 处理关闭弹窗 const handleCloseModal = () => { setIsModalVisible(false); }; return ( <> {/* 标题和加号按钮 */} 喝水 + {/* 柱状图 */} {chartData.map((data, index) => { // 判断是否是当前小时或者有活动的小时 const isActive = data.amount > 0; const isCurrent = isToday && index <= currentHour; // 动画变换:高度从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 ? `${currentIntake}ml` : '——'} / {targetIntake}ml {/* 完成率显示 */} {waterStats && ( {Math.round(waterStats.completionRate)}% )} {/* 配置饮水弹窗 */} ); }; 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, }, title: { fontSize: 14, color: '#192126', fontWeight: '500', }, addButton: { width: 22, height: 22, borderRadius: 16, backgroundColor: '#E1E7FF', alignItems: 'center', justifyContent: 'center', }, addButtonText: { fontSize: 14, color: '#6366F1', fontWeight: '700', lineHeight: 14, }, 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, }, completionContainer: { alignItems: 'flex-start', marginTop: 2, }, completionText: { fontSize: 12, color: '#10B981', fontWeight: '500', }, }); export default WaterIntakeCard;