feat(water): 重构饮水模块并新增自定义提醒设置功能
- 新增饮水详情页面 `/water/detail` 展示每日饮水记录与统计 - 新增饮水设置页面 `/water/settings` 支持目标与快速添加配置 - 新增喝水提醒设置页面 `/water/reminder-settings` 支持自定义时间段与间隔 - 重构 `useWaterData` Hook,支持按日期查询与实时刷新 - 新增 `WaterNotificationHelpers.scheduleCustomWaterReminders` 实现个性化提醒 - 优化心情编辑页键盘体验,新增 `KeyboardAvoidingView` 与滚动逻辑 - 升级版本号至 1.0.14 并补充路由常量 - 补充用户偏好存储字段 `waterReminderEnabled/startTime/endTime/interval` - 废弃后台定时任务中的旧版喝水提醒逻辑,改为用户手动管理
This commit is contained in:
2
app.json
2
app.json
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Out Live",
|
"name": "Out Live",
|
||||||
"slug": "digital-pilates",
|
"slug": "digital-pilates",
|
||||||
"version": "1.0.13",
|
"version": "1.0.14",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"scheme": "digitalpilates",
|
"scheme": "digitalpilates",
|
||||||
"userInterfaceStyle": "light",
|
"userInterfaceStyle": "light",
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
||||||
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||||||
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="water-detail" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="water-settings" options={{ headerShown: false }} />
|
<Stack.Screen name="water-settings" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="+not-found" />
|
<Stack.Screen name="+not-found" />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ import { Ionicons } from '@expo/vector-icons';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { router, useLocalSearchParams } from 'expo-router';
|
import { router, useLocalSearchParams } from 'expo-router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert, Image,
|
Alert, Image,
|
||||||
|
Keyboard,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
@@ -43,6 +46,9 @@ export default function MoodEditScreen() {
|
|||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [existingMood, setExistingMood] = useState<any>(null);
|
const [existingMood, setExistingMood] = useState<any>(null);
|
||||||
|
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
const textInputRef = useRef<TextInput>(null);
|
||||||
|
|
||||||
const moodOptions = getMoodOptions();
|
const moodOptions = getMoodOptions();
|
||||||
|
|
||||||
// 从 Redux 获取数据
|
// 从 Redux 获取数据
|
||||||
@@ -66,6 +72,25 @@ export default function MoodEditScreen() {
|
|||||||
}
|
}
|
||||||
}, [moodId, moodRecords]);
|
}, [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 () => {
|
const handleSave = async () => {
|
||||||
if (!selectedMood) {
|
if (!selectedMood) {
|
||||||
Alert.alert('提示', '请选择心情');
|
Alert.alert('提示', '请选择心情');
|
||||||
@@ -163,7 +188,18 @@ export default function MoodEditScreen() {
|
|||||||
tone="light"
|
tone="light"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ScrollView style={styles.content}>
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
style={styles.keyboardAvoidingView}
|
||||||
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
|
style={styles.content}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
{/* 日期显示 */}
|
{/* 日期显示 */}
|
||||||
<View style={styles.dateSection}>
|
<View style={styles.dateSection}>
|
||||||
<Text style={styles.dateTitle}>
|
<Text style={styles.dateTitle}>
|
||||||
@@ -211,6 +247,7 @@ export default function MoodEditScreen() {
|
|||||||
<Text style={styles.sectionTitle}>心情日记</Text>
|
<Text style={styles.sectionTitle}>心情日记</Text>
|
||||||
<Text style={styles.diarySubtitle}>记录你的心情,珍藏美好回忆</Text>
|
<Text style={styles.diarySubtitle}>记录你的心情,珍藏美好回忆</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
ref={textInputRef}
|
||||||
style={styles.descriptionInput}
|
style={styles.descriptionInput}
|
||||||
placeholder={`今天的心情如何?
|
placeholder={`今天的心情如何?
|
||||||
|
|
||||||
@@ -225,11 +262,18 @@ export default function MoodEditScreen() {
|
|||||||
multiline
|
multiline
|
||||||
maxLength={1000}
|
maxLength={1000}
|
||||||
textAlignVertical="top"
|
textAlignVertical="top"
|
||||||
|
onFocus={() => {
|
||||||
|
// 当文本输入框获得焦点时,滚动到输入框
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||||
|
}, 300);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.characterCount}>{description.length}/1000</Text>
|
<Text style={styles.characterCount}>{description.length}/1000</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
{/* 底部按钮 */}
|
{/* 底部按钮 */}
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
@@ -294,10 +338,15 @@ const styles = StyleSheet.create({
|
|||||||
safeArea: {
|
safeArea: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
keyboardAvoidingView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
scrollContent: {
|
||||||
|
paddingBottom: 100, // 为底部按钮留出空间
|
||||||
|
},
|
||||||
dateSection: {
|
dateSection: {
|
||||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
margin: 12,
|
margin: 12,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useWaterDataByDate } from '@/hooks/useWaterData';
|
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 { Ionicons } from '@expo/vector-icons';
|
||||||
import { Picker } from '@react-native-picker/picker';
|
import { Picker } from '@react-native-picker/picker';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
Pressable,
|
Pressable,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View
|
View
|
||||||
@@ -25,11 +27,11 @@ import { Swipeable } from 'react-native-gesture-handler';
|
|||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
interface WaterSettingsProps {
|
interface WaterDetailProps {
|
||||||
selectedDate?: string;
|
selectedDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
const WaterDetail: React.FC<WaterDetailProps> = () => {
|
||||||
const { selectedDate } = useLocalSearchParams<{ selectedDate?: string }>();
|
const { selectedDate } = useLocalSearchParams<{ selectedDate?: string }>();
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
@@ -37,14 +39,7 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
|||||||
const [dailyGoal, setDailyGoal] = useState<string>('2000');
|
const [dailyGoal, setDailyGoal] = useState<string>('2000');
|
||||||
const [quickAddAmount, setQuickAddAmount] = useState<string>('250');
|
const [quickAddAmount, setQuickAddAmount] = useState<string>('250');
|
||||||
|
|
||||||
// 设置弹窗状态
|
// Remove modal states as they are now in separate settings page
|
||||||
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
|
|
||||||
const [goalModalVisible, setGoalModalVisible] = useState(false);
|
|
||||||
const [quickAddModalVisible, setQuickAddModalVisible] = useState(false);
|
|
||||||
|
|
||||||
// 临时选中值
|
|
||||||
const [tempGoal, setTempGoal] = useState<number>(parseInt(dailyGoal));
|
|
||||||
const [tempQuickAdd, setTempQuickAdd] = useState<number>(parseInt(quickAddAmount));
|
|
||||||
|
|
||||||
// 使用新的 hook 来处理指定日期的饮水数据
|
// 使用新的 hook 来处理指定日期的饮水数据
|
||||||
const { waterRecords, dailyWaterGoal, updateWaterGoal, removeWaterRecord } = useWaterDataByDate(selectedDate);
|
const { waterRecords, dailyWaterGoal, updateWaterGoal, removeWaterRecord } = useWaterDataByDate(selectedDate);
|
||||||
@@ -52,51 +47,12 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 处理设置按钮点击
|
// 处理设置按钮点击 - 跳转到设置页面
|
||||||
const handleSettingsPress = () => {
|
const handleSettingsPress = () => {
|
||||||
setSettingsModalVisible(true);
|
router.push('/water/settings');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开饮水目标弹窗时初始化临时值
|
// Remove all modal-related functions as they are now in separate settings page
|
||||||
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('设置失败', '无法保存快速添加默认值,请重试');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// 删除饮水记录
|
// 删除饮水记录
|
||||||
@@ -123,15 +79,6 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
|||||||
loadUserPreferences();
|
loadUserPreferences();
|
||||||
}, [dailyWaterGoal]);
|
}, [dailyWaterGoal]);
|
||||||
|
|
||||||
// 当dailyGoal或quickAddAmount更新时,同步更新临时状态
|
|
||||||
useEffect(() => {
|
|
||||||
setTempGoal(parseInt(dailyGoal));
|
|
||||||
}, [dailyGoal]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTempQuickAdd(parseInt(quickAddAmount));
|
|
||||||
}, [quickAddAmount]);
|
|
||||||
|
|
||||||
// 新增:饮水记录卡片组件
|
// 新增:饮水记录卡片组件
|
||||||
const WaterRecordCard = ({ record, onDelete }: { record: any; onDelete: () => void }) => {
|
const WaterRecordCard = ({ record, onDelete }: { record: any; onDelete: () => void }) => {
|
||||||
const swipeableRef = React.useRef<Swipeable>(null);
|
const swipeableRef = React.useRef<Swipeable>(null);
|
||||||
@@ -225,7 +172,7 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
|||||||
<View style={styles.decorativeCircle2} />
|
<View style={styles.decorativeCircle2} />
|
||||||
|
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
title="饮水设置"
|
title="饮水详情"
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
// 这里会通过路由自动处理返回
|
// 这里会通过路由自动处理返回
|
||||||
router.back();
|
router.back();
|
||||||
@@ -288,129 +235,7 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
{/* 设置主弹窗 */}
|
{/* All modals have been moved to the separate water-settings page */}
|
||||||
<Modal
|
|
||||||
visible={settingsModalVisible}
|
|
||||||
transparent
|
|
||||||
animationType="fade"
|
|
||||||
onRequestClose={() => setSettingsModalVisible(false)}
|
|
||||||
>
|
|
||||||
<Pressable style={styles.modalBackdrop} onPress={() => setSettingsModalVisible(false)} />
|
|
||||||
<View style={styles.settingsModalSheet}>
|
|
||||||
<View style={styles.modalHandle} />
|
|
||||||
<Text style={[styles.settingsModalTitle, { color: colorTokens.text }]}>饮水设置</Text>
|
|
||||||
|
|
||||||
{/* 菜单项 */}
|
|
||||||
<View style={styles.settingsMenuContainer}>
|
|
||||||
<TouchableOpacity style={styles.settingsMenuItem} onPress={openGoalModal}>
|
|
||||||
<View style={styles.settingsMenuItemLeft}>
|
|
||||||
<View style={[styles.settingsIconContainer, { backgroundColor: 'rgba(147, 112, 219, 0.1)' }]}>
|
|
||||||
<Ionicons name="flag-outline" size={20} color="#9370DB" />
|
|
||||||
</View>
|
|
||||||
<View style={styles.settingsMenuItemContent}>
|
|
||||||
<Text style={[styles.settingsMenuItemTitle, { color: colorTokens.text }]}>每日饮水目标</Text>
|
|
||||||
<Text style={[styles.settingsMenuItemValue, { color: colorTokens.textSecondary }]}>{dailyWaterGoal || dailyGoal}ml</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<Ionicons name="chevron-forward" size={20} color="#CCCCCC" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity style={[styles.settingsMenuItem, { borderBottomWidth: 0 }]} onPress={openQuickAddModal}>
|
|
||||||
<View style={styles.settingsMenuItemLeft}>
|
|
||||||
<View style={[styles.settingsIconContainer, { backgroundColor: 'rgba(147, 112, 219, 0.1)' }]}>
|
|
||||||
<Ionicons name="add-outline" size={20} color="#9370DB" />
|
|
||||||
</View>
|
|
||||||
<View style={styles.settingsMenuItemContent}>
|
|
||||||
<Text style={[styles.settingsMenuItemTitle, { color: colorTokens.text }]}>快速添加默认值</Text>
|
|
||||||
<Text style={[styles.settingsMenuItemSubtitle, { color: colorTokens.textSecondary }]}>
|
|
||||||
设置点击"+"按钮时添加的默认饮水量
|
|
||||||
</Text>
|
|
||||||
<Text style={[styles.settingsMenuItemValue, { color: colorTokens.textSecondary }]}>{quickAddAmount}ml</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<Ionicons name="chevron-forward" size={20} color="#CCCCCC" />
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* 饮水目标编辑弹窗 */}
|
|
||||||
<Modal
|
|
||||||
visible={goalModalVisible}
|
|
||||||
transparent
|
|
||||||
animationType="fade"
|
|
||||||
onRequestClose={() => setGoalModalVisible(false)}
|
|
||||||
>
|
|
||||||
<Pressable style={styles.modalBackdrop} onPress={() => setGoalModalVisible(false)} />
|
|
||||||
<View style={styles.modalSheet}>
|
|
||||||
<View style={styles.modalHandle} />
|
|
||||||
<Text style={[styles.modalTitle, { color: colorTokens.text }]}>每日饮水目标</Text>
|
|
||||||
<View style={styles.pickerContainer}>
|
|
||||||
<Picker
|
|
||||||
selectedValue={tempGoal}
|
|
||||||
onValueChange={(value) => setTempGoal(value)}
|
|
||||||
style={styles.picker}
|
|
||||||
>
|
|
||||||
{Array.from({ length: 96 }, (_, i) => 500 + i * 100).map(goal => (
|
|
||||||
<Picker.Item key={goal} label={`${goal}ml`} value={goal} />
|
|
||||||
))}
|
|
||||||
</Picker>
|
|
||||||
</View>
|
|
||||||
<View style={styles.modalActions}>
|
|
||||||
<Pressable
|
|
||||||
onPress={() => setGoalModalVisible(false)}
|
|
||||||
style={[styles.modalBtn, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}
|
|
||||||
>
|
|
||||||
<Text style={[styles.modalBtnText, { color: colorTokens.text }]}>取消</Text>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
|
||||||
onPress={handleGoalConfirm}
|
|
||||||
style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colorTokens.primary }]}
|
|
||||||
>
|
|
||||||
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary, { color: colorTokens.onPrimary }]}>确定</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* 快速添加默认值编辑弹窗 */}
|
|
||||||
<Modal
|
|
||||||
visible={quickAddModalVisible}
|
|
||||||
transparent
|
|
||||||
animationType="fade"
|
|
||||||
onRequestClose={() => setQuickAddModalVisible(false)}
|
|
||||||
>
|
|
||||||
<Pressable style={styles.modalBackdrop} onPress={() => setQuickAddModalVisible(false)} />
|
|
||||||
<View style={styles.modalSheet}>
|
|
||||||
<View style={styles.modalHandle} />
|
|
||||||
<Text style={[styles.modalTitle, { color: colorTokens.text }]}>快速添加默认值</Text>
|
|
||||||
<View style={styles.pickerContainer}>
|
|
||||||
<Picker
|
|
||||||
selectedValue={tempQuickAdd}
|
|
||||||
onValueChange={(value) => setTempQuickAdd(value)}
|
|
||||||
style={styles.picker}
|
|
||||||
>
|
|
||||||
{Array.from({ length: 41 }, (_, i) => 50 + i * 10).map(amount => (
|
|
||||||
<Picker.Item key={amount} label={`${amount}ml`} value={amount} />
|
|
||||||
))}
|
|
||||||
</Picker>
|
|
||||||
</View>
|
|
||||||
<View style={styles.modalActions}>
|
|
||||||
<Pressable
|
|
||||||
onPress={() => setQuickAddModalVisible(false)}
|
|
||||||
style={[styles.modalBtn, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}
|
|
||||||
>
|
|
||||||
<Text style={[styles.modalBtnText, { color: colorTokens.text }]}>取消</Text>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
|
||||||
onPress={handleQuickAddConfirm}
|
|
||||||
style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colorTokens.primary }]}
|
|
||||||
>
|
|
||||||
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary, { color: colorTokens.onPrimary }]}>确定</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -724,6 +549,151 @@ const styles = StyleSheet.create({
|
|||||||
settingsMenuItemValue: {
|
settingsMenuItemValue: {
|
||||||
fontSize: 14,
|
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;
|
||||||
618
app/water/reminder-settings.tsx
Normal file
618
app/water/reminder-settings.tsx
Normal file
@@ -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 (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* 背景渐变 */}
|
||||||
|
<LinearGradient
|
||||||
|
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
|
||||||
|
style={styles.gradientBackground}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 0, y: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 装饰性圆圈 */}
|
||||||
|
<View style={styles.decorativeCircle1} />
|
||||||
|
<View style={styles.decorativeCircle2} />
|
||||||
|
|
||||||
|
<HeaderBar
|
||||||
|
title="喝水提醒"
|
||||||
|
onBack={() => {
|
||||||
|
router.back();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.keyboardAvoidingView}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
style={styles.scrollView}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{/* 开启/关闭提醒 */}
|
||||||
|
<View style={styles.waterReminderSection}>
|
||||||
|
<View style={styles.waterReminderSectionHeader}>
|
||||||
|
<View style={styles.waterReminderSectionTitleContainer}>
|
||||||
|
<Ionicons name="notifications-outline" size={20} color={colorTokens.text} />
|
||||||
|
<Text style={[styles.waterReminderSectionTitle, { color: colorTokens.text }]}>推送提醒</Text>
|
||||||
|
</View>
|
||||||
|
<Switch
|
||||||
|
value={waterReminderSettings.enabled}
|
||||||
|
onValueChange={(enabled) => setWaterReminderSettings(prev => ({ ...prev, enabled }))}
|
||||||
|
trackColor={{ false: '#E5E5E5', true: '#3498DB' }}
|
||||||
|
thumbColor={waterReminderSettings.enabled ? '#FFFFFF' : '#FFFFFF'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={[styles.waterReminderSectionDesc, { color: colorTokens.textSecondary }]}>
|
||||||
|
开启后将在指定时间段内定期推送喝水提醒
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 时间段设置 */}
|
||||||
|
{waterReminderSettings.enabled && (
|
||||||
|
<>
|
||||||
|
<View style={styles.waterReminderSection}>
|
||||||
|
<Text style={[styles.waterReminderSectionTitle, { color: colorTokens.text }]}>提醒时间段</Text>
|
||||||
|
<Text style={[styles.waterReminderSectionDesc, { color: colorTokens.textSecondary }]}>
|
||||||
|
只在指定时间段内发送提醒,避免打扰您的休息
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={styles.timeRangeContainer}>
|
||||||
|
{/* 开始时间 */}
|
||||||
|
<View style={styles.timePickerContainer}>
|
||||||
|
<Text style={[styles.timeLabel, { color: colorTokens.text }]}>开始时间</Text>
|
||||||
|
<Pressable
|
||||||
|
style={[styles.timePicker, { backgroundColor: 'white' }]}
|
||||||
|
onPress={openStartTimePicker}
|
||||||
|
>
|
||||||
|
<Text style={[styles.timePickerText, { color: colorTokens.text }]}>{waterReminderSettings.startTime}</Text>
|
||||||
|
<Ionicons name="chevron-down" size={16} color={colorTokens.textSecondary} style={styles.timePickerIcon} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 结束时间 */}
|
||||||
|
<View style={styles.timePickerContainer}>
|
||||||
|
<Text style={[styles.timeLabel, { color: colorTokens.text }]}>结束时间</Text>
|
||||||
|
<Pressable
|
||||||
|
style={[styles.timePicker, { backgroundColor: 'white' }]}
|
||||||
|
onPress={openEndTimePicker}
|
||||||
|
>
|
||||||
|
<Text style={[styles.timePickerText, { color: colorTokens.text }]}>{waterReminderSettings.endTime}</Text>
|
||||||
|
<Ionicons name="chevron-down" size={16} color={colorTokens.textSecondary} style={styles.timePickerIcon} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 提醒间隔设置 */}
|
||||||
|
<View style={styles.waterReminderSection}>
|
||||||
|
<Text style={[styles.waterReminderSectionTitle, { color: colorTokens.text }]}>提醒间隔</Text>
|
||||||
|
<Text style={[styles.waterReminderSectionDesc, { color: colorTokens.textSecondary }]}>
|
||||||
|
选择提醒的频率,建议30-120分钟为宜
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={styles.intervalContainer}>
|
||||||
|
<View style={styles.intervalPickerContainer}>
|
||||||
|
<Picker
|
||||||
|
selectedValue={waterReminderSettings.interval}
|
||||||
|
onValueChange={(interval) => setWaterReminderSettings(prev => ({ ...prev, interval }))}
|
||||||
|
style={styles.intervalPicker}
|
||||||
|
>
|
||||||
|
{[30, 45, 60, 90, 120, 150, 180].map(interval => (
|
||||||
|
<Picker.Item key={interval} label={`${interval}分钟`} value={interval} />
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 保存按钮 */}
|
||||||
|
<View style={styles.saveButtonContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.saveButton, { backgroundColor: colorTokens.primary }]}
|
||||||
|
onPress={handleWaterReminderSave}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={[styles.saveButtonText, { color: colorTokens.onPrimary }]}>保存设置</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
|
{/* 开始时间选择器弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={startTimePickerVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setStartTimePickerVisible(false)}
|
||||||
|
>
|
||||||
|
<Pressable style={styles.modalBackdrop} onPress={() => setStartTimePickerVisible(false)} />
|
||||||
|
<View style={styles.timePickerModalSheet}>
|
||||||
|
<View style={styles.modalHandle} />
|
||||||
|
<Text style={[styles.modalTitle, { color: colorTokens.text }]}>选择开始时间</Text>
|
||||||
|
|
||||||
|
<View style={styles.timePickerContent}>
|
||||||
|
<View style={styles.timePickerSection}>
|
||||||
|
<Text style={[styles.timePickerLabel, { color: colorTokens.text }]}>小时</Text>
|
||||||
|
<View style={styles.hourPickerContainer}>
|
||||||
|
<Picker
|
||||||
|
selectedValue={tempStartHour}
|
||||||
|
onValueChange={(hour) => setTempStartHour(hour)}
|
||||||
|
style={styles.hourPicker}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 24 }, (_, i) => (
|
||||||
|
<Picker.Item key={i} label={`${String(i).padStart(2, '0')}:00`} value={i} />
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.timeRangePreview}>
|
||||||
|
<Text style={[styles.timeRangePreviewLabel, { color: colorTokens.textSecondary }]}>时间段预览</Text>
|
||||||
|
<Text style={[styles.timeRangePreviewText, { color: colorTokens.text }]}>
|
||||||
|
{String(tempStartHour).padStart(2, '0')}:00 - {waterReminderSettings.endTime}
|
||||||
|
</Text>
|
||||||
|
{!isValidTimeRange(`${String(tempStartHour).padStart(2, '0')}:00`, waterReminderSettings.endTime) && (
|
||||||
|
<Text style={styles.timeRangeWarning}>⚠️ 开始时间不能晚于或等于结束时间</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setStartTimePickerVisible(false)}
|
||||||
|
style={[styles.modalBtn, { backgroundColor: 'white' }]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.modalBtnText, { color: colorTokens.text }]}>取消</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={confirmStartTime}
|
||||||
|
style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colorTokens.primary }]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary, { color: colorTokens.onPrimary }]}>确定</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 结束时间选择器弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={endTimePickerVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setEndTimePickerVisible(false)}
|
||||||
|
>
|
||||||
|
<Pressable style={styles.modalBackdrop} onPress={() => setEndTimePickerVisible(false)} />
|
||||||
|
<View style={styles.timePickerModalSheet}>
|
||||||
|
<View style={styles.modalHandle} />
|
||||||
|
<Text style={[styles.modalTitle, { color: colorTokens.text }]}>选择结束时间</Text>
|
||||||
|
|
||||||
|
<View style={styles.timePickerContent}>
|
||||||
|
<View style={styles.timePickerSection}>
|
||||||
|
<Text style={[styles.timePickerLabel, { color: colorTokens.text }]}>小时</Text>
|
||||||
|
<View style={styles.hourPickerContainer}>
|
||||||
|
<Picker
|
||||||
|
selectedValue={tempEndHour}
|
||||||
|
onValueChange={(hour) => setTempEndHour(hour)}
|
||||||
|
style={styles.hourPicker}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 24 }, (_, i) => (
|
||||||
|
<Picker.Item key={i} label={`${String(i).padStart(2, '0')}:00`} value={i} />
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.timeRangePreview}>
|
||||||
|
<Text style={[styles.timeRangePreviewLabel, { color: colorTokens.textSecondary }]}>时间段预览</Text>
|
||||||
|
<Text style={[styles.timeRangePreviewText, { color: colorTokens.text }]}>
|
||||||
|
{waterReminderSettings.startTime} - {String(tempEndHour).padStart(2, '0')}:00
|
||||||
|
</Text>
|
||||||
|
{!isValidTimeRange(waterReminderSettings.startTime, `${String(tempEndHour).padStart(2, '0')}:00`) && (
|
||||||
|
<Text style={styles.timeRangeWarning}>⚠️ 结束时间不能早于或等于开始时间</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setEndTimePickerVisible(false)}
|
||||||
|
style={[styles.modalBtn, { backgroundColor: 'white' }]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.modalBtnText, { color: colorTokens.text }]}>取消</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={confirmEndTime}
|
||||||
|
style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colorTokens.primary }]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary, { color: colorTokens.onPrimary }]}>确定</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
585
app/water/settings.tsx
Normal file
585
app/water/settings.tsx
Normal file
@@ -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<string>('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<number>(2000);
|
||||||
|
const [tempQuickAdd, setTempQuickAdd] = useState<number>(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 (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* 背景渐变 */}
|
||||||
|
<LinearGradient
|
||||||
|
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
|
||||||
|
style={styles.gradientBackground}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 0, y: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 装饰性圆圈 */}
|
||||||
|
<View style={styles.decorativeCircle1} />
|
||||||
|
<View style={styles.decorativeCircle2} />
|
||||||
|
|
||||||
|
<HeaderBar
|
||||||
|
title="饮水设置"
|
||||||
|
onBack={() => {
|
||||||
|
router.back();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.keyboardAvoidingView}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
style={styles.scrollView}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{/* 设置列表 */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<View style={styles.settingsMenuContainer}>
|
||||||
|
<TouchableOpacity style={styles.settingsMenuItem} onPress={openGoalModal}>
|
||||||
|
<View style={styles.settingsMenuItemLeft}>
|
||||||
|
<View style={[styles.settingsIconContainer, { backgroundColor: 'rgba(147, 112, 219, 0.1)' }]}>
|
||||||
|
<Ionicons name="flag-outline" size={20} color="#9370DB" />
|
||||||
|
</View>
|
||||||
|
<View style={styles.settingsMenuItemContent}>
|
||||||
|
<Text style={[styles.settingsMenuItemTitle, { color: colorTokens.text }]}>每日饮水目标</Text>
|
||||||
|
<Text style={[styles.settingsMenuItemValue, { color: colorTokens.textSecondary }]}>{currentWaterGoal}ml</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Ionicons name="chevron-forward" size={20} color="#CCCCCC" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.settingsMenuItem} onPress={openQuickAddModal}>
|
||||||
|
<View style={styles.settingsMenuItemLeft}>
|
||||||
|
<View style={[styles.settingsIconContainer, { backgroundColor: 'rgba(147, 112, 219, 0.1)' }]}>
|
||||||
|
<Ionicons name="add-outline" size={20} color="#9370DB" />
|
||||||
|
</View>
|
||||||
|
<View style={styles.settingsMenuItemContent}>
|
||||||
|
<Text style={[styles.settingsMenuItemTitle, { color: colorTokens.text }]}>快速添加默认值</Text>
|
||||||
|
<Text style={[styles.settingsMenuItemSubtitle, { color: colorTokens.textSecondary }]}>
|
||||||
|
设置点击"+"按钮时添加的默认饮水量
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.settingsMenuItemValue, { color: colorTokens.textSecondary }]}>{quickAddAmount}ml</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Ionicons name="chevron-forward" size={20} color="#CCCCCC" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity style={[styles.settingsMenuItem, { borderBottomWidth: 0 }]} onPress={openWaterReminderSettings}>
|
||||||
|
<View style={styles.settingsMenuItemLeft}>
|
||||||
|
<View style={[styles.settingsIconContainer, { backgroundColor: 'rgba(52, 152, 219, 0.1)' }]}>
|
||||||
|
<Ionicons name="notifications-outline" size={20} color="#3498DB" />
|
||||||
|
</View>
|
||||||
|
<View style={styles.settingsMenuItemContent}>
|
||||||
|
<Text style={[styles.settingsMenuItemTitle, { color: colorTokens.text }]}>喝水提醒</Text>
|
||||||
|
<Text style={[styles.settingsMenuItemSubtitle, { color: colorTokens.textSecondary }]}>
|
||||||
|
设置定时提醒您补充水分
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.settingsMenuItemValue, { color: colorTokens.textSecondary }]}>
|
||||||
|
{waterReminderSettings.enabled ? `${waterReminderSettings.startTime}-${waterReminderSettings.endTime}, 每${waterReminderSettings.interval}分钟` : '已关闭'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Ionicons name="chevron-forward" size={20} color="#CCCCCC" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
|
{/* 饮水目标编辑弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={goalModalVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setGoalModalVisible(false)}
|
||||||
|
>
|
||||||
|
<Pressable style={styles.modalBackdrop} onPress={() => setGoalModalVisible(false)} />
|
||||||
|
<View style={styles.modalSheet}>
|
||||||
|
<View style={styles.modalHandle} />
|
||||||
|
<Text style={[styles.modalTitle, { color: colorTokens.text }]}>每日饮水目标</Text>
|
||||||
|
<View style={styles.pickerContainer}>
|
||||||
|
<Picker
|
||||||
|
selectedValue={tempGoal}
|
||||||
|
onValueChange={(value) => setTempGoal(value)}
|
||||||
|
style={styles.picker}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 96 }, (_, i) => 500 + i * 100).map(goal => (
|
||||||
|
<Picker.Item key={goal} label={`${goal}ml`} value={goal} />
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setGoalModalVisible(false)}
|
||||||
|
style={[styles.modalBtn, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.modalBtnText, { color: colorTokens.text }]}>取消</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={handleGoalConfirm}
|
||||||
|
style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colorTokens.primary }]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary, { color: colorTokens.onPrimary }]}>确定</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 快速添加默认值编辑弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={quickAddModalVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setQuickAddModalVisible(false)}
|
||||||
|
>
|
||||||
|
<Pressable style={styles.modalBackdrop} onPress={() => setQuickAddModalVisible(false)} />
|
||||||
|
<View style={styles.modalSheet}>
|
||||||
|
<View style={styles.modalHandle} />
|
||||||
|
<Text style={[styles.modalTitle, { color: colorTokens.text }]}>快速添加默认值</Text>
|
||||||
|
<View style={styles.pickerContainer}>
|
||||||
|
<Picker
|
||||||
|
selectedValue={tempQuickAdd}
|
||||||
|
onValueChange={(value) => setTempQuickAdd(value)}
|
||||||
|
style={styles.picker}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 41 }, (_, i) => 50 + i * 10).map(amount => (
|
||||||
|
<Picker.Item key={amount} label={`${amount}ml`} value={amount} />
|
||||||
|
))}
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
<View style={styles.modalActions}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setQuickAddModalVisible(false)}
|
||||||
|
style={[styles.modalBtn, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.modalBtnText, { color: colorTokens.text }]}>取消</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={handleQuickAddConfirm}
|
||||||
|
style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colorTokens.primary }]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary, { color: colorTokens.onPrimary }]}>确定</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -27,7 +27,7 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
selectedDate
|
selectedDate
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord } = useWaterDataByDate(selectedDate);
|
const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord, getWaterRecordsByDate } = useWaterDataByDate(selectedDate);
|
||||||
const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载
|
const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载
|
||||||
|
|
||||||
// 计算当前饮水量和目标
|
// 计算当前饮水量和目标
|
||||||
@@ -76,21 +76,25 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
// 判断是否是今天
|
// 判断是否是今天
|
||||||
const isToday = selectedDate === dayjs().format('YYYY-MM-DD') || !selectedDate;
|
const isToday = selectedDate === dayjs().format('YYYY-MM-DD') || !selectedDate;
|
||||||
|
|
||||||
// 加载用户偏好的快速添加饮水默认值
|
// 页面聚焦时重新加载数据
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
const loadQuickWaterAmount = async () => {
|
const loadDataOnFocus = async () => {
|
||||||
try {
|
try {
|
||||||
|
// 重新加载快速添加饮水默认值
|
||||||
const amount = await getQuickWaterAmount();
|
const amount = await getQuickWaterAmount();
|
||||||
setQuickWaterAmount(amount);
|
setQuickWaterAmount(amount);
|
||||||
|
|
||||||
|
// 重新获取水数据以刷新显示
|
||||||
|
const targetDate = selectedDate || dayjs().format('YYYY-MM-DD');
|
||||||
|
await getWaterRecordsByDate(targetDate);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载快速添加饮水默认值失败:', error);
|
console.error('页面聚焦时加载数据失败:', error);
|
||||||
// 保持默认值 250ml
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadQuickWaterAmount();
|
loadDataOnFocus();
|
||||||
}, [])
|
}, [selectedDate, getWaterRecordsByDate])
|
||||||
);
|
);
|
||||||
|
|
||||||
// 触发柱体动画
|
// 触发柱体动画
|
||||||
@@ -135,16 +139,16 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
|
|||||||
await addWaterRecord(waterAmount, recordedAt);
|
await addWaterRecord(waterAmount, recordedAt);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理卡片点击 - 跳转到饮水设置页面
|
// 处理卡片点击 - 跳转到饮水详情页面
|
||||||
const handleCardPress = async () => {
|
const handleCardPress = async () => {
|
||||||
// 触发震动反馈
|
// 触发震动反馈
|
||||||
if (process.env.EXPO_OS === 'ios') {
|
if (process.env.EXPO_OS === 'ios') {
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到饮水设置页面,传递选中的日期参数
|
// 跳转到饮水详情页面,传递选中的日期参数
|
||||||
router.push({
|
router.push({
|
||||||
pathname: '/water-settings',
|
pathname: '/water/detail',
|
||||||
params: selectedDate ? { selectedDate } : undefined
|
params: selectedDate ? { selectedDate } : undefined
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ export const ROUTES = {
|
|||||||
SLEEP_DETAIL: '/sleep-detail',
|
SLEEP_DETAIL: '/sleep-detail',
|
||||||
BASAL_METABOLISM_DETAIL: '/basal-metabolism-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',
|
TASK_DETAIL: '/task-detail',
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { deleteWaterIntakeFromHealthKit, getWaterIntakeFromHealthKit, saveWaterIntakeToHealthKit } from '@/utils/health';
|
import { deleteWaterIntakeFromHealthKit, getWaterIntakeFromHealthKit, saveWaterIntakeToHealthKit } from '@/utils/health';
|
||||||
|
import { logger } from '@/utils/logger';
|
||||||
import { Toast } from '@/utils/toast.utils';
|
import { Toast } from '@/utils/toast.utils';
|
||||||
import { getQuickWaterAmount, getWaterGoalFromStorage, setWaterGoalToStorage } from '@/utils/userPreferences';
|
import { getQuickWaterAmount, getWaterGoalFromStorage, setWaterGoalToStorage } from '@/utils/userPreferences';
|
||||||
import { refreshWidget, syncWaterDataToWidget } from '@/utils/widgetDataSync';
|
import { refreshWidget, syncWaterDataToWidget } from '@/utils/widgetDataSync';
|
||||||
@@ -495,18 +496,25 @@ export const useWaterDataByDate = (targetDate?: string) => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
logger.debug('🚰 开始获取饮水记录,日期:', date);
|
||||||
const options = createDateRange(date);
|
const options = createDateRange(date);
|
||||||
|
logger.debug('🚰 查询选项:', options);
|
||||||
|
|
||||||
const healthKitRecords = await getWaterIntakeFromHealthKit(options);
|
const healthKitRecords = await getWaterIntakeFromHealthKit(options);
|
||||||
|
logger.debug('🚰 从HealthKit获取到的原始数据:', healthKitRecords);
|
||||||
|
|
||||||
// 转换数据格式并按时间排序
|
// 转换数据格式并按时间排序
|
||||||
const convertedRecords = healthKitRecords
|
const convertedRecords = healthKitRecords
|
||||||
.map(convertHealthKitToWaterRecord)
|
.map(convertHealthKitToWaterRecord)
|
||||||
.sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());
|
.sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());
|
||||||
|
|
||||||
|
logger.debug('🚰 转换后的记录:', convertedRecords);
|
||||||
|
logger.debug('🚰 记录数量:', convertedRecords.length);
|
||||||
|
|
||||||
setWaterRecords(convertedRecords);
|
setWaterRecords(convertedRecords);
|
||||||
return convertedRecords;
|
return convertedRecords;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取饮水记录失败:', error);
|
console.error('🚰 获取饮水记录失败:', error);
|
||||||
setError('获取饮水记录失败');
|
setError('获取饮水记录失败');
|
||||||
Toast.error('获取饮水记录失败');
|
Toast.error('获取饮水记录失败');
|
||||||
return [];
|
return [];
|
||||||
@@ -638,7 +646,11 @@ export const useWaterDataByDate = (targetDate?: string) => {
|
|||||||
|
|
||||||
// 计算指定日期的统计数据
|
// 计算指定日期的统计数据
|
||||||
const waterStats = useMemo(() => {
|
const waterStats = useMemo(() => {
|
||||||
|
logger.debug('🚰 计算waterStats - waterRecords:', waterRecords);
|
||||||
|
logger.debug('🚰 计算waterStats - dailyWaterGoal:', dailyWaterGoal);
|
||||||
|
|
||||||
if (!waterRecords || waterRecords.length === 0) {
|
if (!waterRecords || waterRecords.length === 0) {
|
||||||
|
logger.debug('🚰 没有饮水记录,返回默认值');
|
||||||
return {
|
return {
|
||||||
totalAmount: 0,
|
totalAmount: 0,
|
||||||
completionRate: 0,
|
completionRate: 0,
|
||||||
@@ -649,6 +661,10 @@ export const useWaterDataByDate = (targetDate?: string) => {
|
|||||||
const totalAmount = waterRecords.reduce((total, record) => total + record.amount, 0);
|
const totalAmount = waterRecords.reduce((total, record) => total + record.amount, 0);
|
||||||
const completionRate = dailyWaterGoal > 0 ? Math.min((totalAmount / dailyWaterGoal) * 100, 100) : 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 {
|
return {
|
||||||
totalAmount,
|
totalAmount,
|
||||||
completionRate,
|
completionRate,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.13</string>
|
<string>1.0.14</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ async function executeBackgroundTasks(): Promise<void> {
|
|||||||
|
|
||||||
// await sendTestNotification()
|
// await sendTestNotification()
|
||||||
|
|
||||||
// 执行喝水提醒检查任务
|
// 执行喝水提醒检查任务 - 已禁用,改为由用户手动在设置页面管理
|
||||||
await executeWaterReminderTask();
|
await executeWaterReminderTask();
|
||||||
|
|
||||||
// 执行站立提醒检查任务
|
// 执行站立提醒检查任务
|
||||||
|
|||||||
@@ -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<string[]> {
|
||||||
|
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) {
|
for (const notification of notifications) {
|
||||||
if (notification.content.data?.type === 'water_reminder' ||
|
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);
|
await notificationService.cancelNotification(notification.identifier);
|
||||||
console.log('已取消喝水提醒:', notification.identifier);
|
console.log('已取消喝水提醒:', notification.identifier);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ const PREFERENCES_KEYS = {
|
|||||||
NOTIFICATION_ENABLED: 'user_preference_notification_enabled',
|
NOTIFICATION_ENABLED: 'user_preference_notification_enabled',
|
||||||
FITNESS_EXERCISE_MINUTES_INFO_DISMISSED: 'user_preference_fitness_exercise_minutes_info_dismissed',
|
FITNESS_EXERCISE_MINUTES_INFO_DISMISSED: 'user_preference_fitness_exercise_minutes_info_dismissed',
|
||||||
FITNESS_ACTIVE_HOURS_INFO_DISMISSED: 'user_preference_fitness_active_hours_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;
|
} as const;
|
||||||
|
|
||||||
// 用户偏好设置接口
|
// 用户偏好设置接口
|
||||||
@@ -16,6 +20,10 @@ export interface UserPreferences {
|
|||||||
notificationEnabled: boolean;
|
notificationEnabled: boolean;
|
||||||
fitnessExerciseMinutesInfoDismissed: boolean;
|
fitnessExerciseMinutesInfoDismissed: boolean;
|
||||||
fitnessActiveHoursInfoDismissed: 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, // 默认开启消息推送
|
notificationEnabled: true, // 默认开启消息推送
|
||||||
fitnessExerciseMinutesInfoDismissed: false, // 默认显示锻炼分钟说明
|
fitnessExerciseMinutesInfoDismissed: false, // 默认显示锻炼分钟说明
|
||||||
fitnessActiveHoursInfoDismissed: 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<UserPreferences> => {
|
|||||||
const notificationEnabled = await AsyncStorage.getItem(PREFERENCES_KEYS.NOTIFICATION_ENABLED);
|
const notificationEnabled = await AsyncStorage.getItem(PREFERENCES_KEYS.NOTIFICATION_ENABLED);
|
||||||
const fitnessExerciseMinutesInfoDismissed = await AsyncStorage.getItem(PREFERENCES_KEYS.FITNESS_EXERCISE_MINUTES_INFO_DISMISSED);
|
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 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 {
|
return {
|
||||||
quickWaterAmount: quickWaterAmount ? parseInt(quickWaterAmount, 10) : DEFAULT_PREFERENCES.quickWaterAmount,
|
quickWaterAmount: quickWaterAmount ? parseInt(quickWaterAmount, 10) : DEFAULT_PREFERENCES.quickWaterAmount,
|
||||||
@@ -44,6 +60,10 @@ export const getUserPreferences = async (): Promise<UserPreferences> => {
|
|||||||
notificationEnabled: notificationEnabled !== null ? notificationEnabled === 'true' : DEFAULT_PREFERENCES.notificationEnabled,
|
notificationEnabled: notificationEnabled !== null ? notificationEnabled === 'true' : DEFAULT_PREFERENCES.notificationEnabled,
|
||||||
fitnessExerciseMinutesInfoDismissed: fitnessExerciseMinutesInfoDismissed !== null ? fitnessExerciseMinutesInfoDismissed === 'true' : DEFAULT_PREFERENCES.fitnessExerciseMinutesInfoDismissed,
|
fitnessExerciseMinutesInfoDismissed: fitnessExerciseMinutesInfoDismissed !== null ? fitnessExerciseMinutesInfoDismissed === 'true' : DEFAULT_PREFERENCES.fitnessExerciseMinutesInfoDismissed,
|
||||||
fitnessActiveHoursInfoDismissed: fitnessActiveHoursInfoDismissed !== null ? fitnessActiveHoursInfoDismissed === 'true' : DEFAULT_PREFERENCES.fitnessActiveHoursInfoDismissed,
|
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) {
|
} catch (error) {
|
||||||
console.error('获取用户偏好设置失败:', error);
|
console.error('获取用户偏好设置失败:', error);
|
||||||
@@ -185,6 +205,163 @@ export const getFitnessActiveHoursInfoDismissed = async (): Promise<boolean> =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置喝水提醒开关
|
||||||
|
* @param enabled 是否开启喝水提醒
|
||||||
|
*/
|
||||||
|
export const setWaterReminderEnabled = async (enabled: boolean): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(PREFERENCES_KEYS.WATER_REMINDER_ENABLED, enabled.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置喝水提醒开关失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取喝水提醒开关状态
|
||||||
|
*/
|
||||||
|
export const getWaterReminderEnabled = async (): Promise<boolean> => {
|
||||||
|
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<void> => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(PREFERENCES_KEYS.WATER_REMINDER_START_TIME, startTime);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置喝水提醒开始时间失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取喝水提醒开始时间
|
||||||
|
*/
|
||||||
|
export const getWaterReminderStartTime = async (): Promise<string> => {
|
||||||
|
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<void> => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(PREFERENCES_KEYS.WATER_REMINDER_END_TIME, endTime);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置喝水提醒结束时间失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取喝水提醒结束时间
|
||||||
|
*/
|
||||||
|
export const getWaterReminderEndTime = async (): Promise<string> => {
|
||||||
|
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<void> => {
|
||||||
|
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<number> => {
|
||||||
|
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<void> => {
|
||||||
|
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<void> => {
|
|||||||
await AsyncStorage.removeItem(PREFERENCES_KEYS.NOTIFICATION_ENABLED);
|
await AsyncStorage.removeItem(PREFERENCES_KEYS.NOTIFICATION_ENABLED);
|
||||||
await AsyncStorage.removeItem(PREFERENCES_KEYS.FITNESS_EXERCISE_MINUTES_INFO_DISMISSED);
|
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.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) {
|
} catch (error) {
|
||||||
console.error('重置用户偏好设置失败:', error);
|
console.error('重置用户偏好设置失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Reference in New Issue
Block a user