Files
digital-pilates/app/water-settings.tsx
richarjiang 94899fbc5c feat(ui): 添加设置弹窗功能并重构水摄入设置界面
- 添加设置弹窗状态管理
- 实现设置按钮点击处理函数
- 在头部添加设置图标按钮
- 移除原有的设置行界面,改为弹窗形式展示
- 添加新的样式定义支持设置弹窗布局
- 优化设置项的展示和交互体验
2025-09-25 18:56:01 +08:00

730 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useWaterDataByDate } from '@/hooks/useWaterData';
import { getQuickWaterAmount, setQuickWaterAmount } from '@/utils/userPreferences';
import { Ionicons } from '@expo/vector-icons';
import { Picker } from '@react-native-picker/picker';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { router, useLocalSearchParams } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
Alert,
KeyboardAvoidingView,
Modal,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
import { HeaderBar } from '@/components/ui/HeaderBar';
import dayjs from 'dayjs';
interface WaterSettingsProps {
selectedDate?: string;
}
const WaterSettings: React.FC<WaterSettingsProps> = () => {
const { selectedDate } = useLocalSearchParams<{ selectedDate?: string }>();
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const [dailyGoal, setDailyGoal] = useState<string>('2000');
const [quickAddAmount, setQuickAddAmount] = useState<string>('250');
// 设置弹窗状态
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 来处理指定日期的饮水数据
const { waterRecords, dailyWaterGoal, updateWaterGoal, removeWaterRecord } = useWaterDataByDate(selectedDate);
// 处理设置按钮点击
const handleSettingsPress = () => {
setSettingsModalVisible(true);
};
// 打开饮水目标弹窗时初始化临时值
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('设置失败', '无法保存快速添加默认值,请重试');
}
};
// 删除饮水记录
const handleDeleteRecord = async (recordId: string) => {
await removeWaterRecord(recordId);
};
// 加载用户偏好设置和当前饮水目标
useEffect(() => {
const loadUserPreferences = async () => {
try {
const amount = await getQuickWaterAmount();
setQuickAddAmount(amount.toString());
// 设置当前的饮水目标
if (dailyWaterGoal) {
setDailyGoal(dailyWaterGoal.toString());
}
} catch (error) {
console.error('加载用户偏好设置失败:', error);
}
};
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<Swipeable>(null);
// 处理删除操作
const handleDelete = () => {
Alert.alert(
'确认删除',
'确定要删除这条饮水记录吗?此操作无法撤销。',
[
{
text: '取消',
style: 'cancel',
},
{
text: '删除',
style: 'destructive',
onPress: () => {
onDelete();
swipeableRef.current?.close();
},
},
]
);
};
// 渲染右侧删除按钮
const renderRightActions = () => {
return (
<TouchableOpacity
style={styles.deleteSwipeButton}
onPress={handleDelete}
activeOpacity={0.8}
>
<Ionicons name="trash" size={20} color="#FFFFFF" />
<Text style={styles.deleteSwipeButtonText}></Text>
</TouchableOpacity>
);
};
return (
<View style={styles.recordCardContainer}>
<Swipeable
ref={swipeableRef}
renderRightActions={renderRightActions}
rightThreshold={40}
overshootRight={false}
>
<View style={[styles.recordCard, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
<View style={styles.recordMainContent}>
<View style={[styles.recordIconContainer, { backgroundColor: colorTokens.background }]}>
<Image
source={require('@/assets/images/icons/IconGlass.png')}
style={styles.recordIcon}
/>
</View>
<View style={styles.recordInfo}>
<Text style={[styles.recordLabel, { color: colorTokens.text }]}></Text>
<View style={styles.recordTimeContainer}>
<Ionicons name="time-outline" size={14} color={colorTokens.textSecondary} />
<Text style={[styles.recordTimeText, { color: colorTokens.textSecondary }]}>
{dayjs(record.recordedAt || record.createdAt).format('HH:mm')}
</Text>
</View>
</View>
<View style={styles.recordAmountContainer}>
<Text style={[styles.recordAmount, { color: colorTokens.text }]}>{record.amount}ml</Text>
</View>
</View>
{record.note && (
<Text style={[styles.recordNote, { color: colorTokens.textSecondary }]}>{record.note}</Text>
)}
</View>
</Swipeable>
</View>
);
};
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();
}}
right={
<TouchableOpacity
style={styles.settingsButton}
onPress={handleSettingsPress}
activeOpacity={0.7}
>
<Ionicons name="settings-outline" size={24} color={colorTokens.text} />
</TouchableOpacity>
}
/>
<KeyboardAvoidingView
style={styles.keyboardAvoidingView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* 第二部分:饮水记录 */}
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colorTokens.text }]}>
{selectedDate ? dayjs(selectedDate).format('MM月DD日') : '今日'}
</Text>
{waterRecords && waterRecords.length > 0 ? (
<View style={styles.recordsList}>
{waterRecords.map((record) => (
<WaterRecordCard
key={record.id}
record={record}
onDelete={() => handleDeleteRecord(record.id)}
/>
))}
{/* 总计显示 */}
<View style={[styles.recordsSummary, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
<Text style={[styles.summaryText, { color: colorTokens.text }]}>
{waterRecords.reduce((sum, record) => sum + record.amount, 0)}ml
</Text>
<Text style={[styles.summaryGoal, { color: colorTokens.textSecondary }]}>
{dailyWaterGoal}ml
</Text>
</View>
</View>
) : (
<View style={styles.noRecordsContainer}>
<Ionicons name="water-outline" size={48} color={colorTokens.textSecondary} />
<Text style={[styles.noRecordsText, { color: colorTokens.textSecondary }]}></Text>
<Text style={[styles.noRecordsSubText, { color: colorTokens.textSecondary }]}>&quot;&quot;</Text>
</View>
)}
</View>
</ScrollView>
</KeyboardAvoidingView>
{/* 设置主弹窗 */}
<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>
);
};
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,
},
sectionTitle: {
fontSize: 16,
fontWeight: '500',
marginBottom: 20,
letterSpacing: -0.5,
},
subsectionTitle: {
fontSize: 14,
fontWeight: '500',
marginBottom: 12,
letterSpacing: -0.3,
},
sectionSubtitle: {
fontSize: 12,
fontWeight: '400',
lineHeight: 18,
},
// 饮水记录相关样式
recordsList: {
gap: 12,
},
recordCardContainer: {
// iOS 阴影效果
shadowColor: '#000000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 4,
// Android 阴影效果
elevation: 2,
},
recordCard: {
borderRadius: 12,
padding: 10,
},
recordMainContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
recordIconContainer: {
width: 40,
height: 40,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
recordIcon: {
width: 20,
height: 20,
},
recordInfo: {
flex: 1,
marginLeft: 12,
},
recordLabel: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
},
recordTimeContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
recordAmountContainer: {
alignItems: 'flex-end',
},
recordAmount: {
fontSize: 14,
fontWeight: '500',
},
deleteSwipeButton: {
backgroundColor: '#EF4444',
justifyContent: 'center',
alignItems: 'center',
width: 80,
borderRadius: 12,
marginLeft: 8,
},
deleteSwipeButtonText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
marginTop: 4,
},
recordTimeText: {
fontSize: 12,
fontWeight: '400',
},
recordNote: {
marginTop: 8,
fontSize: 14,
fontStyle: 'italic',
lineHeight: 20,
},
recordsSummary: {
marginTop: 20,
padding: 16,
borderRadius: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
summaryText: {
fontSize: 12,
fontWeight: '500',
},
summaryGoal: {
fontSize: 12,
fontWeight: '500',
},
noRecordsContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 40,
gap: 16,
},
noRecordsText: {
fontSize: 15,
fontWeight: '500',
lineHeight: 20,
},
noRecordsSubText: {
fontSize: 13,
textAlign: 'center',
lineHeight: 18,
opacity: 0.7,
},
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,
// iOS 阴影效果
shadowColor: '#000000',
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.1,
shadowRadius: 8,
// Android 阴影效果
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
},
settingsButton: {
width: 32,
height: 32,
alignItems: 'center',
justifyContent: 'center',
},
settingsModalSheet: {
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,
},
settingsModalTitle: {
fontSize: 18,
fontWeight: '600',
textAlign: 'center',
marginBottom: 20,
},
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: 14,
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: 15,
fontWeight: '500',
marginBottom: 2,
},
settingsMenuItemSubtitle: {
fontSize: 12,
marginBottom: 4,
},
settingsMenuItemValue: {
fontSize: 14,
},
});
export default WaterSettings;