feat: 初始提交(仅核心代码,已排除大文件)
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Modal,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Switch,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
import { theme } from '../theme';
|
||||
import { Event } from '../types';
|
||||
import { safeParseDate } from '../utils/recurrence';
|
||||
|
||||
interface EventModalProps {
|
||||
visible: boolean;
|
||||
event: Event | null;
|
||||
onClose: () => void;
|
||||
onSave: (event: Partial<Event>) => void;
|
||||
}
|
||||
|
||||
export default function EventModal({
|
||||
visible,
|
||||
event,
|
||||
onClose,
|
||||
onSave,
|
||||
}: EventModalProps) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
const [startDate, setStartDate] = useState(new Date());
|
||||
const [endDate, setEndDate] = useState<Date | null>(null);
|
||||
const [enableReminder, setEnableReminder] = useState(true);
|
||||
const [remindDate, setRemindDate] = useState(new Date());
|
||||
const [recurrenceType, setRecurrenceType] = useState<'NONE' | 'DAILY' | 'WEEKLY'>('NONE');
|
||||
const [showStartPicker, setShowStartPicker] = useState(false);
|
||||
const [showEndPicker, setShowEndPicker] = useState(false);
|
||||
const [showRemindPicker, setShowRemindPicker] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (event) {
|
||||
setTitle(event.title);
|
||||
setContent(event.content || '');
|
||||
setStartDate(safeParseDate(event.start));
|
||||
setEndDate(event.end ? safeParseDate(event.end) : null);
|
||||
setEnableReminder(!!event.remindAt);
|
||||
setRemindDate(event.remindAt ? safeParseDate(event.remindAt) : new Date());
|
||||
|
||||
if (event.recurrence?.startsWith('DAILY')) {
|
||||
setRecurrenceType('DAILY');
|
||||
} else if (event.recurrence?.startsWith('WEEKLY')) {
|
||||
setRecurrenceType('WEEKLY');
|
||||
} else {
|
||||
setRecurrenceType('NONE');
|
||||
}
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
}, [event, visible]);
|
||||
|
||||
const resetForm = () => {
|
||||
setTitle('');
|
||||
setContent('');
|
||||
setStartDate(new Date());
|
||||
setEndDate(null);
|
||||
setEnableReminder(true);
|
||||
setRemindDate(new Date());
|
||||
setRecurrenceType('NONE');
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!title.trim()) {
|
||||
alert('请输入标题');
|
||||
return;
|
||||
}
|
||||
|
||||
let recurrence = '';
|
||||
if (recurrenceType === 'DAILY') {
|
||||
recurrence = 'DAILY;interval=1';
|
||||
} else if (recurrenceType === 'WEEKLY') {
|
||||
recurrence = 'WEEKLY;interval=1';
|
||||
}
|
||||
|
||||
const eventData: Partial<Event> = {
|
||||
title: title.trim(),
|
||||
content: content.trim(),
|
||||
start: startDate.toISOString(),
|
||||
end: endDate ? endDate.toISOString() : null,
|
||||
allDay: false,
|
||||
remindAt: enableReminder ? remindDate.toISOString() : null,
|
||||
recurrence: recurrence || null,
|
||||
completed: event?.completed || false,
|
||||
};
|
||||
|
||||
onSave(eventData);
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={onClose}>
|
||||
<Ionicons name="close" size={28} color={theme.colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>
|
||||
{event ? '编辑事件' : '新建事件'}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={handleSave}>
|
||||
<Text style={styles.saveButton}>保存</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>标题</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={title}
|
||||
onChangeText={setTitle}
|
||||
placeholder="输入事件标题"
|
||||
placeholderTextColor={theme.colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>内容</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.textArea]}
|
||||
value={content}
|
||||
onChangeText={setContent}
|
||||
placeholder="输入事件内容(可选)"
|
||||
placeholderTextColor={theme.colors.textSecondary}
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>开始时间</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.dateButton}
|
||||
onPress={() => setShowStartPicker(true)}
|
||||
>
|
||||
<Text style={styles.dateButtonText}>{formatDate(startDate)}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>结束时间(可选)</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.dateButton}
|
||||
onPress={() => setShowEndPicker(true)}
|
||||
>
|
||||
<Text style={styles.dateButtonText}>
|
||||
{endDate ? formatDate(endDate) : '未设置'}
|
||||
</Text>
|
||||
{endDate && (
|
||||
<TouchableOpacity
|
||||
onPress={() => setEndDate(null)}
|
||||
style={styles.clearButton}
|
||||
>
|
||||
<Ionicons name="close-circle" size={20} color={theme.colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.switchField}>
|
||||
<Text style={styles.label}>启用提醒</Text>
|
||||
<Switch
|
||||
value={enableReminder}
|
||||
onValueChange={setEnableReminder}
|
||||
trackColor={{ false: theme.colors.border, true: theme.colors.primary }}
|
||||
thumbColor={theme.colors.text}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{enableReminder && (
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>提醒时间</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.dateButton}
|
||||
onPress={() => setShowRemindPicker(true)}
|
||||
>
|
||||
<Text style={styles.dateButtonText}>{formatDate(remindDate)}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>重复</Text>
|
||||
<View style={styles.recurrenceOptions}>
|
||||
{[
|
||||
{ label: '不重复', value: 'NONE' },
|
||||
{ label: '每天', value: 'DAILY' },
|
||||
{ label: '每周', value: 'WEEKLY' },
|
||||
].map((option) => (
|
||||
<TouchableOpacity
|
||||
key={option.value}
|
||||
style={[
|
||||
styles.recurrenceButton,
|
||||
recurrenceType === option.value && styles.recurrenceButtonActive,
|
||||
]}
|
||||
onPress={() => setRecurrenceType(option.value as any)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.recurrenceButtonText,
|
||||
recurrenceType === option.value && styles.recurrenceButtonTextActive,
|
||||
]}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{showStartPicker && (
|
||||
<DateTimePicker
|
||||
value={startDate}
|
||||
mode="datetime"
|
||||
onChange={(event, date) => {
|
||||
setShowStartPicker(false);
|
||||
if (date) setStartDate(date);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showEndPicker && (
|
||||
<DateTimePicker
|
||||
value={endDate || new Date()}
|
||||
mode="datetime"
|
||||
onChange={(event, date) => {
|
||||
setShowEndPicker(false);
|
||||
if (date) setEndDate(date);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showRemindPicker && (
|
||||
<DateTimePicker
|
||||
value={remindDate}
|
||||
mode="datetime"
|
||||
onChange={(event, date) => {
|
||||
setShowRemindPicker(false);
|
||||
if (date) setRemindDate(date);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
container: {
|
||||
backgroundColor: theme.colors.background,
|
||||
borderTopLeftRadius: theme.borderRadius.xl,
|
||||
borderTopRightRadius: theme.borderRadius.xl,
|
||||
maxHeight: '90%',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
headerTitle: {
|
||||
...theme.typography.h3,
|
||||
color: theme.colors.text,
|
||||
},
|
||||
saveButton: {
|
||||
...theme.typography.body,
|
||||
color: theme.colors.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
content: {
|
||||
padding: theme.spacing.md,
|
||||
},
|
||||
field: {
|
||||
marginBottom: theme.spacing.lg,
|
||||
},
|
||||
label: {
|
||||
...theme.typography.body,
|
||||
color: theme.colors.text,
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: theme.colors.surface,
|
||||
borderRadius: theme.borderRadius.md,
|
||||
padding: theme.spacing.md,
|
||||
color: theme.colors.text,
|
||||
fontSize: 16,
|
||||
},
|
||||
textArea: {
|
||||
height: 100,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
dateButton: {
|
||||
backgroundColor: theme.colors.surface,
|
||||
borderRadius: theme.borderRadius.md,
|
||||
padding: theme.spacing.md,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
dateButtonText: {
|
||||
...theme.typography.body,
|
||||
color: theme.colors.text,
|
||||
},
|
||||
clearButton: {
|
||||
marginLeft: theme.spacing.sm,
|
||||
},
|
||||
switchField: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing.lg,
|
||||
},
|
||||
recurrenceOptions: {
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
recurrenceButton: {
|
||||
flex: 1,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
backgroundColor: theme.colors.surface,
|
||||
borderRadius: theme.borderRadius.md,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
recurrenceButtonActive: {
|
||||
borderColor: theme.colors.primary,
|
||||
backgroundColor: theme.colors.surfaceVariant,
|
||||
},
|
||||
recurrenceButtonText: {
|
||||
...theme.typography.bodySmall,
|
||||
color: theme.colors.text,
|
||||
},
|
||||
recurrenceButtonTextActive: {
|
||||
color: theme.colors.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user