Compare commits

...

3 Commits

Author SHA1 Message Date
zdl
b5d054d89f feat: 概念中心历史时间轴弹窗UI调整 2025-12-04 16:26:52 +08:00
zdl
b66c1585f7 feat: 提取日历选择器组件 2025-12-04 16:20:58 +08:00
zdl
5efd598694 refactor: 提取 ConceptStocksModal 为通用组件,统一概念中心和个股中心弹窗
- 将 ConceptStocksModal 从 StockOverview/components 移到 components 目录
- 概念中心复用 ConceptStocksModal,删除冗余的 renderStockTable 函数(约100行)
- 统一 H5 端弹窗体验:响应式尺寸、高度限制(70vh)、左右滑动、垂直居中
- 移除重复的底部关闭按钮,只保留右上角关闭按钮
- 添加"板块原因"列,表头改为中文
- 使用 @components 路径别名

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 15:57:32 +08:00
5 changed files with 279 additions and 259 deletions

View File

@@ -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>
);

View 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;

View File

@@ -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>
)}

View File

@@ -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}

View File

@@ -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';