Files
digital-pilates/components/AddWaterModal.tsx

684 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 dayjs from 'dayjs';
import { Image } from 'expo-image';
import React, { useEffect, useState } from 'react';
import {
Alert,
KeyboardAvoidingView,
Modal,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
interface AddWaterModalProps {
visible: boolean;
onClose: () => void;
selectedDate?: string; // 新增:选中的日期,格式为 YYYY-MM-DD
}
const AddWaterModal: React.FC<AddWaterModalProps> = ({ visible, onClose, selectedDate }) => {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const [activeTab, setActiveTab] = useState<'manage' | 'records'>('manage');
const [waterAmount, setWaterAmount] = useState<string>('250');
const [note, setNote] = useState<string>('');
const [dailyGoal, setDailyGoal] = useState<string>('2000');
const [quickAddAmount, setQuickAddAmount] = useState<string>('250'); // 快速添加默认值
// 使用新的 hook 来处理指定日期的饮水数据
const { waterRecords, dailyWaterGoal, addWaterRecord, updateWaterGoal, removeWaterRecord } = useWaterDataByDate(selectedDate);
const quickAmounts = [100, 150, 200, 250, 300, 350, 400, 500];
const goalPresets = [1500, 2000, 2500, 3000, 3500, 4000];
const quickAddPresets = [100, 150, 200, 250, 300, 350, 400, 500]; // 快速添加默认值选项
const handleAddWater = async () => {
const amount = parseInt(waterAmount);
if (amount > 0) {
// 如果有选中日期,则为该日期添加记录;否则为今天添加记录
const recordedAt = selectedDate ? dayjs(selectedDate).toISOString() : dayjs().toISOString();
const success = await addWaterRecord(amount, recordedAt);
if (success) {
setWaterAmount('250');
setNote('');
onClose();
}
}
};
const handleUpdateGoal = async () => {
const goal = parseInt(dailyGoal);
if (goal >= 500 && goal <= 10000) {
const success = await updateWaterGoal(goal);
if (success) {
setDailyGoal('2000');
onClose();
}
}
};
// 处理保存所有设置(饮水目标 + 快速添加默认值)
const handleSaveSettings = async () => {
const goal = parseInt(dailyGoal);
const amount = parseInt(quickAddAmount);
let hasError = false;
// 验证饮水目标
if (goal < 500 || goal > 10000) {
Alert.alert('输入错误', '每日饮水目标应在500ml到10000ml之间');
return;
}
// 验证快速添加默认值
if (amount < 50 || amount > 1000) {
Alert.alert('输入错误', '快速添加默认值应在50ml到1000ml之间');
return;
}
try {
// 保存饮水目标
const goalSuccess = await updateWaterGoal(goal);
if (!goalSuccess) {
hasError = true;
}
// 保存快速添加默认值
await setQuickWaterAmount(amount);
if (!hasError) {
onClose();
}
} catch (error) {
Alert.alert('设置失败', '无法保存设置,请重试');
}
};
// 删除饮水记录
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);
}
};
if (visible) {
loadUserPreferences();
}
}, [visible, dailyWaterGoal]);
// 渲染Tab切换器 - 参照营养记录页面的实现
const renderTabToggle = () => (
<View style={[styles.tabContainer, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
<TouchableOpacity
style={[
styles.tabButton,
activeTab === 'manage' && { backgroundColor: colorTokens.primary }
]}
onPress={() => setActiveTab('manage')}
>
<Text style={[
styles.tabButtonText,
{ color: activeTab === 'manage' ? colorTokens.onPrimary : colorTokens.textSecondary }
]}>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.tabButton,
activeTab === 'records' && { backgroundColor: colorTokens.primary }
]}
onPress={() => setActiveTab('records')}
>
<Text style={[
styles.tabButtonText,
{ color: activeTab === 'records' ? colorTokens.onPrimary : colorTokens.textSecondary }
]}>
</Text>
</TouchableOpacity>
</View>
);
// 合并后的管理Tab包含添加记录和设置目标
const renderManageTab = () => (
<View style={styles.tabContent}>
{/* 设置目标部分 */}
<Text style={[styles.sectionTitle, { color: colorTokens.text }]}> (ml)</Text>
<TextInput
style={[styles.input, {
borderColor: colorTokens.border,
backgroundColor: colorTokens.pageBackgroundEmphasis,
color: colorTokens.text
}]}
value={dailyGoal}
onChangeText={setDailyGoal}
keyboardType="numeric"
placeholder="请输入每日饮水目标"
placeholderTextColor={colorTokens.textSecondary}
/>
<Text style={[styles.sectionTitle, { color: colorTokens.text }]}></Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.quickAmountsContainer}
>
<View style={styles.quickAmountsWrapper}>
{goalPresets.map((goal) => (
<TouchableOpacity
key={goal}
style={[
styles.quickAmountButton,
{
borderColor: colorTokens.border,
backgroundColor: colorTokens.pageBackgroundEmphasis
},
parseInt(dailyGoal) === goal && {
backgroundColor: colorTokens.primary,
borderColor: colorTokens.primary
}
]}
onPress={() => setDailyGoal(goal.toString())}
>
<Text style={[
styles.quickAmountText,
{ color: parseInt(dailyGoal) === goal ? colorTokens.onPrimary : colorTokens.textSecondary }
]}>
{goal}ml
</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
{/* 快速添加默认值设置部分 */}
<Text style={[styles.sectionTitle, { color: colorTokens.text, marginTop: 20 }]}> (ml)</Text>
<Text style={[styles.sectionSubtitle, { color: colorTokens.textSecondary, marginBottom: 12 }]}>
"+"
</Text>
<TextInput
style={[styles.input, {
borderColor: colorTokens.border,
backgroundColor: colorTokens.pageBackgroundEmphasis,
color: colorTokens.text
}]}
value={quickAddAmount}
onChangeText={setQuickAddAmount}
keyboardType="numeric"
placeholder="请输入快速添加默认值"
placeholderTextColor={colorTokens.textSecondary}
/>
<Text style={[styles.sectionTitle, { color: colorTokens.text }]}></Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.quickAmountsContainer}
>
<View style={styles.quickAmountsWrapper}>
{quickAddPresets.map((amount) => (
<TouchableOpacity
key={amount}
style={[
styles.quickAmountButton,
{
borderColor: colorTokens.border,
backgroundColor: colorTokens.pageBackgroundEmphasis
},
parseInt(quickAddAmount) === amount && {
backgroundColor: colorTokens.primary,
borderColor: colorTokens.primary
}
]}
onPress={() => setQuickAddAmount(amount.toString())}
>
<Text style={[
styles.quickAmountText,
{ color: parseInt(quickAddAmount) === amount ? colorTokens.onPrimary : colorTokens.textSecondary }
]}>
{amount}ml
</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[styles.button, styles.cancelButton, {
backgroundColor: colorTokens.pageBackgroundEmphasis,
}]}
onPress={onClose}
>
<Text style={[styles.cancelButtonText, { color: colorTokens.textSecondary }]}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.confirmButton, { backgroundColor: colorTokens.primary }]}
onPress={handleSaveSettings}
>
<Text style={[styles.confirmButtonText, { color: colorTokens.onPrimary }]}></Text>
</TouchableOpacity>
</View>
</View>
);
// 新增:饮水记录卡片组件
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>
);
};
// 新增饮水记录Tab内容
const renderRecordsTab = () => (
<View style={styles.tabContent}>
<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 }]}>"添加记录"</Text>
</View>
)}
</View>
);
return (
<Modal
animationType="fade"
transparent={true}
visible={visible}
onRequestClose={onClose}
>
<KeyboardAvoidingView
style={styles.centeredView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={[styles.modalView, { backgroundColor: colorTokens.background }]}>
<View style={styles.header}>
<Text style={[styles.modalTitle, { color: colorTokens.text }]}></Text>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
<Ionicons name="close" size={18} color={colorTokens.textSecondary} />
</TouchableOpacity>
</View>
{renderTabToggle()}
<ScrollView style={styles.contentScrollView}>
{activeTab === 'manage' ? renderManageTab() : renderRecordsTab()}
</ScrollView>
</View>
</KeyboardAvoidingView>
</Modal>
);
};
const styles = StyleSheet.create({
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
modalView: {
width: '90%',
maxWidth: 400,
height: 650, // 固定高度
borderRadius: 24,
paddingTop: 24,
paddingHorizontal: 20,
paddingBottom: 20,
shadowColor: '#000000',
shadowOffset: {
width: 0,
height: 8,
},
shadowOpacity: 0.12,
shadowRadius: 20,
elevation: 8,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
modalTitle: {
fontSize: 16,
fontWeight: '600',
letterSpacing: -0.5,
},
closeButton: {
padding: 5,
},
tabContainer: {
flexDirection: 'row',
borderRadius: 20,
padding: 2,
marginBottom: 24,
},
tabButton: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 18,
minWidth: 80,
alignItems: 'center',
flex: 1,
},
tabButtonText: {
fontSize: 14,
fontWeight: '600',
},
contentScrollView: {
flex: 1,
},
tabContent: {
paddingVertical: 10,
},
sectionTitle: {
fontSize: 14,
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,
},
remarkInput: {
height: 80,
textAlignVertical: 'top',
},
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',
},
buttonContainer: {
flexDirection: 'row',
gap: 10,
marginTop: 20,
},
button: {
flex: 1,
paddingVertical: 14,
borderRadius: 12,
alignItems: 'center',
},
cancelButton: {
},
confirmButton: {
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 3,
},
cancelButtonText: {
fontSize: 16,
fontWeight: '600',
},
confirmButtonText: {
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',
},
});
export default AddWaterModal;