import React, { useState, useCallback, useEffect } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Alert, FlatList, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { theme } from '../theme'; import { Event } from '../types'; import { eventApi } from '../api'; import EventModal from '../components/EventModal'; import EventCard from '../components/EventCard'; import { shouldShowEventOnDate, safeParseDate } from '../utils/recurrence'; const showError = (title: string, error: any) => { const message = error instanceof Error ? error.message : '加载事件失败'; Alert.alert(title, message); }; export default function CalendarScreen() { const navigation = useNavigation(); const [currentDate, setCurrentDate] = useState(new Date()); const [events, setEvents] = useState([]); const [selectedDate, setSelectedDate] = useState(null); const [modalVisible, setModalVisible] = useState(false); const [selectedEvent, setSelectedEvent] = useState(null); const loadEvents = useCallback(async () => { try { const allEvents: Event[] = []; const seenIds = new Set(); // 加载前 6 个月 + 当前月 + 后 1 个月的数据,确保覆盖所有持续事件 for (let i = -6; i <= 1; i++) { let targetDate = new Date(currentDate); targetDate.setMonth(targetDate.getMonth() + i); const year = targetDate.getFullYear(); const month = targetDate.getMonth() + 1; const monthEvents = await eventApi.getEvents(year, month); for (const event of monthEvents) { if (!seenIds.has(event.id)) { seenIds.add(event.id); allEvents.push(event); } } } setEvents(allEvents.filter(e => !e.deleted)); } catch (error) { showError('加载失败', error); } }, [currentDate]); useFocusEffect( useCallback(() => { loadEvents(); }, [loadEvents]) ); const getDaysInMonth = (date: Date) => { const year = date.getFullYear(); const month = date.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const days = []; // 添加上个月的日期 const prevMonthLastDay = new Date(year, month, 0).getDate(); for (let i = firstDay.getDay() - 1; i >= 0; i--) { const prevMonthDay = new Date(year, month - 1, prevMonthLastDay - i); days.push({ date: prevMonthDay, isCurrentMonth: false }); } // 添加当前月的日期 for (let i = 1; i <= lastDay.getDate(); i++) { days.push({ date: new Date(year, month, i), isCurrentMonth: true }); } // 添加下个月的日期 const remainingDays = 42 - days.length; for (let i = 1; i <= remainingDays; i++) { days.push({ date: new Date(year, month + 1, i), isCurrentMonth: false }); } return days; }; const hasEventOnDate = (date: Date) => { return events.some(event => { if (event.deleted) return false; return shouldShowEventOnDate(event, date); }); }; const isToday = (date: Date) => { const today = new Date(); return date.toDateString() === today.toDateString(); }; const isSelected = (date: Date) => { if (!selectedDate) return false; return date.toDateString() === selectedDate.toDateString(); }; const getEventsForSelectedDate = () => { if (!selectedDate) return []; return events .filter(event => { if (event.deleted) return false; return shouldShowEventOnDate(event, selectedDate); }) .sort((a, b) => safeParseDate(a.start).getTime() - safeParseDate(b.start).getTime()); }; const previousMonth = () => { setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1)); setSelectedDate(null); }; const nextMonth = () => { setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1)); setSelectedDate(null); }; const handleAddEvent = () => { setSelectedEvent(null); setModalVisible(true); }; const handleEditEvent = (event: Event) => { setSelectedEvent(event); setModalVisible(true); }; const handleToggleComplete = async (event: Event) => { try { await eventApi.updateEvent(event.id, { completed: !event.completed }); await loadEvents(); } catch (error) { showError('更新失败', error); } }; const handleDeleteEvent = async (event: Event) => { Alert.alert( '确认删除', '确定要删除这个事件吗?', [ { text: '取消', style: 'cancel' }, { text: '删除', style: 'destructive', onPress: async () => { try { await eventApi.deleteEvent(event.id); await loadEvents(); } catch (error) { showError('删除失败', error); } }, }, ] ); }; const handleSaveEvent = async (eventData: Partial) => { try { if (selectedEvent) { await eventApi.updateEvent(selectedEvent.id, eventData); } else { await eventApi.createEvent({ ...eventData, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), deleted: false, } as Event); } setModalVisible(false); await loadEvents(); } catch (error) { showError('保存失败', error); } }; const days = getDaysInMonth(currentDate); const selectedDateEvents = getEventsForSelectedDate(); return ( {currentDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', })} {['日', '一', '二', '三', '四', '五', '六'].map((day, index) => ( {day} ))} {days.map(({ date, isCurrentMonth }, index) => ( setSelectedDate(date)} > {date.getDate()} {hasEventOnDate(date) && } ))} {selectedDate && ( <> {selectedDate.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'long', })} item.id} style={styles.eventsListContainer} renderItem={({ item }) => ( )} contentContainerStyle={styles.eventsList} showsVerticalScrollIndicator={false} ListEmptyComponent={ 这一天没有事件 } /> )} {!selectedDate && ( 点击日期查看事项 )} setModalVisible(false)} onSave={handleSaveEvent} /> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: theme.colors.background, padding: theme.spacing.md, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: theme.spacing.lg, }, monthTitle: { ...theme.typography.h2, color: theme.colors.text, }, weekdays: { flexDirection: 'row', marginBottom: theme.spacing.sm, }, weekday: { flex: 1, textAlign: 'center', ...theme.typography.caption, color: theme.colors.textSecondary, }, calendarGrid: { flexDirection: 'row', flexWrap: 'wrap', marginBottom: theme.spacing.sm, }, dayCell: { width: '14.28%', aspectRatio: 1, justifyContent: 'center', alignItems: 'center', borderRadius: theme.borderRadius.md, }, selectedDay: { backgroundColor: theme.colors.surface, borderWidth: 2, borderColor: theme.colors.primary, }, today: { backgroundColor: theme.colors.surface, }, dayText: { ...theme.typography.body, color: theme.colors.text, }, otherMonthText: { color: theme.colors.textSecondary, }, selectedDayText: { color: theme.colors.primary, fontWeight: 'bold', }, todayText: { color: theme.colors.primary, }, eventDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: theme.colors.primary, marginTop: 4, }, eventsHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: theme.spacing.sm, }, eventsHeaderTitle: { ...theme.typography.h3, color: theme.colors.text, }, eventsListContainer: { flex: 1, }, eventsList: { paddingBottom: 20, }, emptyContainer: { padding: theme.spacing.xl, alignItems: 'center', }, emptyText: { ...theme.typography.body, color: theme.colors.textSecondary, }, noDateSelected: { flex: 1, justifyContent: 'center', alignItems: 'center', }, noDateText: { ...theme.typography.body, color: theme.colors.textSecondary, }, });