import { useWaterDataByDate } from '@/hooks/useWaterData'; import { appStoreReviewService } from '@/services/appStoreReview'; import { getQuickWaterAmount } from '@/utils/userPreferences'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; import * as Haptics from 'expo-haptics'; import { Image } from 'expo-image'; import { useRouter } from 'expo-router'; import LottieView from 'lottie-react-native'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; 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 { t } = useTranslation(); const router = useRouter(); const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord, getWaterRecordsByDate } = 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 loadDataOnFocus = async () => { try { // 重新加载快速添加饮水默认值 const amount = await getQuickWaterAmount(); setQuickWaterAmount(amount); // 重新获取水数据以刷新显示 const targetDate = selectedDate || dayjs().format('YYYY-MM-DD'); await getWaterRecordsByDate(targetDate); } catch (error) { console.error('Failed to load data on focus:', error); } }; loadDataOnFocus(); }, [selectedDate, getWaterRecordsByDate]) ); // 触发柱体动画 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 = dayjs().toISOString() await addWaterRecord(waterAmount, recordedAt); // 记录饮水后尝试请求应用评分 await appStoreReviewService.requestReview(); }; // 处理卡片点击 - 跳转到饮水详情页面 const handleCardPress = async () => { // 触发震动反馈 if (process.env.EXPO_OS === 'ios') { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } // 跳转到饮水详情页面,传递选中的日期参数 router.push({ pathname: '/water/detail', params: selectedDate ? { selectedDate } : undefined }); }; return ( {/* 标题和加号按钮 */} {t('statistics.components.water.title')} {isToday && ( {t('statistics.components.water.addButton', { amount: quickWaterAmount })} )} {/* 柱状图 */} {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)}${t('statistics.components.water.unit')}`} resetToken={selectedDate} /> ) : ( -- )} / {targetIntake}{t('statistics.components.water.unit')} ); }; 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, }, titleIcon: { width: 16, height: 16, marginRight: 6, resizeMode: 'contain', }, title: { fontSize: 14, color: '#192126', fontWeight: '600', fontFamily: 'AliBold', }, addButton: { borderRadius: 16, backgroundColor: '#E1E7FF', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 6, paddingVertical: 5, }, addButtonText: { fontSize: 10, color: '#6366F1', fontWeight: '700', lineHeight: 10, fontFamily: 'AliBold', }, 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', fontFamily: 'AliBold', }, targetIntake: { fontSize: 12, color: '#6B7280', marginLeft: 4, fontFamily: 'AliRegular', }, }); export default WaterIntakeCard;