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,23 +54,22 @@ 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(() =>
allEvents.map(event => ({
...event, ...event,
id: `${event.source || 'user'}-${event.id}`, id: `${event.source || 'user'}-${event.id}`,
title: event.title, title: event.title,
@@ -88,41 +81,34 @@ export const CalendarPanel: React.FC = () => {
...event, ...event,
isSystem: event.source === 'future', 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">
<Spinner size="xl" color="purple.500" />
</Center>
) : (
<Box height={{ base: '500px', md: '600px' }}>
<FullCalendar <FullCalendar
plugins={[dayGridPlugin, interactionPlugin]} plugins={[dayGridPlugin, interactionPlugin]}
initialView="dayGridMonth" initialView="dayGridMonth"
@@ -136,7 +122,7 @@ export const CalendarPanel: React.FC = () => {
dateClick={handleDateClick} dateClick={handleDateClick}
eventClick={handleEventClick} eventClick={handleEventClick}
height="100%" height="100%"
dayMaxEvents={3} dayMaxEvents={2}
moreLinkText="更多" moreLinkText="更多"
buttonText={{ buttonText={{
today: '今天', today: '今天',
@@ -145,99 +131,41 @@ export const CalendarPanel: React.FC = () => {
}} }}
/> />
</Box> </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日')}
</ModalHeader>
<ModalCloseButton />
<ModalBody>
{selectedDateEvents.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
<Icon as={FiCalendar} boxSize={10} color="gray.300" />
<Text color={secondaryText}></Text>
<Text fontSize="sm" color={secondaryText}>
<Link
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} borderColor={borderColor}
secondaryText={secondaryText} secondaryText={secondaryText}
onNavigateToPlan={() => {
setViewMode('list');
setListTab(0);
}}
onNavigateToReview={() => {
setViewMode('list');
setListTab(1);
}}
onOpenInvestmentCalendar={() => {
setIsInvestmentCalendarOpen(true);
}}
/> />
))}
</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>