refactor: CalendarPanel 性能优化,统一弹窗状态管理
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user