community增加事件详情

This commit is contained in:
2026-01-07 12:57:48 +08:00
parent c463b21cd2
commit c43db446db

View File

@@ -51,7 +51,6 @@ import { eventService } from '@services/eventService';
import { getApiBase } from '@utils/apiConfig'; import { getApiBase } from '@utils/apiConfig';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import RelatedStocksSection from '@components/EventDetailPanel/RelatedStocksSection';
const { TabPane } = Tabs; const { TabPane } = Tabs;
const { Text: AntText } = Typography; const { Text: AntText } = Typography;
@@ -415,6 +414,9 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
const [selectedEventStocks, setSelectedEventStocks] = useState([]); const [selectedEventStocks, setSelectedEventStocks] = useState([]);
const [selectedEventTime, setSelectedEventTime] = useState(null); const [selectedEventTime, setSelectedEventTime] = useState(null);
const [selectedEventTitle, setSelectedEventTitle] = useState(''); const [selectedEventTitle, setSelectedEventTitle] = useState('');
const [stockQuotes, setStockQuotes] = useState({});
const [stockQuotesLoading, setStockQuotesLoading] = useState(false);
const [expandedReasons, setExpandedReasons] = useState({});
// 板块数据处理 - 必须在条件返回之前调用所有hooks // 板块数据处理 - 必须在条件返回之前调用所有hooks
const sectorList = useMemo(() => { const sectorList = useMemo(() => {
@@ -473,6 +475,235 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
setDetailDrawerVisible(true); setDetailDrawerVisible(true);
}; };
// 获取六位股票代码(去掉后缀)
const getSixDigitCode = (code) => {
if (!code) return code;
return code.split('.')[0];
};
// 加载股票行情
const loadStockQuotes = async (stocks) => {
if (!stocks || stocks.length === 0) return;
setStockQuotesLoading(true);
const quotes = {};
for (const stock of stocks) {
const code = getSixDigitCode(stock.code);
try {
const response = await fetch(`${getApiBase()}/api/market/trade/${code}?days=1`);
if (response.ok) {
const data = await response.json();
if (data.success && data.data && data.data.length > 0) {
const latest = data.data[data.data.length - 1];
quotes[stock.code] = {
price: latest.close,
change: latest.change_amount,
changePercent: latest.change_percent
};
}
}
} catch (err) {
console.error('加载股票行情失败:', code, err);
}
}
setStockQuotes(quotes);
setStockQuotesLoading(false);
};
// 显示相关股票
const showRelatedStocks = (stocks, eventTime, eventTitle) => {
if (!stocks || stocks.length === 0) return;
// 归一化股票数据格式
const normalizedStocks = stocks.map(stock => {
if (typeof stock === 'object' && !Array.isArray(stock)) {
return {
code: stock.code || stock.stock_code || '',
name: stock.name || stock.stock_name || '',
description: stock.description || stock.relation_desc || '',
score: stock.score || 0,
report: stock.report || null,
};
}
if (Array.isArray(stock)) {
return {
code: stock[0] || '',
name: stock[1] || '',
description: stock[2] || '',
score: stock[3] || 0,
report: null,
};
}
return null;
}).filter(Boolean);
// 按相关度排序
const sortedStocks = normalizedStocks.sort((a, b) => (b.score || 0) - (a.score || 0));
setSelectedEventStocks(sortedStocks);
setSelectedEventTime(eventTime);
setSelectedEventTitle(eventTitle);
setStocksDrawerVisible(true);
setExpandedReasons({});
loadStockQuotes(sortedStocks);
};
// 相关股票表格列定义(和投资日历保持一致)
const stockColumns = [
{
title: '代码',
dataIndex: 'code',
key: 'code',
width: 90,
render: (code) => {
const sixDigitCode = getSixDigitCode(code);
return (
<a
href={`https://valuefrontier.cn/company?scode=${sixDigitCode}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#3B82F6' }}
>
{sixDigitCode}
</a>
);
}
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
width: 100,
render: (name, record) => {
const sixDigitCode = getSixDigitCode(record.code);
return (
<a
href={`https://valuefrontier.cn/company?scode=${sixDigitCode}`}
target="_blank"
rel="noopener noreferrer"
>
<AntText strong>{name}</AntText>
</a>
);
}
},
{
title: '现价',
key: 'price',
width: 80,
render: (_, record) => {
const quote = stockQuotes[record.code];
if (quote && quote.price !== undefined) {
return (
<AntText type={quote.change > 0 ? 'danger' : 'success'}>
{quote.price?.toFixed(2)}
</AntText>
);
}
return <AntText>-</AntText>;
}
},
{
title: '涨跌幅',
key: 'change',
width: 100,
render: (_, record) => {
const quote = stockQuotes[record.code];
if (quote && quote.changePercent !== undefined) {
const changePercent = quote.changePercent || 0;
return (
<Tag color={changePercent > 0 ? 'red' : changePercent < 0 ? 'green' : 'default'}>
{changePercent > 0 ? '+' : ''}{changePercent.toFixed(2)}%
</Tag>
);
}
return <AntText>-</AntText>;
}
},
{
title: '关联理由',
dataIndex: 'description',
key: 'reason',
render: (description, record) => {
const stockCode = record.code;
const isExpanded = expandedReasons[stockCode] || false;
const reason = typeof description === 'string' ? description : '';
const shouldTruncate = reason && reason.length > 80;
const toggleExpanded = () => {
setExpandedReasons(prev => ({
...prev,
[stockCode]: !prev[stockCode]
}));
};
return (
<div>
<AntText style={{ fontSize: '13px', lineHeight: '1.6' }}>
{isExpanded || !shouldTruncate
? reason || '-'
: `${reason?.slice(0, 80)}...`
}
</AntText>
{shouldTruncate && (
<Button
type="link"
size="small"
onClick={toggleExpanded}
style={{ padding: 0, marginLeft: 4, fontSize: '12px' }}
>
({isExpanded ? '收起' : '展开'})
</Button>
)}
{reason && (
<div style={{ marginTop: 4 }}>
<AntText type="secondary" style={{ fontSize: '11px' }}>(AI合成)</AntText>
</div>
)}
</div>
);
}
},
{
title: '研报引用',
dataIndex: 'report',
key: 'report',
width: 180,
render: (report) => {
if (!report || !report.title) {
return <AntText type="secondary">-</AntText>;
}
return (
<div style={{ fontSize: '12px' }}>
<Tooltip title={report.sentences || report.title}>
<div>
<AntText strong style={{ display: 'block', marginBottom: 2 }}>
{report.title.length > 18 ? `${report.title.slice(0, 18)}...` : report.title}
</AntText>
{report.author && (
<AntText type="secondary" style={{ display: 'block', fontSize: '11px' }}>
{report.author}
</AntText>
)}
{report.declare_date && (
<AntText type="secondary" style={{ fontSize: '11px' }}>
{dayjs(report.declare_date).format('YYYY-MM-DD')}
</AntText>
)}
{report.match_score && (
<Tag color={report.match_score === '好' ? 'green' : 'blue'} style={{ marginLeft: 4, fontSize: '10px' }}>
匹配度: {report.match_score}
</Tag>
)}
</div>
</Tooltip>
</div>
);
}
},
];
// 涨停板块表格列 // 涨停板块表格列
const sectorColumns = [ const sectorColumns = [
{ {
@@ -713,12 +944,7 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
type="link" type="link"
size="small" size="small"
icon={<StockOutlined />} icon={<StockOutlined />}
onClick={() => { onClick={() => showRelatedStocks(stocks, record.calendar_time, record.title)}
setSelectedEventStocks(stocks);
setSelectedEventTime(record.calendar_time);
setSelectedEventTitle(record.title);
setStocksDrawerVisible(true);
}}
> >
{stocks.length} {stocks.length}
</Button> </Button>
@@ -974,22 +1200,25 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
isOpen={stocksDrawerVisible} isOpen={stocksDrawerVisible}
placement="right" placement="right"
size="xl" size="xl"
onClose={() => setStocksDrawerVisible(false)} onClose={() => {
setStocksDrawerVisible(false);
setExpandedReasons({});
}}
> >
<DrawerOverlay bg="blackAlpha.600" /> <DrawerOverlay bg="blackAlpha.600" />
<DrawerContent <DrawerContent
bg="linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)" bg="linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)"
borderLeft="1px solid rgba(255,215,0,0.2)" borderLeft="1px solid rgba(255,215,0,0.2)"
maxW="900px" maxW="1000px"
> >
<DrawerCloseButton color="whiteAlpha.600" /> <DrawerCloseButton color="whiteAlpha.600" />
<DrawerHeader borderBottom="1px solid rgba(255,215,0,0.2)" color="white"> <DrawerHeader borderBottom="1px solid rgba(255,215,0,0.2)" color="white">
<HStack spacing={3}> <HStack spacing={3}>
<Icon as={StockOutlined} color="gold" /> <StockOutlined style={{ color: '#FFD700', fontSize: '20px' }} />
<VStack align="start" spacing={0}> <VStack align="start" spacing={0}>
<Text fontSize="lg">相关股票</Text> <Text fontSize="lg">相关股票</Text>
{selectedEventTitle && ( {selectedEventTitle && (
<Text fontSize="sm" color="whiteAlpha.600" fontWeight="normal" noOfLines={1}> <Text fontSize="sm" color="whiteAlpha.600" fontWeight="normal" noOfLines={1} maxW="600px">
{selectedEventTitle} {selectedEventTitle}
</Text> </Text>
)} )}
@@ -997,13 +1226,18 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
<Badge colorScheme="blue" ml={2}> <Badge colorScheme="blue" ml={2}>
{selectedEventStocks?.length || 0} {selectedEventStocks?.length || 0}
</Badge> </Badge>
{stockQuotesLoading && <Spin size="small" />}
</HStack> </HStack>
</DrawerHeader> </DrawerHeader>
<DrawerBody py={4}> <DrawerBody py={4} className="hero-panel-modal">
{selectedEventStocks && selectedEventStocks.length > 0 ? ( {selectedEventStocks && selectedEventStocks.length > 0 ? (
<RelatedStocksSection <Table
stocks={selectedEventStocks} dataSource={selectedEventStocks}
eventTime={selectedEventTime} columns={stockColumns}
rowKey={(record) => record.code}
size="middle"
pagination={false}
scroll={{ y: 500 }}
/> />
) : ( ) : (
<Center h="200px"> <Center h="200px">