- 迁移 klineDataCache.js 到 src/utils/stock/(被 StockChart 使用) - 迁移 InvestmentCalendar 到 src/components/InvestmentCalendar/(被 Navbar、Dashboard 使用) - 迁移 DynamicNewsDetail 到 src/components/EventDetailPanel/(被 EventDetail 使用) - 更新所有相关导入路径,使用路径别名 - 保持 Community 目录其余结构不变 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
204 lines
6.0 KiB
TypeScript
204 lines
6.0 KiB
TypeScript
/**
|
||
* CalendarPanel - 投资日历面板组件
|
||
* 使用 FullCalendar 展示投资计划、复盘等事件
|
||
*/
|
||
|
||
import React, { useState, lazy, Suspense, useMemo, useCallback } from 'react';
|
||
import {
|
||
Box,
|
||
Modal,
|
||
ModalOverlay,
|
||
ModalContent,
|
||
ModalHeader,
|
||
ModalBody,
|
||
ModalCloseButton,
|
||
Spinner,
|
||
Center,
|
||
} from '@chakra-ui/react';
|
||
import FullCalendar from '@fullcalendar/react';
|
||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||
import interactionPlugin from '@fullcalendar/interaction';
|
||
import type { DateClickArg } from '@fullcalendar/interaction';
|
||
import type { EventClickArg } from '@fullcalendar/core';
|
||
import dayjs, { Dayjs } from 'dayjs';
|
||
import 'dayjs/locale/zh-cn';
|
||
|
||
import { usePlanningData } from './PlanningContext';
|
||
import { EventDetailModal } from './EventDetailModal';
|
||
import type { InvestmentEvent } from '@/types';
|
||
import './InvestmentCalendar.less';
|
||
|
||
// 懒加载投资日历组件
|
||
const InvestmentCalendar = lazy(() => import('@components/InvestmentCalendar'));
|
||
|
||
dayjs.locale('zh-cn');
|
||
|
||
/**
|
||
* FullCalendar 事件类型
|
||
*/
|
||
interface CalendarEvent {
|
||
id: string;
|
||
title: string;
|
||
start: string;
|
||
date: string;
|
||
backgroundColor: string;
|
||
borderColor: string;
|
||
extendedProps: InvestmentEvent & {
|
||
isSystem: boolean;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* CalendarPanel 组件
|
||
* 日历视图面板,显示所有投资事件
|
||
*/
|
||
export const CalendarPanel: React.FC = () => {
|
||
const {
|
||
allEvents,
|
||
borderColor,
|
||
secondaryText,
|
||
setViewMode,
|
||
setListTab,
|
||
} = usePlanningData();
|
||
|
||
// 弹窗状态(统一使用 useState)
|
||
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
||
const [isInvestmentCalendarOpen, setIsInvestmentCalendarOpen] = useState(false);
|
||
|
||
const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
|
||
const [selectedDateEvents, setSelectedDateEvents] = useState<InvestmentEvent[]>([]);
|
||
|
||
// 转换数据为 FullCalendar 格式(使用 useMemo 缓存)
|
||
const calendarEvents: CalendarEvent[] = useMemo(() =>
|
||
allEvents.map(event => ({
|
||
...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,
|
||
isSystem: event.source === 'future',
|
||
}
|
||
})), [allEvents]);
|
||
|
||
// 抽取公共的打开事件详情函数
|
||
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);
|
||
setIsDetailModalOpen(true);
|
||
}, [allEvents]);
|
||
|
||
// 处理日期点击
|
||
const handleDateClick = useCallback((info: DateClickArg): void => {
|
||
openEventDetail(info.date);
|
||
}, [openEventDetail]);
|
||
|
||
// 处理事件点击
|
||
const handleEventClick = useCallback((info: EventClickArg): void => {
|
||
openEventDetail(info.event.start);
|
||
}, [openEventDetail]);
|
||
|
||
return (
|
||
<Box>
|
||
<Box
|
||
height={{ base: '380px', md: '560px' }}
|
||
sx={{
|
||
// FullCalendar 按钮样式覆盖(与日历视图按钮颜色一致)
|
||
'.fc .fc-button': {
|
||
backgroundColor: '#805AD5 !important',
|
||
borderColor: '#805AD5 !important',
|
||
color: '#fff !important',
|
||
'&:hover': {
|
||
backgroundColor: '#6B46C1 !important',
|
||
borderColor: '#6B46C1 !important',
|
||
},
|
||
'&:disabled': {
|
||
backgroundColor: '#6B46C1 !important',
|
||
borderColor: '#6B46C1 !important',
|
||
opacity: '1 !important',
|
||
},
|
||
},
|
||
// 今天日期高亮边框
|
||
'.fc-daygrid-day.fc-day-today': {
|
||
border: '2px solid #805AD5 !important',
|
||
},
|
||
}}
|
||
>
|
||
<FullCalendar
|
||
plugins={[dayGridPlugin, interactionPlugin]}
|
||
initialView="dayGridMonth"
|
||
locale="zh-cn"
|
||
headerToolbar={{
|
||
left: 'prev,next today',
|
||
center: 'title',
|
||
right: ''
|
||
}}
|
||
events={calendarEvents}
|
||
dateClick={handleDateClick}
|
||
eventClick={handleEventClick}
|
||
height="100%"
|
||
dayMaxEvents={1}
|
||
moreLinkText="+更多"
|
||
buttonText={{
|
||
today: '今天',
|
||
month: '月',
|
||
week: '周'
|
||
}}
|
||
titleFormat={{ year: 'numeric', month: 'long' }}
|
||
/>
|
||
</Box>
|
||
|
||
{/* 查看事件详情 Modal */}
|
||
<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);
|
||
}}
|
||
/>
|
||
|
||
{/* 投资日历 Modal */}
|
||
{isInvestmentCalendarOpen && (
|
||
<Modal
|
||
isOpen={isInvestmentCalendarOpen}
|
||
onClose={() => setIsInvestmentCalendarOpen(false)}
|
||
size={{ base: 'full', md: '6xl' }}
|
||
>
|
||
<ModalOverlay />
|
||
<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={{ base: 6, md: 8 }}><Spinner size={{ base: 'lg', md: 'xl' }} color="blue.500" /></Center>}>
|
||
<InvestmentCalendar />
|
||
</Suspense>
|
||
</ModalBody>
|
||
</ModalContent>
|
||
</Modal>
|
||
)}
|
||
|
||
</Box>
|
||
);
|
||
};
|