import React, { useState, useEffect } from 'react'; import { View, Text, StyleSheet, Modal, TextInput, TouchableOpacity, ScrollView, Switch, ActivityIndicator, } 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) => void; } const WEEKDAY_LABELS = ['一', '二', '三', '四', '五', '六', '日']; // 15 分钟对齐 const snapTo15 = (date: Date): Date => { const d = new Date(date); const minutes = d.getMinutes(); const snapped = Math.round(minutes / 15) * 15; d.setMinutes(snapped, 0, 0); return d; }; // 解析循环规则字符串 const parseRecurrence = (str: string | null): { type: 'NONE' | 'DAILY' | 'WEEKLY'; interval: number; by: number[] } => { if (!str) return { type: 'NONE', interval: 1, by: [] }; const parts = str.split(';'); const type = (parts[0] === 'DAILY' || parts[0] === 'WEEKLY') ? parts[0] as 'DAILY' | 'WEEKLY' : 'NONE'; let interval = 1; let by: number[] = []; for (let i = 1; i < parts.length; i++) { const [key, val] = parts[i].split('='); if (key === 'interval') interval = Math.max(1, parseInt(val) || 1); if (key === 'by') by = val.split(',').map(Number).filter(n => n >= 0 && n <= 6); } return { type, interval, by }; }; 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(null); const [allDay, setAllDay] = useState(false); const [enableReminder, setEnableReminder] = useState(true); const [remindDate, setRemindDate] = useState(new Date()); const [recurrenceType, setRecurrenceType] = useState<'NONE' | 'DAILY' | 'WEEKLY'>('NONE'); const [interval, setInterval_] = useState(1); const [weekdays, setWeekdays] = useState([]); const [saving, setSaving] = useState(false); 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); setAllDay(!!event.allDay); setEnableReminder(!!event.remindAt); setRemindDate(event.remindAt ? safeParseDate(event.remindAt) : new Date()); const parsed = parseRecurrence(event.recurrence); setRecurrenceType(parsed.type); setInterval_(parsed.interval); setWeekdays(parsed.by); } else { resetForm(); } }, [event, visible]); const resetForm = () => { setTitle(''); setContent(''); setStartDate(snapTo15(new Date())); setEndDate(null); setAllDay(false); setEnableReminder(true); setRemindDate(snapTo15(new Date())); setRecurrenceType('NONE'); setInterval_(1); setWeekdays([]); setSaving(false); }; const handleSave = () => { if (!title.trim()) { alert('请输入标题'); return; } if (!startDate) { alert('请设置开始时间'); return; } setSaving(true); // 结束时间自动补全:未设置时 +1h(全天则 +24h) let finalEnd = endDate; if (!finalEnd) { finalEnd = new Date(startDate.getTime() + (allDay ? 24 : 1) * 3600 * 1000); } // 组装循环规则 let recurrence = ''; if (recurrenceType === 'DAILY') { recurrence = `DAILY;interval=${interval}`; } else if (recurrenceType === 'WEEKLY') { recurrence = `WEEKLY;interval=${interval}`; if (weekdays.length > 0) { recurrence += `;by=${weekdays.join(',')}`; } } const eventData: Partial = { title: title.trim(), content: content.trim(), start: startDate.toISOString(), end: finalEnd.toISOString(), allDay, remindAt: enableReminder ? remindDate.toISOString() : null, recurrence: recurrence || null, completed: event?.completed || false, }; onSave(eventData); }; const toggleWeekday = (day: number) => { setWeekdays(prev => prev.includes(day) ? prev.filter(d => d !== day) : [...prev, day].sort() ); }; const formatDate = (date: Date) => { return date.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); }; return ( {event ? '编辑事件' : '新建事件'} {saving ? ( ) : ( 保存 )} 标题 内容 全天事件 开始时间 setShowStartPicker(true)} > {formatDate(startDate)} 结束时间(可选,默认+1小时) setShowEndPicker(true)} > {endDate ? formatDate(endDate) : '未设置'} {endDate && ( setEndDate(null)} style={styles.clearButton} > )} 启用提醒 {enableReminder && ( 提醒时间 setShowRemindPicker(true)} > {formatDate(remindDate)} )} 重复 {[ { label: '不重复', value: 'NONE' }, { label: '每天', value: 'DAILY' }, { label: '每周', value: 'WEEKLY' }, ].map((option) => ( setRecurrenceType(option.value as any)} > {option.label} ))} {recurrenceType !== 'NONE' && ( 每 {interval} {recurrenceType === 'DAILY' ? '天' : '周'} setInterval_(Math.max(1, interval - 1))} > {interval} setInterval_(interval + 1)} > )} {recurrenceType === 'WEEKLY' && ( 重复星期 {WEEKDAY_LABELS.map((label, idx) => ( toggleWeekday(idx)} > {label} ))} )} {showStartPicker && ( { setShowStartPicker(false); if (date) setStartDate(snapTo15(date)); }} /> )} {showEndPicker && ( { setShowEndPicker(false); if (date) setEndDate(snapTo15(date)); }} /> )} {showRemindPicker && ( { setShowRemindPicker(false); if (date) setRemindDate(snapTo15(date)); }} /> )} ); } 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', }, intervalRow: { flexDirection: 'row', alignItems: 'center', gap: theme.spacing.md, }, intervalBtn: { width: 40, height: 40, borderRadius: theme.borderRadius.md, backgroundColor: theme.colors.surface, justifyContent: 'center', alignItems: 'center', borderWidth: 1, borderColor: theme.colors.border, }, intervalValue: { ...theme.typography.h3, color: theme.colors.text, minWidth: 30, textAlign: 'center', }, weekdayRow: { flexDirection: 'row', gap: theme.spacing.xs, }, weekdayBtn: { flex: 1, paddingVertical: theme.spacing.sm, borderRadius: theme.borderRadius.md, backgroundColor: theme.colors.surface, alignItems: 'center', borderWidth: 1, borderColor: theme.colors.border, }, weekdayBtnActive: { borderColor: theme.colors.primary, backgroundColor: theme.colors.surfaceVariant, }, weekdayBtnText: { ...theme.typography.bodySmall, color: theme.colors.textSecondary, }, weekdayBtnTextActive: { color: theme.colors.primary, fontWeight: '600', }, });