feat: 添加饮水设置页面,支持每日饮水目标和快速添加默认值的配置

This commit is contained in:
richarjiang
2025-09-05 16:31:52 +08:00
parent 83805a4b07
commit 8d71d751d6
4 changed files with 781 additions and 133 deletions

View File

@@ -1,9 +1,11 @@
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, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Animated,
StyleSheet,
@@ -12,7 +14,6 @@ import {
View,
ViewStyle
} from 'react-native';
import AddWaterModal from './AddWaterModal';
import { AnimatedNumber } from './AnimatedNumber';
interface WaterIntakeCardProps {
@@ -24,8 +25,8 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
style,
selectedDate
}) => {
const router = useRouter();
const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord } = useWaterDataByDate(selectedDate);
const [isModalVisible, setIsModalVisible] = useState(false);
const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载
// 计算当前饮水量和目标
@@ -75,19 +76,21 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
const isToday = selectedDate === dayjs().format('YYYY-MM-DD') || !selectedDate;
// 加载用户偏好的快速添加饮水默认值
useEffect(() => {
const loadQuickWaterAmount = async () => {
try {
const amount = await getQuickWaterAmount();
setQuickWaterAmount(amount);
} catch (error) {
console.error('加载快速添加饮水默认值失败:', error);
// 保持默认值 250ml
}
};
useFocusEffect(
useCallback(() => {
const loadQuickWaterAmount = async () => {
try {
const amount = await getQuickWaterAmount();
setQuickWaterAmount(amount);
} catch (error) {
console.error('加载快速添加饮水默认值失败:', error);
// 保持默认值 250ml
}
};
loadQuickWaterAmount();
}, []);
loadQuickWaterAmount();
}, [])
);
// 触发柱体动画
useEffect(() => {
@@ -131,140 +134,123 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
await addWaterRecord(waterAmount, recordedAt);
};
// 处理卡片点击 - 打开配置饮水弹窗
// 处理卡片点击 - 跳转到饮水设置页面
const handleCardPress = () => {
// 触发震动反馈
if (process.env.EXPO_OS === 'ios') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
setIsModalVisible(true);
// 跳转到饮水设置页面,传递选中的日期参数
router.push({
pathname: '/water-settings',
params: selectedDate ? { selectedDate } : undefined
});
};
// 处理关闭弹窗
const handleCloseModal = async () => {
setIsModalVisible(false);
// 弹窗关闭后重新加载快速添加默认值,以防用户修改了设置
try {
const amount = await getQuickWaterAmount();
setQuickWaterAmount(amount);
} catch (error) {
console.error('刷新快速添加默认值失败:', error);
}
};
return (
<>
<TouchableOpacity
style={[styles.container, style]}
onPress={handleCardPress}
activeOpacity={0.8}
>
<LottieView
ref={animationRef}
autoPlay={false}
loop={false}
source={require('@/assets/lottie/Confetti.json')}
style={{
width: 150,
height: 150,
position: 'absolute',
left: '15%',
}}
/>
<TouchableOpacity
style={[styles.container, style]}
onPress={handleCardPress}
activeOpacity={0.8}
>
<LottieView
ref={animationRef}
autoPlay={false}
loop={false}
source={require('@/assets/lottie/Confetti.json')}
style={{
width: 150,
height: 150,
position: 'absolute',
left: '15%',
}}
/>
{/* 标题和加号按钮 */}
<View style={styles.header}>
<Text style={styles.title}></Text>
{isToday && (
<TouchableOpacity style={styles.addButton} onPress={handleQuickAddWater}>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
)}
</View>
{/* 标题和加号按钮 */}
<View style={styles.header}>
<Text style={styles.title}></Text>
{isToday && (
<TouchableOpacity style={styles.addButton} onPress={handleQuickAddWater}>
<Text style={styles.addButtonText}>+ {quickWaterAmount}ml</Text>
</TouchableOpacity>
)}
</View>
{/* 柱状图 */}
<View style={styles.chartContainer}>
<View style={styles.chartWrapper}>
<View style={styles.chartArea}>
{chartData.map((data, index) => {
// 判断是否有活动的小时
const isActive = data.amount > 0;
{/* 柱状图 */}
<View style={styles.chartContainer}>
<View style={styles.chartWrapper}>
<View style={styles.chartArea}>
{chartData.map((data, index) => {
// 判断是否有活动的小时
const isActive = data.amount > 0;
// 动画变换高度从0到目标高度
const animatedHeight = animatedValues[index].interpolate({
inputRange: [0, 1],
outputRange: [0, data.height],
});
// 动画变换高度从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],
});
// 动画变换透明度从0到1保持柱体在动画过程中可见
const animatedOpacity = animatedValues[index].interpolate({
inputRange: [0, 0.1, 1],
outputRange: [0, 1, 1],
});
return (
<View key={`bar-container-${index}`} style={styles.barContainer}>
{/* 背景柱体 - 始终显示,使用蓝色系的淡色 */}
<View
return (
<View key={`bar-container-${index}`} style={styles.barContainer}>
{/* 背景柱体 - 始终显示,使用蓝色系的淡色 */}
<View
style={[
styles.chartBar,
{
height: 20, // 背景柱体占满整个高度
backgroundColor: '#F0F9FF', // 蓝色系淡色
}
]}
/>
{/* 数据柱体 - 只有当有数据时才显示并执行动画 */}
{isActive && (
<Animated.View
style={[
styles.chartBar,
{
height: 20, // 背景柱体占满整个高度
backgroundColor: '#F0F9FF', // 蓝色系淡色
height: animatedHeight,
backgroundColor: '#7DD3FC', // 蓝色系
opacity: animatedOpacity,
}
]}
/>
{/* 数据柱体 - 只有当有数据时才显示并执行动画 */}
{isActive && (
<Animated.View
style={[
styles.chartBar,
{
height: animatedHeight,
backgroundColor: '#7DD3FC', // 蓝色系
opacity: animatedOpacity,
}
]}
/>
)}
</View>
);
})}
</View>
)}
</View>
);
})}
</View>
</View>
</View>
{/* 饮水量显示 */}
<View style={styles.statsContainer}>
{currentIntake !== null ? (
<AnimatedNumber
value={currentIntake}
style={styles.currentIntake}
format={(value) => `${Math.round(value)}ml`}
resetToken={selectedDate}
/>
) : (
<Text style={styles.currentIntake}></Text>
)}
<Text style={styles.targetIntake}>
/ {targetIntake}ml
</Text>
</View>
{/* 饮水量显示 */}
<View style={styles.statsContainer}>
{currentIntake !== null ? (
<AnimatedNumber
value={currentIntake}
style={styles.currentIntake}
format={(value) => `${Math.round(value)}ml`}
resetToken={selectedDate}
/>
) : (
<Text style={styles.currentIntake}></Text>
)}
<Text style={styles.targetIntake}>
/ {targetIntake}ml
</Text>
</View>
</TouchableOpacity>
{/* 配置饮水弹窗 */}
<AddWaterModal
visible={isModalVisible}
onClose={handleCloseModal}
selectedDate={selectedDate}
/>
</>
</TouchableOpacity>
);
};
@@ -297,18 +283,18 @@ const styles = StyleSheet.create({
fontWeight: '500',
},
addButton: {
width: 22,
height: 22,
borderRadius: 16,
backgroundColor: '#E1E7FF',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 6,
paddingVertical: 5,
},
addButtonText: {
fontSize: 14,
fontSize: 10,
color: '#6366F1',
fontWeight: '700',
lineHeight: 14,
lineHeight: 10,
},
chartContainer: {
flex: 1,