update pay function
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { useConceptTimelineEvents } from './hooks/useConceptTimelineEvents';
|
import { useConceptTimelineEvents } from './hooks/useConceptTimelineEvents';
|
||||||
import RiskDisclaimer from '../../components/RiskDisclaimer';
|
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 {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
@@ -24,21 +29,28 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
useToast,
|
useToast,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
|
SimpleGrid,
|
||||||
|
Tooltip,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
ViewIcon
|
ViewIcon,
|
||||||
|
CalendarIcon,
|
||||||
} from '@chakra-ui/icons';
|
} from '@chakra-ui/icons';
|
||||||
import {
|
import {
|
||||||
FaChartLine,
|
FaChartLine,
|
||||||
FaArrowUp,
|
FaArrowUp,
|
||||||
FaArrowDown,
|
FaArrowDown,
|
||||||
FaHistory
|
FaHistory,
|
||||||
|
FaNewspaper,
|
||||||
|
FaFileAlt,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { keyframes } from '@emotion/react';
|
import { keyframes } from '@emotion/react';
|
||||||
|
|
||||||
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
// 动画定义
|
// 动画定义
|
||||||
const pulseAnimation = keyframes`
|
const pulseAnimation = keyframes`
|
||||||
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.5; }
|
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.5; }
|
||||||
@@ -79,7 +91,11 @@ const ConceptTimelineModal = ({
|
|||||||
|
|
||||||
const [timelineData, setTimelineData] = useState([]);
|
const [timelineData, setTimelineData] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [expandedDates, setExpandedDates] = useState({});
|
const [selectedDate, setSelectedDate] = useState(null);
|
||||||
|
const [selectedDateData, setSelectedDateData] = useState(null);
|
||||||
|
|
||||||
|
// 日期详情Modal
|
||||||
|
const { isOpen: isDateDetailOpen, onOpen: onDateDetailOpen, onClose: onDateDetailClose } = useDisclosure();
|
||||||
|
|
||||||
// 研报全文Modal相关状态
|
// 研报全文Modal相关状态
|
||||||
const [selectedReport, setSelectedReport] = useState(null);
|
const [selectedReport, setSelectedReport] = useState(null);
|
||||||
@@ -89,6 +105,71 @@ const ConceptTimelineModal = ({
|
|||||||
const [selectedNews, setSelectedNews] = useState(null);
|
const [selectedNews, setSelectedNews] = useState(null);
|
||||||
const [isNewsModalOpen, setIsNewsModalOpen] = useState(false);
|
const [isNewsModalOpen, setIsNewsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// 转换时间轴数据为日历事件格式
|
||||||
|
const calendarEvents = useMemo(() => {
|
||||||
|
return timelineData.map(item => {
|
||||||
|
const priceInfo = getPriceInfo(item.price);
|
||||||
|
const hasEvents = item.events && item.events.length > 0;
|
||||||
|
const newsCount = item.events.filter(e => e.type === 'news').length;
|
||||||
|
const reportCount = item.events.filter(e => e.type === 'report').length;
|
||||||
|
|
||||||
|
// 根据涨跌幅和事件确定颜色
|
||||||
|
let backgroundColor = '#e2e8f0'; // 默认灰色(无数据)
|
||||||
|
if (hasEvents) {
|
||||||
|
backgroundColor = '#9F7AEA'; // 紫色(有事件)
|
||||||
|
} else if (item.price) {
|
||||||
|
if (priceInfo.color === 'red') {
|
||||||
|
backgroundColor = '#FC8181'; // 红色(上涨)
|
||||||
|
} else if (priceInfo.color === 'green') {
|
||||||
|
backgroundColor = '#68D391'; // 绿色(下跌)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.date,
|
||||||
|
title: hasEvents
|
||||||
|
? `📰${newsCount} 📊${reportCount}`
|
||||||
|
: (item.price ? priceInfo.text : ''),
|
||||||
|
date: item.date,
|
||||||
|
backgroundColor,
|
||||||
|
borderColor: backgroundColor,
|
||||||
|
extendedProps: {
|
||||||
|
...item,
|
||||||
|
newsCount,
|
||||||
|
reportCount,
|
||||||
|
priceInfo,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [timelineData]);
|
||||||
|
|
||||||
|
// 处理日期点击
|
||||||
|
const handleDateClick = (info) => {
|
||||||
|
const clickedDate = info.dateStr;
|
||||||
|
const dateData = timelineData.find(item => item.date === clickedDate);
|
||||||
|
|
||||||
|
if (dateData) {
|
||||||
|
setSelectedDate(clickedDate);
|
||||||
|
setSelectedDateData(dateData);
|
||||||
|
onDateDetailOpen();
|
||||||
|
|
||||||
|
// 追踪日期点击
|
||||||
|
trackDateToggled(clickedDate, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理事件点击
|
||||||
|
const handleEventClick = (info) => {
|
||||||
|
const clickedDate = info.event.id;
|
||||||
|
const dateData = timelineData.find(item => item.date === clickedDate);
|
||||||
|
|
||||||
|
if (dateData) {
|
||||||
|
setSelectedDate(clickedDate);
|
||||||
|
setSelectedDateData(dateData);
|
||||||
|
onDateDetailOpen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 获取时间轴数据
|
// 获取时间轴数据
|
||||||
const fetchTimelineData = async () => {
|
const fetchTimelineData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -535,321 +616,121 @@ const ConceptTimelineModal = ({
|
|||||||
</VStack>
|
</VStack>
|
||||||
</Center>
|
</Center>
|
||||||
) : timelineData.length > 0 ? (
|
) : timelineData.length > 0 ? (
|
||||||
<Box position="relative" maxW="1000px" mx="auto" px={4}>
|
<Box position="relative" maxW="1200px" mx="auto" px={4}>
|
||||||
<VStack spacing={6} align="stretch" position="relative">
|
{/* 图例说明 */}
|
||||||
{timelineData.map((item, index) => {
|
<Flex justify="center" mb={4} flexWrap="wrap" gap={3}>
|
||||||
const priceInfo = getPriceInfo(item.price);
|
<HStack spacing={2}>
|
||||||
const hasEvents = item.events.length > 0;
|
<Box w={4} h={4} bg="#9F7AEA" borderRadius="sm" />
|
||||||
const isExpanded = expandedDates[item.date];
|
<Text fontSize="sm">有新闻/研报</Text>
|
||||||
const isLeft = index % 2 === 0; // 偶数项在左,奇数项在右
|
</HStack>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Box w={4} h={4} bg="#FC8181" borderRadius="sm" />
|
||||||
|
<Text fontSize="sm">上涨</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Box w={4} h={4} bg="#68D391" borderRadius="sm" />
|
||||||
|
<Text fontSize="sm">下跌</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Box w={4} h={4} bg="#e2e8f0" borderRadius="sm" />
|
||||||
|
<Text fontSize="sm">无数据</Text>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
return (
|
{/* FullCalendar 日历组件 */}
|
||||||
<Box key={`${item.date}-${index}`} position="relative">
|
|
||||||
<Flex
|
|
||||||
direction={isLeft ? 'row' : 'row-reverse'}
|
|
||||||
align="flex-start"
|
|
||||||
position="relative"
|
|
||||||
gap={4}
|
|
||||||
>
|
|
||||||
{/* 内容区域 */}
|
|
||||||
<Box
|
<Box
|
||||||
flex={1}
|
height={{ base: '600px', md: '700px' }}
|
||||||
maxW="450px"
|
|
||||||
bg="white"
|
bg="white"
|
||||||
p={5}
|
|
||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
boxShadow="lg"
|
boxShadow="lg"
|
||||||
border="2px solid"
|
p={4}
|
||||||
borderColor={hasEvents ? 'purple.200' : 'gray.200'}
|
sx={{
|
||||||
position="relative"
|
// FullCalendar 样式定制
|
||||||
_hover={{
|
'.fc': {
|
||||||
transform: 'translateY(-4px)',
|
height: '100%',
|
||||||
boxShadow: '2xl',
|
},
|
||||||
borderColor: hasEvents ? 'purple.400' : 'gray.300'
|
'.fc-header-toolbar': {
|
||||||
}}
|
marginBottom: '1.5rem',
|
||||||
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
},
|
||||||
_before={{
|
'.fc-toolbar-title': {
|
||||||
content: '""',
|
fontSize: '1.5rem',
|
||||||
position: 'absolute',
|
fontWeight: 'bold',
|
||||||
top: '20px',
|
color: 'purple.600',
|
||||||
[isLeft ? 'right' : 'left']: '-12px',
|
},
|
||||||
width: 0,
|
'.fc-button': {
|
||||||
height: 0,
|
backgroundColor: '#9F7AEA',
|
||||||
borderStyle: 'solid',
|
borderColor: '#9F7AEA',
|
||||||
borderWidth: isLeft ? '12px 12px 12px 0' : '12px 0 12px 12px',
|
color: 'white',
|
||||||
borderColor: isLeft
|
'&:hover': {
|
||||||
? `transparent ${hasEvents ? 'var(--chakra-colors-purple-200)' : 'var(--chakra-colors-gray-200)'} transparent transparent`
|
backgroundColor: '#805AD5',
|
||||||
: `transparent transparent transparent ${hasEvents ? 'var(--chakra-colors-purple-200)' : 'var(--chakra-colors-gray-200)'}`
|
borderColor: '#805AD5',
|
||||||
|
},
|
||||||
|
'&:active, &:focus': {
|
||||||
|
backgroundColor: '#6B46C1',
|
||||||
|
borderColor: '#6B46C1',
|
||||||
|
boxShadow: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'.fc-button-active': {
|
||||||
|
backgroundColor: '#6B46C1',
|
||||||
|
borderColor: '#6B46C1',
|
||||||
|
},
|
||||||
|
'.fc-daygrid-day': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'purple.50',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'.fc-daygrid-day-number': {
|
||||||
|
padding: '4px',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
},
|
||||||
|
'.fc-event': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: 'none',
|
||||||
|
padding: '2px 4px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
borderRadius: '4px',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.05)',
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'.fc-daygrid-event-harness': {
|
||||||
|
marginBottom: '2px',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<VStack align="stretch" spacing={3}>
|
<FullCalendar
|
||||||
{/* 日期标签 */}
|
plugins={[dayGridPlugin, interactionPlugin]}
|
||||||
<HStack justify="space-between" flexWrap="wrap">
|
initialView="dayGridMonth"
|
||||||
<Badge
|
locale="zh-cn"
|
||||||
colorScheme="purple"
|
headerToolbar={{
|
||||||
variant="subtle"
|
left: 'prev,next today',
|
||||||
px={3}
|
center: 'title',
|
||||||
py={1}
|
right: '',
|
||||||
borderRadius="full"
|
}}
|
||||||
fontSize="xs"
|
events={calendarEvents}
|
||||||
fontWeight="bold"
|
dateClick={handleDateClick}
|
||||||
>
|
eventClick={handleEventClick}
|
||||||
{formatDateDisplay(item.date)}
|
height="100%"
|
||||||
</Badge>
|
dayMaxEvents={3}
|
||||||
|
moreLinkText="更多"
|
||||||
{item.price && (
|
buttonText={{
|
||||||
<Badge
|
today: '今天',
|
||||||
colorScheme={priceInfo.color}
|
month: '月',
|
||||||
variant="solid"
|
week: '周',
|
||||||
px={3}
|
}}
|
||||||
py={1}
|
eventDisplay="block"
|
||||||
borderRadius="full"
|
displayEventTime={false}
|
||||||
fontSize="sm"
|
|
||||||
>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
{priceInfo.icon && (
|
|
||||||
<Icon as={priceInfo.icon} boxSize={3} />
|
|
||||||
)}
|
|
||||||
<Text>{priceInfo.text}</Text>
|
|
||||||
</HStack>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
{/* 股票数量 */}
|
|
||||||
{item.price && item.price.stock_count && (
|
|
||||||
<Text fontSize="xs" color="gray.600">
|
|
||||||
📊 统计股票: {item.price.stock_count} 只
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 事件信息 */}
|
|
||||||
{hasEvents ? (
|
|
||||||
<Box>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
colorScheme="purple"
|
|
||||||
width="full"
|
|
||||||
justifyContent="space-between"
|
|
||||||
rightIcon={
|
|
||||||
<Icon
|
|
||||||
as={isExpanded ? ChevronDownIcon : ChevronRightIcon}
|
|
||||||
transition="transform 0.2s"
|
|
||||||
/>
|
/>
|
||||||
}
|
|
||||||
onClick={() => toggleDateExpand(item.date)}
|
|
||||||
>
|
|
||||||
<HStack spacing={2} fontSize="sm">
|
|
||||||
{item.events.filter(e => e.type === 'news').length > 0 && (
|
|
||||||
<Badge colorScheme="blue" variant="solid">
|
|
||||||
📰 {item.events.filter(e => e.type === 'news').length}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{item.events.filter(e => e.type === 'report').length > 0 && (
|
|
||||||
<Badge colorScheme="green" variant="solid">
|
|
||||||
📊 {item.events.filter(e => e.type === 'report').length}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Collapse in={isExpanded} animateOpacity>
|
|
||||||
<VStack
|
|
||||||
align="stretch"
|
|
||||||
spacing={3}
|
|
||||||
mt={3}
|
|
||||||
maxH="300px"
|
|
||||||
overflowY="auto"
|
|
||||||
css={{
|
|
||||||
'&::-webkit-scrollbar': { width: '6px' },
|
|
||||||
'&::-webkit-scrollbar-track': { background: '#f1f1f1', borderRadius: '10px' },
|
|
||||||
'&::-webkit-scrollbar-thumb': { background: '#c1c1c1', borderRadius: '10px' },
|
|
||||||
'&::-webkit-scrollbar-thumb:hover': { background: '#a8a8a8' }
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.events.map((event, eventIdx) => (
|
|
||||||
<Box
|
|
||||||
key={eventIdx}
|
|
||||||
p={3}
|
|
||||||
bg={event.type === 'news' ? 'blue.50' : 'green.50'}
|
|
||||||
borderRadius="md"
|
|
||||||
borderLeft="3px solid"
|
|
||||||
borderLeftColor={event.type === 'news' ? 'blue.400' : 'green.400'}
|
|
||||||
_hover={{ transform: 'translateX(4px)' }}
|
|
||||||
transition="all 0.2s"
|
|
||||||
>
|
|
||||||
<VStack align="start" spacing={2}>
|
|
||||||
<HStack spacing={2} flexWrap="wrap">
|
|
||||||
<Badge
|
|
||||||
colorScheme={event.type === 'news' ? 'blue' : 'green'}
|
|
||||||
variant="solid"
|
|
||||||
>
|
|
||||||
{event.type === 'news' ? '新闻' : '研报'}
|
|
||||||
</Badge>
|
|
||||||
{event.source && (
|
|
||||||
<Badge variant="subtle">{event.source}</Badge>
|
|
||||||
)}
|
|
||||||
{event.publisher && (
|
|
||||||
<Badge colorScheme="purple" variant="subtle">
|
|
||||||
{event.publisher}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{event.rating && (
|
|
||||||
<Badge colorScheme="orange" variant="solid">
|
|
||||||
{event.rating}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
<Text fontWeight="bold" fontSize="sm" color="gray.800">
|
|
||||||
{event.title}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="xs" color="gray.600" noOfLines={3}>
|
|
||||||
{event.content || '暂无内容'}
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant="link"
|
|
||||||
colorScheme="blue"
|
|
||||||
leftIcon={<ViewIcon />}
|
|
||||||
onClick={() => {
|
|
||||||
if (event.type === 'news') {
|
|
||||||
trackNewsClicked(event, item.date);
|
|
||||||
trackNewsDetailOpened(event);
|
|
||||||
setSelectedNews({
|
|
||||||
title: event.title,
|
|
||||||
content: event.content,
|
|
||||||
source: event.source,
|
|
||||||
time: event.time,
|
|
||||||
url: event.url
|
|
||||||
});
|
|
||||||
setIsNewsModalOpen(true);
|
|
||||||
} else if (event.type === 'report') {
|
|
||||||
trackReportClicked(event, item.date);
|
|
||||||
trackReportDetailOpened(event);
|
|
||||||
setSelectedReport({
|
|
||||||
title: event.title,
|
|
||||||
content: event.content,
|
|
||||||
publisher: event.publisher,
|
|
||||||
author: event.author,
|
|
||||||
time: event.time,
|
|
||||||
rating: event.rating,
|
|
||||||
security_name: event.security_name,
|
|
||||||
content_url: event.content_url
|
|
||||||
});
|
|
||||||
setIsReportModalOpen(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
查看详情
|
|
||||||
</Button>
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
</Collapse>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Text fontSize="sm" color="gray.400" fontStyle="italic" textAlign="center" py={2}>
|
|
||||||
📭 当日无相关资讯
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 时间轴节点 */}
|
{/* 底部说明 */}
|
||||||
<Box
|
<Box textAlign="center" mt={6}>
|
||||||
position="relative"
|
|
||||||
zIndex={2}
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
w={hasEvents ? 16 : 12}
|
|
||||||
h={hasEvents ? 16 : 12}
|
|
||||||
borderRadius="full"
|
|
||||||
bgGradient={
|
|
||||||
hasEvents
|
|
||||||
? 'linear(135deg, purple.400, pink.400)'
|
|
||||||
: item.price
|
|
||||||
? (priceInfo.color === 'red'
|
|
||||||
? 'linear(135deg, red.400, red.500)'
|
|
||||||
: priceInfo.color === 'green'
|
|
||||||
? 'linear(135deg, green.400, green.500)'
|
|
||||||
: 'linear(135deg, gray.300, gray.400)')
|
|
||||||
: 'linear(135deg, gray.200, gray.300)'
|
|
||||||
}
|
|
||||||
border="4px solid white"
|
|
||||||
boxShadow="0 4px 12px rgba(0,0,0,0.15)"
|
|
||||||
position="relative"
|
|
||||||
cursor={hasEvents ? 'pointer' : 'default'}
|
|
||||||
onClick={() => hasEvents && toggleDateExpand(item.date)}
|
|
||||||
_hover={hasEvents ? {
|
|
||||||
transform: 'scale(1.15)',
|
|
||||||
boxShadow: '0 6px 20px rgba(139, 92, 246, 0.4)'
|
|
||||||
} : {}}
|
|
||||||
transition="all 0.3s"
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
>
|
|
||||||
{hasEvents && (
|
|
||||||
<>
|
|
||||||
<Badge
|
|
||||||
position="absolute"
|
|
||||||
top="-1"
|
|
||||||
right="-1"
|
|
||||||
colorScheme="red"
|
|
||||||
borderRadius="full"
|
|
||||||
minW="22px"
|
|
||||||
h="22px"
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
fontSize="xs"
|
|
||||||
fontWeight="bold"
|
|
||||||
boxShadow="md"
|
|
||||||
border="2px solid white"
|
|
||||||
>
|
|
||||||
{item.events.length}
|
|
||||||
</Badge>
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
top="50%"
|
|
||||||
left="50%"
|
|
||||||
transform="translate(-50%, -50%)"
|
|
||||||
w="100%"
|
|
||||||
h="100%"
|
|
||||||
borderRadius="full"
|
|
||||||
border="2px solid"
|
|
||||||
borderColor="purple.300"
|
|
||||||
opacity={0.5}
|
|
||||||
animation={`${pulseAnimation} 2s infinite`}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 连接线到下一个节点 */}
|
|
||||||
{index < timelineData.length - 1 && (
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
top="100%"
|
|
||||||
left="50%"
|
|
||||||
transform="translateX(-50%)"
|
|
||||||
width="2px"
|
|
||||||
height="40px"
|
|
||||||
bgGradient="linear(to-b, purple.300, pink.300)"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</VStack>
|
|
||||||
|
|
||||||
{/* 时间轴结束标记 */}
|
|
||||||
<Box textAlign="center" py={8}>
|
|
||||||
<Badge
|
<Badge
|
||||||
colorScheme="purple"
|
colorScheme="purple"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
@@ -888,6 +769,172 @@ const ConceptTimelineModal = ({
|
|||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 日期详情 Modal */}
|
||||||
|
{isDateDetailOpen && selectedDateData && (
|
||||||
|
<Modal
|
||||||
|
isOpen={isDateDetailOpen}
|
||||||
|
onClose={onDateDetailClose}
|
||||||
|
size="4xl"
|
||||||
|
scrollBehavior="inside"
|
||||||
|
>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader bgGradient="linear(to-r, purple.500, pink.500)" color="white">
|
||||||
|
<VStack align="start" spacing={2}>
|
||||||
|
<HStack>
|
||||||
|
<Icon as={CalendarIcon} />
|
||||||
|
<Text>{formatDateDisplay(selectedDate)}</Text>
|
||||||
|
</HStack>
|
||||||
|
{selectedDateData.price && (
|
||||||
|
<HStack spacing={3} fontSize="sm">
|
||||||
|
<Badge colorScheme={getPriceInfo(selectedDateData.price).color} variant="solid" px={3} py={1}>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon
|
||||||
|
as={getPriceInfo(selectedDateData.price).icon}
|
||||||
|
boxSize={3}
|
||||||
|
/>
|
||||||
|
<Text>{getPriceInfo(selectedDateData.price).text}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Badge>
|
||||||
|
{selectedDateData.price.stock_count && (
|
||||||
|
<Text opacity={0.9}>
|
||||||
|
📊 统计股票: {selectedDateData.price.stock_count} 只
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton color="white" />
|
||||||
|
|
||||||
|
<ModalBody py={6}>
|
||||||
|
{selectedDateData.events && selectedDateData.events.length > 0 ? (
|
||||||
|
<VStack align="stretch" spacing={4}>
|
||||||
|
{selectedDateData.events.map((event, eventIdx) => (
|
||||||
|
<Box
|
||||||
|
key={eventIdx}
|
||||||
|
p={4}
|
||||||
|
bg={event.type === 'news' ? 'blue.50' : 'green.50'}
|
||||||
|
borderRadius="lg"
|
||||||
|
borderLeft="4px solid"
|
||||||
|
borderLeftColor={event.type === 'news' ? 'blue.400' : 'green.400'}
|
||||||
|
_hover={{
|
||||||
|
transform: 'translateX(4px)',
|
||||||
|
boxShadow: 'md',
|
||||||
|
}}
|
||||||
|
transition="all 0.2s"
|
||||||
|
>
|
||||||
|
<VStack align="start" spacing={3}>
|
||||||
|
<HStack spacing={2} flexWrap="wrap">
|
||||||
|
<Badge
|
||||||
|
colorScheme={event.type === 'news' ? 'blue' : 'green'}
|
||||||
|
variant="solid"
|
||||||
|
fontSize="sm"
|
||||||
|
>
|
||||||
|
{event.type === 'news' ? '📰 新闻' : '📊 研报'}
|
||||||
|
</Badge>
|
||||||
|
{event.source && (
|
||||||
|
<Badge variant="subtle" fontSize="xs">
|
||||||
|
{event.source}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{event.publisher && (
|
||||||
|
<Badge colorScheme="purple" variant="subtle" fontSize="xs">
|
||||||
|
{event.publisher}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{event.rating && (
|
||||||
|
<Badge colorScheme="orange" variant="solid" fontSize="xs">
|
||||||
|
{event.rating}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{event.security_name && (
|
||||||
|
<Badge colorScheme="cyan" variant="subtle" fontSize="xs">
|
||||||
|
{event.security_name}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Text fontWeight="bold" fontSize="md" color="gray.800">
|
||||||
|
{event.title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text fontSize="sm" color="gray.600" noOfLines={4}>
|
||||||
|
{event.content || '暂无内容'}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{event.time && (
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
🕐 {formatDateTime(event.time)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="outline"
|
||||||
|
leftIcon={<ViewIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
if (event.type === 'news') {
|
||||||
|
trackNewsClicked(event, selectedDate);
|
||||||
|
trackNewsDetailOpened(event);
|
||||||
|
setSelectedNews({
|
||||||
|
title: event.title,
|
||||||
|
content: event.content,
|
||||||
|
source: event.source,
|
||||||
|
time: event.time,
|
||||||
|
url: event.url,
|
||||||
|
});
|
||||||
|
setIsNewsModalOpen(true);
|
||||||
|
} else if (event.type === 'report') {
|
||||||
|
trackReportClicked(event, selectedDate);
|
||||||
|
trackReportDetailOpened(event);
|
||||||
|
setSelectedReport({
|
||||||
|
title: event.title,
|
||||||
|
content: event.content,
|
||||||
|
publisher: event.publisher,
|
||||||
|
author: event.author,
|
||||||
|
time: event.time,
|
||||||
|
rating: event.rating,
|
||||||
|
security_name: event.security_name,
|
||||||
|
content_url: event.content_url,
|
||||||
|
});
|
||||||
|
setIsReportModalOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
查看全文
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
) : (
|
||||||
|
<Center py={12}>
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<Icon as={FaHistory} boxSize={16} color="gray.300" />
|
||||||
|
<Text fontSize="lg" color="gray.500">
|
||||||
|
当日无新闻或研报
|
||||||
|
</Text>
|
||||||
|
{selectedDateData.price && (
|
||||||
|
<Text fontSize="sm" color="gray.400">
|
||||||
|
仅有涨跌幅数据
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter borderTop="1px solid" borderColor="gray.200">
|
||||||
|
<Button colorScheme="purple" onClick={onDateDetailClose}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 研报全文Modal */}
|
{/* 研报全文Modal */}
|
||||||
{isReportModalOpen && (
|
{isReportModalOpen && (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
Reference in New Issue
Block a user