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