// src/views/Community/components/HeroPanel.js // 综合日历面板:融合涨停分析 + 投资日历 // 点击日期弹出详情弹窗(TAB切换历史涨停/未来事件) import React, { useEffect, useState, useCallback, useMemo, memo, lazy, Suspense } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { loadWatchlist, toggleWatchlist } from '@store/slices/stockSlice'; import { Box, Card, CardBody, Flex, VStack, HStack, Text, Heading, useColorModeValue, useDisclosure, Icon, Spinner, Center, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Tooltip, Badge, SimpleGrid, IconButton, Drawer, DrawerOverlay, DrawerContent, DrawerHeader, DrawerBody, DrawerCloseButton, } from '@chakra-ui/react'; import { Table, Tabs, Tag, Space, Button, Spin, Typography, message } from 'antd'; import { CalendarOutlined, StarFilled, LinkOutlined, StockOutlined, TagsOutlined, ClockCircleOutlined, RobotOutlined, FireOutlined, LineChartOutlined, StarOutlined, } from '@ant-design/icons'; import { AlertCircle, Clock, Info, Calendar, ChevronLeft, ChevronRight, Flame, TrendingUp, TrendingDown, FileText, Star } from 'lucide-react'; import { GLASS_BLUR } from '@/constants/glassConfig'; import { eventService } from '@services/eventService'; import { getApiBase } from '@utils/apiConfig'; import ReactMarkdown from 'react-markdown'; import dayjs from 'dayjs'; import KLineChartModal from '@components/StockChart/KLineChartModal'; // 懒加载 FullCalendar(约 60KB gzip,延迟加载提升首屏性能) const FullCalendarPro = lazy(() => import('@components/Calendar').then(module => ({ default: module.FullCalendarPro })) ); import ThemeCometChart from './ThemeCometChart'; import EventDailyStats from './EventDailyStats'; const { TabPane } = Tabs; const { Text: AntText } = Typography; // 定义动画和深色主题样式 const animations = ` @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.6; transform: scale(1.1); } } @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } /* Chakra Drawer 滚动修复 */ .chakra-modal__content-container { overflow: hidden !important; } .hero-detail-drawer .chakra-modal__body { overflow-y: auto !important; max-height: calc(100vh - 80px) !important; } .hero-detail-drawer .chakra-modal__body::-webkit-scrollbar { width: 8px; } .hero-detail-drawer .chakra-modal__body::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); border-radius: 4px; } .hero-detail-drawer .chakra-modal__body::-webkit-scrollbar-thumb { background: rgba(255, 215, 0, 0.4); border-radius: 4px; } .hero-detail-drawer .chakra-modal__body::-webkit-scrollbar-thumb:hover { background: rgba(255, 215, 0, 0.6); } /* Ant Design 深色主题覆盖 - 弹窗专用 */ .hero-panel-modal .ant-tabs { color: rgba(255, 255, 255, 0.85); } .hero-panel-modal .ant-tabs-nav::before { border-color: rgba(255, 215, 0, 0.2) !important; } .hero-panel-modal .ant-tabs-tab { color: rgba(255, 255, 255, 0.65) !important; font-size: 15px !important; } .hero-panel-modal .ant-tabs-tab:hover { color: #FFD700 !important; } .hero-panel-modal .ant-tabs-tab-active .ant-tabs-tab-btn { color: #FFD700 !important; } .hero-panel-modal .ant-tabs-ink-bar { background: linear-gradient(90deg, #FFD700, #FFA500) !important; } /* 表格深色主题 */ .hero-panel-modal .ant-table { background: transparent !important; color: rgba(255, 255, 255, 0.85) !important; } .hero-panel-modal .ant-table-thead > tr > th { background: rgba(255, 215, 0, 0.1) !important; color: #FFD700 !important; border-bottom: 1px solid rgba(255, 215, 0, 0.2) !important; font-weight: 600 !important; font-size: 14px !important; } .hero-panel-modal .ant-table-tbody > tr > td { background: transparent !important; border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important; color: rgba(255, 255, 255, 0.85) !important; font-size: 14px !important; } .hero-panel-modal .ant-table-tbody > tr:hover > td { background: rgba(255, 215, 0, 0.08) !important; } .hero-panel-modal .ant-table-tbody > tr.ant-table-row:hover > td { background: rgba(255, 215, 0, 0.1) !important; } .hero-panel-modal .ant-table-cell-row-hover { background: rgba(255, 215, 0, 0.08) !important; } .hero-panel-modal .ant-table-placeholder { background: transparent !important; } .hero-panel-modal .ant-empty-description { color: rgba(255, 255, 255, 0.45) !important; } /* 滚动条样式 */ .hero-panel-modal .ant-table-body::-webkit-scrollbar { width: 6px; height: 6px; } .hero-panel-modal .ant-table-body::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); border-radius: 3px; } .hero-panel-modal .ant-table-body::-webkit-scrollbar-thumb { background: rgba(255, 215, 0, 0.3); border-radius: 3px; } .hero-panel-modal .ant-table-body::-webkit-scrollbar-thumb:hover { background: rgba(255, 215, 0, 0.5); } /* 板块股票表格滚动 - 针对 Ant Design 5.x */ .sector-stocks-table-wrapper { max-height: 450px; overflow: hidden; } .sector-stocks-table-wrapper .ant-table-wrapper, .sector-stocks-table-wrapper .ant-table, .sector-stocks-table-wrapper .ant-table-container { max-height: 100%; } .sector-stocks-table-wrapper .ant-table-body { max-height: 380px !important; overflow-y: auto !important; scrollbar-width: thin; scrollbar-color: rgba(255, 215, 0, 0.4) rgba(255, 255, 255, 0.05); } /* 相关股票表格滚动 */ .related-stocks-table-wrapper .ant-table-body { scrollbar-width: thin; scrollbar-color: rgba(255, 215, 0, 0.4) rgba(255, 255, 255, 0.05); } /* Tag 样式优化 */ .hero-panel-modal .ant-tag { border-radius: 4px !important; } /* Button link 样式 */ .hero-panel-modal .ant-btn-link { color: #FFD700 !important; } .hero-panel-modal .ant-btn-link:hover { color: #FFA500 !important; } .hero-panel-modal .ant-btn-link:disabled { color: rgba(255, 255, 255, 0.25) !important; } /* Typography 样式 */ .hero-panel-modal .ant-typography { color: rgba(255, 255, 255, 0.85) !important; } .hero-panel-modal .ant-typography-secondary { color: rgba(255, 255, 255, 0.45) !important; } /* Spin 加载样式 */ .hero-panel-modal .ant-spin-text { color: #FFD700 !important; } .hero-panel-modal .ant-spin-dot-item { background-color: #FFD700 !important; } `; // 注入样式 if (typeof document !== 'undefined') { const styleId = 'hero-panel-animations'; if (!document.getElementById(styleId)) { const styleSheet = document.createElement('style'); styleSheet.id = styleId; styleSheet.innerText = animations; document.head.appendChild(styleSheet); } } /** * 判断当前是否在交易时间内 */ const isInTradingTime = () => { const now = new Date(); const timeInMinutes = now.getHours() * 60 + now.getMinutes(); return timeInMinutes >= 570 && timeInMinutes <= 900; }; // 主题色配置 const goldColors = { primary: '#D4AF37', light: '#F4D03F', dark: '#B8860B', glow: 'rgba(212, 175, 55, 0.4)', }; const textColors = { primary: '#ffffff', secondary: 'rgba(255, 255, 255, 0.85)', muted: 'rgba(255, 255, 255, 0.5)', }; // 热度级别配置 const HEAT_LEVELS = [ { key: 'high', threshold: 80, colors: { bg: 'rgba(147, 51, 234, 0.55)', text: '#d8b4fe', border: 'rgba(147, 51, 234, 0.65)' } }, { key: 'medium', threshold: 60, colors: { bg: 'rgba(239, 68, 68, 0.50)', text: '#fca5a5', border: 'rgba(239, 68, 68, 0.60)' } }, { key: 'low', threshold: 40, colors: { bg: 'rgba(251, 146, 60, 0.45)', text: '#fed7aa', border: 'rgba(251, 146, 60, 0.55)' } }, { key: 'cold', threshold: 0, colors: { bg: 'rgba(59, 130, 246, 0.35)', text: '#93c5fd', border: 'rgba(59, 130, 246, 0.45)' } }, ]; const DEFAULT_HEAT_COLORS = { bg: 'rgba(60, 60, 70, 0.12)', text: textColors.muted, border: 'transparent', }; const getHeatColor = (count) => { if (!count) return DEFAULT_HEAT_COLORS; const level = HEAT_LEVELS.find((l) => count >= l.threshold); return level?.colors || DEFAULT_HEAT_COLORS; }; // 日期格式化 const formatDateStr = (date) => { if (!date) return ''; const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}${month}${day}`; }; const WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六']; const MONTH_NAMES = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; /** * 趋势图标 */ const TrendIcon = memo(({ current, previous }) => { if (!current || !previous) return null; const diff = current - previous; if (diff === 0) return null; const isUp = diff > 0; return ( ); }); TrendIcon.displayName = 'TrendIcon'; /** * 日历单元格 - 显示涨停数和事件数(加大尺寸) * 新增:连续概念连接展示(connectLeft/connectRight 表示与左右格子是否同一概念) */ const CalendarCell = memo(({ date, ztData, eventCount, previousZtData, isSelected, isToday, isWeekend, onClick, connectLeft, connectRight }) => { if (!date) { return ; } const hasZtData = !!ztData; const hasEventData = eventCount > 0; const ztCount = ztData?.count || 0; const heatColors = getHeatColor(ztCount); const topSector = ztData?.top_sector || ''; // 是否有连接线(连续概念) const hasConnection = connectLeft || connectRight; // 周末无数据显示"休市" if (isWeekend && !hasZtData && !hasEventData) { return ( {date.getDate()} 休市 ); } // 正常日期 return ( {`${date.getMonth() + 1}月${date.getDate()}日`} {hasZtData && 涨停: {ztCount}家 {topSector && `| ${topSector}`}} {hasEventData && 未来事件: {eventCount}个} {!hasZtData && !hasEventData && 暂无数据} } placement="top" hasArrow bg="rgba(15, 15, 22, 0.95)" border="1px solid rgba(212, 175, 55, 0.3)" borderRadius="10px" > onClick && onClick(date)} w="full" minH="75px" > {/* 今天标记 */} {isToday && ( 今天 )} {/* 日期 */} {date.getDate()} {/* 涨停数 + 趋势 */} {hasZtData && ( {ztCount} )} {/* 事件数 */} {hasEventData && ( {eventCount} )} {/* 主要板块 - 连续概念用连接样式 */} {hasZtData && topSector && ( {/* 左连接线 */} {connectLeft && ( )} {topSector} {/* 右连接线 */} {connectRight && ( )} )} ); }); CalendarCell.displayName = 'CalendarCell'; /** * 详情弹窗组件 - 完整展示涨停分析和事件详情 */ const DetailModal = ({ isOpen, onClose, selectedDate, ztDetail, events, loading }) => { const dispatch = useDispatch(); const reduxWatchlist = useSelector(state => state.stock.watchlist); const [detailDrawerVisible, setDetailDrawerVisible] = useState(false); const [selectedContent, setSelectedContent] = useState(null); const [ztViewMode, setZtViewMode] = useState('sector'); // 'sector' | 'stock' 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); const [selectedEventTitle, setSelectedEventTitle] = useState(''); const [stockQuotes, setStockQuotes] = useState({}); const [stockQuotesLoading, setStockQuotesLoading] = useState(false); const [expandedReasons, setExpandedReasons] = useState({}); const [klineModalVisible, setKlineModalVisible] = useState(false); const [selectedKlineStock, setSelectedKlineStock] = useState(null); // 关联事件弹窗状态 const [relatedEventsModalVisible, setRelatedEventsModalVisible] = useState(false); const [selectedRelatedEvents, setSelectedRelatedEvents] = useState({ sectorName: '', events: [] }); // 板块数据处理 - 必须在条件返回之前调用所有hooks const sectorList = useMemo(() => { if (!ztDetail?.sector_data) return []; return Object.entries(ztDetail.sector_data) .filter(([name]) => name !== '其他') .map(([name, data]) => ({ name, count: data.count, stocks: data.stock_codes || [], // 新增:关联事件数据(涨停归因) related_events: data.related_events || [], })) .sort((a, b) => b.count - a.count); }, [ztDetail]); // 股票详情数据处理 - 支持两种字段名:stocks 和 stock_infos // 按连板天数降序排列(高连板在前) const stockList = useMemo(() => { const stocksData = ztDetail?.stocks || ztDetail?.stock_infos; if (!stocksData) return []; // 解析连板天数的辅助函数 const parseContinuousDays = (text) => { if (!text || text === '首板') return 1; const match = text.match(/(\d+)/); return match ? parseInt(match[1]) : 1; }; return stocksData .map(stock => ({ ...stock, key: stock.scode, _continuousDays: parseContinuousDays(stock.continuous_days), // 用于排序 })) .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 []; return ztDetail.word_freq_data.slice(0, 12); }, [ztDetail]); // 涨停统计数据 const ztStats = useMemo(() => { if (!stockList.length) return null; // 连板分布统计 const continuousStats = { '首板': 0, '2连板': 0, '3连板': 0, '4连板+': 0 }; // 涨停时间分布统计 const timeStats = { '秒板': 0, '早盘': 0, '盘中': 0, '尾盘': 0 }; // 公告驱动统计 let announcementCount = 0; stockList.forEach(stock => { // 连板统计 const days = stock.continuous_days || '首板'; if (days === '首板' || days.includes('1')) { continuousStats['首板']++; } else { const match = days.match(/(\d+)/); const num = match ? parseInt(match[1]) : 1; if (num === 2) continuousStats['2连板']++; else if (num === 3) continuousStats['3连板']++; else if (num >= 4) continuousStats['4连板+']++; else continuousStats['首板']++; } // 时间统计 const time = stock.formatted_time || '15:00:00'; if (time <= '09:30:00') timeStats['秒板']++; else if (time <= '10:00:00') timeStats['早盘']++; else if (time <= '14:00:00') timeStats['盘中']++; else timeStats['尾盘']++; // 公告驱动 if (stock.is_announcement) announcementCount++; }); return { total: stockList.length, continuousStats, timeStats, announcementCount, announcementRatio: stockList.length > 0 ? Math.round(announcementCount / stockList.length * 100) : 0 }; }, [stockList]); // 获取六位股票代码(去掉后缀)- 纯函数,不是hook const getSixDigitCode = (code) => { if (!code) return code; return code.split('.')[0]; }; // 检查股票是否已在自选中 - 必须在条件返回之前 const isStockInWatchlist = useCallback((stockCode) => { const sixDigitCode = getSixDigitCode(stockCode); return reduxWatchlist?.some(item => getSixDigitCode(item.stock_code) === sixDigitCode ); }, [reduxWatchlist]); // 条件返回必须在所有hooks之后 if (!selectedDate) return null; const dateStr = `${selectedDate.getFullYear()}年${selectedDate.getMonth() + 1}月${selectedDate.getDate()}日`; const isPastDate = selectedDate < new Date(new Date().setHours(0, 0, 0, 0)); // 渲染重要性星级 const renderStars = (star) => { const stars = []; for (let i = 1; i <= 5; i++) { stars.push( ); } return {stars}; }; // 显示内容详情 const showContentDetail = (content, title) => { setSelectedContent({ content, title }); setDetailDrawerVisible(true); }; // 加载股票行情 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 addExchangeSuffix = (code) => { const sixDigitCode = getSixDigitCode(code); if (code.includes('.')) return code; if (sixDigitCode.startsWith('6')) { return `${sixDigitCode}.SH`; } else if (sixDigitCode.startsWith('0') || sixDigitCode.startsWith('3')) { return `${sixDigitCode}.SZ`; } return sixDigitCode; }; // 显示K线图 const showKline = (stock) => { const code = stock.code; const name = stock.name; const stockCode = addExchangeSuffix(code); setSelectedKlineStock({ stock_code: stockCode, stock_name: name, }); setKlineModalVisible(true); }; // 添加单只股票到自选 const addSingleToWatchlist = async (stock) => { const code = stock.code; const name = stock.name; const stockCode = getSixDigitCode(code); if (isStockInWatchlist(code)) { message.info(`${name} 已在自选中`); return; } try { await dispatch(toggleWatchlist({ stockCode, stockName: name, isInWatchlist: false })).unwrap(); message.success(`已将 ${name}(${stockCode}) 添加到自选`); } catch (error) { console.error('添加自选失败:', error); message.error('添加失败,请重试'); } }; // 相关股票表格列定义(和投资日历保持一致) const stockColumns = [ { title: '代码', dataIndex: 'code', key: 'code', width: 90, render: (code) => { const sixDigitCode = getSixDigitCode(code); return ( {sixDigitCode} ); } }, { title: '名称', dataIndex: 'name', key: 'name', width: 100, render: (name, record) => { const sixDigitCode = getSixDigitCode(record.code); return ( {name} ); } }, { title: '现价', key: 'price', width: 80, render: (_, record) => { const quote = stockQuotes[record.code]; if (quote && quote.price !== undefined) { return ( 0 ? 'danger' : 'success'}> {quote.price?.toFixed(2)} ); } return -; } }, { title: '涨跌幅', key: 'change', width: 100, render: (_, record) => { const quote = stockQuotes[record.code]; if (quote && quote.changePercent !== undefined) { const changePercent = quote.changePercent || 0; return ( 0 ? 'red' : changePercent < 0 ? 'green' : 'default'}> {changePercent > 0 ? '+' : ''}{changePercent.toFixed(2)}% ); } return -; } }, { 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 (
{isExpanded || !shouldTruncate ? reason || '-' : `${reason?.slice(0, 80)}...` } {shouldTruncate && ( )} {reason && (
(AI合成)
)}
); } }, { title: '研报引用', dataIndex: 'report', key: 'report', width: 180, render: (report) => { if (!report || !report.title) { return -; } return (
{report.title.length > 18 ? `${report.title.slice(0, 18)}...` : report.title} {report.author && ( {report.author} )} {report.declare_date && ( {dayjs(report.declare_date).format('YYYY-MM-DD')} )} {report.match_score && ( 匹配度: {report.match_score} )}
); } }, { title: 'K线图', key: 'kline', width: 80, render: (_, record) => ( ) }, { title: '操作', key: 'action', width: 90, render: (_, record) => { const inWatchlist = isStockInWatchlist(record.code); return ( ); } }, ]; // 涨停板块表格列 - 精致风格设计 const sectorColumns = [ { title: '排名', key: 'rank', width: 60, align: 'center', render: (_, __, index) => { const getRankStyle = (idx) => { if (idx === 0) return { background: 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)', color: '#000', fontWeight: 'bold' }; if (idx === 1) return { background: 'linear-gradient(135deg, #C0C0C0 0%, #A8A8A8 100%)', color: '#000', fontWeight: 'bold' }; if (idx === 2) return { background: 'linear-gradient(135deg, #CD7F32 0%, #A0522D 100%)', color: '#fff', fontWeight: 'bold' }; return { background: 'rgba(255,255,255,0.1)', color: '#888' }; }; const style = getRankStyle(index); return (
{index + 1}
); }, }, { title: '板块名称', dataIndex: 'name', key: 'name', width: 130, render: (name, record, index) => ( {name} ), }, { title: '涨停数', dataIndex: 'count', key: 'count', width: 90, align: 'center', render: (count) => { const getCountColor = (c) => { if (c >= 8) return { bg: '#ff4d4f', text: '#fff' }; if (c >= 5) return { bg: '#fa541c', text: '#fff' }; if (c >= 3) return { bg: '#fa8c16', text: '#fff' }; return { bg: 'rgba(255,215,0,0.2)', text: '#FFD700' }; }; const colors = getCountColor(count); return ( {count} ); }, }, { title: '涨停股票', dataIndex: 'stocks', key: 'stocks', render: (stocks, record) => { // 根据股票代码查找股票详情,并按连板天数排序 const getStockInfoList = () => { return stocks .map(code => { const stockInfo = stockList.find(s => s.scode === code); return stockInfo || { sname: code, scode: code, _continuousDays: 1 }; }) .sort((a, b) => (b._continuousDays || 1) - (a._continuousDays || 1)); }; const stockInfoList = getStockInfoList(); const displayStocks = stockInfoList.slice(0, 4); const handleShowAll = (e) => { e.stopPropagation(); setSelectedSectorInfo({ name: record.name, count: record.count, stocks: stockInfoList, }); setSectorStocksModalVisible(true); }; return ( {displayStocks.map((info) => (
{info.sname}
{info.scode}
{info.continuous_days && (
{info.continuous_days}
)}
} placement="top" > = 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', }} > = 3 ? '#ff4d4f' : info._continuousDays >= 2 ? '#fa8c16' : '#60A5FA', fontSize: '13px' }} > {info.sname} {info._continuousDays > 1 && ( ({info._continuousDays}板) )} ))} {stocks.length > 4 && ( )} ); }, }, { title: '涨停归因', dataIndex: 'related_events', key: 'related_events', width: 280, render: (events, record) => { if (!events || events.length === 0) { return -; } // 取相关度最高的事件 const sortedEvents = [...events].sort((a, b) => (b.relevance_score || 0) - (a.relevance_score || 0)); const topEvent = sortedEvents[0]; // 相关度颜色 const getRelevanceColor = (score) => { if (score >= 80) return '#10B981'; if (score >= 60) return '#F59E0B'; return '#6B7280'; }; // 点击打开事件详情弹窗 const handleClick = (e) => { e.stopPropagation(); setSelectedRelatedEvents({ sectorName: record.name, events: sortedEvents, count: record.count, }); setRelatedEventsModalVisible(true); }; return ( {topEvent.title} 相关度 {topEvent.relevance_score || 0} {events.length > 1 && ( +{events.length - 1}条 )} ); }, }, ]; // 涨停股票详情表格列 - 精致风格 + K线图 + 加自选 const ztStockColumns = [ { title: '股票信息', key: 'stock', width: 140, fixed: 'left', render: (_, record) => ( {record.sname} {record.scode} ), }, { 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', label: '秒板' }; if (t <= '09:35:00') return { bg: '#fa541c', text: '#fff', label: '早板' }; if (t <= '10:00:00') return { bg: '#fa8c16', text: '#fff', label: '盘初' }; if (t <= '11:00:00') return { bg: '#52c41a', text: '#fff', label: '盘中' }; return { bg: 'rgba(255,255,255,0.1)', text: '#888', label: '尾盘' }; }; const style = getTimeStyle(time || '15:00:00'); return ( {time?.substring(0, 5) || '-'} {style.label} ); }, }, { title: '连板', dataIndex: 'continuous_days', key: 'continuous', width: 70, align: 'center', render: (text) => { if (!text || text === '首板') { return ( 首板 ); } const match = text.match(/(\d+)/); const days = match ? parseInt(match[1]) : 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 ( {text} ); }, }, { title: '核心板块', dataIndex: 'core_sectors', key: 'sectors', width: 200, render: (sectors) => ( {(sectors || []).slice(0, 3).map((sector, idx) => ( {sector} ))} ), }, { title: '涨停简报', dataIndex: 'brief', key: 'brief', width: 200, render: (text, record) => { if (!text) return -; // 移除HTML标签 const cleanText = text.replace(//gi, ' ').replace(/<[^>]+>/g, ''); return (
{record.sname} 涨停简报
{cleanText}
} placement="topLeft" overlayStyle={{ maxWidth: 450 }} >
); }, }, { title: 'K线图', key: 'kline', width: 80, align: 'center', render: (_, record) => ( ), }, { title: '操作', key: 'action', width: 90, align: 'center', render: (_, record) => { const code = record.scode; const inWatchlist = isStockInWatchlist(code); return ( ); }, }, ]; // 事件表格列(参考投资日历)- 去掉相关概念列 const eventColumns = [ { title: '时间', dataIndex: 'calendar_time', key: 'time', width: 80, render: (time) => ( {dayjs(time).format('HH:mm')} ), }, { title: '重要度', dataIndex: 'star', key: 'star', width: 120, render: renderStars, }, { title: '标题', dataIndex: 'title', key: 'title', ellipsis: true, render: (text) => ( {text} ), }, { title: '背景', dataIndex: 'former', key: 'former', width: 80, render: (text) => ( ), }, { title: '未来推演', dataIndex: 'forecast', key: 'forecast', width: 90, render: (text) => ( ), }, { title: '相关股票', dataIndex: 'related_stocks', key: 'stocks', width: 120, render: (stocks, record) => { const hasStocks = stocks && stocks.length > 0; if (!hasStocks) { return ; } return ( ); }, }, ]; return ( <> {dateStr} {isPastDate ? '历史数据' : '未来事件'} {ztDetail && ( 涨停 {ztDetail.total_stocks || 0} 家 )} {events?.length > 0 && ( 事件 {events.length} 个 )} {loading ? (
) : ( {/* 涨停分析 Tab */} 涨停分析 ({ztDetail?.total_stocks || 0}) } key="zt" disabled={!ztDetail} > {(sectorList.length > 0 || stockList.length > 0) ? ( {/* 热门关键词 - 更精致的词云展示 */} {hotKeywords.length > 0 && ( {/* 装饰线 */} 今日热词 词频越高排名越前 {hotKeywords.map((kw, idx) => { // 根据排名计算样式 const getKeywordStyle = (index) => { if (index < 3) return { fontSize: '15px', fontWeight: 'bold', background: 'linear-gradient(135deg, rgba(255,215,0,0.3) 0%, rgba(255,165,0,0.2) 100%)', border: '1px solid rgba(255,215,0,0.5)', color: '#FFD700', px: 3, py: 1.5, }; if (index < 6) return { fontSize: '14px', fontWeight: 'semibold', background: 'rgba(255,215,0,0.15)', border: '1px solid rgba(255,215,0,0.3)', color: '#D4A84B', px: 2.5, py: 1, }; return { fontSize: '13px', fontWeight: 'normal', background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.15)', color: '#888', px: 2, py: 0.5, }; }; const style = getKeywordStyle(idx); return ( {kw.name} ); })} )} {/* 涨停统计卡片 */} {ztStats && ( {/* 连板分布 */} 连板分布 {Object.entries(ztStats.continuousStats).map(([key, value]) => ( {value} {key} ))} {/* 涨停时间分布 */} 封板时间 {Object.entries(ztStats.timeStats).map(([key, value]) => ( {value} {key} ))} {/* 公告驱动 */} 公告驱动 {ztStats.announcementCount} 只 ({ztStats.announcementRatio}%) )} {/* 视图切换按钮 - 更精致的样式 */} setZtViewMode('sector')} transition="all 0.2s" _hover={{ bg: 'rgba(255,215,0,0.15)' }} display="flex" alignItems="center" gap={2} > 按板块 ({sectorList.length}) setZtViewMode('stock')} transition="all 0.2s" _hover={{ bg: 'rgba(59,130,246,0.15)' }} display="flex" alignItems="center" gap={2} > 按个股 ({stockList.length}) {ztDetail?.total_stocks || 0} 只涨停 {/* 板块视图 */} {ztViewMode === 'sector' && ( )} {/* 个股视图 */} {ztViewMode === 'stock' && ( {/* 板块筛选器 */} 板块筛选: setSelectedSectorFilter(null)} transition="all 0.2s" _hover={{ bg: 'rgba(255,215,0,0.15)' }} > 全部 ({stockList.length}) {sectorList.slice(0, 10).map((sector) => ( setSelectedSectorFilter( selectedSectorFilter === sector.name ? null : sector.name )} transition="all 0.2s" _hover={{ bg: 'rgba(59,130,246,0.1)' }} > {sector.name} ({sector.count}) ))} {/* 筛选结果提示 */} {selectedSectorFilter && ( 当前筛选:{selectedSectorFilter} 共 {filteredStockList.length} 只 )}
)} ) : (
暂无涨停数据 该日期没有涨停股票记录
)} {/* 未来事件 Tab */} 未来事件 ({events?.length || 0}) } key="event" disabled={!events?.length} > {events?.length > 0 ? (
) : (
暂无事件数据
)} )} {/* 内容详情抽屉 */} setDetailDrawerVisible(false)} > {selectedContent?.title} {typeof selectedContent?.content === 'string' ? selectedContent.content : selectedContent?.content?.data ? selectedContent.content.data.map(item => item.sentence || '').join('\n\n') : '暂无内容'} (AI合成内容) {/* 相关股票弹窗 */} { setStocksDrawerVisible(false); setExpandedReasons({}); }} size="6xl" scrollBehavior="inside" > 相关股票 {selectedEventTitle && ( {selectedEventTitle} )} {selectedEventStocks?.length || 0}只 {stockQuotesLoading && } {selectedEventStocks && selectedEventStocks.length > 0 ? (
record.code} size="middle" pagination={false} scroll={{ y: 500 }} /> ) : (
暂无相关股票
)} {/* K线图弹窗 */} {selectedKlineStock && ( { setKlineModalVisible(false); setSelectedKlineStock(null); }} stock={selectedKlineStock} eventTime={selectedEventTime} size="5xl" /> )} {/* 板块股票弹窗 */} { setSectorStocksModalVisible(false); setSelectedSectorInfo(null); }} size="4xl" scrollBehavior="inside" > {selectedSectorInfo?.name} {selectedSectorInfo?.count} 只涨停 按连板天数降序排列 {selectedSectorInfo?.stocks?.length > 0 ? ( {/* 快速统计 */} {(() => { 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 && ( {key}: {value} ) )); })()} {/* 股票列表 - 使用 Ant Design Table 内置滚动 */}
( {record.sname} {record.scode} ), }, { 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 ( {text || '首板'} ); }, }, { 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 ( {time?.substring(0, 5) || '-'} ); }, }, { title: '核心板块', dataIndex: 'core_sectors', key: 'sectors', render: (sectors) => ( {(sectors || []).slice(0, 2).map((sector, idx) => ( {sector} ))} ), }, { title: 'K线图', key: 'kline', width: 80, align: 'center', render: (_, record) => ( ), }, { title: '操作', key: 'action', width: 90, align: 'center', render: (_, record) => { const code = record.scode; const inWatchlist = isStockInWatchlist(code); return ( ); }, }, ]} rowKey="scode" size="small" pagination={false} scroll={{ x: 650, y: 450 }} /> ) : (
暂无股票数据
)} {/* 关联事件弹窗 - 涨停归因详情 */} { setRelatedEventsModalVisible(false); setSelectedRelatedEvents({ sectorName: '', events: [] }); }} size="xl" scrollBehavior="inside" > {selectedRelatedEvents.sectorName} - 涨停归因 涨停 {selectedRelatedEvents.count || 0} 关联事件 {selectedRelatedEvents.events?.length || 0} {selectedRelatedEvents.events?.length > 0 ? ( {selectedRelatedEvents.events.map((event, idx) => { const getRelevanceColor = (score) => { if (score >= 80) return '#10B981'; if (score >= 60) return '#F59E0B'; return '#6B7280'; }; const relevanceColor = getRelevanceColor(event.relevance_score || 0); return ( { // 跳转到事件详情页 window.open(`/community?event_id=${event.event_id}`, '_blank'); }} _hover={{ bg: 'rgba(40,40,70,0.9)', borderColor: 'rgba(96,165,250,0.3)', transform: 'translateY(-2px)', }} transition="all 0.2s" > {/* 标题 */} {event.title} 相关度 {event.relevance_score || 0} {/* 相关原因 */} {event.relevance_reason && ( {event.relevance_reason} )} {/* 匹配概念 */} {event.matched_concepts?.length > 0 && ( 匹配概念: {event.matched_concepts.slice(0, 6).map((concept, i) => ( {concept} ))} {event.matched_concepts.length > 6 && ( +{event.matched_concepts.length - 6} )} )} ); })} ) : (
暂无关联事件
)}
); }; /** * 综合日历组件 - 使用 FullCalendarPro 实现跨天事件条效果 */ const CombinedCalendar = () => { const [currentMonth, setCurrentMonth] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(null); // 日历综合数据(涨停 + 事件 + 上证涨跌幅)- 使用新的综合 API const [calendarData, setCalendarData] = useState([]); const [ztDailyDetails, setZtDailyDetails] = useState({}); const [selectedZtDetail, setSelectedZtDetail] = useState(null); const [selectedEvents, setSelectedEvents] = useState([]); const [detailLoading, setDetailLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); // 加载日历综合数据(一次 API 调用获取所有数据) useEffect(() => { const loadCalendarCombinedData = async () => { try { const year = currentMonth.getFullYear(); const month = currentMonth.getMonth() + 1; const response = await fetch(`${getApiBase()}/api/v1/calendar/combined-data?year=${year}&month=${month}`); if (response.ok) { const result = await response.json(); if (result.success && result.data) { // 转换为 FullCalendarPro 需要的格式 const formattedData = result.data.map(item => ({ date: item.date, count: item.zt_count || 0, topSector: item.top_sector || '', eventCount: item.event_count || 0, indexChange: item.index_change, })); console.log('[HeroPanel] 加载日历综合数据成功,数据条数:', formattedData.length); setCalendarData(formattedData); } } } catch (error) { console.error('Failed to load calendar combined data:', error); } }; loadCalendarCombinedData(); }, [currentMonth]); // 处理日期点击 - 打开弹窗 const handleDateClick = useCallback(async (date) => { setSelectedDate(date); setModalOpen(true); setDetailLoading(true); const ztDateStr = formatDateStr(date); const eventDateStr = dayjs(date).format('YYYY-MM-DD'); // 加载涨停详情 const detail = ztDailyDetails[ztDateStr]; if (detail?.fullData) { setSelectedZtDetail(detail.fullData); } else { try { const response = await fetch(`/data/zt/daily/${ztDateStr}.json`); if (response.ok) { const data = await response.json(); setSelectedZtDetail(data); setZtDailyDetails(prev => ({ ...prev, [ztDateStr]: { ...prev[ztDateStr], fullData: data } })); } else { setSelectedZtDetail(null); } } catch { setSelectedZtDetail(null); } } // 加载事件详情 try { const response = await eventService.calendar.getEventsForDate(eventDateStr); if (response.success) { setSelectedEvents(response.data || []); } else { setSelectedEvents([]); } } catch { setSelectedEvents([]); } setDetailLoading(false); }, [ztDailyDetails]); // 月份变化回调 const handleMonthChange = useCallback((year, month) => { setCurrentMonth(new Date(year, month - 1, 1)); }, []); return ( <> {/* 顶部装饰条 */} {/* FullCalendar Pro - 炫酷跨天事件条日历(懒加载) */} 加载日历组件... }> {/* 图例说明 */} 连续热门概念 涨停≥60 涨停<60 N 未来事件数 +0.5% / -0.5% 上证涨跌 {/* 详情弹窗 */} setModalOpen(false)} selectedDate={selectedDate} ztDetail={selectedZtDetail} events={selectedEvents} loading={detailLoading} /> ); }; /** * 右侧 Tab 面板 - HeroUI 风格毛玻璃 */ const RightPanelTabs = () => { // 默认显示日历 const [activeTab, setActiveTab] = useState('calendar'); return ( {/* 背景光效 */} {/* Tab 切换头 */} setActiveTab('calendar')} > 涨停与未来日历 setActiveTab('comet')} > 连板情绪监测 {/* Tab 内容区域 */} {activeTab === 'comet' ? ( ) : ( )} ); }; /** * 使用说明弹窗组件 */ const InfoModal = () => { const { isOpen, onOpen, onClose } = useDisclosure(); return ( <> 使用说明 事件中心使用指南 📅 综合日历 日历同时展示历史涨停数据未来事件, 点击日期查看详细信息。 🔥 涨停板块 点击历史日期,查看当日涨停板块排行、涨停数量、涨停股票代码,帮助理解市场主线。 📊 未来事件 点击未来日期,查看事件详情,包括背景分析未来推演相关股票等。 💡 颜色越深表示涨停数越多 · 绿色标记表示有未来事件 ); }; /** * 顶部说明面板主组件 */ const HeroPanel = () => { const gradientBg = 'linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 25%, #16213e 50%, #1a1a2e 75%, #0a0a0a 100%)'; const borderColor = useColorModeValue('rgba(255, 215, 0, 0.3)', 'rgba(255, 215, 0, 0.25)'); return ( {/* 装饰性光晕 */} {/* 标题行 */} 事件中心 {isInTradingTime() && ( 交易中 )} {/* AI舆情时空决策驾驶舱 - 左侧今日统计(2/5),右侧Tab切换(3/5) */} {/* 左侧:今日事件统计 */} {/* 右侧:连板情绪 / 日历 Tab 切换 */} ); }; export default HeroPanel;