Compare commits
3 Commits
b1d5b217d3
...
b5d054d89f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5d054d89f | ||
|
|
b66c1585f7 | ||
|
|
5efd598694 |
@@ -7,8 +7,6 @@ import {
|
||||
ModalHeader,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
@@ -22,6 +20,7 @@ import {
|
||||
Icon,
|
||||
Spinner,
|
||||
useColorModeValue,
|
||||
useBreakpointValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaTable } from 'react-icons/fa';
|
||||
import marketService from '@services/marketService';
|
||||
@@ -31,6 +30,8 @@ import { logger } from '@utils/logger';
|
||||
interface StockInfo {
|
||||
stock_code: string;
|
||||
stock_name: string;
|
||||
reason?: string;
|
||||
change_pct?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -72,6 +73,12 @@ const ConceptStocksModal: React.FC<ConceptStocksModalProps> = ({
|
||||
const cardBg = useColorModeValue('white', '#1a1a1a');
|
||||
const hoverBg = useColorModeValue('gray.50', '#2a2a2a');
|
||||
|
||||
// 响应式配置 - 添加 fallback 避免首次渲染时返回 undefined 导致弹窗异常
|
||||
const isMobile = useBreakpointValue({ base: true, md: false }, { fallback: 'md' });
|
||||
// H5 使用 xl 而非 full,配合 maxH 限制高度
|
||||
const modalSize = useBreakpointValue({ base: 'xl', md: '4xl' }, { fallback: 'md' });
|
||||
const tableMaxH = useBreakpointValue({ base: '45vh', md: '60vh' }, { fallback: 'md' });
|
||||
|
||||
// 批量获取股票行情数据
|
||||
const fetchStockMarketData = useCallback(async (stocks: StockInfo[]) => {
|
||||
if (!stocks || stocks.length === 0) return;
|
||||
@@ -131,11 +138,12 @@ const ConceptStocksModal: React.FC<ConceptStocksModalProps> = ({
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size="4xl"
|
||||
size={modalSize}
|
||||
scrollBehavior="inside"
|
||||
isCentered
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent bg={cardBg}>
|
||||
<ModalContent bg={cardBg} maxH={isMobile ? '70vh' : undefined}>
|
||||
<ModalHeader bg="purple.500" color="white" borderTopRadius="md">
|
||||
<HStack>
|
||||
<Icon as={FaTable} />
|
||||
@@ -156,14 +164,15 @@ const ConceptStocksModal: React.FC<ConceptStocksModalProps> = ({
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<TableContainer maxH="60vh" overflowY="auto">
|
||||
<Table variant="simple" size="sm">
|
||||
<TableContainer maxH={tableMaxH} overflowY="auto" overflowX="auto">
|
||||
<Table variant="simple" size="sm" minW={isMobile ? '600px' : undefined}>
|
||||
<Thead position="sticky" top={0} bg={cardBg} zIndex={1}>
|
||||
<Tr>
|
||||
<Th>股票名称</Th>
|
||||
<Th>股票代码</Th>
|
||||
<Th isNumeric>现价</Th>
|
||||
<Th isNumeric>涨跌幅</Th>
|
||||
<Th whiteSpace="nowrap">股票名称</Th>
|
||||
<Th whiteSpace="nowrap">股票代码</Th>
|
||||
<Th isNumeric whiteSpace="nowrap">现价</Th>
|
||||
<Th isNumeric whiteSpace="nowrap">当日涨跌幅</Th>
|
||||
<Th whiteSpace="nowrap" minW="200px">板块原因</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
@@ -210,6 +219,9 @@ const ConceptStocksModal: React.FC<ConceptStocksModalProps> = ({
|
||||
'-'
|
||||
)}
|
||||
</Td>
|
||||
<Td fontSize="xs" color="gray.600" maxW="300px">
|
||||
<Text noOfLines={2}>{stock.reason || '-'}</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
@@ -219,12 +231,6 @@ const ConceptStocksModal: React.FC<ConceptStocksModalProps> = ({
|
||||
</Box>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button colorScheme="purple" onClick={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
130
src/components/TradeDatePicker/index.tsx
Normal file
130
src/components/TradeDatePicker/index.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
HStack,
|
||||
Input,
|
||||
Text,
|
||||
Icon,
|
||||
Tooltip,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { InfoIcon } from '@chakra-ui/icons';
|
||||
import { FaCalendarAlt } from 'react-icons/fa';
|
||||
|
||||
export interface TradeDatePickerProps {
|
||||
/** 当前选中的日期 */
|
||||
value: Date | null;
|
||||
/** 日期变化回调 */
|
||||
onChange: (date: Date) => void;
|
||||
/** 默认日期(组件初始化时使用) */
|
||||
defaultDate?: Date;
|
||||
/** 最新交易日期(用于显示提示) */
|
||||
latestTradeDate?: Date | null;
|
||||
/** 最大可选日期,默认今天 */
|
||||
maxDate?: Date;
|
||||
/** 标签文字,默认"交易日期" */
|
||||
label?: string;
|
||||
/** 输入框宽度 */
|
||||
inputWidth?: string | object;
|
||||
/** 是否显示标签图标 */
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 交易日期选择器组件
|
||||
*
|
||||
* 提供日期输入框和最新交易日期提示,供概念中心、个股中心等页面复用。
|
||||
* 快捷按钮(今天、昨天等)由各页面自行实现。
|
||||
*/
|
||||
const TradeDatePicker: React.FC<TradeDatePickerProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
defaultDate,
|
||||
latestTradeDate,
|
||||
maxDate,
|
||||
label = '交易日期',
|
||||
inputWidth = { base: '100%', lg: '200px' },
|
||||
showIcon = true,
|
||||
}) => {
|
||||
// 颜色主题
|
||||
const labelColor = useColorModeValue('purple.700', 'purple.300');
|
||||
const iconColor = useColorModeValue('purple.500', 'purple.400');
|
||||
const inputBorderColor = useColorModeValue('purple.200', 'purple.600');
|
||||
const tipBg = useColorModeValue('blue.50', 'blue.900');
|
||||
const tipBorderColor = useColorModeValue('blue.200', 'blue.600');
|
||||
const tipTextColor = useColorModeValue('blue.600', 'blue.200');
|
||||
const tipIconColor = useColorModeValue('blue.500', 'blue.300');
|
||||
|
||||
// 使用默认日期初始化(仅在 value 为 null 且有 defaultDate 时)
|
||||
React.useEffect(() => {
|
||||
if (value === null && defaultDate) {
|
||||
onChange(defaultDate);
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 处理日期变化
|
||||
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const dateStr = e.target.value;
|
||||
if (dateStr) {
|
||||
const date = new Date(dateStr);
|
||||
onChange(date);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化日期为 YYYY-MM-DD
|
||||
const formatDateValue = (date: Date | null): string => {
|
||||
if (!date) return '';
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
// 计算最大日期
|
||||
const maxDateStr = maxDate
|
||||
? formatDateValue(maxDate)
|
||||
: new Date().toISOString().split('T')[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 标签 */}
|
||||
<HStack spacing={3}>
|
||||
{showIcon && <Icon as={FaCalendarAlt} color={iconColor} boxSize={5} />}
|
||||
<Text fontWeight="bold" color={labelColor}>
|
||||
{label}:
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 日期输入框 */}
|
||||
<Input
|
||||
type="date"
|
||||
value={formatDateValue(value)}
|
||||
onChange={handleDateChange}
|
||||
max={maxDateStr}
|
||||
width={inputWidth}
|
||||
focusBorderColor="purple.500"
|
||||
borderColor={inputBorderColor}
|
||||
borderRadius="lg"
|
||||
fontWeight="medium"
|
||||
/>
|
||||
|
||||
{/* 最新交易日期提示 */}
|
||||
{latestTradeDate && (
|
||||
<Tooltip label="数据库中最新的交易日期">
|
||||
<HStack
|
||||
spacing={2}
|
||||
bg={tipBg}
|
||||
px={3}
|
||||
py={1.5}
|
||||
borderRadius="full"
|
||||
border="1px solid"
|
||||
borderColor={tipBorderColor}
|
||||
>
|
||||
<Icon as={InfoIcon} color={tipIconColor} boxSize={3} />
|
||||
<Text fontSize="sm" color={tipTextColor} fontWeight="medium">
|
||||
最新: {latestTradeDate.toLocaleDateString('zh-CN')}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradeDatePicker;
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
useDisclosure,
|
||||
SimpleGrid,
|
||||
Tooltip,
|
||||
useBreakpointValue,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
@@ -111,6 +112,9 @@ const ConceptTimelineModal = ({
|
||||
const [selectedNews, setSelectedNews] = useState(null);
|
||||
const [isNewsModalOpen, setIsNewsModalOpen] = useState(false);
|
||||
|
||||
// 响应式配置
|
||||
const isMobile = useBreakpointValue({ base: true, md: false }, { fallback: 'md' });
|
||||
|
||||
// 辅助函数:格式化日期显示(包含年份)
|
||||
const formatDateDisplay = (dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
@@ -602,37 +606,41 @@ const ConceptTimelineModal = ({
|
||||
onClose={onClose}
|
||||
size="full"
|
||||
scrollBehavior="inside"
|
||||
isCentered
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW="1400px" m={4}>
|
||||
<ModalContent maxW="1400px" m={{ base: 0, md: 'auto' }} mx="auto">
|
||||
<ModalHeader
|
||||
bgGradient="linear(135deg, purple.600 0%, purple.500 50%, pink.500 100%)"
|
||||
color="white"
|
||||
position="sticky"
|
||||
top={0}
|
||||
zIndex={10}
|
||||
py={6}
|
||||
py={{ base: 3, md: 6 }}
|
||||
px={{ base: 3, md: 6 }}
|
||||
boxShadow="lg"
|
||||
>
|
||||
<HStack spacing={4} flexWrap="wrap">
|
||||
<HStack spacing={{ base: 2, md: 4 }} flexWrap="wrap">
|
||||
<Icon
|
||||
as={FaChartLine}
|
||||
boxSize={6}
|
||||
boxSize={{ base: 4, md: 6 }}
|
||||
filter="drop-shadow(0 2px 4px rgba(0,0,0,0.2))"
|
||||
/>
|
||||
<Text
|
||||
fontSize="xl"
|
||||
fontSize={{ base: 'md', md: 'xl' }}
|
||||
fontWeight="bold"
|
||||
textShadow="0 2px 4px rgba(0,0,0,0.2)"
|
||||
noOfLines={1}
|
||||
maxW={{ base: '120px', md: 'none' }}
|
||||
>
|
||||
{conceptName} - 历史时间轴
|
||||
</Text>
|
||||
<Badge
|
||||
colorScheme="yellow"
|
||||
px={3}
|
||||
px={{ base: 2, md: 3 }}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
fontSize="sm"
|
||||
fontSize={{ base: 'xs', md: 'sm' }}
|
||||
boxShadow="md"
|
||||
>
|
||||
最近100天
|
||||
@@ -640,20 +648,29 @@ const ConceptTimelineModal = ({
|
||||
<Badge
|
||||
bg="whiteAlpha.300"
|
||||
color="white"
|
||||
px={3}
|
||||
px={{ base: 2, md: 3 }}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
fontSize="xs"
|
||||
backdropFilter="blur(10px)"
|
||||
display={{ base: 'none', sm: 'flex' }}
|
||||
>
|
||||
🔥 Max版功能
|
||||
</Badge>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton color="white" />
|
||||
<ModalCloseButton
|
||||
color="white"
|
||||
size="lg"
|
||||
top={{ base: 2, md: 4 }}
|
||||
right={{ base: 2, md: 4 }}
|
||||
_hover={{ bg: 'whiteAlpha.300' }}
|
||||
zIndex={20}
|
||||
/>
|
||||
|
||||
<ModalBody
|
||||
py={6}
|
||||
py={{ base: 2, md: 6 }}
|
||||
px={{ base: 0, md: 6 }}
|
||||
bg="gray.50"
|
||||
css={{
|
||||
'&::-webkit-scrollbar': {
|
||||
@@ -680,103 +697,116 @@ const ConceptTimelineModal = ({
|
||||
</VStack>
|
||||
</Center>
|
||||
) : timelineData.length > 0 ? (
|
||||
<Box position="relative" maxW="1200px" mx="auto" px={4}>
|
||||
{/* 图例说明 */}
|
||||
<Flex justify="center" mb={6} flexWrap="wrap" gap={4}>
|
||||
<Box position="relative" maxW="1200px" mx="auto" px={{ base: 2, md: 4 }}>
|
||||
{/* 图例说明 - H5端保持一行 */}
|
||||
<Flex
|
||||
justify="center"
|
||||
mb={{ base: 3, md: 6 }}
|
||||
flexWrap={{ base: 'nowrap', md: 'wrap' }}
|
||||
gap={{ base: 1, md: 4 }}
|
||||
overflowX={{ base: 'auto', md: 'visible' }}
|
||||
pb={{ base: 2, md: 0 }}
|
||||
css={{
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
scrollbarWidth: 'none',
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
spacing={2}
|
||||
px={4}
|
||||
py={2}
|
||||
spacing={{ base: 1, md: 2 }}
|
||||
px={{ base: 2, md: 4 }}
|
||||
py={{ base: 1, md: 2 }}
|
||||
bg="purple.50"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor="purple.200"
|
||||
boxShadow="sm"
|
||||
transition="all 0.2s"
|
||||
_hover={{ transform: 'translateY(-2px)', boxShadow: 'md' }}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Box w={3} h={3} bg="#9F7AEA" borderRadius="full" boxShadow="sm" />
|
||||
<Text fontSize="sm" fontWeight="medium" color="gray.700">📰 新闻</Text>
|
||||
<Box w={{ base: 2, md: 3 }} h={{ base: 2, md: 3 }} bg="#9F7AEA" borderRadius="full" />
|
||||
<Text fontSize={{ base: 'xs', md: 'sm' }} fontWeight="medium" color="gray.700" whiteSpace="nowrap">📰 新闻</Text>
|
||||
</HStack>
|
||||
<HStack
|
||||
spacing={2}
|
||||
px={4}
|
||||
py={2}
|
||||
spacing={{ base: 1, md: 2 }}
|
||||
px={{ base: 2, md: 4 }}
|
||||
py={{ base: 1, md: 2 }}
|
||||
bg="purple.50"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor="purple.300"
|
||||
boxShadow="sm"
|
||||
transition="all 0.2s"
|
||||
_hover={{ transform: 'translateY(-2px)', boxShadow: 'md' }}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Box w={3} h={3} bg="#805AD5" borderRadius="full" boxShadow="sm" />
|
||||
<Text fontSize="sm" fontWeight="medium" color="gray.700">📊 研报</Text>
|
||||
<Box w={{ base: 2, md: 3 }} h={{ base: 2, md: 3 }} bg="#805AD5" borderRadius="full" />
|
||||
<Text fontSize={{ base: 'xs', md: 'sm' }} fontWeight="medium" color="gray.700" whiteSpace="nowrap">📊 研报</Text>
|
||||
</HStack>
|
||||
<HStack
|
||||
spacing={2}
|
||||
px={4}
|
||||
py={2}
|
||||
spacing={{ base: 1, md: 2 }}
|
||||
px={{ base: 2, md: 4 }}
|
||||
py={{ base: 1, md: 2 }}
|
||||
bg="red.50"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor="red.200"
|
||||
boxShadow="sm"
|
||||
transition="all 0.2s"
|
||||
_hover={{ transform: 'translateY(-2px)', boxShadow: 'md' }}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Icon as={FaArrowUp} color="red.500" boxSize={3} />
|
||||
<Text fontSize="sm" fontWeight="medium" color="gray.700">上涨</Text>
|
||||
<Icon as={FaArrowUp} color="red.500" boxSize={{ base: 2, md: 3 }} />
|
||||
<Text fontSize={{ base: 'xs', md: 'sm' }} fontWeight="medium" color="gray.700" whiteSpace="nowrap">上涨</Text>
|
||||
</HStack>
|
||||
<HStack
|
||||
spacing={2}
|
||||
px={4}
|
||||
py={2}
|
||||
spacing={{ base: 1, md: 2 }}
|
||||
px={{ base: 2, md: 4 }}
|
||||
py={{ base: 1, md: 2 }}
|
||||
bg="green.50"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor="green.200"
|
||||
boxShadow="sm"
|
||||
transition="all 0.2s"
|
||||
_hover={{ transform: 'translateY(-2px)', boxShadow: 'md' }}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Icon as={FaArrowDown} color="green.500" boxSize={3} />
|
||||
<Text fontSize="sm" fontWeight="medium" color="gray.700">下跌</Text>
|
||||
<Icon as={FaArrowDown} color="green.500" boxSize={{ base: 2, md: 3 }} />
|
||||
<Text fontSize={{ base: 'xs', md: 'sm' }} fontWeight="medium" color="gray.700" whiteSpace="nowrap">下跌</Text>
|
||||
</HStack>
|
||||
<HStack
|
||||
spacing={2}
|
||||
px={4}
|
||||
py={2}
|
||||
spacing={{ base: 1, md: 2 }}
|
||||
px={{ base: 2, md: 4 }}
|
||||
py={{ base: 1, md: 2 }}
|
||||
bg="orange.50"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor="orange.200"
|
||||
boxShadow="sm"
|
||||
transition="all 0.2s"
|
||||
_hover={{ transform: 'translateY(-2px)', boxShadow: 'md' }}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text fontSize="sm" fontWeight="bold">🔥</Text>
|
||||
<Text fontSize="sm" fontWeight="medium" color="gray.700">涨3%+</Text>
|
||||
<Text fontSize={{ base: 'xs', md: 'sm' }} fontWeight="bold">🔥</Text>
|
||||
<Text fontSize={{ base: 'xs', md: 'sm' }} fontWeight="medium" color="gray.700" whiteSpace="nowrap">涨3%+</Text>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{/* FullCalendar 日历组件 */}
|
||||
<Box
|
||||
height={{ base: '600px', md: '700px' }}
|
||||
height={{ base: '500px', md: '700px' }}
|
||||
bg="white"
|
||||
borderRadius="xl"
|
||||
boxShadow="lg"
|
||||
p={4}
|
||||
borderRadius={{ base: 'none', md: 'xl' }}
|
||||
boxShadow={{ base: 'none', md: 'lg' }}
|
||||
p={{ base: 1, md: 4 }}
|
||||
sx={{
|
||||
// FullCalendar 样式定制
|
||||
'.fc': {
|
||||
height: '100%',
|
||||
},
|
||||
'.fc-header-toolbar': {
|
||||
marginBottom: '1.5rem',
|
||||
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: '1.5rem',
|
||||
fontSize: { base: '1rem', md: '1.5rem' },
|
||||
fontWeight: 'bold',
|
||||
color: 'purple.600',
|
||||
},
|
||||
@@ -784,6 +814,8 @@ const ConceptTimelineModal = ({
|
||||
backgroundColor: '#9F7AEA',
|
||||
borderColor: '#9F7AEA',
|
||||
color: 'white',
|
||||
padding: { base: '4px 8px', md: '6px 12px' },
|
||||
fontSize: { base: '12px', md: '14px' },
|
||||
'&:hover': {
|
||||
backgroundColor: '#805AD5',
|
||||
borderColor: '#805AD5',
|
||||
@@ -806,14 +838,18 @@ const ConceptTimelineModal = ({
|
||||
},
|
||||
},
|
||||
'.fc-daygrid-day-number': {
|
||||
padding: '4px',
|
||||
fontSize: '0.875rem',
|
||||
padding: { base: '2px', md: '4px' },
|
||||
fontSize: { base: '0.75rem', md: '0.875rem' },
|
||||
},
|
||||
'.fc-col-header-cell-cushion': {
|
||||
fontSize: { base: '0.75rem', md: '0.875rem' },
|
||||
padding: { base: '4px 2px', md: '8px' },
|
||||
},
|
||||
'.fc-event': {
|
||||
cursor: 'pointer',
|
||||
border: 'none',
|
||||
padding: '2px 4px',
|
||||
fontSize: '0.75rem',
|
||||
padding: { base: '1px 2px', md: '2px 4px' },
|
||||
fontSize: { base: '0.65rem', md: '0.75rem' },
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '4px',
|
||||
transition: 'all 0.2s',
|
||||
@@ -823,7 +859,13 @@ const ConceptTimelineModal = ({
|
||||
},
|
||||
},
|
||||
'.fc-daygrid-event-harness': {
|
||||
marginBottom: '2px',
|
||||
marginBottom: { base: '1px', md: '2px' },
|
||||
},
|
||||
// H5 端隐藏事件文字,只显示色块
|
||||
'@media (max-width: 768px)': {
|
||||
'.fc-event-title': {
|
||||
fontSize: '0.6rem',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -882,32 +924,11 @@ const ConceptTimelineModal = ({
|
||||
)}
|
||||
|
||||
{/* 风险提示 */}
|
||||
<Box px={6}>
|
||||
<Box px={{ base: 2, md: 6 }}>
|
||||
<RiskDisclaimer variant="default" />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter
|
||||
borderTop="2px solid"
|
||||
borderColor="purple.100"
|
||||
bg="gray.50"
|
||||
py={4}
|
||||
>
|
||||
<Button
|
||||
colorScheme="purple"
|
||||
size="lg"
|
||||
px={8}
|
||||
onClick={onClose}
|
||||
boxShadow="md"
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: 'lg',
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
@@ -86,6 +86,8 @@ import { BsGraphUp, BsLightningFill } from 'react-icons/bs';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import ConceptTimelineModal from './ConceptTimelineModal';
|
||||
import ConceptStatsPanel from './components/ConceptStatsPanel';
|
||||
import ConceptStocksModal from '@components/ConceptStocksModal';
|
||||
import TradeDatePicker from '@components/TradeDatePicker';
|
||||
// 导航栏已由 MainLayout 提供,无需在此导入
|
||||
// 导入订阅权限管理
|
||||
import { useSubscription } from '../../hooks/useSubscription';
|
||||
@@ -528,109 +530,6 @@ const ConceptCenter = () => {
|
||||
return `https://valuefrontier.cn/company?scode=${seccode}`;
|
||||
};
|
||||
|
||||
// 渲染动态表格列
|
||||
const renderStockTable = () => {
|
||||
if (!selectedConceptStocks || selectedConceptStocks.length === 0) {
|
||||
return <Text>暂无相关股票数据</Text>;
|
||||
}
|
||||
|
||||
const allFields = new Set();
|
||||
selectedConceptStocks.forEach(stock => {
|
||||
Object.keys(stock).forEach(key => allFields.add(key));
|
||||
});
|
||||
|
||||
// 定义固定的列顺序,包含新增的现价和涨跌幅列
|
||||
const orderedFields = ['stock_name', 'stock_code', 'current_price', 'change_percent'];
|
||||
allFields.forEach(field => {
|
||||
if (!orderedFields.includes(field)) {
|
||||
orderedFields.push(field);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{loadingStockData && (
|
||||
<Box mb={4} textAlign="center">
|
||||
<HStack justify="center" spacing={2}>
|
||||
<Spinner size="sm" color="purple.500" />
|
||||
<Text fontSize="sm" color="gray.600">正在获取行情数据...</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<TableContainer maxH="60vh" overflowY="auto">
|
||||
<Table variant="simple" size="sm">
|
||||
<Thead position="sticky" top={0} bg="white" zIndex={1}>
|
||||
<Tr>
|
||||
{orderedFields.map(field => (
|
||||
<Th key={field}>
|
||||
{field === 'stock_name' ? '股票名称' :
|
||||
field === 'stock_code' ? '股票代码' :
|
||||
field === 'current_price' ? '现价' :
|
||||
field === 'change_percent' ? '当日涨跌幅' : field}
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{selectedConceptStocks.map((stock, idx) => {
|
||||
const marketData = stockMarketData[stock.stock_code];
|
||||
const companyLink = generateCompanyLink(stock.stock_code);
|
||||
|
||||
return (
|
||||
<Tr key={idx} _hover={{ bg: 'gray.50' }}>
|
||||
{orderedFields.map(field => {
|
||||
let cellContent = stock[field] || '-';
|
||||
let cellProps = {};
|
||||
|
||||
// 处理特殊字段
|
||||
if (field === 'current_price') {
|
||||
cellContent = marketData ? formatPrice(marketData.close) : (loadingStockData ? <Spinner size="xs" /> : '-');
|
||||
} else if (field === 'change_percent') {
|
||||
if (marketData) {
|
||||
cellContent = formatStockChangePercent(marketData.change_percent);
|
||||
cellProps.color = `${getStockChangeColor(marketData.change_percent)}.500`;
|
||||
cellProps.fontWeight = 'bold';
|
||||
} else {
|
||||
cellContent = loadingStockData ? <Spinner size="xs" /> : '-';
|
||||
}
|
||||
} else if (field === 'stock_name' || field === 'stock_code') {
|
||||
// 添加超链接
|
||||
cellContent = (
|
||||
<Text
|
||||
as="a"
|
||||
href={companyLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
color="blue.600"
|
||||
textDecoration="underline"
|
||||
_hover={{
|
||||
color: 'blue.800',
|
||||
textDecoration: 'underline'
|
||||
}}
|
||||
cursor="pointer"
|
||||
>
|
||||
{stock[field] || '-'}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Td key={field} {...cellProps}>
|
||||
{cellContent}
|
||||
</Td>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// 格式化添加日期显示
|
||||
const formatAddedDate = (concept) => {
|
||||
// 优先使用 created_at 或 added_date 字段
|
||||
@@ -1184,23 +1083,23 @@ const ConceptCenter = () => {
|
||||
align={{ base: 'stretch', lg: 'center' }}
|
||||
gap={4}
|
||||
>
|
||||
<HStack spacing={3}>
|
||||
<Icon as={FaCalendarAlt} color="purple.500" boxSize={5} />
|
||||
<Text fontWeight="bold" color="purple.700">交易日期:</Text>
|
||||
</HStack>
|
||||
|
||||
<Input
|
||||
type="date"
|
||||
value={selectedDate ? selectedDate.toISOString().split('T')[0] : ''}
|
||||
onChange={handleDateChange}
|
||||
max={new Date().toISOString().split('T')[0]}
|
||||
width={{ base: '100%', lg: '200px' }}
|
||||
focusBorderColor="purple.500"
|
||||
borderColor="purple.200"
|
||||
borderRadius="lg"
|
||||
fontWeight="medium"
|
||||
{/* 使用通用日期选择器组件 */}
|
||||
<TradeDatePicker
|
||||
value={selectedDate}
|
||||
onChange={(date) => {
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
const previousDate = selectedDate ? selectedDate.toISOString().split('T')[0] : null;
|
||||
trackFilterApplied('date', dateStr, previousDate);
|
||||
setSelectedDate(date);
|
||||
setCurrentPage(1);
|
||||
updateUrlParams({ date: dateStr, page: 1 });
|
||||
fetchConcepts(searchQuery, 1, date, sortBy);
|
||||
}}
|
||||
latestTradeDate={latestTradeDate}
|
||||
label="交易日期"
|
||||
/>
|
||||
|
||||
{/* 快捷按钮保留在页面内 */}
|
||||
<ButtonGroup size="sm" variant="outline" flexWrap="wrap">
|
||||
<Button
|
||||
onClick={() => handleQuickDateSelect(0)}
|
||||
@@ -1251,25 +1150,6 @@ const ConceptCenter = () => {
|
||||
一月前
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
{latestTradeDate && (
|
||||
<Tooltip label="数据库中最新的交易日期">
|
||||
<HStack
|
||||
spacing={2}
|
||||
bg="blue.50"
|
||||
px={3}
|
||||
py={1.5}
|
||||
borderRadius="full"
|
||||
border="1px solid"
|
||||
borderColor="blue.200"
|
||||
>
|
||||
<Icon as={InfoIcon} color="blue.500" boxSize={3} />
|
||||
<Text fontSize="sm" color="blue.600" fontWeight="medium">
|
||||
最新: {latestTradeDate.toLocaleDateString('zh-CN')}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
@@ -1763,32 +1643,15 @@ const ConceptCenter = () => {
|
||||
</Flex>
|
||||
</Container>
|
||||
|
||||
{/* 股票详情Modal */}
|
||||
<Modal
|
||||
{/* 股票详情Modal - 复用通用组件 */}
|
||||
<ConceptStocksModal
|
||||
isOpen={isStockModalOpen}
|
||||
onClose={() => setIsStockModalOpen(false)}
|
||||
size="6xl"
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader bg="purple.500" color="white">
|
||||
<HStack>
|
||||
<Icon as={FaTable} />
|
||||
<Text>{selectedConceptName} - 相关个股</Text>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton color="white" />
|
||||
<ModalBody py={6}>
|
||||
{renderStockTable()}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme="purple" onClick={() => setIsStockModalOpen(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
concept={{
|
||||
concept_name: selectedConceptName,
|
||||
stocks: selectedConceptStocks
|
||||
}}
|
||||
/>
|
||||
{/* 时间轴Modal */}
|
||||
<ConceptTimelineModal
|
||||
isOpen={isTimelineModalOpen}
|
||||
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { SearchIcon, CloseIcon, ArrowForwardIcon, TrendingUpIcon, InfoIcon, ChevronRightIcon, MoonIcon, SunIcon, CalendarIcon } from '@chakra-ui/icons';
|
||||
import { FaChartLine, FaFire, FaRocket, FaBrain, FaCalendarAlt, FaChevronRight, FaArrowUp, FaArrowDown, FaChartBar } from 'react-icons/fa';
|
||||
import ConceptStocksModal from './components/ConceptStocksModal';
|
||||
import ConceptStocksModal from '@components/ConceptStocksModal';
|
||||
import { BsGraphUp, BsLightningFill } from 'react-icons/bs';
|
||||
import * as echarts from 'echarts';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
Reference in New Issue
Block a user