feat: 支持饮水记录卡片

This commit is contained in:
richarjiang
2025-09-02 15:50:35 +08:00
parent ed694f6142
commit 85a3c742df
16 changed files with 2066 additions and 56 deletions

View File

@@ -0,0 +1,377 @@
import { useWaterDataByDate } from '@/hooks/useWaterData';
import { Ionicons } from '@expo/vector-icons';
import dayjs from 'dayjs';
import React, { useState } from 'react';
import {
KeyboardAvoidingView,
Modal,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
interface AddWaterModalProps {
visible: boolean;
onClose: () => void;
selectedDate?: string; // 新增:选中的日期,格式为 YYYY-MM-DD
}
interface TabButtonProps {
title: string;
isActive: boolean;
onPress: () => void;
}
const TabButton: React.FC<TabButtonProps> = ({ title, isActive, onPress }) => (
<TouchableOpacity
style={[styles.tabButton, isActive && styles.activeTabButton]}
onPress={onPress}
>
<Text style={[styles.tabButtonText, isActive && styles.activeTabButtonText]}>
{title}
</Text>
</TouchableOpacity>
);
const AddWaterModal: React.FC<AddWaterModalProps> = ({ visible, onClose, selectedDate }) => {
const [activeTab, setActiveTab] = useState<'add' | 'goal'>('add');
const [waterAmount, setWaterAmount] = useState<string>('250');
const [note, setNote] = useState<string>('');
const [dailyGoal, setDailyGoal] = useState<string>('2000');
// 使用新的 hook 来处理指定日期的饮水数据
const { addWaterRecord, updateWaterGoal } = useWaterDataByDate(selectedDate);
const quickAmounts = [100, 150, 200, 250, 300, 350, 400, 500];
const goalPresets = [1500, 2000, 2500, 3000, 3500, 4000];
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 renderAddRecordTab = () => (
<View style={styles.tabContent}>
<Text style={styles.sectionTitle}> (ml)</Text>
<TextInput
style={styles.input}
value={waterAmount}
onChangeText={setWaterAmount}
keyboardType="numeric"
placeholder="请输入饮水量"
placeholderTextColor="#999"
/>
<Text style={styles.sectionTitle}></Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.quickAmountsContainer}
>
<View style={styles.quickAmountsWrapper}>
{quickAmounts.map((amount) => (
<TouchableOpacity
key={amount}
style={[
styles.quickAmountButton,
parseInt(waterAmount) === amount && styles.quickAmountButtonActive
]}
onPress={() => setWaterAmount(amount.toString())}
>
<Text style={[
styles.quickAmountText,
parseInt(waterAmount) === amount && styles.quickAmountTextActive
]}>
{amount}ml
</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
<Text style={styles.sectionTitle}> ()</Text>
<TextInput
style={[styles.input, styles.remarkInput]}
value={note}
onChangeText={setNote}
placeholder="添加备注..."
placeholderTextColor="#999"
multiline
/>
<View style={styles.buttonContainer}>
<TouchableOpacity style={[styles.button, styles.cancelButton]} onPress={onClose}>
<Text style={styles.cancelButtonText}></Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, styles.confirmButton]} onPress={handleAddWater}>
<Text style={styles.confirmButtonText}></Text>
</TouchableOpacity>
</View>
</View>
);
const renderGoalTab = () => (
<View style={styles.tabContent}>
<Text style={styles.sectionTitle}> (ml)</Text>
<TextInput
style={styles.input}
value={dailyGoal}
onChangeText={setDailyGoal}
keyboardType="numeric"
placeholder="请输入每日饮水目标"
placeholderTextColor="#999"
/>
<Text style={styles.sectionTitle}></Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.quickAmountsContainer}
>
<View style={styles.quickAmountsWrapper}>
{goalPresets.map((goal) => (
<TouchableOpacity
key={goal}
style={[
styles.quickAmountButton,
parseInt(dailyGoal) === goal && styles.quickAmountButtonActive
]}
onPress={() => setDailyGoal(goal.toString())}
>
<Text style={[
styles.quickAmountText,
parseInt(dailyGoal) === goal && styles.quickAmountTextActive
]}>
{goal}ml
</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
<View style={styles.buttonContainer}>
<TouchableOpacity style={[styles.button, styles.cancelButton]} onPress={onClose}>
<Text style={styles.cancelButtonText}></Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, styles.confirmButton]} onPress={handleUpdateGoal}>
<Text style={styles.confirmButtonText}></Text>
</TouchableOpacity>
</View>
</View>
);
return (
<Modal
animationType="slide"
transparent={true}
visible={visible}
onRequestClose={onClose}
>
<KeyboardAvoidingView
style={styles.centeredView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.modalView}>
<View style={styles.header}>
<Text style={styles.modalTitle}></Text>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
<Ionicons name="close" size={24} color="#666" />
</TouchableOpacity>
</View>
<View style={styles.tabContainer}>
<TabButton
title="添加记录"
isActive={activeTab === 'add'}
onPress={() => setActiveTab('add')}
/>
<TabButton
title="设置目标"
isActive={activeTab === 'goal'}
onPress={() => setActiveTab('goal')}
/>
</View>
<ScrollView style={styles.contentScrollView}>
{activeTab === 'add' ? renderAddRecordTab() : renderGoalTab()}
</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: 350,
maxHeight: '80%',
backgroundColor: 'white',
borderRadius: 20,
padding: 20,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
modalTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#333',
},
closeButton: {
padding: 5,
},
tabContainer: {
flexDirection: 'row',
marginBottom: 20,
borderRadius: 10,
backgroundColor: '#f5f5f5',
padding: 4,
},
tabButton: {
flex: 1,
paddingVertical: 10,
alignItems: 'center',
borderRadius: 8,
},
activeTabButton: {
backgroundColor: '#007AFF',
},
tabButtonText: {
fontSize: 14,
color: '#666',
fontWeight: '500',
},
activeTabButtonText: {
color: 'white',
fontWeight: '600',
},
contentScrollView: {
maxHeight: 400,
},
tabContent: {
paddingVertical: 10,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
color: '#333',
marginBottom: 10,
},
input: {
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 10,
paddingHorizontal: 15,
paddingVertical: 12,
fontSize: 16,
color: '#333',
marginBottom: 15,
},
remarkInput: {
height: 80,
textAlignVertical: 'top',
},
quickAmountsContainer: {
marginBottom: 15,
},
quickAmountsWrapper: {
flexDirection: 'row',
gap: 10,
paddingRight: 10,
},
quickAmountButton: {
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 20,
borderWidth: 1,
borderColor: '#e0e0e0',
backgroundColor: '#f9f9f9',
minWidth: 60,
alignItems: 'center',
},
quickAmountButtonActive: {
backgroundColor: '#007AFF',
borderColor: '#007AFF',
},
quickAmountText: {
fontSize: 14,
color: '#666',
fontWeight: '500',
},
quickAmountTextActive: {
color: 'white',
fontWeight: '600',
},
buttonContainer: {
flexDirection: 'row',
gap: 10,
marginTop: 20,
},
button: {
flex: 1,
paddingVertical: 12,
borderRadius: 10,
alignItems: 'center',
},
cancelButton: {
backgroundColor: '#f5f5f5',
},
confirmButton: {
backgroundColor: '#007AFF',
},
cancelButtonText: {
fontSize: 16,
color: '#666',
fontWeight: '500',
},
confirmButtonText: {
fontSize: 16,
color: 'white',
fontWeight: '600',
},
});
export default AddWaterModal;