refactor(Concept): ConceptTimelineModal 迁移到 BaseCalendar

This commit is contained in:
zdl
2025-12-23 17:44:48 +08:00
parent 12a57f2fa2
commit d9dbf65e7d

View File

@@ -2,11 +2,10 @@ import React, { useState, useEffect, useMemo } from 'react';
import { logger } from '../../utils/logger';
import { useConceptTimelineEvents } from './hooks/useConceptTimelineEvents';
import RiskDisclaimer from '../../components/RiskDisclaimer';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
// 使用新的公共日历组件
import { BaseCalendar } from '@components/Calendar';
import {
Modal,
ModalOverlay,
@@ -198,93 +197,19 @@ const ConceptTimelineModal = ({
}
};
// 转换时间轴数据为日历事件格式(一天拆分为多个独立事件
const calendarEvents = useMemo(() => {
const events = [];
// 按日期索引的事件数据(用于日历单元格渲染
const eventsByDate = useMemo(() => {
const map = {};
timelineData.forEach(item => {
const priceInfo = getPriceInfo(item.price);
const newsCount = (item.events || []).filter(e => e.type === 'news').length;
const reportCount = (item.events || []).filter(e => e.type === 'report').length;
const hasPriceData = item.price && item.price.avg_change_pct !== null;
// 如果有新闻,添加新闻事件
if (newsCount > 0) {
events.push({
id: `${item.date}-news`,
title: `📰 ${newsCount} 条新闻`,
date: item.date,
start: item.date,
backgroundColor: '#9F7AEA',
borderColor: '#9F7AEA',
extendedProps: {
eventType: 'news',
count: newsCount,
originalData: item,
}
map[item.date] = item;
});
}
// 如果有研报,添加研报事件
if (reportCount > 0) {
events.push({
id: `${item.date}-report`,
title: `📊 ${reportCount} 篇研报`,
date: item.date,
start: item.date,
backgroundColor: '#805AD5',
borderColor: '#805AD5',
extendedProps: {
eventType: 'report',
count: reportCount,
originalData: item,
}
});
}
// 如果有价格数据,添加价格事件
if (hasPriceData) {
const changePercent = item.price.avg_change_pct;
const isSignificantRise = changePercent >= 3; // 涨幅 >= 3% 为重大利好
let bgColor = '#e2e8f0';
let title = priceInfo.text;
if (priceInfo.color === 'red') {
if (isSignificantRise) {
// 涨幅 >= 3%,使用醒目的橙红色 + 火焰图标
bgColor = '#F56565'; // 更深的红色
title = `🔥 ${priceInfo.text}`;
} else {
bgColor = '#FC8181'; // 普通红色(上涨)
}
} else if (priceInfo.color === 'green') {
bgColor = '#68D391'; // 绿色(下跌)
}
events.push({
id: `${item.date}-price`,
title: title,
date: item.date,
start: item.date,
backgroundColor: bgColor,
borderColor: isSignificantRise ? '#C53030' : bgColor, // 深红色边框强调
extendedProps: {
eventType: 'price',
priceInfo,
originalData: item,
isSignificantRise, // 标记重大涨幅
}
});
}
});
return events;
return map;
}, [timelineData]);
// 处理日期点击
const handleDateClick = (info) => {
const clickedDate = info.dateStr;
const dateData = timelineData.find(item => item.date === clickedDate);
// 处理日期选择(点击日期单元格)
const handleDateSelect = (date) => {
const clickedDate = date.format('YYYY-MM-DD');
const dateData = eventsByDate[clickedDate];
if (dateData) {
setSelectedDate(clickedDate);
@@ -296,16 +221,128 @@ const ConceptTimelineModal = ({
}
};
// 处理事件点击
const handleEventClick = (info) => {
// 从事件的 extendedProps 中获取原始数据
const dateData = info.event.extendedProps?.originalData;
// 自定义日期单元格内容渲染
const renderCellContent = (date) => {
const dateStr = date.format('YYYY-MM-DD');
const item = eventsByDate[dateStr];
if (dateData) {
setSelectedDate(dateData.date);
setSelectedDateData(dateData);
onDateDetailOpen();
if (!item) return null;
const priceInfo = getPriceInfo(item.price);
const newsCount = (item.events || []).filter(e => e.type === 'news').length;
const reportCount = (item.events || []).filter(e => e.type === 'report').length;
const hasPriceData = item.price && item.price.avg_change_pct !== null;
const events = [];
// 新闻事件
if (newsCount > 0) {
events.push(
<HStack
key="news"
spacing={1}
fontSize="10px"
color="#9F7AEA"
cursor="pointer"
px={1}
py={0.5}
borderRadius="sm"
bg="rgba(159, 122, 234, 0.2)"
_hover={{ bg: 'rgba(159, 122, 234, 0.3)' }}
w="100%"
overflow="hidden"
>
<Text fontWeight="600" fontSize="10px" isTruncated flex="1" minW={0}>
📰 {newsCount} 条新闻
</Text>
</HStack>
);
}
// 研报事件
if (reportCount > 0) {
events.push(
<HStack
key="report"
spacing={1}
fontSize="10px"
color="#805AD5"
cursor="pointer"
px={1}
py={0.5}
borderRadius="sm"
bg="rgba(128, 90, 213, 0.2)"
_hover={{ bg: 'rgba(128, 90, 213, 0.3)' }}
w="100%"
overflow="hidden"
>
<Text fontWeight="600" fontSize="10px" isTruncated flex="1" minW={0}>
📊 {reportCount} 篇研报
</Text>
</HStack>
);
}
// 涨跌数据
if (hasPriceData) {
const changePercent = item.price.avg_change_pct;
const isSignificantRise = changePercent >= 3;
let bgColor = 'rgba(226, 232, 240, 0.2)';
let textColor = '#e2e8f0';
let title = priceInfo.text;
if (priceInfo.color === 'red') {
if (isSignificantRise) {
bgColor = 'rgba(245, 101, 101, 0.3)';
textColor = '#F56565';
title = `🔥 ${priceInfo.text}`;
} else {
bgColor = 'rgba(252, 129, 129, 0.2)';
textColor = '#FC8181';
}
} else if (priceInfo.color === 'green') {
bgColor = 'rgba(104, 211, 145, 0.2)';
textColor = '#68D391';
}
events.push(
<HStack
key="price"
spacing={1}
fontSize="10px"
color={textColor}
cursor="pointer"
px={1}
py={0.5}
borderRadius="sm"
bg={bgColor}
border={isSignificantRise ? '1px solid #C53030' : 'none'}
_hover={{ opacity: 0.8 }}
w="100%"
overflow="hidden"
>
<Text fontWeight="bold" fontSize="10px" isTruncated flex="1" minW={0}>
{title}
</Text>
</HStack>
);
}
// 最多显示 3 个事件,超出显示 "更多"
const maxDisplay = 3;
const displayEvents = events.slice(0, maxDisplay);
const remainingCount = events.length - maxDisplay;
return (
<VStack spacing={0.5} align="stretch" w="100%" mt={1}>
{displayEvents}
{remainingCount > 0 && (
<Text fontSize="9px" color="whiteAlpha.600" px={1}>
+{remainingCount} 更多
</Text>
)}
</VStack>
);
};
// 获取时间轴数据
@@ -833,7 +870,7 @@ const ConceptTimelineModal = ({
</HStack>
</Flex>
{/* FullCalendar 日历组件 */}
{/* Ant Design 日历组件 */}
<Box
height={{ base: '500px', md: '700px' }}
bg="rgba(15, 23, 42, 0.6)"
@@ -841,129 +878,12 @@ const ConceptTimelineModal = ({
border="1px solid"
borderColor="whiteAlpha.100"
p={{ base: 1, md: 4 }}
sx={{
// FullCalendar 深色主题样式定制
'.fc': {
height: '100%',
},
'.fc-header-toolbar': {
marginBottom: { base: '0.5rem', md: '1.5rem' },
padding: { base: '0 4px', md: '0' },
flexWrap: 'nowrap',
gap: { base: '4px', md: '8px' },
},
'.fc-toolbar-chunk': {
display: 'flex',
alignItems: 'center',
},
'.fc-toolbar-title': {
fontSize: { base: '1rem', md: '1.5rem' },
fontWeight: 'bold',
color: 'white',
},
'.fc-button': {
backgroundColor: 'rgba(139, 92, 246, 0.6)',
borderColor: 'rgba(139, 92, 246, 0.8)',
color: 'white',
padding: { base: '4px 8px', md: '6px 12px' },
fontSize: { base: '12px', md: '14px' },
'&:hover': {
backgroundColor: 'rgba(139, 92, 246, 0.8)',
borderColor: 'rgba(139, 92, 246, 1)',
},
'&:active, &:focus': {
backgroundColor: 'rgba(139, 92, 246, 1)',
borderColor: 'rgba(139, 92, 246, 1)',
boxShadow: 'none',
},
},
'.fc-button-active': {
backgroundColor: 'rgba(139, 92, 246, 1)',
borderColor: 'rgba(139, 92, 246, 1)',
},
// 深色主题 - 表格边框和背景
'.fc-theme-standard td, .fc-theme-standard th': {
borderColor: 'rgba(255, 255, 255, 0.1)',
},
'.fc-theme-standard .fc-scrollgrid': {
borderColor: 'rgba(255, 255, 255, 0.1)',
},
'.fc-col-header-cell': {
backgroundColor: 'rgba(15, 23, 42, 0.8)',
},
'.fc-col-header-cell-cushion': {
color: 'rgba(255, 255, 255, 0.8)',
fontSize: { base: '0.75rem', md: '0.875rem' },
padding: { base: '4px 2px', md: '8px' },
},
'.fc-daygrid-day': {
cursor: 'pointer',
transition: 'all 0.2s',
backgroundColor: 'transparent',
'&:hover': {
backgroundColor: 'rgba(139, 92, 246, 0.2)',
},
},
'.fc-daygrid-day-number': {
color: 'rgba(255, 255, 255, 0.9)',
padding: { base: '2px', md: '4px' },
fontSize: { base: '0.75rem', md: '0.875rem' },
},
'.fc-day-today': {
backgroundColor: 'rgba(139, 92, 246, 0.15) !important',
},
'.fc-day-other .fc-daygrid-day-number': {
color: 'rgba(255, 255, 255, 0.4)',
},
'.fc-event': {
cursor: 'pointer',
border: 'none',
padding: { base: '1px 2px', md: '2px 4px' },
fontSize: { base: '0.65rem', md: '0.75rem' },
fontWeight: 'bold',
borderRadius: '4px',
transition: 'all 0.2s',
'&:hover': {
transform: 'scale(1.05)',
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
},
},
'.fc-daygrid-event-harness': {
marginBottom: { base: '1px', md: '2px' },
},
'.fc-more-link': {
color: 'rgba(255, 255, 255, 0.8)',
},
// H5 端隐藏事件文字,只显示色块
'@media (max-width: 768px)': {
'.fc-event-title': {
fontSize: '0.6rem',
},
},
}}
>
<FullCalendar
plugins={[dayGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
locale="zh-cn"
headerToolbar={{
left: 'prev,next today',
center: 'title',
right: '',
}}
events={calendarEvents}
dateClick={handleDateClick}
eventClick={handleEventClick}
<BaseCalendar
onSelect={handleDateSelect}
cellRender={renderCellContent}
height="100%"
dayMaxEvents={3}
moreLinkText="更多"
buttonText={{
today: '今天',
month: '月',
week: '周',
}}
eventDisplay="block"
displayEventTime={false}
showToolbar={true}
/>
</Box>
</Box>