diff --git a/app.json b/app.json
index 9c1d9f7..563a72b 100644
--- a/app.json
+++ b/app.json
@@ -2,7 +2,7 @@
"expo": {
"name": "Out Live",
"slug": "digital-pilates",
- "version": "1.0.13",
+ "version": "1.0.14",
"orientation": "portrait",
"scheme": "digitalpilates",
"userInterfaceStyle": "light",
diff --git a/app/_layout.tsx b/app/_layout.tsx
index b15cb83..b463549 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -193,6 +193,7 @@ export default function RootLayout() {
+
diff --git a/app/mood/edit.tsx b/app/mood/edit.tsx
index d1a1e78..ec573da 100644
--- a/app/mood/edit.tsx
+++ b/app/mood/edit.tsx
@@ -15,9 +15,12 @@ import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import { router, useLocalSearchParams } from 'expo-router';
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import {
Alert, Image,
+ Keyboard,
+ KeyboardAvoidingView,
+ Platform,
ScrollView,
StyleSheet,
Text,
@@ -43,6 +46,9 @@ export default function MoodEditScreen() {
const [isDeleting, setIsDeleting] = useState(false);
const [existingMood, setExistingMood] = useState(null);
+ const scrollViewRef = useRef(null);
+ const textInputRef = useRef(null);
+
const moodOptions = getMoodOptions();
// 从 Redux 获取数据
@@ -66,6 +72,25 @@ export default function MoodEditScreen() {
}
}, [moodId, moodRecords]);
+ // 键盘事件监听器
+ useEffect(() => {
+ const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
+ // 键盘出现时,延迟滚动到文本输入框
+ setTimeout(() => {
+ scrollViewRef.current?.scrollToEnd({ animated: true });
+ }, 100);
+ });
+
+ const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
+ // 键盘隐藏时,可以进行必要的调整
+ });
+
+ return () => {
+ keyboardDidShowListener?.remove();
+ keyboardDidHideListener?.remove();
+ };
+ }, []);
+
const handleSave = async () => {
if (!selectedMood) {
Alert.alert('提示', '请选择心情');
@@ -163,7 +188,18 @@ export default function MoodEditScreen() {
tone="light"
/>
-
+
+
{/* 日期显示 */}
@@ -211,6 +247,7 @@ export default function MoodEditScreen() {
心情日记
记录你的心情,珍藏美好回忆
{
+ // 当文本输入框获得焦点时,滚动到输入框
+ setTimeout(() => {
+ scrollViewRef.current?.scrollToEnd({ animated: true });
+ }, 300);
+ }}
/>
{description.length}/1000
-
+
+
{/* 底部按钮 */}
@@ -294,10 +338,15 @@ const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
-
+ keyboardAvoidingView: {
+ flex: 1,
+ },
content: {
flex: 1,
},
+ scrollContent: {
+ paddingBottom: 100, // 为底部按钮留出空间
+ },
dateSection: {
backgroundColor: 'rgba(255,255,255,0.95)',
margin: 12,
diff --git a/app/water-settings.tsx b/app/water/detail.tsx
similarity index 62%
rename from app/water-settings.tsx
rename to app/water/detail.tsx
index f46afd6..47ce8cf 100644
--- a/app/water-settings.tsx
+++ b/app/water/detail.tsx
@@ -1,7 +1,8 @@
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useWaterDataByDate } from '@/hooks/useWaterData';
-import { getQuickWaterAmount, setQuickWaterAmount } from '@/utils/userPreferences';
+import { WaterNotificationHelpers } from '@/utils/notificationHelpers';
+import { getQuickWaterAmount, getWaterReminderSettings, setWaterReminderSettings as saveWaterReminderSettings, setQuickWaterAmount } from '@/utils/userPreferences';
import { Ionicons } from '@expo/vector-icons';
import { Picker } from '@react-native-picker/picker';
import { Image } from 'expo-image';
@@ -16,6 +17,7 @@ import {
Pressable,
ScrollView,
StyleSheet,
+ Switch,
Text,
TouchableOpacity,
View
@@ -25,11 +27,11 @@ import { Swipeable } from 'react-native-gesture-handler';
import { HeaderBar } from '@/components/ui/HeaderBar';
import dayjs from 'dayjs';
-interface WaterSettingsProps {
+interface WaterDetailProps {
selectedDate?: string;
}
-const WaterSettings: React.FC = () => {
+const WaterDetail: React.FC = () => {
const { selectedDate } = useLocalSearchParams<{ selectedDate?: string }>();
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
@@ -37,14 +39,7 @@ const WaterSettings: React.FC = () => {
const [dailyGoal, setDailyGoal] = useState('2000');
const [quickAddAmount, setQuickAddAmount] = useState('250');
- // 设置弹窗状态
- const [settingsModalVisible, setSettingsModalVisible] = useState(false);
- const [goalModalVisible, setGoalModalVisible] = useState(false);
- const [quickAddModalVisible, setQuickAddModalVisible] = useState(false);
-
- // 临时选中值
- const [tempGoal, setTempGoal] = useState(parseInt(dailyGoal));
- const [tempQuickAdd, setTempQuickAdd] = useState(parseInt(quickAddAmount));
+ // Remove modal states as they are now in separate settings page
// 使用新的 hook 来处理指定日期的饮水数据
const { waterRecords, dailyWaterGoal, updateWaterGoal, removeWaterRecord } = useWaterDataByDate(selectedDate);
@@ -52,51 +47,12 @@ const WaterSettings: React.FC = () => {
- // 处理设置按钮点击
+ // 处理设置按钮点击 - 跳转到设置页面
const handleSettingsPress = () => {
- setSettingsModalVisible(true);
+ router.push('/water/settings');
};
- // 打开饮水目标弹窗时初始化临时值
- const openGoalModal = () => {
- setTempGoal(parseInt(dailyGoal));
- setSettingsModalVisible(false);
- setGoalModalVisible(true);
- };
-
- // 打开快速添加弹窗时初始化临时值
- const openQuickAddModal = () => {
- setTempQuickAdd(parseInt(quickAddAmount));
- setSettingsModalVisible(false);
- setQuickAddModalVisible(true);
- };
-
- // 处理饮水目标确认
- const handleGoalConfirm = async () => {
- setDailyGoal(tempGoal.toString());
- setGoalModalVisible(false);
-
- try {
- const success = await updateWaterGoal(tempGoal);
- if (!success) {
- Alert.alert('设置失败', '无法保存饮水目标,请重试');
- }
- } catch {
- Alert.alert('设置失败', '无法保存饮水目标,请重试');
- }
- };
-
- // 处理快速添加默认值确认
- const handleQuickAddConfirm = async () => {
- setQuickAddAmount(tempQuickAdd.toString());
- setQuickAddModalVisible(false);
-
- try {
- await setQuickWaterAmount(tempQuickAdd);
- } catch {
- Alert.alert('设置失败', '无法保存快速添加默认值,请重试');
- }
- };
+ // Remove all modal-related functions as they are now in separate settings page
// 删除饮水记录
@@ -123,15 +79,6 @@ const WaterSettings: React.FC = () => {
loadUserPreferences();
}, [dailyWaterGoal]);
- // 当dailyGoal或quickAddAmount更新时,同步更新临时状态
- useEffect(() => {
- setTempGoal(parseInt(dailyGoal));
- }, [dailyGoal]);
-
- useEffect(() => {
- setTempQuickAdd(parseInt(quickAddAmount));
- }, [quickAddAmount]);
-
// 新增:饮水记录卡片组件
const WaterRecordCard = ({ record, onDelete }: { record: any; onDelete: () => void }) => {
const swipeableRef = React.useRef(null);
@@ -225,7 +172,7 @@ const WaterSettings: React.FC = () => {
{
// 这里会通过路由自动处理返回
router.back();
@@ -288,129 +235,7 @@ const WaterSettings: React.FC = () => {
- {/* 设置主弹窗 */}
- setSettingsModalVisible(false)}
- >
- setSettingsModalVisible(false)} />
-
-
- 饮水设置
-
- {/* 菜单项 */}
-
-
-
-
-
-
-
- 每日饮水目标
- {dailyWaterGoal || dailyGoal}ml
-
-
-
-
-
-
-
-
-
-
-
- 快速添加默认值
-
- 设置点击"+"按钮时添加的默认饮水量
-
- {quickAddAmount}ml
-
-
-
-
-
-
-
-
- {/* 饮水目标编辑弹窗 */}
- setGoalModalVisible(false)}
- >
- setGoalModalVisible(false)} />
-
-
- 每日饮水目标
-
- setTempGoal(value)}
- style={styles.picker}
- >
- {Array.from({ length: 96 }, (_, i) => 500 + i * 100).map(goal => (
-
- ))}
-
-
-
- setGoalModalVisible(false)}
- style={[styles.modalBtn, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}
- >
- 取消
-
-
- 确定
-
-
-
-
-
- {/* 快速添加默认值编辑弹窗 */}
- setQuickAddModalVisible(false)}
- >
- setQuickAddModalVisible(false)} />
-
-
- 快速添加默认值
-
- setTempQuickAdd(value)}
- style={styles.picker}
- >
- {Array.from({ length: 41 }, (_, i) => 50 + i * 10).map(amount => (
-
- ))}
-
-
-
- setQuickAddModalVisible(false)}
- style={[styles.modalBtn, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}
- >
- 取消
-
-
- 确定
-
-
-
-
+ {/* All modals have been moved to the separate water-settings page */}
);
};
@@ -724,6 +549,151 @@ const styles = StyleSheet.create({
settingsMenuItemValue: {
fontSize: 14,
},
+ // 喝水提醒配置弹窗样式
+ waterReminderModalSheet: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 0,
+ padding: 16,
+ backgroundColor: '#FFFFFF',
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ maxHeight: '80%',
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: -2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 16,
+ },
+ waterReminderContent: {
+ flex: 1,
+ marginBottom: 20,
+ },
+ waterReminderSection: {
+ marginBottom: 24,
+ },
+ waterReminderSectionHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginBottom: 8,
+ },
+ waterReminderSectionTitleContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ waterReminderSectionTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ waterReminderSectionDesc: {
+ fontSize: 14,
+ lineHeight: 20,
+ marginTop: 4,
+ },
+ timeRangeContainer: {
+ flexDirection: 'row',
+ gap: 16,
+ marginTop: 16,
+ },
+ timePickerContainer: {
+ flex: 1,
+ },
+ timeLabel: {
+ fontSize: 14,
+ fontWeight: '500',
+ marginBottom: 8,
+ },
+ timePicker: {
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 8,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 8,
+ },
+ timePickerText: {
+ fontSize: 16,
+ fontWeight: '500',
+ },
+ timePickerIcon: {
+ opacity: 0.6,
+ },
+ intervalContainer: {
+ marginTop: 16,
+ },
+ intervalPickerContainer: {
+ backgroundColor: '#F8F9FA',
+ borderRadius: 8,
+ overflow: 'hidden',
+ },
+ intervalPicker: {
+ height: 120,
+ },
+ // 时间选择器弹窗样式
+ timePickerModalSheet: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 0,
+ padding: 16,
+ backgroundColor: '#FFFFFF',
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ maxHeight: '60%',
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: -2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 16,
+ },
+ timePickerContent: {
+ flex: 1,
+ marginBottom: 20,
+ },
+ timePickerSection: {
+ marginBottom: 20,
+ },
+ timePickerLabel: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 12,
+ textAlign: 'center',
+ },
+ hourPickerContainer: {
+ backgroundColor: '#F8F9FA',
+ borderRadius: 8,
+ overflow: 'hidden',
+ },
+ hourPicker: {
+ height: 160,
+ },
+ timeRangePreview: {
+ backgroundColor: '#F0F8FF',
+ borderRadius: 8,
+ padding: 16,
+ marginTop: 16,
+ alignItems: 'center',
+ },
+ timeRangePreviewLabel: {
+ fontSize: 12,
+ fontWeight: '500',
+ marginBottom: 4,
+ },
+ timeRangePreviewText: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginBottom: 8,
+ },
+ timeRangeWarning: {
+ fontSize: 12,
+ color: '#FF6B6B',
+ textAlign: 'center',
+ lineHeight: 18,
+ },
});
-export default WaterSettings;
+export default WaterDetail;
diff --git a/app/water/reminder-settings.tsx b/app/water/reminder-settings.tsx
new file mode 100644
index 0000000..098f47a
--- /dev/null
+++ b/app/water/reminder-settings.tsx
@@ -0,0 +1,618 @@
+import { Colors } from '@/constants/Colors';
+import { useColorScheme } from '@/hooks/useColorScheme';
+import { WaterNotificationHelpers } from '@/utils/notificationHelpers';
+import { getWaterReminderSettings, setWaterReminderSettings as saveWaterReminderSettings } from '@/utils/userPreferences';
+import { Ionicons } from '@expo/vector-icons';
+import { Picker } from '@react-native-picker/picker';
+import { LinearGradient } from 'expo-linear-gradient';
+import { router } from 'expo-router';
+import React, { useEffect, useState } from 'react';
+import {
+ Alert,
+ KeyboardAvoidingView,
+ Modal,
+ Platform,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Switch,
+ Text,
+ TouchableOpacity,
+ View
+} from 'react-native';
+
+import { HeaderBar } from '@/components/ui/HeaderBar';
+
+const WaterReminderSettings: React.FC = () => {
+ const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
+ const colorTokens = Colors[theme];
+
+ const [startTimePickerVisible, setStartTimePickerVisible] = useState(false);
+ const [endTimePickerVisible, setEndTimePickerVisible] = useState(false);
+
+ // 喝水提醒相关状态
+ const [waterReminderSettings, setWaterReminderSettings] = useState({
+ enabled: false,
+ startTime: '08:00',
+ endTime: '22:00',
+ interval: 60,
+ });
+
+ // 时间选择器临时值
+ const [tempStartHour, setTempStartHour] = useState(8);
+ const [tempEndHour, setTempEndHour] = useState(22);
+
+ // 打开开始时间选择器
+ const openStartTimePicker = () => {
+ const currentHour = parseInt(waterReminderSettings.startTime.split(':')[0]);
+ setTempStartHour(currentHour);
+ setStartTimePickerVisible(true);
+ };
+
+ // 打开结束时间选择器
+ const openEndTimePicker = () => {
+ const currentHour = parseInt(waterReminderSettings.endTime.split(':')[0]);
+ setTempEndHour(currentHour);
+ setEndTimePickerVisible(true);
+ };
+
+ // 确认开始时间选择
+ const confirmStartTime = () => {
+ const newStartTime = `${String(tempStartHour).padStart(2, '0')}:00`;
+
+ // 检查时间合理性
+ if (isValidTimeRange(newStartTime, waterReminderSettings.endTime)) {
+ setWaterReminderSettings(prev => ({
+ ...prev,
+ startTime: newStartTime
+ }));
+ setStartTimePickerVisible(false);
+ } else {
+ Alert.alert(
+ '时间设置提示',
+ '开始时间不能晚于或等于结束时间,请重新选择',
+ [{ text: '确定' }]
+ );
+ }
+ };
+
+ // 确认结束时间选择
+ const confirmEndTime = () => {
+ const newEndTime = `${String(tempEndHour).padStart(2, '0')}:00`;
+
+ // 检查时间合理性
+ if (isValidTimeRange(waterReminderSettings.startTime, newEndTime)) {
+ setWaterReminderSettings(prev => ({
+ ...prev,
+ endTime: newEndTime
+ }));
+ setEndTimePickerVisible(false);
+ } else {
+ Alert.alert(
+ '时间设置提示',
+ '结束时间不能早于或等于开始时间,请重新选择',
+ [{ text: '确定' }]
+ );
+ }
+ };
+
+ // 验证时间范围是否有效
+ const isValidTimeRange = (startTime: string, endTime: string): boolean => {
+ const [startHour] = startTime.split(':').map(Number);
+ const [endHour] = endTime.split(':').map(Number);
+
+ // 支持跨天的情况,如果结束时间小于开始时间,认为是跨天有效的
+ if (endHour < startHour) {
+ return true; // 跨天情况,如 22:00 到 08:00
+ }
+
+ // 同一天内,结束时间必须大于开始时间
+ return endHour > startHour;
+ };
+
+ // 处理喝水提醒配置保存
+ const handleWaterReminderSave = async () => {
+ try {
+ // 保存设置到本地存储
+ await saveWaterReminderSettings(waterReminderSettings);
+
+ // 设置或取消通知
+ // 这里使用 "用户" 作为默认用户名,实际项目中应该从用户状态获取
+ const userName = '用户';
+ await WaterNotificationHelpers.scheduleCustomWaterReminders(userName, waterReminderSettings);
+
+ if (waterReminderSettings.enabled) {
+ const timeInfo = `${waterReminderSettings.startTime}-${waterReminderSettings.endTime}`;
+ const intervalInfo = `每${waterReminderSettings.interval}分钟`;
+ Alert.alert(
+ '设置成功',
+ `喝水提醒已开启\n\n时间段:${timeInfo}\n提醒间隔:${intervalInfo}\n\n我们将在指定时间段内定期提醒您喝水`,
+ [{ text: '确定', onPress: () => router.back() }]
+ );
+ } else {
+ Alert.alert('设置成功', '喝水提醒已关闭', [{ text: '确定', onPress: () => router.back() }]);
+ }
+ } catch (error) {
+ console.error('保存喝水提醒设置失败:', error);
+ Alert.alert('保存失败', '无法保存喝水提醒设置,请重试');
+ }
+ };
+
+ // 加载用户偏好设置
+ useEffect(() => {
+ const loadUserPreferences = async () => {
+ try {
+ // 加载喝水提醒设置
+ const reminderSettings = await getWaterReminderSettings();
+ setWaterReminderSettings(reminderSettings);
+
+ // 初始化时间选择器临时值
+ const startHour = parseInt(reminderSettings.startTime.split(':')[0]);
+ const endHour = parseInt(reminderSettings.endTime.split(':')[0]);
+ setTempStartHour(startHour);
+ setTempEndHour(endHour);
+ } catch (error) {
+ console.error('加载用户偏好设置失败:', error);
+ }
+ };
+
+ loadUserPreferences();
+ }, []);
+
+ return (
+
+ {/* 背景渐变 */}
+
+
+ {/* 装饰性圆圈 */}
+
+
+
+ {
+ router.back();
+ }}
+ />
+
+
+
+ {/* 开启/关闭提醒 */}
+
+
+
+
+ 推送提醒
+
+ setWaterReminderSettings(prev => ({ ...prev, enabled }))}
+ trackColor={{ false: '#E5E5E5', true: '#3498DB' }}
+ thumbColor={waterReminderSettings.enabled ? '#FFFFFF' : '#FFFFFF'}
+ />
+
+
+ 开启后将在指定时间段内定期推送喝水提醒
+
+
+
+ {/* 时间段设置 */}
+ {waterReminderSettings.enabled && (
+ <>
+
+ 提醒时间段
+
+ 只在指定时间段内发送提醒,避免打扰您的休息
+
+
+
+ {/* 开始时间 */}
+
+ 开始时间
+
+ {waterReminderSettings.startTime}
+
+
+
+
+ {/* 结束时间 */}
+
+ 结束时间
+
+ {waterReminderSettings.endTime}
+
+
+
+
+
+
+ {/* 提醒间隔设置 */}
+
+ 提醒间隔
+
+ 选择提醒的频率,建议30-120分钟为宜
+
+
+
+
+ setWaterReminderSettings(prev => ({ ...prev, interval }))}
+ style={styles.intervalPicker}
+ >
+ {[30, 45, 60, 90, 120, 150, 180].map(interval => (
+
+ ))}
+
+
+
+
+ >
+ )}
+
+ {/* 保存按钮 */}
+
+
+ 保存设置
+
+
+
+
+
+ {/* 开始时间选择器弹窗 */}
+ setStartTimePickerVisible(false)}
+ >
+ setStartTimePickerVisible(false)} />
+
+
+ 选择开始时间
+
+
+
+ 小时
+
+ setTempStartHour(hour)}
+ style={styles.hourPicker}
+ >
+ {Array.from({ length: 24 }, (_, i) => (
+
+ ))}
+
+
+
+
+
+ 时间段预览
+
+ {String(tempStartHour).padStart(2, '0')}:00 - {waterReminderSettings.endTime}
+
+ {!isValidTimeRange(`${String(tempStartHour).padStart(2, '0')}:00`, waterReminderSettings.endTime) && (
+ ⚠️ 开始时间不能晚于或等于结束时间
+ )}
+
+
+
+
+ setStartTimePickerVisible(false)}
+ style={[styles.modalBtn, { backgroundColor: 'white' }]}
+ >
+ 取消
+
+
+ 确定
+
+
+
+
+
+ {/* 结束时间选择器弹窗 */}
+ setEndTimePickerVisible(false)}
+ >
+ setEndTimePickerVisible(false)} />
+
+
+ 选择结束时间
+
+
+
+ 小时
+
+ setTempEndHour(hour)}
+ style={styles.hourPicker}
+ >
+ {Array.from({ length: 24 }, (_, i) => (
+
+ ))}
+
+
+
+
+
+ 时间段预览
+
+ {waterReminderSettings.startTime} - {String(tempEndHour).padStart(2, '0')}:00
+
+ {!isValidTimeRange(waterReminderSettings.startTime, `${String(tempEndHour).padStart(2, '0')}:00`) && (
+ ⚠️ 结束时间不能早于或等于开始时间
+ )}
+
+
+
+
+ setEndTimePickerVisible(false)}
+ style={[styles.modalBtn, { backgroundColor: 'white' }]}
+ >
+ 取消
+
+
+ 确定
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ gradientBackground: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ },
+ decorativeCircle1: {
+ position: 'absolute',
+ top: 40,
+ right: 20,
+ width: 60,
+ height: 60,
+ borderRadius: 30,
+ backgroundColor: '#0EA5E9',
+ opacity: 0.1,
+ },
+ decorativeCircle2: {
+ position: 'absolute',
+ bottom: -15,
+ left: -15,
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: '#0EA5E9',
+ opacity: 0.05,
+ },
+ keyboardAvoidingView: {
+ flex: 1,
+ },
+ scrollView: {
+ flex: 1,
+ },
+ scrollContent: {
+ padding: 20,
+ },
+ waterReminderSection: {
+ marginBottom: 32,
+ },
+ waterReminderSectionHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginBottom: 8,
+ },
+ waterReminderSectionTitleContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ waterReminderSectionTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ },
+ waterReminderSectionDesc: {
+ fontSize: 14,
+ lineHeight: 20,
+ marginTop: 4,
+ },
+ timeRangeContainer: {
+ flexDirection: 'row',
+ gap: 16,
+ marginTop: 16,
+ },
+ timePickerContainer: {
+ flex: 1,
+ },
+ timeLabel: {
+ fontSize: 14,
+ fontWeight: '500',
+ marginBottom: 8,
+ },
+ timePicker: {
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 8,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 8,
+ },
+ timePickerText: {
+ fontSize: 16,
+ fontWeight: '500',
+ },
+ timePickerIcon: {
+ opacity: 0.6,
+ },
+ intervalContainer: {
+ marginTop: 16,
+ },
+ intervalPickerContainer: {
+ backgroundColor: 'white',
+ borderRadius: 8,
+ overflow: 'hidden',
+ },
+ intervalPicker: {
+ height: 200,
+ },
+ saveButtonContainer: {
+ marginTop: 20,
+ marginBottom: 40,
+ },
+ saveButton: {
+ paddingVertical: 16,
+ borderRadius: 12,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ saveButtonText: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ modalBackdrop: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0,0,0,0.4)',
+ },
+ timePickerModalSheet: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 0,
+ padding: 16,
+ backgroundColor: '#FFFFFF',
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ maxHeight: '60%',
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: -2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 16,
+ },
+ modalHandle: {
+ width: 36,
+ height: 4,
+ backgroundColor: '#E0E0E0',
+ borderRadius: 2,
+ alignSelf: 'center',
+ marginBottom: 20,
+ },
+ modalTitle: {
+ fontSize: 20,
+ fontWeight: '600',
+ textAlign: 'center',
+ marginBottom: 20,
+ },
+ timePickerContent: {
+ flex: 1,
+ marginBottom: 20,
+ },
+ timePickerSection: {
+ marginBottom: 20,
+ },
+ timePickerLabel: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 12,
+ textAlign: 'center',
+ },
+ hourPickerContainer: {
+ backgroundColor: '#F8F9FA',
+ borderRadius: 8,
+ overflow: 'hidden',
+ },
+ hourPicker: {
+ height: 160,
+ },
+ timeRangePreview: {
+ backgroundColor: '#F0F8FF',
+ borderRadius: 8,
+ padding: 16,
+ marginTop: 16,
+ alignItems: 'center',
+ },
+ timeRangePreviewLabel: {
+ fontSize: 12,
+ fontWeight: '500',
+ marginBottom: 4,
+ },
+ timeRangePreviewText: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginBottom: 8,
+ },
+ timeRangeWarning: {
+ fontSize: 12,
+ color: '#FF6B6B',
+ textAlign: 'center',
+ lineHeight: 18,
+ },
+ modalActions: {
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ gap: 12,
+ },
+ modalBtn: {
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ borderRadius: 10,
+ minWidth: 80,
+ alignItems: 'center',
+ },
+ modalBtnPrimary: {
+ // backgroundColor will be set dynamically
+ },
+ modalBtnText: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ modalBtnTextPrimary: {
+ // color will be set dynamically
+ },
+});
+
+export default WaterReminderSettings;
\ No newline at end of file
diff --git a/app/water/settings.tsx b/app/water/settings.tsx
new file mode 100644
index 0000000..ebaa889
--- /dev/null
+++ b/app/water/settings.tsx
@@ -0,0 +1,585 @@
+import { Colors } from '@/constants/Colors';
+import { useColorScheme } from '@/hooks/useColorScheme';
+import { getQuickWaterAmount, getWaterReminderSettings, setQuickWaterAmount } from '@/utils/userPreferences';
+import { Ionicons } from '@expo/vector-icons';
+import { Picker } from '@react-native-picker/picker';
+import { LinearGradient } from 'expo-linear-gradient';
+import { router } from 'expo-router';
+import React, { useCallback, useEffect, useState } from 'react';
+import { useFocusEffect } from '@react-navigation/native';
+import {
+ Alert,
+ KeyboardAvoidingView,
+ Modal,
+ Platform,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Switch,
+ Text,
+ TouchableOpacity,
+ View
+} from 'react-native';
+
+import { HeaderBar } from '@/components/ui/HeaderBar';
+
+const WaterSettings: React.FC = () => {
+ const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
+ const colorTokens = Colors[theme];
+
+ const [quickAddAmount, setQuickAddAmount] = useState('250');
+
+ // 喝水提醒设置状态(用于显示当前设置)
+ const [waterReminderSettings, setWaterReminderSettings] = useState({
+ enabled: false,
+ startTime: '08:00',
+ endTime: '22:00',
+ interval: 60,
+ });
+
+ // 弹窗状态
+ const [goalModalVisible, setGoalModalVisible] = useState(false);
+ const [quickAddModalVisible, setQuickAddModalVisible] = useState(false);
+
+ // 临时选中值
+ const [tempGoal, setTempGoal] = useState(2000);
+ const [tempQuickAdd, setTempQuickAdd] = useState(250);
+
+
+ // 当前饮水目标(从本地存储获取)
+ const [currentWaterGoal, setCurrentWaterGoal] = useState(2000);
+
+ // 打开饮水目标弹窗时初始化临时值
+ const openGoalModal = () => {
+ setTempGoal(currentWaterGoal);
+ setGoalModalVisible(true);
+ };
+
+ // 打开快速添加弹窗时初始化临时值
+ const openQuickAddModal = () => {
+ setTempQuickAdd(parseInt(quickAddAmount));
+ setQuickAddModalVisible(true);
+ };
+
+ // 打开喝水提醒页面
+ const openWaterReminderSettings = () => {
+ router.push('/water/reminder-settings');
+ };
+
+
+ // 处理饮水目标确认
+ const handleGoalConfirm = async () => {
+ setCurrentWaterGoal(tempGoal);
+ setGoalModalVisible(false);
+
+ // 这里可以添加保存到本地存储或发送到后端的逻辑
+ Alert.alert('设置成功', `每日饮水目标已设置为 ${tempGoal}ml`);
+ };
+
+ // 处理快速添加默认值确认
+ const handleQuickAddConfirm = async () => {
+ setQuickAddAmount(tempQuickAdd.toString());
+ setQuickAddModalVisible(false);
+
+ try {
+ await setQuickWaterAmount(tempQuickAdd);
+ Alert.alert('设置成功', `快速添加默认值已设置为 ${tempQuickAdd}ml`);
+ } catch {
+ Alert.alert('设置失败', '无法保存快速添加默认值,请重试');
+ }
+ };
+
+ // 加载用户偏好设置
+ const loadUserPreferences = useCallback(async () => {
+ try {
+ const amount = await getQuickWaterAmount();
+ setQuickAddAmount(amount.toString());
+ setTempQuickAdd(amount);
+
+ // 加载喝水提醒设置来显示当前设置状态
+ const reminderSettings = await getWaterReminderSettings();
+ setWaterReminderSettings(reminderSettings);
+ } catch (error) {
+ console.error('加载用户偏好设置失败:', error);
+ }
+ }, []);
+
+ // 初始化加载
+ useEffect(() => {
+ loadUserPreferences();
+ }, [loadUserPreferences]);
+
+ // 页面聚焦时重新加载设置(从提醒设置页面返回时)
+ useFocusEffect(
+ useCallback(() => {
+ loadUserPreferences();
+ }, [loadUserPreferences])
+ );
+
+ return (
+
+ {/* 背景渐变 */}
+
+
+ {/* 装饰性圆圈 */}
+
+
+
+ {
+ router.back();
+ }}
+ />
+
+
+
+ {/* 设置列表 */}
+
+
+
+
+
+
+
+
+ 每日饮水目标
+ {currentWaterGoal}ml
+
+
+
+
+
+
+
+
+
+
+
+ 快速添加默认值
+
+ 设置点击"+"按钮时添加的默认饮水量
+
+ {quickAddAmount}ml
+
+
+
+
+
+
+
+
+
+
+
+ 喝水提醒
+
+ 设置定时提醒您补充水分
+
+
+ {waterReminderSettings.enabled ? `${waterReminderSettings.startTime}-${waterReminderSettings.endTime}, 每${waterReminderSettings.interval}分钟` : '已关闭'}
+
+
+
+
+
+
+
+
+
+
+ {/* 饮水目标编辑弹窗 */}
+ setGoalModalVisible(false)}
+ >
+ setGoalModalVisible(false)} />
+
+
+ 每日饮水目标
+
+ setTempGoal(value)}
+ style={styles.picker}
+ >
+ {Array.from({ length: 96 }, (_, i) => 500 + i * 100).map(goal => (
+
+ ))}
+
+
+
+ setGoalModalVisible(false)}
+ style={[styles.modalBtn, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}
+ >
+ 取消
+
+
+ 确定
+
+
+
+
+
+ {/* 快速添加默认值编辑弹窗 */}
+ setQuickAddModalVisible(false)}
+ >
+ setQuickAddModalVisible(false)} />
+
+
+ 快速添加默认值
+
+ setTempQuickAdd(value)}
+ style={styles.picker}
+ >
+ {Array.from({ length: 41 }, (_, i) => 50 + i * 10).map(amount => (
+
+ ))}
+
+
+
+ setQuickAddModalVisible(false)}
+ style={[styles.modalBtn, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}
+ >
+ 取消
+
+
+ 确定
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ gradientBackground: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ },
+ decorativeCircle1: {
+ position: 'absolute',
+ top: 40,
+ right: 20,
+ width: 60,
+ height: 60,
+ borderRadius: 30,
+ backgroundColor: '#0EA5E9',
+ opacity: 0.1,
+ },
+ decorativeCircle2: {
+ position: 'absolute',
+ bottom: -15,
+ left: -15,
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: '#0EA5E9',
+ opacity: 0.05,
+ },
+ keyboardAvoidingView: {
+ flex: 1,
+ },
+ scrollView: {
+ flex: 1,
+ },
+ scrollContent: {
+ padding: 20,
+ },
+ section: {
+ marginBottom: 32,
+ },
+ settingsMenuContainer: {
+ backgroundColor: '#FFFFFF',
+ borderRadius: 12,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.05,
+ shadowRadius: 4,
+ elevation: 2,
+ overflow: 'hidden',
+ },
+ settingsMenuItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: 16,
+ paddingHorizontal: 16,
+ borderBottomWidth: 1,
+ borderBottomColor: '#F1F3F4',
+ },
+ settingsMenuItemLeft: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1,
+ },
+ settingsIconContainer: {
+ width: 32,
+ height: 32,
+ borderRadius: 6,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginRight: 12,
+ },
+ settingsMenuItemContent: {
+ flex: 1,
+ },
+ settingsMenuItemTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 4,
+ },
+ settingsMenuItemSubtitle: {
+ fontSize: 13,
+ marginBottom: 6,
+ },
+ settingsMenuItemValue: {
+ fontSize: 14,
+ fontWeight: '500',
+ },
+ modalBackdrop: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0,0,0,0.4)',
+ },
+ modalSheet: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 0,
+ padding: 16,
+ backgroundColor: '#FFFFFF',
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: -2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 16,
+ },
+ modalHandle: {
+ width: 36,
+ height: 4,
+ backgroundColor: '#E0E0E0',
+ borderRadius: 2,
+ alignSelf: 'center',
+ marginBottom: 20,
+ },
+ modalTitle: {
+ fontSize: 20,
+ fontWeight: '600',
+ textAlign: 'center',
+ marginBottom: 20,
+ },
+ pickerContainer: {
+ height: 200,
+ marginBottom: 20,
+ },
+ picker: {
+ height: 200,
+ },
+ modalActions: {
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ gap: 12,
+ },
+ modalBtn: {
+ paddingHorizontal: 14,
+ paddingVertical: 10,
+ borderRadius: 10,
+ minWidth: 80,
+ alignItems: 'center',
+ },
+ modalBtnPrimary: {
+ // backgroundColor will be set dynamically
+ },
+ modalBtnText: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ modalBtnTextPrimary: {
+ // color will be set dynamically
+ },
+ // 喝水提醒配置弹窗样式
+ waterReminderModalSheet: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 0,
+ padding: 16,
+ backgroundColor: '#FFFFFF',
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ maxHeight: '80%',
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: -2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 16,
+ },
+ waterReminderContent: {
+ flex: 1,
+ marginBottom: 20,
+ },
+ waterReminderSection: {
+ marginBottom: 24,
+ },
+ waterReminderSectionHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginBottom: 8,
+ },
+ waterReminderSectionTitleContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ },
+ waterReminderSectionTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ waterReminderSectionDesc: {
+ fontSize: 14,
+ lineHeight: 20,
+ marginTop: 4,
+ },
+ timeRangeContainer: {
+ flexDirection: 'row',
+ gap: 16,
+ marginTop: 16,
+ },
+ timePickerContainer: {
+ flex: 1,
+ },
+ timeLabel: {
+ fontSize: 14,
+ fontWeight: '500',
+ marginBottom: 8,
+ },
+ timePicker: {
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 8,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 8,
+ },
+ timePickerText: {
+ fontSize: 16,
+ fontWeight: '500',
+ },
+ timePickerIcon: {
+ opacity: 0.6,
+ },
+ intervalContainer: {
+ marginTop: 16,
+ },
+ intervalPickerContainer: {
+ backgroundColor: '#F8F9FA',
+ borderRadius: 8,
+ overflow: 'hidden',
+ },
+ intervalPicker: {
+ height: 120,
+ },
+ // 时间选择器弹窗样式
+ timePickerModalSheet: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ bottom: 0,
+ padding: 16,
+ backgroundColor: '#FFFFFF',
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ maxHeight: '60%',
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: -2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 8,
+ elevation: 16,
+ },
+ timePickerContent: {
+ flex: 1,
+ marginBottom: 20,
+ },
+ timePickerSection: {
+ marginBottom: 20,
+ },
+ timePickerLabel: {
+ fontSize: 16,
+ fontWeight: '600',
+ marginBottom: 12,
+ textAlign: 'center',
+ },
+ hourPickerContainer: {
+ backgroundColor: '#F8F9FA',
+ borderRadius: 8,
+ overflow: 'hidden',
+ },
+ hourPicker: {
+ height: 160,
+ },
+ timeRangePreview: {
+ backgroundColor: '#F0F8FF',
+ borderRadius: 8,
+ padding: 16,
+ marginTop: 16,
+ alignItems: 'center',
+ },
+ timeRangePreviewLabel: {
+ fontSize: 12,
+ fontWeight: '500',
+ marginBottom: 4,
+ },
+ timeRangePreviewText: {
+ fontSize: 18,
+ fontWeight: '600',
+ marginBottom: 8,
+ },
+ timeRangeWarning: {
+ fontSize: 12,
+ color: '#FF6B6B',
+ textAlign: 'center',
+ lineHeight: 18,
+ },
+});
+
+export default WaterSettings;
\ No newline at end of file
diff --git a/components/WaterIntakeCard.tsx b/components/WaterIntakeCard.tsx
index 22b969d..7a4d320 100644
--- a/components/WaterIntakeCard.tsx
+++ b/components/WaterIntakeCard.tsx
@@ -27,7 +27,7 @@ const WaterIntakeCard: React.FC = ({
selectedDate
}) => {
const router = useRouter();
- const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord } = useWaterDataByDate(selectedDate);
+ const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord, getWaterRecordsByDate } = useWaterDataByDate(selectedDate);
const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载
// 计算当前饮水量和目标
@@ -76,21 +76,25 @@ const WaterIntakeCard: React.FC = ({
// 判断是否是今天
const isToday = selectedDate === dayjs().format('YYYY-MM-DD') || !selectedDate;
- // 加载用户偏好的快速添加饮水默认值
+ // 页面聚焦时重新加载数据
useFocusEffect(
useCallback(() => {
- const loadQuickWaterAmount = async () => {
+ 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('加载快速添加饮水默认值失败:', error);
- // 保持默认值 250ml
+ console.error('页面聚焦时加载数据失败:', error);
}
};
- loadQuickWaterAmount();
- }, [])
+ loadDataOnFocus();
+ }, [selectedDate, getWaterRecordsByDate])
);
// 触发柱体动画
@@ -135,16 +139,16 @@ const WaterIntakeCard: React.FC = ({
await addWaterRecord(waterAmount, recordedAt);
};
- // 处理卡片点击 - 跳转到饮水设置页面
+ // 处理卡片点击 - 跳转到饮水详情页面
const handleCardPress = async () => {
// 触发震动反馈
if (process.env.EXPO_OS === 'ios') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
- // 跳转到饮水设置页面,传递选中的日期参数
+ // 跳转到饮水详情页面,传递选中的日期参数
router.push({
- pathname: '/water-settings',
+ pathname: '/water/detail',
params: selectedDate ? { selectedDate } : undefined
});
};
diff --git a/constants/Routes.ts b/constants/Routes.ts
index 7610d17..c4b608b 100644
--- a/constants/Routes.ts
+++ b/constants/Routes.ts
@@ -47,6 +47,11 @@ export const ROUTES = {
SLEEP_DETAIL: '/sleep-detail',
BASAL_METABOLISM_DETAIL: '/basal-metabolism-detail',
+ // 饮水相关路由
+ WATER_DETAIL: '/water/detail',
+ WATER_SETTINGS: '/water/settings',
+ WATER_REMINDER_SETTINGS: '/water/reminder-settings',
+
// 任务相关路由
TASK_DETAIL: '/task-detail',
diff --git a/hooks/useWaterData.ts b/hooks/useWaterData.ts
index 8077ec3..db753b1 100644
--- a/hooks/useWaterData.ts
+++ b/hooks/useWaterData.ts
@@ -1,4 +1,5 @@
import { deleteWaterIntakeFromHealthKit, getWaterIntakeFromHealthKit, saveWaterIntakeToHealthKit } from '@/utils/health';
+import { logger } from '@/utils/logger';
import { Toast } from '@/utils/toast.utils';
import { getQuickWaterAmount, getWaterGoalFromStorage, setWaterGoalToStorage } from '@/utils/userPreferences';
import { refreshWidget, syncWaterDataToWidget } from '@/utils/widgetDataSync';
@@ -495,18 +496,25 @@ export const useWaterDataByDate = (targetDate?: string) => {
setError(null);
try {
+ logger.debug('🚰 开始获取饮水记录,日期:', date);
const options = createDateRange(date);
+ logger.debug('🚰 查询选项:', options);
+
const healthKitRecords = await getWaterIntakeFromHealthKit(options);
+ logger.debug('🚰 从HealthKit获取到的原始数据:', healthKitRecords);
// 转换数据格式并按时间排序
const convertedRecords = healthKitRecords
.map(convertHealthKitToWaterRecord)
.sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());
+ logger.debug('🚰 转换后的记录:', convertedRecords);
+ logger.debug('🚰 记录数量:', convertedRecords.length);
+
setWaterRecords(convertedRecords);
return convertedRecords;
} catch (error) {
- console.error('获取饮水记录失败:', error);
+ console.error('🚰 获取饮水记录失败:', error);
setError('获取饮水记录失败');
Toast.error('获取饮水记录失败');
return [];
@@ -638,7 +646,11 @@ export const useWaterDataByDate = (targetDate?: string) => {
// 计算指定日期的统计数据
const waterStats = useMemo(() => {
+ logger.debug('🚰 计算waterStats - waterRecords:', waterRecords);
+ logger.debug('🚰 计算waterStats - dailyWaterGoal:', dailyWaterGoal);
+
if (!waterRecords || waterRecords.length === 0) {
+ logger.debug('🚰 没有饮水记录,返回默认值');
return {
totalAmount: 0,
completionRate: 0,
@@ -649,6 +661,10 @@ export const useWaterDataByDate = (targetDate?: string) => {
const totalAmount = waterRecords.reduce((total, record) => total + record.amount, 0);
const completionRate = dailyWaterGoal > 0 ? Math.min((totalAmount / dailyWaterGoal) * 100, 100) : 0;
+ logger.debug('🚰 计算结果 - totalAmount:', totalAmount);
+ logger.debug('🚰 计算结果 - completionRate:', completionRate);
+ logger.debug('🚰 计算结果 - recordCount:', waterRecords.length);
+
return {
totalAmount,
completionRate,
diff --git a/ios/OutLive/Info.plist b/ios/OutLive/Info.plist
index 2f26a49..656bebe 100644
--- a/ios/OutLive/Info.plist
+++ b/ios/OutLive/Info.plist
@@ -25,7 +25,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.0.13
+ 1.0.14
CFBundleSignature
????
CFBundleURLTypes
diff --git a/services/backgroundTaskManager.ts b/services/backgroundTaskManager.ts
index 7b25fb7..9bad20c 100644
--- a/services/backgroundTaskManager.ts
+++ b/services/backgroundTaskManager.ts
@@ -143,7 +143,7 @@ async function executeBackgroundTasks(): Promise {
// await sendTestNotification()
- // 执行喝水提醒检查任务
+ // 执行喝水提醒检查任务 - 已禁用,改为由用户手动在设置页面管理
await executeWaterReminderTask();
// 执行站立提醒检查任务
diff --git a/utils/notificationHelpers.ts b/utils/notificationHelpers.ts
index 44c4976..626d8e1 100644
--- a/utils/notificationHelpers.ts
+++ b/utils/notificationHelpers.ts
@@ -822,6 +822,102 @@ export class WaterNotificationHelpers {
}
}
+ /**
+ * 根据用户设置安排喝水提醒通知
+ * @param userName 用户名
+ * @param settings 喝水提醒设置
+ * @returns 通知ID数组
+ */
+ static async scheduleCustomWaterReminders(
+ userName: string,
+ settings: {
+ enabled: boolean;
+ startTime: string; // 格式: "HH:mm"
+ endTime: string; // 格式: "HH:mm"
+ interval: number; // 分钟
+ }
+ ): Promise {
+ try {
+ const notificationIds: string[] = [];
+
+ // 如果不启用提醒,先取消所有提醒
+ if (!settings.enabled) {
+ await this.cancelAllWaterReminders();
+ return notificationIds;
+ }
+
+ // 先取消现有的喝水提醒
+ await this.cancelAllWaterReminders();
+
+ // 解析开始和结束时间
+ const [startHour, startMinute] = settings.startTime.split(':').map(Number);
+ const [endHour, endMinute] = settings.endTime.split(':').map(Number);
+
+ // 计算一天内所有的提醒时间点
+ const reminderTimes: { hour: number; minute: number }[] = [];
+
+ // 创建开始时间的 Date 对象
+ let currentTime = new Date();
+ currentTime.setHours(startHour, startMinute, 0, 0);
+
+ // 创建结束时间的 Date 对象
+ let endTime = new Date();
+ endTime.setHours(endHour, endMinute, 0, 0);
+
+ // 如果结束时间小于开始时间,说明跨天了,结束时间设为第二天
+ if (endTime <= currentTime) {
+ endTime.setDate(endTime.getDate() + 1);
+ }
+
+ // 生成所有的提醒时间点
+ while (currentTime < endTime) {
+ reminderTimes.push({
+ hour: currentTime.getHours(),
+ minute: currentTime.getMinutes(),
+ });
+
+ // 增加间隔时间
+ currentTime.setTime(currentTime.getTime() + settings.interval * 60 * 1000);
+ }
+
+ console.log(`计算出${reminderTimes.length}个喝水提醒时间点:`, reminderTimes);
+
+ // 为每个时间点创建重复通知
+ for (const time of reminderTimes) {
+ const notificationId = await notificationService.scheduleCalendarRepeatingNotification(
+ {
+ title: '💧 喝水时间到啦!',
+ body: `${userName},记得补充水分哦~保持身体健康!`,
+ data: {
+ type: 'custom_water_reminder',
+ isCustomReminder: true,
+ reminderHour: time.hour,
+ reminderMinute: time.minute,
+ url: '/statistics'
+ },
+ sound: true,
+ priority: 'normal',
+ },
+ {
+ type: Notifications.SchedulableTriggerInputTypes.DAILY,
+ hour: time.hour,
+ minute: time.minute,
+ }
+ );
+
+ notificationIds.push(notificationId);
+ console.log(`已安排${time.hour}:${String(time.minute).padStart(2, '0')}的喝水提醒,通知ID: ${notificationId}`);
+ }
+
+ console.log(`自定义喝水提醒设置完成,共${notificationIds.length}个通知`);
+ return notificationIds;
+
+ } catch (error) {
+ console.error('设置自定义喝水提醒失败:', error);
+ throw error;
+ }
+ }
+
/**
* 取消所有喝水提醒
*/
@@ -831,7 +927,8 @@ export class WaterNotificationHelpers {
for (const notification of notifications) {
if (notification.content.data?.type === 'water_reminder' ||
- notification.content.data?.type === 'regular_water_reminder') {
+ notification.content.data?.type === 'regular_water_reminder' ||
+ notification.content.data?.type === 'custom_water_reminder') {
await notificationService.cancelNotification(notification.identifier);
console.log('已取消喝水提醒:', notification.identifier);
}
diff --git a/utils/userPreferences.ts b/utils/userPreferences.ts
index 504930f..b107229 100644
--- a/utils/userPreferences.ts
+++ b/utils/userPreferences.ts
@@ -7,6 +7,10 @@ const PREFERENCES_KEYS = {
NOTIFICATION_ENABLED: 'user_preference_notification_enabled',
FITNESS_EXERCISE_MINUTES_INFO_DISMISSED: 'user_preference_fitness_exercise_minutes_info_dismissed',
FITNESS_ACTIVE_HOURS_INFO_DISMISSED: 'user_preference_fitness_active_hours_info_dismissed',
+ WATER_REMINDER_ENABLED: 'user_preference_water_reminder_enabled',
+ WATER_REMINDER_START_TIME: 'user_preference_water_reminder_start_time',
+ WATER_REMINDER_END_TIME: 'user_preference_water_reminder_end_time',
+ WATER_REMINDER_INTERVAL: 'user_preference_water_reminder_interval',
} as const;
// 用户偏好设置接口
@@ -16,6 +20,10 @@ export interface UserPreferences {
notificationEnabled: boolean;
fitnessExerciseMinutesInfoDismissed: boolean;
fitnessActiveHoursInfoDismissed: boolean;
+ waterReminderEnabled: boolean;
+ waterReminderStartTime: string; // 格式: "08:00"
+ waterReminderEndTime: string; // 格式: "22:00"
+ waterReminderInterval: number; // 分钟
}
// 默认的用户偏好设置
@@ -25,6 +33,10 @@ const DEFAULT_PREFERENCES: UserPreferences = {
notificationEnabled: true, // 默认开启消息推送
fitnessExerciseMinutesInfoDismissed: false, // 默认显示锻炼分钟说明
fitnessActiveHoursInfoDismissed: false, // 默认显示活动小时说明
+ waterReminderEnabled: true, // 默认关闭喝水提醒
+ waterReminderStartTime: '08:00', // 默认开始时间早上8点
+ waterReminderEndTime: '22:00', // 默认结束时间晚上10点
+ waterReminderInterval: 60, // 默认提醒间隔60分钟
};
/**
@@ -37,6 +49,10 @@ export const getUserPreferences = async (): Promise => {
const notificationEnabled = await AsyncStorage.getItem(PREFERENCES_KEYS.NOTIFICATION_ENABLED);
const fitnessExerciseMinutesInfoDismissed = await AsyncStorage.getItem(PREFERENCES_KEYS.FITNESS_EXERCISE_MINUTES_INFO_DISMISSED);
const fitnessActiveHoursInfoDismissed = await AsyncStorage.getItem(PREFERENCES_KEYS.FITNESS_ACTIVE_HOURS_INFO_DISMISSED);
+ const waterReminderEnabled = await AsyncStorage.getItem(PREFERENCES_KEYS.WATER_REMINDER_ENABLED);
+ const waterReminderStartTime = await AsyncStorage.getItem(PREFERENCES_KEYS.WATER_REMINDER_START_TIME);
+ const waterReminderEndTime = await AsyncStorage.getItem(PREFERENCES_KEYS.WATER_REMINDER_END_TIME);
+ const waterReminderInterval = await AsyncStorage.getItem(PREFERENCES_KEYS.WATER_REMINDER_INTERVAL);
return {
quickWaterAmount: quickWaterAmount ? parseInt(quickWaterAmount, 10) : DEFAULT_PREFERENCES.quickWaterAmount,
@@ -44,6 +60,10 @@ export const getUserPreferences = async (): Promise => {
notificationEnabled: notificationEnabled !== null ? notificationEnabled === 'true' : DEFAULT_PREFERENCES.notificationEnabled,
fitnessExerciseMinutesInfoDismissed: fitnessExerciseMinutesInfoDismissed !== null ? fitnessExerciseMinutesInfoDismissed === 'true' : DEFAULT_PREFERENCES.fitnessExerciseMinutesInfoDismissed,
fitnessActiveHoursInfoDismissed: fitnessActiveHoursInfoDismissed !== null ? fitnessActiveHoursInfoDismissed === 'true' : DEFAULT_PREFERENCES.fitnessActiveHoursInfoDismissed,
+ waterReminderEnabled: waterReminderEnabled !== null ? waterReminderEnabled === 'true' : DEFAULT_PREFERENCES.waterReminderEnabled,
+ waterReminderStartTime: waterReminderStartTime || DEFAULT_PREFERENCES.waterReminderStartTime,
+ waterReminderEndTime: waterReminderEndTime || DEFAULT_PREFERENCES.waterReminderEndTime,
+ waterReminderInterval: waterReminderInterval ? parseInt(waterReminderInterval, 10) : DEFAULT_PREFERENCES.waterReminderInterval,
};
} catch (error) {
console.error('获取用户偏好设置失败:', error);
@@ -185,6 +205,163 @@ export const getFitnessActiveHoursInfoDismissed = async (): Promise =>
}
};
+/**
+ * 设置喝水提醒开关
+ * @param enabled 是否开启喝水提醒
+ */
+export const setWaterReminderEnabled = async (enabled: boolean): Promise => {
+ try {
+ await AsyncStorage.setItem(PREFERENCES_KEYS.WATER_REMINDER_ENABLED, enabled.toString());
+ } catch (error) {
+ console.error('设置喝水提醒开关失败:', error);
+ throw error;
+ }
+};
+
+/**
+ * 获取喝水提醒开关状态
+ */
+export const getWaterReminderEnabled = async (): Promise => {
+ try {
+ const enabled = await AsyncStorage.getItem(PREFERENCES_KEYS.WATER_REMINDER_ENABLED);
+ return enabled !== null ? enabled === 'true' : DEFAULT_PREFERENCES.waterReminderEnabled;
+ } catch (error) {
+ console.error('获取喝水提醒开关状态失败:', error);
+ return DEFAULT_PREFERENCES.waterReminderEnabled;
+ }
+};
+
+/**
+ * 设置喝水提醒开始时间
+ * @param startTime 开始时间,格式为 "HH:mm"
+ */
+export const setWaterReminderStartTime = async (startTime: string): Promise => {
+ try {
+ await AsyncStorage.setItem(PREFERENCES_KEYS.WATER_REMINDER_START_TIME, startTime);
+ } catch (error) {
+ console.error('设置喝水提醒开始时间失败:', error);
+ throw error;
+ }
+};
+
+/**
+ * 获取喝水提醒开始时间
+ */
+export const getWaterReminderStartTime = async (): Promise => {
+ try {
+ const startTime = await AsyncStorage.getItem(PREFERENCES_KEYS.WATER_REMINDER_START_TIME);
+ return startTime || DEFAULT_PREFERENCES.waterReminderStartTime;
+ } catch (error) {
+ console.error('获取喝水提醒开始时间失败:', error);
+ return DEFAULT_PREFERENCES.waterReminderStartTime;
+ }
+};
+
+/**
+ * 设置喝水提醒结束时间
+ * @param endTime 结束时间,格式为 "HH:mm"
+ */
+export const setWaterReminderEndTime = async (endTime: string): Promise => {
+ try {
+ await AsyncStorage.setItem(PREFERENCES_KEYS.WATER_REMINDER_END_TIME, endTime);
+ } catch (error) {
+ console.error('设置喝水提醒结束时间失败:', error);
+ throw error;
+ }
+};
+
+/**
+ * 获取喝水提醒结束时间
+ */
+export const getWaterReminderEndTime = async (): Promise => {
+ try {
+ const endTime = await AsyncStorage.getItem(PREFERENCES_KEYS.WATER_REMINDER_END_TIME);
+ return endTime || DEFAULT_PREFERENCES.waterReminderEndTime;
+ } catch (error) {
+ console.error('获取喝水提醒结束时间失败:', error);
+ return DEFAULT_PREFERENCES.waterReminderEndTime;
+ }
+};
+
+/**
+ * 设置喝水提醒间隔时间
+ * @param interval 间隔时间(分钟),范围 30-180
+ */
+export const setWaterReminderInterval = async (interval: number): Promise => {
+ try {
+ // 确保值在合理范围内(30-180分钟)
+ const validInterval = Math.max(30, Math.min(180, interval));
+ await AsyncStorage.setItem(PREFERENCES_KEYS.WATER_REMINDER_INTERVAL, validInterval.toString());
+ } catch (error) {
+ console.error('设置喝水提醒间隔时间失败:', error);
+ throw error;
+ }
+};
+
+/**
+ * 获取喝水提醒间隔时间
+ */
+export const getWaterReminderInterval = async (): Promise => {
+ try {
+ const interval = await AsyncStorage.getItem(PREFERENCES_KEYS.WATER_REMINDER_INTERVAL);
+ return interval ? parseInt(interval, 10) : DEFAULT_PREFERENCES.waterReminderInterval;
+ } catch (error) {
+ console.error('获取喝水提醒间隔时间失败:', error);
+ return DEFAULT_PREFERENCES.waterReminderInterval;
+ }
+};
+
+/**
+ * 获取完整的喝水提醒配置
+ */
+export const getWaterReminderSettings = async () => {
+ try {
+ const [enabled, startTime, endTime, interval] = await Promise.all([
+ getWaterReminderEnabled(),
+ getWaterReminderStartTime(),
+ getWaterReminderEndTime(),
+ getWaterReminderInterval(),
+ ]);
+
+ return {
+ enabled,
+ startTime,
+ endTime,
+ interval,
+ };
+ } catch (error) {
+ console.error('获取喝水提醒配置失败:', error);
+ return {
+ enabled: DEFAULT_PREFERENCES.waterReminderEnabled,
+ startTime: DEFAULT_PREFERENCES.waterReminderStartTime,
+ endTime: DEFAULT_PREFERENCES.waterReminderEndTime,
+ interval: DEFAULT_PREFERENCES.waterReminderInterval,
+ };
+ }
+};
+
+/**
+ * 批量设置喝水提醒配置
+ */
+export const setWaterReminderSettings = async (settings: {
+ enabled: boolean;
+ startTime: string;
+ endTime: string;
+ interval: number;
+}): Promise => {
+ try {
+ await Promise.all([
+ setWaterReminderEnabled(settings.enabled),
+ setWaterReminderStartTime(settings.startTime),
+ setWaterReminderEndTime(settings.endTime),
+ setWaterReminderInterval(settings.interval),
+ ]);
+ } catch (error) {
+ console.error('批量设置喝水提醒配置失败:', error);
+ throw error;
+ }
+};
+
/**
* 重置所有用户偏好设置为默认值
*/
@@ -194,6 +371,10 @@ export const resetUserPreferences = async (): Promise => {
await AsyncStorage.removeItem(PREFERENCES_KEYS.NOTIFICATION_ENABLED);
await AsyncStorage.removeItem(PREFERENCES_KEYS.FITNESS_EXERCISE_MINUTES_INFO_DISMISSED);
await AsyncStorage.removeItem(PREFERENCES_KEYS.FITNESS_ACTIVE_HOURS_INFO_DISMISSED);
+ await AsyncStorage.removeItem(PREFERENCES_KEYS.WATER_REMINDER_ENABLED);
+ await AsyncStorage.removeItem(PREFERENCES_KEYS.WATER_REMINDER_START_TIME);
+ await AsyncStorage.removeItem(PREFERENCES_KEYS.WATER_REMINDER_END_TIME);
+ await AsyncStorage.removeItem(PREFERENCES_KEYS.WATER_REMINDER_INTERVAL);
} catch (error) {
console.error('重置用户偏好设置失败:', error);
throw error;