Files
digital-pilates/app/water-settings.tsx

662 lines
19 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 { 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 [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 goalPresets = [1500, 2000, 2500, 3000, 3500, 4000];
const quickAddPresets = [100, 150, 200, 250, 300, 350, 400, 500];
// 打开饮水目标弹窗时初始化临时值
const openGoalModal = () => {
setTempGoal(parseInt(dailyGoal));
setGoalModalVisible(true);
};
// 打开快速添加弹窗时初始化临时值
const openQuickAddModal = () => {
setTempQuickAdd(parseInt(quickAddAmount));
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, { backgroundColor: colorTokens.background }]}>
<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}>
<Text style={[styles.sectionTitle, { color: colorTokens.text }]}></Text>
{/* 设置目标部分 */}
<TouchableOpacity
style={[styles.settingRow, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}
onPress={openGoalModal}
activeOpacity={0.8}
>
<View style={styles.settingLeft}>
<Text style={[styles.settingTitle, { color: colorTokens.text }]}></Text>
<Text style={[styles.settingValue, { color: colorTokens.textSecondary }]}>{dailyGoal}ml</Text>
</View>
<View style={styles.settingRight}>
<Ionicons name="chevron-forward" size={16} color={colorTokens.icon} />
</View>
</TouchableOpacity>
{/* 快速添加默认值设置部分 */}
<TouchableOpacity
style={[styles.settingRow, { backgroundColor: colorTokens.pageBackgroundEmphasis, marginTop: 24 }]}
onPress={openQuickAddModal}
activeOpacity={0.8}
>
<View style={styles.settingLeft}>
<Text style={[styles.settingTitle, { color: colorTokens.text }]}></Text>
<Text style={[styles.settingSubtitle, { color: colorTokens.textSecondary }]}>
"+"
</Text>
<Text style={[styles.settingValue, { color: colorTokens.textSecondary }]}>{quickAddAmount}ml</Text>
</View>
<View style={styles.settingRight}>
<Ionicons name="chevron-forward" size={16} color={colorTokens.icon} />
</View>
</TouchableOpacity>
</View>
{/* 第二部分:饮水记录 */}
<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={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,
},
keyboardAvoidingView: {
flex: 1,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: 20,
},
section: {
marginBottom: 32,
},
sectionTitle: {
fontSize: 20,
fontWeight: '600',
marginBottom: 20,
letterSpacing: -0.5,
},
subsectionTitle: {
fontSize: 16,
fontWeight: '500',
marginBottom: 12,
letterSpacing: -0.3,
},
sectionSubtitle: {
fontSize: 14,
fontWeight: '400',
lineHeight: 18,
},
input: {
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 14,
fontSize: 16,
fontWeight: '500',
marginBottom: 16,
},
settingRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingHorizontal: 16,
borderRadius: 12,
marginBottom: 16,
},
settingLeft: {
flex: 1,
},
settingTitle: {
fontSize: 16,
fontWeight: '500',
marginBottom: 4,
},
settingSubtitle: {
fontSize: 14,
marginBottom: 8,
},
settingValue: {
fontSize: 16,
},
settingRight: {
marginLeft: 12,
},
quickAmountsContainer: {
marginBottom: 15,
},
quickAmountsWrapper: {
flexDirection: 'row',
gap: 10,
paddingRight: 10,
},
quickAmountButton: {
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 20,
minWidth: 70,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
quickAmountText: {
fontSize: 15,
fontWeight: '500',
},
saveButton: {
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
marginTop: 24,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 3,
},
saveButtonText: {
fontSize: 16,
fontWeight: '700',
},
// 饮水记录相关样式
recordsList: {
gap: 12,
},
recordCardContainer: {
// iOS 阴影效果
shadowColor: '#000000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 4,
// Android 阴影效果
elevation: 2,
},
recordCard: {
borderRadius: 12,
padding: 10,
},
recordMainContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
recordIconContainer: {
width: 40,
height: 40,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
recordIcon: {
width: 20,
height: 20,
},
recordInfo: {
flex: 1,
marginLeft: 12,
},
recordLabel: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
},
recordTimeContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
recordAmountContainer: {
alignItems: 'flex-end',
},
recordAmount: {
fontSize: 14,
fontWeight: '500',
},
deleteSwipeButton: {
backgroundColor: '#EF4444',
justifyContent: 'center',
alignItems: 'center',
width: 80,
borderRadius: 12,
marginLeft: 8,
},
deleteSwipeButtonText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
marginTop: 4,
},
recordTimeText: {
fontSize: 12,
fontWeight: '400',
},
recordNote: {
marginTop: 8,
fontSize: 14,
fontStyle: 'italic',
lineHeight: 20,
},
recordsSummary: {
marginTop: 20,
padding: 16,
borderRadius: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
summaryText: {
fontSize: 12,
fontWeight: '500',
},
summaryGoal: {
fontSize: 12,
fontWeight: '500',
},
noRecordsContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 40,
gap: 12,
},
noRecordsText: {
fontSize: 16,
fontWeight: '600',
},
noRecordsSubText: {
fontSize: 14,
textAlign: 'center',
},
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
},
});
export default WaterSettings;