community增加事件详情
This commit is contained in:
@@ -418,7 +418,9 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
|
||||
const [detailDrawerVisible, setDetailDrawerVisible] = useState(false);
|
||||
const [selectedContent, setSelectedContent] = useState(null);
|
||||
const [ztViewMode, setZtViewMode] = useState('sector'); // 'sector' | 'stock'
|
||||
const [expandedSectors, setExpandedSectors] = useState({}); // 跟踪展开的板块
|
||||
const [sectorStocksModalVisible, setSectorStocksModalVisible] = useState(false); // 板块股票弹窗
|
||||
const [selectedSectorInfo, setSelectedSectorInfo] = useState(null); // 选中的板块信息
|
||||
const [selectedSectorFilter, setSelectedSectorFilter] = useState(null); // 按个股视图的板块筛选
|
||||
const [stocksDrawerVisible, setStocksDrawerVisible] = useState(false);
|
||||
const [selectedEventStocks, setSelectedEventStocks] = useState([]);
|
||||
const [selectedEventTime, setSelectedEventTime] = useState(null);
|
||||
@@ -464,6 +466,16 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
|
||||
.sort((a, b) => b._continuousDays - a._continuousDays); // 降序排列
|
||||
}, [ztDetail]);
|
||||
|
||||
// 筛选后的股票列表(按板块筛选)
|
||||
const filteredStockList = useMemo(() => {
|
||||
if (!selectedSectorFilter) return stockList;
|
||||
// 根据选中板块筛选
|
||||
const sectorData = ztDetail?.sector_data?.[selectedSectorFilter];
|
||||
if (!sectorData?.stock_codes) return stockList;
|
||||
const sectorStockCodes = new Set(sectorData.stock_codes);
|
||||
return stockList.filter(stock => sectorStockCodes.has(stock.scode));
|
||||
}, [stockList, selectedSectorFilter, ztDetail]);
|
||||
|
||||
// 热门关键词
|
||||
const hotKeywords = useMemo(() => {
|
||||
if (!ztDetail?.word_freq_data) return [];
|
||||
@@ -956,44 +968,40 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
|
||||
dataIndex: 'stocks',
|
||||
key: 'stocks',
|
||||
render: (stocks, record) => {
|
||||
// 根据股票代码查找股票详情
|
||||
const getStockInfo = (code) => {
|
||||
// 根据股票代码查找股票详情,并按连板天数排序
|
||||
const getStockInfoList = () => {
|
||||
return stocks
|
||||
.map(code => {
|
||||
const stockInfo = stockList.find(s => s.scode === code);
|
||||
return stockInfo || { sname: code, scode: code };
|
||||
return stockInfo || { sname: code, scode: code, _continuousDays: 1 };
|
||||
})
|
||||
.sort((a, b) => (b._continuousDays || 1) - (a._continuousDays || 1));
|
||||
};
|
||||
|
||||
const sectorName = record.name;
|
||||
const isExpanded = expandedSectors[sectorName];
|
||||
const displayStocks = isExpanded ? stocks : stocks.slice(0, 5);
|
||||
const hasMore = stocks.length > 5;
|
||||
const stockInfoList = getStockInfoList();
|
||||
const displayStocks = stockInfoList.slice(0, 4);
|
||||
|
||||
const toggleExpand = (e) => {
|
||||
const handleShowAll = (e) => {
|
||||
e.stopPropagation();
|
||||
setExpandedSectors(prev => ({
|
||||
...prev,
|
||||
[sectorName]: !prev[sectorName]
|
||||
}));
|
||||
setSelectedSectorInfo({
|
||||
name: record.name,
|
||||
count: record.count,
|
||||
stocks: stockInfoList,
|
||||
});
|
||||
setSectorStocksModalVisible(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack align="start" spacing={1}>
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{displayStocks.map((code) => {
|
||||
const info = getStockInfo(code);
|
||||
return (
|
||||
<HStack spacing={1} flexWrap="wrap" align="center">
|
||||
{displayStocks.map((info) => (
|
||||
<Tooltip
|
||||
key={code}
|
||||
key={info.scode}
|
||||
title={
|
||||
<Box>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>{info.sname}</div>
|
||||
<div style={{ fontSize: '12px', color: '#888' }}>{code}</div>
|
||||
{info.core_sectors && (
|
||||
<div style={{ fontSize: '11px', marginTop: 4 }}>
|
||||
{info.core_sectors.slice(0, 2).join(' · ')}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: '12px', color: '#888' }}>{info.scode}</div>
|
||||
{info.continuous_days && (
|
||||
<div style={{ fontSize: '11px', marginTop: 2, color: '#fa8c16' }}>
|
||||
<div style={{ fontSize: '12px', marginTop: 4, color: '#fa8c16' }}>
|
||||
{info.continuous_days}
|
||||
</div>
|
||||
)}
|
||||
@@ -1005,40 +1013,54 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
margin: '2px',
|
||||
background: 'rgba(59, 130, 246, 0.15)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
background: info._continuousDays >= 3
|
||||
? 'rgba(255, 77, 79, 0.2)'
|
||||
: info._continuousDays >= 2
|
||||
? 'rgba(250, 140, 22, 0.2)'
|
||||
: 'rgba(59, 130, 246, 0.15)',
|
||||
border: info._continuousDays >= 3
|
||||
? '1px solid rgba(255, 77, 79, 0.4)'
|
||||
: info._continuousDays >= 2
|
||||
? '1px solid rgba(250, 140, 22, 0.4)'
|
||||
: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
borderRadius: '6px',
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={`https://valuefrontier.cn/company?scode=${code}`}
|
||||
href={`https://valuefrontier.cn/company?scode=${info.scode}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#60A5FA', fontSize: '13px' }}
|
||||
style={{
|
||||
color: info._continuousDays >= 3 ? '#ff4d4f' : info._continuousDays >= 2 ? '#fa8c16' : '#60A5FA',
|
||||
fontSize: '13px'
|
||||
}}
|
||||
>
|
||||
{info.sname}
|
||||
{info._continuousDays > 1 && (
|
||||
<span style={{ fontSize: '10px', marginLeft: 2 }}>
|
||||
({info._continuousDays}板)
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
{hasMore && (
|
||||
))}
|
||||
{stocks.length > 4 && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={toggleExpand}
|
||||
onClick={handleShowAll}
|
||||
style={{
|
||||
padding: 0,
|
||||
padding: '0 4px',
|
||||
height: 'auto',
|
||||
fontSize: '12px',
|
||||
color: isExpanded ? '#888' : '#FFD700',
|
||||
color: '#FFD700',
|
||||
}}
|
||||
>
|
||||
{isExpanded ? '收起 ▲' : `展开全部 ${stocks.length} 只 ▼`}
|
||||
查看全部 {stocks.length} 只 →
|
||||
</Button>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -1702,20 +1724,97 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
|
||||
|
||||
{/* 个股视图 */}
|
||||
{ztViewMode === 'stock' && (
|
||||
<VStack spacing={3} align="stretch">
|
||||
{/* 板块筛选器 */}
|
||||
<Box>
|
||||
<HStack spacing={2} mb={2}>
|
||||
<Text fontSize="xs" color="whiteAlpha.500">板块筛选:</Text>
|
||||
<Box
|
||||
as="button"
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
bg={!selectedSectorFilter ? 'rgba(255,215,0,0.2)' : 'rgba(255,255,255,0.05)'}
|
||||
border={!selectedSectorFilter ? '1px solid rgba(255,215,0,0.4)' : '1px solid rgba(255,255,255,0.1)'}
|
||||
color={!selectedSectorFilter ? '#FFD700' : '#888'}
|
||||
fontSize="xs"
|
||||
onClick={() => setSelectedSectorFilter(null)}
|
||||
transition="all 0.2s"
|
||||
_hover={{ bg: 'rgba(255,215,0,0.15)' }}
|
||||
>
|
||||
全部 ({stockList.length})
|
||||
</Box>
|
||||
</HStack>
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{sectorList.slice(0, 10).map((sector) => (
|
||||
<Box
|
||||
key={sector.name}
|
||||
as="button"
|
||||
px={2.5}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
bg={selectedSectorFilter === sector.name
|
||||
? 'rgba(59,130,246,0.2)'
|
||||
: 'rgba(255,255,255,0.03)'}
|
||||
border={selectedSectorFilter === sector.name
|
||||
? '1px solid rgba(59,130,246,0.4)'
|
||||
: '1px solid rgba(255,255,255,0.08)'}
|
||||
color={selectedSectorFilter === sector.name ? '#60A5FA' : '#888'}
|
||||
fontSize="xs"
|
||||
onClick={() => setSelectedSectorFilter(
|
||||
selectedSectorFilter === sector.name ? null : sector.name
|
||||
)}
|
||||
transition="all 0.2s"
|
||||
_hover={{ bg: 'rgba(59,130,246,0.1)' }}
|
||||
>
|
||||
{sector.name} ({sector.count})
|
||||
</Box>
|
||||
))}
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
{/* 筛选结果提示 */}
|
||||
{selectedSectorFilter && (
|
||||
<HStack
|
||||
px={3}
|
||||
py={2}
|
||||
bg="rgba(59,130,246,0.1)"
|
||||
borderRadius="lg"
|
||||
border="1px solid rgba(59,130,246,0.2)"
|
||||
>
|
||||
<TagsOutlined style={{ color: '#60A5FA' }} />
|
||||
<Text fontSize="sm" color="#60A5FA">
|
||||
当前筛选:<strong>{selectedSectorFilter}</strong>
|
||||
</Text>
|
||||
<Text fontSize="sm" color="whiteAlpha.600">
|
||||
共 {filteredStockList.length} 只
|
||||
</Text>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => setSelectedSectorFilter(null)}
|
||||
style={{ color: '#888', fontSize: '12px' }}
|
||||
>
|
||||
清除筛选
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<Box
|
||||
borderRadius="xl"
|
||||
border="1px solid rgba(59,130,246,0.15)"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Table
|
||||
dataSource={stockList}
|
||||
dataSource={filteredStockList}
|
||||
columns={ztStockColumns}
|
||||
rowKey="scode"
|
||||
size="middle"
|
||||
pagination={false}
|
||||
scroll={{ x: 950, y: 380 }}
|
||||
scroll={{ x: 950, y: selectedSectorFilter ? 320 : 380 }}
|
||||
/>
|
||||
</Box>
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
) : (
|
||||
@@ -1885,6 +1984,285 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
|
||||
size="5xl"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 板块股票弹窗 */}
|
||||
<Modal
|
||||
isOpen={sectorStocksModalVisible}
|
||||
onClose={() => {
|
||||
setSectorStocksModalVisible(false);
|
||||
setSelectedSectorInfo(null);
|
||||
}}
|
||||
size="4xl"
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalOverlay bg="blackAlpha.700" backdropFilter={GLASS_BLUR.sm} />
|
||||
<ModalContent
|
||||
maxW="900px"
|
||||
maxH="85vh"
|
||||
bg="linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)"
|
||||
border="1px solid rgba(255,215,0,0.2)"
|
||||
borderRadius="2xl"
|
||||
>
|
||||
<ModalHeader borderBottom="1px solid rgba(255,215,0,0.2)" py={4}>
|
||||
<HStack spacing={3}>
|
||||
<Box
|
||||
p={2}
|
||||
bg="rgba(255,215,0,0.15)"
|
||||
borderRadius="lg"
|
||||
border="1px solid rgba(255,215,0,0.3)"
|
||||
>
|
||||
<TagsOutlined style={{ color: '#FFD700', fontSize: '18px' }} />
|
||||
</Box>
|
||||
<VStack align="start" spacing={0}>
|
||||
<HStack spacing={2}>
|
||||
<Text fontSize="lg" fontWeight="bold" color="#FFD700">
|
||||
{selectedSectorInfo?.name}
|
||||
</Text>
|
||||
<Badge
|
||||
colorScheme="red"
|
||||
fontSize="sm"
|
||||
px={2}
|
||||
borderRadius="full"
|
||||
>
|
||||
{selectedSectorInfo?.count} 只涨停
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color="whiteAlpha.500">
|
||||
按连板天数降序排列
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton color="whiteAlpha.600" _hover={{ color: 'white' }} />
|
||||
<ModalBody py={4} className="hero-panel-modal">
|
||||
{selectedSectorInfo?.stocks?.length > 0 ? (
|
||||
<VStack spacing={3} align="stretch">
|
||||
{/* 快速统计 */}
|
||||
<HStack spacing={4} flexWrap="wrap">
|
||||
{(() => {
|
||||
const stats = { '首板': 0, '2连板': 0, '3连板': 0, '4连板+': 0 };
|
||||
selectedSectorInfo.stocks.forEach(s => {
|
||||
const days = s._continuousDays || 1;
|
||||
if (days === 1) stats['首板']++;
|
||||
else if (days === 2) stats['2连板']++;
|
||||
else if (days === 3) stats['3连板']++;
|
||||
else stats['4连板+']++;
|
||||
});
|
||||
return Object.entries(stats).map(([key, value]) => (
|
||||
value > 0 && (
|
||||
<Box
|
||||
key={key}
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
bg={key === '4连板+' ? 'rgba(255,77,79,0.15)' : key === '3连板' ? 'rgba(250,84,28,0.15)' : key === '2连板' ? 'rgba(250,140,22,0.15)' : 'rgba(255,255,255,0.05)'}
|
||||
border={`1px solid ${key === '4连板+' ? 'rgba(255,77,79,0.3)' : key === '3连板' ? 'rgba(250,84,28,0.3)' : key === '2连板' ? 'rgba(250,140,22,0.3)' : 'rgba(255,255,255,0.1)'}`}
|
||||
>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={key === '4连板+' ? '#ff4d4f' : key === '3连板' ? '#fa541c' : key === '2连板' ? '#fa8c16' : '#888'}
|
||||
>
|
||||
{key}: <strong>{value}</strong>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
));
|
||||
})()}
|
||||
</HStack>
|
||||
|
||||
{/* 股票列表 */}
|
||||
<Box
|
||||
borderRadius="xl"
|
||||
border="1px solid rgba(255,215,0,0.15)"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Table
|
||||
dataSource={selectedSectorInfo.stocks}
|
||||
columns={[
|
||||
{
|
||||
title: '股票',
|
||||
key: 'stock',
|
||||
width: 130,
|
||||
render: (_, record) => (
|
||||
<VStack align="start" spacing={0}>
|
||||
<a
|
||||
href={`https://valuefrontier.cn/company?scode=${record.scode}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#FFD700', fontWeight: 'bold', fontSize: '14px' }}
|
||||
>
|
||||
{record.sname}
|
||||
</a>
|
||||
<AntText style={{ color: '#60A5FA', fontSize: '12px' }}>{record.scode}</AntText>
|
||||
</VStack>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '连板',
|
||||
dataIndex: 'continuous_days',
|
||||
key: 'continuous',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
render: (text, record) => {
|
||||
const days = record._continuousDays || 1;
|
||||
const getDaysStyle = (d) => {
|
||||
if (d >= 5) return { bg: 'linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%)', text: '#fff' };
|
||||
if (d >= 3) return { bg: 'linear-gradient(135deg, #fa541c 0%, #ff7a45 100%)', text: '#fff' };
|
||||
if (d >= 2) return { bg: 'linear-gradient(135deg, #fa8c16 0%, #ffc53d 100%)', text: '#fff' };
|
||||
return { bg: 'rgba(255,255,255,0.1)', text: '#888' };
|
||||
};
|
||||
const style = getDaysStyle(days);
|
||||
return (
|
||||
<Box
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
bg={style.bg}
|
||||
fontSize="13px"
|
||||
fontWeight="bold"
|
||||
color={style.text}
|
||||
display="inline-block"
|
||||
>
|
||||
{text || '首板'}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '涨停时间',
|
||||
dataIndex: 'formatted_time',
|
||||
key: 'time',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
render: (time) => {
|
||||
const getTimeStyle = (t) => {
|
||||
if (t <= '09:30:00') return { bg: '#ff4d4f', text: '#fff' };
|
||||
if (t <= '09:35:00') return { bg: '#fa541c', text: '#fff' };
|
||||
if (t <= '10:00:00') return { bg: '#fa8c16', text: '#fff' };
|
||||
return { bg: 'rgba(255,255,255,0.1)', text: '#888' };
|
||||
};
|
||||
const style = getTimeStyle(time || '15:00:00');
|
||||
return (
|
||||
<Box
|
||||
px={2}
|
||||
py={0.5}
|
||||
borderRadius="md"
|
||||
bg={style.bg}
|
||||
fontSize="12px"
|
||||
color={style.text}
|
||||
>
|
||||
{time?.substring(0, 5) || '-'}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '核心板块',
|
||||
dataIndex: 'core_sectors',
|
||||
key: 'sectors',
|
||||
render: (sectors) => (
|
||||
<HStack spacing={1} flexWrap="wrap">
|
||||
{(sectors || []).slice(0, 2).map((sector, idx) => (
|
||||
<Tag
|
||||
key={idx}
|
||||
style={{
|
||||
margin: '2px',
|
||||
background: 'rgba(255,215,0,0.1)',
|
||||
border: '1px solid rgba(255,215,0,0.2)',
|
||||
borderRadius: '4px',
|
||||
color: '#D4A84B',
|
||||
fontSize: '11px',
|
||||
}}
|
||||
>
|
||||
{sector}
|
||||
</Tag>
|
||||
))}
|
||||
</HStack>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'K线图',
|
||||
key: 'kline',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<LineChartOutlined />}
|
||||
onClick={() => {
|
||||
const code = record.scode;
|
||||
let stockCode = code;
|
||||
if (!code.includes('.')) {
|
||||
if (code.startsWith('6')) stockCode = `${code}.SH`;
|
||||
else if (code.startsWith('0') || code.startsWith('3')) stockCode = `${code}.SZ`;
|
||||
}
|
||||
setSelectedKlineStock({
|
||||
stock_code: stockCode,
|
||||
stock_name: record.sname
|
||||
});
|
||||
setKlineModalVisible(true);
|
||||
}}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%)',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
const code = record.scode;
|
||||
const inWatchlist = isStockInWatchlist(code);
|
||||
return (
|
||||
<Button
|
||||
type={inWatchlist ? 'primary' : 'default'}
|
||||
size="small"
|
||||
icon={inWatchlist ? <StarFilled /> : <StarOutlined />}
|
||||
onClick={() => addSingleToWatchlist({ code, name: record.sname })}
|
||||
disabled={inWatchlist}
|
||||
style={inWatchlist ? {
|
||||
background: 'linear-gradient(135deg, #faad14 0%, #fa8c16 100%)',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
} : {
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
border: '1px solid rgba(255,215,0,0.3)',
|
||||
borderRadius: '6px',
|
||||
color: '#FFD700',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
{inWatchlist ? '已添加' : '加自选'}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
rowKey="scode"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ y: 400 }}
|
||||
/>
|
||||
</Box>
|
||||
</VStack>
|
||||
) : (
|
||||
<Center h="200px">
|
||||
<Text color="whiteAlpha.500">暂无股票数据</Text>
|
||||
</Center>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user