// 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 && (
)}
);
}
},
{
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) => (
}
onClick={() => showKline(record)}
>
查看
)
},
{
title: '操作',
key: 'action',
width: 90,
render: (_, record) => {
const inWatchlist = isStockInWatchlist(record.code);
return (
: }
onClick={() => addSingleToWatchlist(record)}
disabled={inWatchlist}
>
{inWatchlist ? '已添加' : '加自选'}
);
}
},
];
// 涨停板块表格列 - 精致风格设计
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) => (
}
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`;
else if (code.startsWith('688')) stockCode = `${code}.SH`;
}
setSelectedKlineStock({
stock_code: stockCode,
stock_name: record.sname
});
setKlineModalVisible(true);
}}
style={{
background: 'linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%)',
border: 'none',
borderRadius: '6px',
}}
>
查看
),
},
{
title: '操作',
key: 'action',
width: 90,
align: 'center',
render: (_, record) => {
const code = record.scode;
const inWatchlist = isStockInWatchlist(code);
return (
: }
onClick={() => addSingleToWatchlist({ code, name: record.sname })}
disabled={inWatchlist}
style={inWatchlist ? {
background: 'linear-gradient(135deg, #faad14 0%, #fa8c16 100%)',
border: 'none',
borderRadius: '6px',
} : {
background: 'rgba(255,255,255,0.1)',
border: '1px solid rgba(255,215,0,0.3)',
borderRadius: '6px',
color: '#FFD700',
}}
>
{inWatchlist ? '已添加' : '加自选'}
);
},
},
];
// 事件表格列(参考投资日历)- 去掉相关概念列
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) => (
}
onClick={() => showContentDetail(text, '事件背景')}
disabled={!text}
>
查看
),
},
{
title: '未来推演',
dataIndex: 'forecast',
key: 'forecast',
width: 90,
render: (text) => (
}
onClick={() => showContentDetail(text, '未来推演')}
disabled={!text}
>
{text ? '查看' : '无'}
),
},
{
title: '相关股票',
dataIndex: 'related_stocks',
key: 'stocks',
width: 120,
render: (stocks, record) => {
const hasStocks = stocks && stocks.length > 0;
if (!hasStocks) {
return 无;
}
return (
}
onClick={() => showRelatedStocks(stocks, record.calendar_time, record.title)}
>
{stocks.length}只
);
},
},
];
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) => (
}
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',
}}
>
查看
),
},
{
title: '操作',
key: 'action',
width: 90,
align: 'center',
render: (_, record) => {
const code = record.scode;
const inWatchlist = isStockInWatchlist(code);
return (
: }
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 ? '已添加' : '加自选'}
);
},
},
]}
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;