375 lines
11 KiB
TypeScript
375 lines
11 KiB
TypeScript
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',
|
||
},
|
||
});
|