From ac748dc33953077f18cb3acec3c8911d26b139b9 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Tue, 2 Sep 2025 17:12:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=A5=AE=E6=B0=B4?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=BF=AB=E9=80=9F=E6=B7=BB=E5=8A=A0=E9=A5=AE=E6=B0=B4=E9=87=8F?= =?UTF-8?q?=E5=92=8C=E7=94=A8=E6=88=B7=E5=81=8F=E5=A5=BD=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/images/icons/IconGlass.png | Bin 0 -> 24509 bytes components/AddWaterModal.tsx | 637 ++++++++++++++++++++++-------- components/AnimatedNumber.tsx | 36 +- components/WaterIntakeCard.tsx | 77 ++-- utils/userPreferences.ts | 72 ++++ 5 files changed, 612 insertions(+), 210 deletions(-) create mode 100644 assets/images/icons/IconGlass.png create mode 100644 utils/userPreferences.ts diff --git a/assets/images/icons/IconGlass.png b/assets/images/icons/IconGlass.png new file mode 100644 index 0000000000000000000000000000000000000000..49a179f84f568d1149549262ce459c93c247b695 GIT binary patch literal 24509 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 C42eVl literal 0 HcmV?d00001 diff --git a/components/AddWaterModal.tsx b/components/AddWaterModal.tsx index 4d948b6..ffa2a05 100644 --- a/components/AddWaterModal.tsx +++ b/components/AddWaterModal.tsx @@ -1,8 +1,13 @@ +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; import { useWaterDataByDate } from '@/hooks/useWaterData'; +import { getQuickWaterAmount, setQuickWaterAmount } from '@/utils/userPreferences'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; -import React, { useState } from 'react'; +import { Image } from 'expo-image'; +import React, { useEffect, useState } from 'react'; import { + Alert, KeyboardAvoidingView, Modal, Platform, @@ -13,6 +18,7 @@ import { TouchableOpacity, View, } from 'react-native'; +import { Swipeable } from 'react-native-gesture-handler'; interface AddWaterModalProps { visible: boolean; @@ -20,34 +26,22 @@ interface AddWaterModalProps { selectedDate?: string; // 新增:选中的日期,格式为 YYYY-MM-DD } -interface TabButtonProps { - title: string; - isActive: boolean; - onPress: () => void; -} - -const TabButton: React.FC = ({ title, isActive, onPress }) => ( - - - {title} - - -); - const AddWaterModal: React.FC = ({ visible, onClose, selectedDate }) => { - const [activeTab, setActiveTab] = useState<'add' | 'goal'>('add'); + const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; + const colorTokens = Colors[theme]; + + const [activeTab, setActiveTab] = useState<'manage' | 'records'>('manage'); const [waterAmount, setWaterAmount] = useState('250'); const [note, setNote] = useState(''); const [dailyGoal, setDailyGoal] = useState('2000'); + const [quickAddAmount, setQuickAddAmount] = useState('250'); // 快速添加默认值 // 使用新的 hook 来处理指定日期的饮水数据 - const { addWaterRecord, updateWaterGoal } = useWaterDataByDate(selectedDate); + const { waterRecords, dailyWaterGoal, addWaterRecord, updateWaterGoal, removeWaterRecord } = useWaterDataByDate(selectedDate); const quickAmounts = [100, 150, 200, 250, 300, 350, 400, 500]; const goalPresets = [1500, 2000, 2500, 3000, 3500, 4000]; + const quickAddPresets = [100, 150, 200, 250, 300, 350, 400, 500]; // 快速添加默认值选项 const handleAddWater = async () => { const amount = parseInt(waterAmount); @@ -75,79 +69,122 @@ const AddWaterModal: React.FC = ({ visible, onClose, selecte } }; - const renderAddRecordTab = () => ( - - 饮水量 (ml) - + // 处理保存所有设置(饮水目标 + 快速添加默认值) + const handleSaveSettings = async () => { + const goal = parseInt(dailyGoal); + const amount = parseInt(quickAddAmount); + + let hasError = false; + + // 验证饮水目标 + if (goal < 500 || goal > 10000) { + Alert.alert('输入错误', '每日饮水目标应在500ml到10000ml之间'); + return; + } + + // 验证快速添加默认值 + if (amount < 50 || amount > 1000) { + Alert.alert('输入错误', '快速添加默认值应在50ml到1000ml之间'); + return; + } + + try { + // 保存饮水目标 + const goalSuccess = await updateWaterGoal(goal); + if (!goalSuccess) { + hasError = true; + } + + // 保存快速添加默认值 + await setQuickWaterAmount(amount); + + if (!hasError) { + onClose(); + } + } catch (error) { + Alert.alert('设置失败', '无法保存设置,请重试'); + } + }; - 快速选择 - { + await removeWaterRecord(recordId); + }; + + // 加载用户偏好设置和当前饮水目标 + useEffect(() => { + const loadUserPreferences = async () => { + try { + const amount = await getQuickWaterAmount(); + setQuickAddAmount(amount.toString()); + + // 设置当前的饮水目标 + if (dailyWaterGoal) { + setDailyGoal(dailyWaterGoal.toString()); + } + } catch (error) { + console.error('加载用户偏好设置失败:', error); + } + }; + + if (visible) { + loadUserPreferences(); + } + }, [visible, dailyWaterGoal]); + + // 渲染Tab切换器 - 参照营养记录页面的实现 + const renderTabToggle = () => ( + + setActiveTab('manage')} > - - {quickAmounts.map((amount) => ( - setWaterAmount(amount.toString())} - > - - {amount}ml - - - ))} - - - - 备注 (可选) - - - - - 取消 - - - 添加记录 - - + + 饮水配置 + + + setActiveTab('records')} + > + + 饮水记录 + + ); - const renderGoalTab = () => ( + // 合并后的管理Tab,包含添加记录和设置目标 + const renderManageTab = () => ( - 每日饮水目标 (ml) + {/* 设置目标部分 */} + 每日饮水目标 (ml) - 推荐目标 + 推荐目标 = ({ visible, onClose, selecte key={goal} style={[ styles.quickAmountButton, - parseInt(dailyGoal) === goal && styles.quickAmountButtonActive + { + borderColor: colorTokens.border, + backgroundColor: colorTokens.pageBackgroundEmphasis + }, + parseInt(dailyGoal) === goal && { + backgroundColor: colorTokens.primary, + borderColor: colorTokens.primary + } ]} onPress={() => setDailyGoal(goal.toString())} > {goal}ml @@ -174,20 +218,195 @@ const AddWaterModal: React.FC = ({ visible, onClose, selecte + {/* 快速添加默认值设置部分 */} + 快速添加默认值 (ml) + + 设置点击右上角"+"按钮时添加的默认饮水量 + + + + 推荐设置 + + + {quickAddPresets.map((amount) => ( + setQuickAddAmount(amount.toString())} + > + + {amount}ml + + + ))} + + + - - 取消 + + 取消 - - 更新目标 + + 保存设置 ); + // 新增:饮水记录卡片组件 + const WaterRecordCard = ({ record, onDelete }: { record: any; onDelete: () => void }) => { + const swipeableRef = React.useRef(null); + + // 处理删除操作 + const handleDelete = () => { + Alert.alert( + '确认删除', + '确定要删除这条饮水记录吗?此操作无法撤销。', + [ + { + text: '取消', + style: 'cancel', + }, + { + text: '删除', + style: 'destructive', + onPress: () => { + onDelete(); + swipeableRef.current?.close(); + }, + }, + ] + ); + }; + + // 渲染右侧删除按钮 + const renderRightActions = () => { + return ( + + + 删除 + + ); + }; + + return ( + + + + + + + + + + + + + {dayjs(record.recordedAt || record.createdAt).format('HH:mm')} + + + + + {record.amount}ml + + + {record.note && ( + {record.note} + )} + + + + ); + }; + + // 新增:饮水记录Tab内容 + const renderRecordsTab = () => ( + + + {selectedDate ? dayjs(selectedDate).format('MM月DD日') : '今日'}饮水记录 + + + {waterRecords && waterRecords.length > 0 ? ( + + {waterRecords.map((record) => ( + handleDeleteRecord(record.id)} + /> + ))} + + {/* 总计显示 */} + + + 总计:{waterRecords.reduce((sum, record) => sum + record.amount, 0)}ml + + + 目标:{dailyWaterGoal}ml + + + + ) : ( + + + 暂无饮水记录 + 点击"添加记录"开始记录饮水量 + + )} + + ); + return ( = ({ visible, onClose, selecte style={styles.centeredView} behavior={Platform.OS === 'ios' ? 'padding' : 'height'} > - + - 配置饮水 + 配置饮水 - + - - setActiveTab('add')} - /> - setActiveTab('goal')} - /> - + {renderTabToggle()} - {activeTab === 'add' ? renderAddRecordTab() : renderGoalTab()} + {activeTab === 'manage' ? renderManageTab() : renderRecordsTab()} @@ -235,19 +443,20 @@ const styles = StyleSheet.create({ }, modalView: { width: '90%', - maxWidth: 350, - maxHeight: '80%', - backgroundColor: 'white', - borderRadius: 20, - padding: 20, - shadowColor: '#000', + maxWidth: 400, + height: 650, // 固定高度 + borderRadius: 24, + paddingTop: 24, + paddingHorizontal: 20, + paddingBottom: 20, + shadowColor: '#000000', shadowOffset: { width: 0, - height: 2, + height: 8, }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 5, + shadowOpacity: 0.12, + shadowRadius: 20, + elevation: 8, }, header: { flexDirection: 'row', @@ -256,59 +465,55 @@ const styles = StyleSheet.create({ marginBottom: 20, }, modalTitle: { - fontSize: 20, - fontWeight: 'bold', - color: '#333', + fontSize: 16, + fontWeight: '600', + letterSpacing: -0.5, }, closeButton: { padding: 5, }, tabContainer: { flexDirection: 'row', - marginBottom: 20, - borderRadius: 10, - backgroundColor: '#f5f5f5', - padding: 4, + borderRadius: 20, + padding: 2, + marginBottom: 24, }, tabButton: { - flex: 1, - paddingVertical: 10, + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 18, + minWidth: 80, alignItems: 'center', - borderRadius: 8, - }, - activeTabButton: { - backgroundColor: '#007AFF', + flex: 1, }, tabButtonText: { fontSize: 14, - color: '#666', - fontWeight: '500', - }, - activeTabButtonText: { - color: 'white', fontWeight: '600', }, contentScrollView: { - maxHeight: 400, + flex: 1, }, tabContent: { paddingVertical: 10, }, sectionTitle: { - fontSize: 16, - fontWeight: '600', - color: '#333', - marginBottom: 10, + fontSize: 14, + fontWeight: '500', + marginBottom: 12, + letterSpacing: -0.3, + }, + sectionSubtitle: { + fontSize: 14, + fontWeight: '400', + lineHeight: 18, }, input: { - borderWidth: 1, - borderColor: '#e0e0e0', - borderRadius: 10, - paddingHorizontal: 15, - paddingVertical: 12, + borderRadius: 12, + paddingHorizontal: 16, + paddingVertical: 14, fontSize: 16, - color: '#333', - marginBottom: 15, + fontWeight: '500', + marginBottom: 16, }, remarkInput: { height: 80, @@ -324,27 +529,20 @@ const styles = StyleSheet.create({ }, quickAmountButton: { paddingHorizontal: 20, - paddingVertical: 10, + paddingVertical: 8, borderRadius: 20, - borderWidth: 1, - borderColor: '#e0e0e0', - backgroundColor: '#f9f9f9', - minWidth: 60, + minWidth: 70, alignItems: 'center', - }, - quickAmountButtonActive: { - backgroundColor: '#007AFF', - borderColor: '#007AFF', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 2, + elevation: 1, }, quickAmountText: { - fontSize: 14, - color: '#666', + fontSize: 15, fontWeight: '500', }, - quickAmountTextActive: { - color: 'white', - fontWeight: '600', - }, buttonContainer: { flexDirection: 'row', gap: 10, @@ -352,25 +550,134 @@ const styles = StyleSheet.create({ }, button: { flex: 1, - paddingVertical: 12, - borderRadius: 10, + paddingVertical: 14, + borderRadius: 12, alignItems: 'center', }, cancelButton: { - backgroundColor: '#f5f5f5', + }, confirmButton: { - backgroundColor: '#007AFF', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 3, }, cancelButtonText: { fontSize: 16, - color: '#666', - fontWeight: '500', + fontWeight: '600', }, confirmButtonText: { fontSize: 16, - color: 'white', + fontWeight: '700', + }, + // 饮水记录相关样式 + recordsList: { + gap: 12, + }, + recordCardContainer: { + // iOS 阴影效果 + shadowColor: '#000000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.08, + shadowRadius: 4, + // Android 阴影效果 + elevation: 2, + }, + recordCard: { + borderRadius: 12, + padding: 10, + }, + recordMainContent: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + recordIconContainer: { + width: 40, + height: 40, + borderRadius: 10, + alignItems: 'center', + justifyContent: 'center', + }, + recordIcon: { + width: 20, + height: 20, + }, + recordInfo: { + flex: 1, + marginLeft: 12, + }, + recordLabel: { + fontSize: 16, fontWeight: '600', + marginBottom: 8, + }, + recordTimeContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + recordAmountContainer: { + alignItems: 'flex-end', + }, + recordAmount: { + fontSize: 14, + fontWeight: '500', + }, + deleteSwipeButton: { + backgroundColor: '#EF4444', + justifyContent: 'center', + alignItems: 'center', + width: 80, + borderRadius: 12, + marginLeft: 8, + }, + deleteSwipeButtonText: { + color: '#FFFFFF', + fontSize: 12, + fontWeight: '600', + marginTop: 4, + }, + recordTimeText: { + fontSize: 12, + fontWeight: '400', + }, + recordNote: { + marginTop: 8, + fontSize: 14, + fontStyle: 'italic', + lineHeight: 20, + }, + recordsSummary: { + marginTop: 20, + padding: 16, + borderRadius: 12, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + summaryText: { + fontSize: 12, + fontWeight: '500', + }, + summaryGoal: { + fontSize: 12, + fontWeight: '500', + }, + noRecordsContainer: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 40, + gap: 12, + }, + noRecordsText: { + fontSize: 16, + fontWeight: '600', + }, + noRecordsSubText: { + fontSize: 14, + textAlign: 'center', }, }); diff --git a/components/AnimatedNumber.tsx b/components/AnimatedNumber.tsx index 1f8eb4c..1c41233 100644 --- a/components/AnimatedNumber.tsx +++ b/components/AnimatedNumber.tsx @@ -18,15 +18,26 @@ export function AnimatedNumber({ resetToken, }: AnimatedNumberProps) { const opacity = useRef(new Animated.Value(1)).current; - const [display, setDisplay] = useState('0'); - const [currentValue, setCurrentValue] = useState(0); + const [display, setDisplay] = useState(() => + format ? format(value) : `${Math.round(value)}` + ); + const [currentValue, setCurrentValue] = useState(value); + const [lastResetToken, setLastResetToken] = useState(resetToken); + const [isAnimating, setIsAnimating] = useState(false); useEffect(() => { - // 如果值没有变化,不执行动画 - if (value === currentValue && resetToken === undefined) { + // 检查是否需要触发动画 + const valueChanged = value !== currentValue; + const resetTokenChanged = resetToken !== lastResetToken; + + // 如果值没有变化且resetToken也没有变化,或者正在动画中,则不执行新动画 + if ((!valueChanged && !resetTokenChanged) || isAnimating) { return; } + // 标记开始动画 + setIsAnimating(true); + // 停止当前动画 opacity.stopAnimation(() => { // 创建优雅的透明度变化动画 @@ -48,22 +59,17 @@ export function AnimatedNumber({ fadeOut.start(() => { // 更新当前值和显示 setCurrentValue(value); + setLastResetToken(resetToken); setDisplay(format ? format(value) : `${Math.round(value)}`); // 然后淡入新数字 - fadeIn.start(); + fadeIn.start(() => { + // 动画完成,标记结束 + setIsAnimating(false); + }); }); }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, resetToken]); - - // 初始化显示值 - useEffect(() => { - if (currentValue !== value) { - setCurrentValue(value); - setDisplay(format ? format(value) : `${Math.round(value)}`); - } - }, [value, format, currentValue]); + }, [value, resetToken, currentValue, lastResetToken, isAnimating, durationMs, format]); return ( = ({ }) => { const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord } = useWaterDataByDate(selectedDate); const [isModalVisible, setIsModalVisible] = useState(false); + const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载 // 计算当前饮水量和目标 const currentIntake = waterStats?.totalAmount || 0; @@ -64,9 +67,23 @@ const WaterIntakeCard: React.FC = ({ })); }, [waterRecords]); - // 获取当前小时 - 只有当选中的是今天时才显示当前小时 + // 判断是否是今天 const isToday = selectedDate === dayjs().format('YYYY-MM-DD') || !selectedDate; - const currentHour = isToday ? new Date().getHours() : -1; // 如果不是今天,设为-1表示没有当前小时 + + // 加载用户偏好的快速添加饮水默认值 + useEffect(() => { + const loadQuickWaterAmount = async () => { + try { + const amount = await getQuickWaterAmount(); + setQuickWaterAmount(amount); + } catch (error) { + console.error('加载快速添加饮水默认值失败:', error); + // 保持默认值 250ml + } + }; + + loadQuickWaterAmount(); + }, []); // 触发柱体动画 useEffect(() => { @@ -96,8 +113,8 @@ const WaterIntakeCard: React.FC = ({ // 处理添加喝水 - 右上角按钮直接添加 const handleQuickAddWater = async () => { - // 默认添加250ml水 - const waterAmount = 250; + // 使用用户配置的快速添加饮水量 + const waterAmount = quickWaterAmount; // 如果有选中日期,则为该日期添加记录;否则为今天添加记录 const recordedAt = selectedDate ? dayjs(selectedDate).toISOString() : dayjs().toISOString(); await addWaterRecord(waterAmount, recordedAt); @@ -109,8 +126,16 @@ const WaterIntakeCard: React.FC = ({ }; // 处理关闭弹窗 - const handleCloseModal = () => { + const handleCloseModal = async () => { setIsModalVisible(false); + + // 弹窗关闭后重新加载快速添加默认值,以防用户修改了设置 + try { + const amount = await getQuickWaterAmount(); + setQuickWaterAmount(amount); + } catch (error) { + console.error('刷新快速添加默认值失败:', error); + } }; return ( @@ -123,9 +148,11 @@ const WaterIntakeCard: React.FC = ({ {/* 标题和加号按钮 */} 喝水 - - + - + {isToday && ( + + + + + )} {/* 柱状图 */} @@ -133,9 +160,8 @@ const WaterIntakeCard: React.FC = ({ {chartData.map((data, index) => { - // 判断是否是当前小时或者有活动的小时 + // 判断是否有活动的小时 const isActive = data.amount > 0; - const isCurrent = isToday && index <= currentHour; // 动画变换:高度从0到目标高度 const animatedHeight = animatedValues[index].interpolate({ @@ -184,22 +210,21 @@ const WaterIntakeCard: React.FC = ({ {/* 饮水量显示 */} - - {currentIntake !== null ? `${currentIntake}ml` : '——'} - + {currentIntake !== null ? ( + `${Math.round(value)}ml`} + resetToken={selectedDate} + /> + ) : ( + —— + )} / {targetIntake}ml - {/* 完成率显示 */} - {waterStats && ( - - - {Math.round(waterStats.completionRate)}% - - - )} {/* 配置饮水弹窗 */} @@ -233,6 +258,7 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', alignItems: 'center', marginBottom: 4, + minHeight: 22, }, title: { fontSize: 14, @@ -298,15 +324,6 @@ const styles = StyleSheet.create({ color: '#6B7280', marginLeft: 4, }, - completionContainer: { - alignItems: 'flex-start', - marginTop: 2, - }, - completionText: { - fontSize: 12, - color: '#10B981', - fontWeight: '500', - }, }); export default WaterIntakeCard; \ No newline at end of file diff --git a/utils/userPreferences.ts b/utils/userPreferences.ts new file mode 100644 index 0000000..89d9a29 --- /dev/null +++ b/utils/userPreferences.ts @@ -0,0 +1,72 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +// 用户偏好设置的存储键 +const PREFERENCES_KEYS = { + QUICK_WATER_AMOUNT: 'user_preference_quick_water_amount', +} as const; + +// 用户偏好设置接口 +export interface UserPreferences { + quickWaterAmount: number; +} + +// 默认的用户偏好设置 +const DEFAULT_PREFERENCES: UserPreferences = { + quickWaterAmount: 150, // 默认快速添加饮水量为 250ml +}; + +/** + * 获取用户偏好设置 + */ +export const getUserPreferences = async (): Promise => { + try { + const quickWaterAmount = await AsyncStorage.getItem(PREFERENCES_KEYS.QUICK_WATER_AMOUNT); + + return { + quickWaterAmount: quickWaterAmount ? parseInt(quickWaterAmount, 10) : DEFAULT_PREFERENCES.quickWaterAmount, + }; + } catch (error) { + console.error('获取用户偏好设置失败:', error); + return DEFAULT_PREFERENCES; + } +}; + +/** + * 设置快速添加饮水的默认值 + * @param amount 饮水量(毫升) + */ +export const setQuickWaterAmount = async (amount: number): Promise => { + try { + // 确保值在合理范围内(50ml - 1000ml) + const validAmount = Math.max(50, Math.min(1000, amount)); + await AsyncStorage.setItem(PREFERENCES_KEYS.QUICK_WATER_AMOUNT, validAmount.toString()); + } catch (error) { + console.error('设置快速添加饮水默认值失败:', error); + throw error; + } +}; + +/** + * 获取快速添加饮水的默认值 + */ +export const getQuickWaterAmount = async (): Promise => { + try { + const amount = await AsyncStorage.getItem(PREFERENCES_KEYS.QUICK_WATER_AMOUNT); + return amount ? parseInt(amount, 10) : DEFAULT_PREFERENCES.quickWaterAmount; + } catch (error) { + console.error('获取快速添加饮水默认值失败:', error); + return DEFAULT_PREFERENCES.quickWaterAmount; + } +}; + +/** + * 重置所有用户偏好设置为默认值 + */ +export const resetUserPreferences = async (): Promise => { + try { + await AsyncStorage.removeItem(PREFERENCES_KEYS.QUICK_WATER_AMOUNT); + } catch (error) { + console.error('重置用户偏好设置失败:', error); + throw error; + } +}; \ No newline at end of file