feat: 添加饮水设置页面,支持每日饮水目标和快速添加默认值的配置
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user