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 展示投资计划、复盘等事件
*/
import React, { useState, lazy, Suspense } from 'react';
import React, { useState, lazy, Suspense, useMemo, useCallback } from 'react';
import {
Box,
Modal,
@@ -12,15 +12,9 @@ import {
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure,
VStack,
Text,
Spinner,
Center,
Icon,
Link,
} from '@chakra-ui/react';
import { FiCalendar } from 'react-icons/fi';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
@@ -30,7 +24,7 @@ import dayjs, { Dayjs } from 'dayjs';
import 'dayjs/locale/zh-cn';
import { usePlanningData } from './PlanningContext';
import { EventDetailCard } from './EventDetailCard';
import { EventDetailModal } from './EventDetailModal';
import type { InvestmentEvent } from '@/types';
// 懒加载投资日历组件
@@ -60,23 +54,22 @@ interface CalendarEvent {
export const CalendarPanel: React.FC = () => {
const {
allEvents,
loading,
borderColor,
secondaryText,
setViewMode,
setListTab,
} = usePlanningData();
// 详情弹窗
const { isOpen, onOpen, onClose } = useDisclosure();
// 投资日历弹窗
// 弹窗状态(统一使用 useState
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
const [isInvestmentCalendarOpen, setIsInvestmentCalendarOpen] = useState(false);
const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
const [selectedDateEvents, setSelectedDateEvents] = useState<InvestmentEvent[]>([]);
// 转换数据为 FullCalendar 格式
const calendarEvents: CalendarEvent[] = allEvents.map(event => ({
// 转换数据为 FullCalendar 格式(使用 useMemo 缓存)
const calendarEvents: CalendarEvent[] = useMemo(() =>
allEvents.map(event => ({
...event,
id: `${event.source || 'user'}-${event.id}`,
title: event.title,
@@ -88,41 +81,34 @@ export const CalendarPanel: React.FC = () => {
...event,
isSystem: event.source === 'future',
}
}));
})), [allEvents]);
// 处理日期点击
const handleDateClick = (info: DateClickArg): void => {
const clickedDate = dayjs(info.date);
// 抽取公共的打开事件详情函数
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);
onOpen();
};
setIsDetailModalOpen(true);
}, [allEvents]);
// 处理日期点击
const handleDateClick = useCallback((info: DateClickArg): void => {
openEventDetail(info.date);
}, [openEventDetail]);
// 处理事件点击
const handleEventClick = (info: EventClickArg): void => {
const event = info.event;
const clickedDate = dayjs(event.start);
setSelectedDate(clickedDate);
const dayEvents = allEvents.filter(ev =>
dayjs(ev.event_date).isSame(clickedDate, 'day')
);
setSelectedDateEvents(dayEvents);
onOpen();
};
const handleEventClick = useCallback((info: EventClickArg): void => {
openEventDetail(info.event.start);
}, [openEventDetail]);
return (
<Box>
{loading ? (
<Center h="560px">
<Spinner size="xl" color="purple.500" />
</Center>
) : (
<Box height={{ base: '500px', md: '600px' }}>
<Box height={{ base: '400px', md: '560px' }}>
<FullCalendar
plugins={[dayGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
@@ -136,7 +122,7 @@ export const CalendarPanel: React.FC = () => {
dateClick={handleDateClick}
eventClick={handleEventClick}
height="100%"
dayMaxEvents={3}
dayMaxEvents={2}
moreLinkText="更多"
buttonText={{
today: '今天',
@@ -145,99 +131,41 @@ export const CalendarPanel: React.FC = () => {
}}
/>
</Box>
)}
{/* 查看事件详情 Modal */}
{isOpen && (
<Modal isOpen={isOpen} onClose={onClose} size="xl" closeOnOverlayClick={false} closeOnEsc={true}>
<ModalOverlay />
<ModalContent>
<ModalHeader>
{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}
<EventDetailModal
isOpen={isDetailModalOpen}
onClose={() => setIsDetailModalOpen(false)}
selectedDate={selectedDate}
events={selectedDateEvents}
borderColor={borderColor}
secondaryText={secondaryText}
onNavigateToPlan={() => {
setViewMode('list');
setListTab(0);
}}
onNavigateToReview={() => {
setViewMode('list');
setListTab(1);
}}
onOpenInvestmentCalendar={() => {
setIsInvestmentCalendarOpen(true);
}}
/>
))}
</VStack>
)}
</ModalBody>
</ModalContent>
</Modal>
)}
{/* 投资日历 Modal */}
{isInvestmentCalendarOpen && (
<Modal
isOpen={isInvestmentCalendarOpen}
onClose={() => setIsInvestmentCalendarOpen(false)}
size="6xl"
size={{ base: 'full', md: '6xl' }}
>
<ModalOverlay />
<ModalContent maxW="1200px">
<ModalHeader></ModalHeader>
<ModalContent maxW={{ base: '100%', md: '1200px' }} mx={{ base: 0, md: 4 }}>
<ModalHeader fontSize={{ base: 'md', md: 'lg' }} py={{ base: 3, md: 4 }}></ModalHeader>
<ModalCloseButton />
<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 />
</Suspense>
</ModalBody>