/** * CalendarPanel - 投资日历面板组件 * 使用 FullCalendar 展示投资计划、复盘等事件 */ import React, { useState, lazy, Suspense, useMemo, useCallback } from 'react'; import { Box, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Spinner, Center, } from '@chakra-ui/react'; import FullCalendar from '@fullcalendar/react'; import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import type { DateClickArg } from '@fullcalendar/interaction'; import type { EventClickArg } from '@fullcalendar/core'; import dayjs, { Dayjs } from 'dayjs'; import 'dayjs/locale/zh-cn'; import { usePlanningData } from './PlanningContext'; import { EventDetailModal } from './EventDetailModal'; import type { InvestmentEvent } from '@/types'; import './InvestmentCalendar.less'; // 懒加载投资日历组件 const InvestmentCalendar = lazy(() => import('@components/InvestmentCalendar')); dayjs.locale('zh-cn'); /** * FullCalendar 事件类型 */ interface CalendarEvent { id: string; title: string; start: string; date: string; backgroundColor: string; borderColor: string; extendedProps: InvestmentEvent & { isSystem: boolean; }; } /** * CalendarPanel 组件 * 日历视图面板,显示所有投资事件 */ export const CalendarPanel: React.FC = () => { const { allEvents, borderColor, secondaryText, setViewMode, setListTab, } = usePlanningData(); // 弹窗状态(统一使用 useState) const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); const [isInvestmentCalendarOpen, setIsInvestmentCalendarOpen] = useState(false); const [selectedDate, setSelectedDate] = useState(null); const [selectedDateEvents, setSelectedDateEvents] = useState([]); // 转换数据为 FullCalendar 格式(使用 useMemo 缓存) const calendarEvents: CalendarEvent[] = useMemo(() => allEvents.map(event => ({ ...event, id: `${event.source || 'user'}-${event.id}`, title: event.title, start: event.event_date, date: event.event_date, backgroundColor: event.source === 'future' ? '#3182CE' : event.type === 'plan' ? '#8B5CF6' : '#38A169', borderColor: event.source === 'future' ? '#3182CE' : event.type === 'plan' ? '#8B5CF6' : '#38A169', extendedProps: { ...event, isSystem: event.source === 'future', } })), [allEvents]); // 抽取公共的打开事件详情函数 const openEventDetail = useCallback((date: Date | null): void => { if (!date) return; const clickedDate = dayjs(date); setSelectedDate(clickedDate); const dayEvents = allEvents.filter(event => dayjs(event.event_date).isSame(clickedDate, 'day') ); setSelectedDateEvents(dayEvents); setIsDetailModalOpen(true); }, [allEvents]); // 处理日期点击 const handleDateClick = useCallback((info: DateClickArg): void => { openEventDetail(info.date); }, [openEventDetail]); // 处理事件点击 const handleEventClick = useCallback((info: EventClickArg): void => { openEventDetail(info.event.start); }, [openEventDetail]); return ( {/* 查看事件详情 Modal */} setIsDetailModalOpen(false)} selectedDate={selectedDate} events={selectedDateEvents} borderColor={borderColor} secondaryText={secondaryText} onNavigateToPlan={() => { setViewMode('list'); setListTab(0); }} onNavigateToReview={() => { setViewMode('list'); setListTab(1); }} onOpenInvestmentCalendar={() => { setIsInvestmentCalendarOpen(true); }} /> {/* 投资日历 Modal */} {isInvestmentCalendarOpen && ( setIsInvestmentCalendarOpen(false)} size={{ base: 'full', md: '6xl' }} > 投资日历 }> )} ); };