community增加事件详情
This commit is contained in:
@@ -51,7 +51,6 @@ import { eventService } from '@services/eventService';
|
||||
import { getApiBase } from '@utils/apiConfig';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import dayjs from 'dayjs';
|
||||
import RelatedStocksSection from '@components/EventDetailPanel/RelatedStocksSection';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
const { Text: AntText } = Typography;
|
||||
@@ -415,6 +414,9 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
|
||||
const [selectedEventStocks, setSelectedEventStocks] = useState([]);
|
||||
const [selectedEventTime, setSelectedEventTime] = useState(null);
|
||||
const [selectedEventTitle, setSelectedEventTitle] = useState('');
|
||||
const [stockQuotes, setStockQuotes] = useState({});
|
||||
const [stockQuotesLoading, setStockQuotesLoading] = useState(false);
|
||||
const [expandedReasons, setExpandedReasons] = useState({});
|
||||
|
||||
// 板块数据处理 - 必须在条件返回之前调用所有hooks
|
||||
const sectorList = useMemo(() => {
|
||||
@@ -473,6 +475,235 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
|
||||
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 = [
|
||||
{
|
||||
@@ -713,12 +944,7 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<StockOutlined />}
|
||||
onClick={() => {
|
||||
setSelectedEventStocks(stocks);
|
||||
setSelectedEventTime(record.calendar_time);
|
||||
setSelectedEventTitle(record.title);
|
||||
setStocksDrawerVisible(true);
|
||||
}}
|
||||
onClick={() => showRelatedStocks(stocks, record.calendar_time, record.title)}
|
||||
>
|
||||
{stocks.length}只
|
||||
</Button>
|
||||
@@ -974,22 +1200,25 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
|
||||
isOpen={stocksDrawerVisible}
|
||||
placement="right"
|
||||
size="xl"
|
||||
onClose={() => setStocksDrawerVisible(false)}
|
||||
onClose={() => {
|
||||
setStocksDrawerVisible(false);
|
||||
setExpandedReasons({});
|
||||
}}
|
||||
>
|
||||
<DrawerOverlay bg="blackAlpha.600" />
|
||||
<DrawerContent
|
||||
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)"
|
||||
maxW="900px"
|
||||
maxW="1000px"
|
||||
>
|
||||
<DrawerCloseButton color="whiteAlpha.600" />
|
||||
<DrawerHeader borderBottom="1px solid rgba(255,215,0,0.2)" color="white">
|
||||
<HStack spacing={3}>
|
||||
<Icon as={StockOutlined} color="gold" />
|
||||
<StockOutlined style={{ color: '#FFD700', fontSize: '20px' }} />
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text fontSize="lg">相关股票</Text>
|
||||
{selectedEventTitle && (
|
||||
<Text fontSize="sm" color="whiteAlpha.600" fontWeight="normal" noOfLines={1}>
|
||||
<Text fontSize="sm" color="whiteAlpha.600" fontWeight="normal" noOfLines={1} maxW="600px">
|
||||
{selectedEventTitle}
|
||||
</Text>
|
||||
)}
|
||||
@@ -997,13 +1226,18 @@ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading
|
||||
<Badge colorScheme="blue" ml={2}>
|
||||
{selectedEventStocks?.length || 0}只
|
||||
</Badge>
|
||||
{stockQuotesLoading && <Spin size="small" />}
|
||||
</HStack>
|
||||
</DrawerHeader>
|
||||
<DrawerBody py={4}>
|
||||
<DrawerBody py={4} className="hero-panel-modal">
|
||||
{selectedEventStocks && selectedEventStocks.length > 0 ? (
|
||||
<RelatedStocksSection
|
||||
stocks={selectedEventStocks}
|
||||
eventTime={selectedEventTime}
|
||||
<Table
|
||||
dataSource={selectedEventStocks}
|
||||
columns={stockColumns}
|
||||
rowKey={(record) => record.code}
|
||||
size="middle"
|
||||
pagination={false}
|
||||
scroll={{ y: 500 }}
|
||||
/>
|
||||
) : (
|
||||
<Center h="200px">
|
||||
|
||||
Reference in New Issue
Block a user