update pay function

This commit is contained in:
2025-11-23 12:31:34 +08:00
parent 0bcf6a93f7
commit 6515a47a42

View File

@@ -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"> <Box
<Flex height={{ base: '600px', md: '700px' }}
direction={isLeft ? 'row' : 'row-reverse'} bg="white"
align="flex-start" borderRadius="xl"
position="relative" boxShadow="lg"
gap={4} p={4}
> sx={{
{/* 内容区域 */} // FullCalendar 样式定制
<Box '.fc': {
flex={1} height: '100%',
maxW="450px" },
bg="white" '.fc-header-toolbar': {
p={5} marginBottom: '1.5rem',
borderRadius="xl" },
boxShadow="lg" '.fc-toolbar-title': {
border="2px solid" fontSize: '1.5rem',
borderColor={hasEvents ? 'purple.200' : 'gray.200'} fontWeight: 'bold',
position="relative" color: 'purple.600',
_hover={{ },
transform: 'translateY(-4px)', '.fc-button': {
boxShadow: '2xl', backgroundColor: '#9F7AEA',
borderColor: hasEvents ? 'purple.400' : 'gray.300' borderColor: '#9F7AEA',
}} color: 'white',
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" '&:hover': {
_before={{ backgroundColor: '#805AD5',
content: '""', borderColor: '#805AD5',
position: 'absolute', },
top: '20px', '&:active, &:focus': {
[isLeft ? 'right' : 'left']: '-12px', backgroundColor: '#6B46C1',
width: 0, borderColor: '#6B46C1',
height: 0, boxShadow: 'none',
borderStyle: 'solid', },
borderWidth: isLeft ? '12px 12px 12px 0' : '12px 0 12px 12px', },
borderColor: isLeft '.fc-button-active': {
? `transparent ${hasEvents ? 'var(--chakra-colors-purple-200)' : 'var(--chakra-colors-gray-200)'} transparent transparent` backgroundColor: '#6B46C1',
: `transparent transparent transparent ${hasEvents ? 'var(--chakra-colors-purple-200)' : 'var(--chakra-colors-gray-200)'}` borderColor: '#6B46C1',
}} },
> '.fc-daygrid-day': {
<VStack align="stretch" spacing={3}> cursor: 'pointer',
{/* 日期标签 */} transition: 'all 0.2s',
<HStack justify="space-between" flexWrap="wrap"> '&:hover': {
<Badge backgroundColor: 'purple.50',
colorScheme="purple" },
variant="subtle" },
px={3} '.fc-daygrid-day-number': {
py={1} padding: '4px',
borderRadius="full" fontSize: '0.875rem',
fontSize="xs" },
fontWeight="bold" '.fc-event': {
> cursor: 'pointer',
{formatDateDisplay(item.date)} border: 'none',
</Badge> 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',
},
}}
>
<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={3}
moreLinkText="更多"
buttonText={{
today: '今天',
month: '月',
week: '周',
}}
eventDisplay="block"
displayEventTime={false}
/>
</Box>
{item.price && ( {/* 底部说明 */}
<Badge <Box textAlign="center" mt={6}>
colorScheme={priceInfo.color}
variant="solid"
px={3}
py={1}
borderRadius="full"
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
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