refactor: CalendarPanel 性能优化,统一弹窗状态管理

This commit is contained in:
zdl
2025-12-05 14:44:22 +08:00
parent 0adceb94f8
commit 0f7a3c0cc9

View File

@@ -3,7 +3,7 @@
* 使用 FullCalendar 展示投资计划、复盘等事件 * 使用 FullCalendar 展示投资计划、复盘等事件
*/ */
import React, { useState, lazy, Suspense } from 'react'; import React, { useState, lazy, Suspense, useMemo, useCallback } from 'react';
import { import {
Box, Box,
Modal, Modal,
@@ -12,15 +12,9 @@ import {
ModalHeader, ModalHeader,
ModalBody, ModalBody,
ModalCloseButton, ModalCloseButton,
useDisclosure,
VStack,
Text,
Spinner, Spinner,
Center, Center,
Icon,
Link,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { FiCalendar } from 'react-icons/fi';
import FullCalendar from '@fullcalendar/react'; import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid'; import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction'; import interactionPlugin from '@fullcalendar/interaction';
@@ -30,7 +24,7 @@ import dayjs, { Dayjs } from 'dayjs';
import 'dayjs/locale/zh-cn'; import 'dayjs/locale/zh-cn';
import { usePlanningData } from './PlanningContext'; import { usePlanningData } from './PlanningContext';
import { EventDetailCard } from './EventDetailCard'; import { EventDetailModal } from './EventDetailModal';
import type { InvestmentEvent } from '@/types'; import type { InvestmentEvent } from '@/types';
// 懒加载投资日历组件 // 懒加载投资日历组件
@@ -60,184 +54,118 @@ interface CalendarEvent {
export const CalendarPanel: React.FC = () => { export const CalendarPanel: React.FC = () => {
const { const {
allEvents, allEvents,
loading,
borderColor, borderColor,
secondaryText, secondaryText,
setViewMode, setViewMode,
setListTab, setListTab,
} = usePlanningData(); } = usePlanningData();
// 详情弹窗 // 弹窗状态(统一使用 useState
const { isOpen, onOpen, onClose } = useDisclosure(); const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
// 投资日历弹窗
const [isInvestmentCalendarOpen, setIsInvestmentCalendarOpen] = useState(false); const [isInvestmentCalendarOpen, setIsInvestmentCalendarOpen] = useState(false);
const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null); const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
const [selectedDateEvents, setSelectedDateEvents] = useState<InvestmentEvent[]>([]); const [selectedDateEvents, setSelectedDateEvents] = useState<InvestmentEvent[]>([]);
// 转换数据为 FullCalendar 格式 // 转换数据为 FullCalendar 格式(使用 useMemo 缓存)
const calendarEvents: CalendarEvent[] = allEvents.map(event => ({ const calendarEvents: CalendarEvent[] = useMemo(() =>
...event, allEvents.map(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, ...event,
isSystem: event.source === 'future', 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 handleDateClick = (info: DateClickArg): void => { const openEventDetail = useCallback((date: Date | null): void => {
const clickedDate = dayjs(info.date); if (!date) return;
const clickedDate = dayjs(date);
setSelectedDate(clickedDate); setSelectedDate(clickedDate);
const dayEvents = allEvents.filter(event => const dayEvents = allEvents.filter(event =>
dayjs(event.event_date).isSame(clickedDate, 'day') dayjs(event.event_date).isSame(clickedDate, 'day')
); );
setSelectedDateEvents(dayEvents); setSelectedDateEvents(dayEvents);
onOpen(); setIsDetailModalOpen(true);
}; }, [allEvents]);
// 处理日期点击
const handleDateClick = useCallback((info: DateClickArg): void => {
openEventDetail(info.date);
}, [openEventDetail]);
// 处理事件点击 // 处理事件点击
const handleEventClick = (info: EventClickArg): void => { const handleEventClick = useCallback((info: EventClickArg): void => {
const event = info.event; openEventDetail(info.event.start);
const clickedDate = dayjs(event.start); }, [openEventDetail]);
setSelectedDate(clickedDate);
const dayEvents = allEvents.filter(ev =>
dayjs(ev.event_date).isSame(clickedDate, 'day')
);
setSelectedDateEvents(dayEvents);
onOpen();
};
return ( return (
<Box> <Box>
{loading ? ( <Box height={{ base: '400px', md: '560px' }}>
<Center h="560px"> <FullCalendar
<Spinner size="xl" color="purple.500" /> plugins={[dayGridPlugin, interactionPlugin]}
</Center> initialView="dayGridMonth"
) : ( locale="zh-cn"
<Box height={{ base: '500px', md: '600px' }}> headerToolbar={{
<FullCalendar left: 'prev,next today',
plugins={[dayGridPlugin, interactionPlugin]} center: 'title',
initialView="dayGridMonth" right: ''
locale="zh-cn" }}
headerToolbar={{ events={calendarEvents}
left: 'prev,next today', dateClick={handleDateClick}
center: 'title', eventClick={handleEventClick}
right: '' height="100%"
}} dayMaxEvents={2}
events={calendarEvents} moreLinkText="更多"
dateClick={handleDateClick} buttonText={{
eventClick={handleEventClick} today: '今天',
height="100%" month: '月',
dayMaxEvents={3} week: '周'
moreLinkText="更多" }}
buttonText={{ />
today: '今天', </Box>
month: '月',
week: '周'
}}
/>
</Box>
)}
{/* 查看事件详情 Modal */} {/* 查看事件详情 Modal */}
{isOpen && ( <EventDetailModal
<Modal isOpen={isOpen} onClose={onClose} size="xl" closeOnOverlayClick={false} closeOnEsc={true}> isOpen={isDetailModalOpen}
<ModalOverlay /> onClose={() => setIsDetailModalOpen(false)}
<ModalContent> selectedDate={selectedDate}
<ModalHeader> events={selectedDateEvents}
{selectedDate && selectedDate.format('YYYY年MM月DD日')} borderColor={borderColor}
</ModalHeader> secondaryText={secondaryText}
<ModalCloseButton /> onNavigateToPlan={() => {
<ModalBody> setViewMode('list');
{selectedDateEvents.length === 0 ? ( setListTab(0);
<Center py={8}> }}
<VStack spacing={3}> onNavigateToReview={() => {
<Icon as={FiCalendar} boxSize={10} color="gray.300" /> setViewMode('list');
<Text color={secondaryText}></Text> setListTab(1);
<Text fontSize="sm" color={secondaryText}> }}
onOpenInvestmentCalendar={() => {
<Link setIsInvestmentCalendarOpen(true);
color="purple.500" }}
fontWeight="medium" />
mx={1}
onClick={() => {
onClose();
setViewMode?.('list');
setListTab?.(0);
}}
cursor="pointer"
>
</Link>
<Link
color="green.500"
fontWeight="medium"
mx={1}
onClick={() => {
onClose();
setViewMode?.('list');
setListTab?.(1);
}}
cursor="pointer"
>
</Link>
<Link
color="blue.500"
fontWeight="medium"
mx={1}
onClick={() => {
onClose();
setIsInvestmentCalendarOpen(true);
}}
cursor="pointer"
>
</Link>
</Text>
</VStack>
</Center>
) : (
<VStack align="stretch" spacing={4}>
{selectedDateEvents.map((event, idx) => (
<EventDetailCard
key={idx}
event={event}
borderColor={borderColor}
secondaryText={secondaryText}
/>
))}
</VStack>
)}
</ModalBody>
</ModalContent>
</Modal>
)}
{/* 投资日历 Modal */} {/* 投资日历 Modal */}
{isInvestmentCalendarOpen && ( {isInvestmentCalendarOpen && (
<Modal <Modal
isOpen={isInvestmentCalendarOpen} isOpen={isInvestmentCalendarOpen}
onClose={() => setIsInvestmentCalendarOpen(false)} onClose={() => setIsInvestmentCalendarOpen(false)}
size="6xl" size={{ base: 'full', md: '6xl' }}
> >
<ModalOverlay /> <ModalOverlay />
<ModalContent maxW="1200px"> <ModalContent maxW={{ base: '100%', md: '1200px' }} mx={{ base: 0, md: 4 }}>
<ModalHeader></ModalHeader> <ModalHeader fontSize={{ base: 'md', md: 'lg' }} py={{ base: 3, md: 4 }}></ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<ModalBody pb={6}> <ModalBody pb={6}>
<Suspense fallback={<Center py={8}><Spinner size="xl" color="blue.500" /></Center>}> <Suspense fallback={<Center py={{ base: 6, md: 8 }}><Spinner size={{ base: 'lg', md: 'xl' }} color="blue.500" /></Center>}>
<InvestmentCalendar /> <InvestmentCalendar />
</Suspense> </Suspense>
</ModalBody> </ModalBody>