Files
vf_react/src/views/Community/components/HeroPanel.js
2026-01-07 13:26:48 +08:00

1912 lines
59 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/views/Community/components/HeroPanel.js
// 综合日历面板:融合涨停分析 + 投资日历
// 点击日期弹出详情弹窗TAB切换历史涨停/未来事件)
import React, { useEffect, useState, useCallback, useMemo, memo } 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';
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; }
}
/* 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);
}
/* 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 (
<Icon
as={isUp ? TrendingUp : TrendingDown}
boxSize={3}
color={isUp ? '#22c55e' : '#ef4444'}
/>
);
});
TrendIcon.displayName = 'TrendIcon';
/**
* 日历单元格 - 显示涨停数和事件数(加大尺寸)
*/
const CalendarCell = memo(({ date, ztData, eventCount, previousZtData, isSelected, isToday, isWeekend, onClick }) => {
if (!date) {
return <Box minH="75px" />;
}
const hasZtData = !!ztData;
const hasEventData = eventCount > 0;
const ztCount = ztData?.count || 0;
const heatColors = getHeatColor(ztCount);
const topSector = ztData?.top_sector || '';
// 周末无数据显示"休市"
if (isWeekend && !hasZtData && !hasEventData) {
return (
<Box
p={2}
borderRadius="10px"
bg="rgba(30, 30, 40, 0.3)"
border="1px solid rgba(255, 255, 255, 0.03)"
textAlign="center"
minH="75px"
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
>
<Text fontSize="md" fontWeight="400" color="rgba(255, 255, 255, 0.25)">
{date.getDate()}
</Text>
<Text fontSize="xs" color="rgba(255, 255, 255, 0.2)">
休市
</Text>
</Box>
);
}
// 正常日期
return (
<Tooltip
label={
<VStack spacing={1} align="start" p={1}>
<Text fontWeight="bold" fontSize="md">{`${date.getMonth() + 1}${date.getDate()}`}</Text>
{hasZtData && <Text>涨停: {ztCount} {topSector && `| ${topSector}`}</Text>}
{hasEventData && <Text>未来事件: {eventCount}</Text>}
{!hasZtData && !hasEventData && <Text color="gray.400">暂无数据</Text>}
</VStack>
}
placement="top"
hasArrow
bg="rgba(15, 15, 22, 0.95)"
border="1px solid rgba(212, 175, 55, 0.3)"
borderRadius="10px"
>
<Box
as="button"
p={2}
borderRadius="10px"
bg={hasZtData ? heatColors.bg : hasEventData ? 'rgba(34, 197, 94, 0.2)' : 'rgba(40, 40, 50, 0.3)'}
border={
isSelected
? `2px solid ${goldColors.primary}`
: isToday
? `2px solid ${goldColors.light}`
: hasZtData
? `1px solid ${heatColors.border}`
: hasEventData
? '1px solid rgba(34, 197, 94, 0.4)'
: '1px solid rgba(255, 255, 255, 0.08)'
}
boxShadow={isSelected ? `0 0 15px ${goldColors.glow}` : isToday ? `0 0 10px ${goldColors.glow}` : 'none'}
position="relative"
cursor="pointer"
transition="all 0.2s"
_hover={{
transform: 'scale(1.05)',
boxShadow: '0 6px 20px rgba(0, 0, 0, 0.4)',
borderColor: goldColors.primary,
}}
onClick={() => onClick && onClick(date)}
w="full"
minH="75px"
>
{/* 今天标记 */}
{isToday && (
<Badge
position="absolute"
top="2px"
right="2px"
bg="rgba(239, 68, 68, 0.9)"
color="white"
fontSize="9px"
px={1}
borderRadius="sm"
>
今天
</Badge>
)}
<VStack spacing={0.5} align="center">
{/* 日期 */}
<Text
fontSize="lg"
fontWeight={isSelected || isToday ? 'bold' : '600'}
color={isSelected ? goldColors.primary : isToday ? goldColors.light : textColors.primary}
>
{date.getDate()}
</Text>
{/* 涨停数 + 趋势 */}
{hasZtData && (
<HStack spacing={1} justify="center">
<Icon as={Flame} boxSize={3} color={heatColors.text} />
<Text fontSize="sm" fontWeight="bold" color={heatColors.text}>
{ztCount}
</Text>
<TrendIcon current={ztCount} previous={previousZtData?.count} />
</HStack>
)}
{/* 事件数 */}
{hasEventData && (
<HStack spacing={1} justify="center">
<Icon as={FileText} boxSize={3} color="#22c55e" />
<Text fontSize="sm" fontWeight="bold" color="#22c55e">
{eventCount}
</Text>
</HStack>
)}
{/* 主要板块 */}
{hasZtData && topSector && (
<Text fontSize="xs" color={textColors.secondary} noOfLines={1} maxW="70px">
{topSector}
</Text>
)}
</VStack>
</Box>
</Tooltip>
);
});
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 [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);
// 板块数据处理 - 必须在条件返回之前调用所有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 || [],
}))
.sort((a, b) => b.count - a.count);
}, [ztDetail]);
// 股票详情数据处理
const stockList = useMemo(() => {
if (!ztDetail?.stock_infos) return [];
return ztDetail.stock_infos.map(stock => ({
...stock,
key: stock.scode,
}));
}, [ztDetail]);
// 热门关键词
const hotKeywords = useMemo(() => {
if (!ztDetail?.word_freq_data) return [];
return ztDetail.word_freq_data.slice(0, 12);
}, [ztDetail]);
// 获取六位股票代码(去掉后缀)- 纯函数不是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(
<StarFilled
key={i}
style={{
color: i <= star ? '#faad14' : '#d9d9d9',
fontSize: '14px'
}}
/>
);
}
return <span>{stars}</span>;
};
// 显示内容详情
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 (
<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>
);
}
},
{
title: 'K线图',
key: 'kline',
width: 80,
render: (_, record) => (
<Button
type="primary"
size="small"
icon={<LineChartOutlined />}
onClick={() => showKline(record)}
>
查看
</Button>
)
},
{
title: '操作',
key: 'action',
width: 90,
render: (_, record) => {
const inWatchlist = isStockInWatchlist(record.code);
return (
<Button
type={inWatchlist ? 'primary' : 'default'}
size="small"
icon={inWatchlist ? <StarFilled /> : <StarOutlined />}
onClick={() => addSingleToWatchlist(record)}
disabled={inWatchlist}
>
{inWatchlist ? '已添加' : '加自选'}
</Button>
);
}
},
];
// 涨停板块表格列
const sectorColumns = [
{
title: '排名',
key: 'rank',
width: 55,
align: 'center',
render: (_, __, index) => (
<Tag color={index < 3 ? 'gold' : 'default'} style={{ fontWeight: 'bold', margin: 0 }}>
{index + 1}
</Tag>
),
},
{
title: '板块',
dataIndex: 'name',
key: 'name',
width: 120,
render: (name) => (
<AntText strong style={{ color: '#FFD700' }}>{name}</AntText>
),
},
{
title: '涨停数',
dataIndex: 'count',
key: 'count',
width: 80,
align: 'center',
render: (count) => (
<Tag color={count >= 8 ? 'red' : count >= 5 ? 'volcano' : count >= 3 ? 'orange' : 'blue'}>
<FireOutlined style={{ marginRight: 3 }} />
{count}
</Tag>
),
},
{
title: '涨停股票',
dataIndex: 'stocks',
key: 'stocks',
render: (stocks) => {
// 根据股票代码查找股票名称
const getStockName = (code) => {
const stockInfo = stockList.find(s => s.scode === code);
return stockInfo?.sname || code;
};
return (
<Space wrap size={[4, 4]}>
{stocks.slice(0, 6).map((code) => (
<Tag
key={code}
style={{ cursor: 'pointer', margin: 0 }}
color="processing"
>
<a
href={`https://valuefrontier.cn/company?scode=${code}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'inherit' }}
>
{getStockName(code)}
</a>
</Tag>
))}
{stocks.length > 6 && (
<Tag style={{ margin: 0 }}>+{stocks.length - 6}</Tag>
)}
</Space>
);
},
},
];
// 涨停股票详情表格列
const ztStockColumns = [
{
title: '股票',
key: 'stock',
width: 120,
fixed: 'left',
render: (_, record) => (
<a
href={`https://valuefrontier.cn/company?scode=${record.scode}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#FFD700', fontWeight: 'bold' }}
>
{record.sname}
</a>
),
},
{
title: '涨停时间',
dataIndex: 'formatted_time',
key: 'time',
width: 85,
align: 'center',
render: (time) => (
<Tag color={time <= '09:30:00' ? 'red' : time <= '10:00:00' ? 'volcano' : 'default'}>
{time?.substring(0, 5) || '-'}
</Tag>
),
},
{
title: '连板',
dataIndex: 'continuous_days',
key: 'continuous',
width: 75,
align: 'center',
render: (text) => {
if (!text || text === '首板') return <Tag>首板</Tag>;
const match = text.match(/(\d+)/);
const days = match ? parseInt(match[1]) : 1;
return (
<Tag color={days >= 5 ? 'red' : days >= 3 ? 'volcano' : days >= 2 ? 'orange' : 'default'}>
{text}
</Tag>
);
},
},
{
title: '核心板块',
dataIndex: 'core_sectors',
key: 'sectors',
width: 180,
render: (sectors) => (
<Space wrap size={[2, 2]}>
{(sectors || []).slice(0, 3).map((sector, idx) => (
<Tag key={idx} color="gold" style={{ margin: 0, fontSize: '12px' }}>
{sector}
</Tag>
))}
</Space>
),
},
{
title: '涨停简报',
dataIndex: 'brief',
key: 'brief',
ellipsis: true,
render: (text) => {
if (!text) return <AntText type="secondary">-</AntText>;
// 移除HTML标签
const cleanText = text.replace(/<br\s*\/?>/gi, ' ').replace(/<[^>]+>/g, '');
return (
<Tooltip title={cleanText} placement="topLeft" overlayStyle={{ maxWidth: 500 }}>
<Button
type="link"
size="small"
onClick={() => showContentDetail(text.replace(/<br\s*\/?>/gi, '\n\n'), '涨停简报')}
style={{ padding: 0, height: 'auto', whiteSpace: 'normal', textAlign: 'left' }}
>
{cleanText.length > 40 ? cleanText.substring(0, 40) + '...' : cleanText}
</Button>
</Tooltip>
);
},
},
];
// 事件表格列(参考投资日历)- 去掉相关概念列
const eventColumns = [
{
title: '时间',
dataIndex: 'calendar_time',
key: 'time',
width: 80,
render: (time) => (
<Space>
<ClockCircleOutlined />
<AntText>{dayjs(time).format('HH:mm')}</AntText>
</Space>
),
},
{
title: '重要度',
dataIndex: 'star',
key: 'star',
width: 120,
render: renderStars,
},
{
title: '标题',
dataIndex: 'title',
key: 'title',
ellipsis: true,
render: (text) => (
<Tooltip title={text}>
<AntText strong style={{ fontSize: '14px' }}>{text}</AntText>
</Tooltip>
),
},
{
title: '背景',
dataIndex: 'former',
key: 'former',
width: 80,
render: (text) => (
<Button
type="link"
size="small"
icon={<LinkOutlined />}
onClick={() => showContentDetail(text, '事件背景')}
disabled={!text}
>
查看
</Button>
),
},
{
title: '未来推演',
dataIndex: 'forecast',
key: 'forecast',
width: 90,
render: (text) => (
<Button
type="link"
size="small"
icon={<RobotOutlined />}
onClick={() => showContentDetail(text, '未来推演')}
disabled={!text}
>
{text ? '查看' : '无'}
</Button>
),
},
{
title: '相关股票',
dataIndex: 'related_stocks',
key: 'stocks',
width: 120,
render: (stocks, record) => {
const hasStocks = stocks && stocks.length > 0;
if (!hasStocks) {
return <AntText type="secondary"></AntText>;
}
return (
<Button
type="link"
size="small"
icon={<StockOutlined />}
onClick={() => showRelatedStocks(stocks, record.calendar_time, record.title)}
>
{stocks.length}
</Button>
);
},
},
];
return (
<>
<Modal
isOpen={isOpen}
onClose={onClose}
size="6xl"
scrollBehavior="inside"
>
<ModalOverlay bg="blackAlpha.700" backdropFilter={GLASS_BLUR.sm} />
<ModalContent
maxW="1300px"
bg="linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)"
border="1px solid rgba(255,215,0,0.2)"
borderRadius="2xl"
>
<ModalHeader borderBottom="1px solid rgba(255,215,0,0.2)" py={4}>
<HStack spacing={4}>
<Box
p={2}
bg="rgba(255,215,0,0.15)"
borderRadius="lg"
border="1px solid rgba(255,215,0,0.3)"
>
<Icon as={Calendar} color="gold" boxSize={6} />
</Box>
<VStack align="start" spacing={0}>
<Text fontSize="xl" fontWeight="bold" color="white">
{dateStr}
</Text>
<HStack spacing={2}>
<Badge colorScheme={isPastDate ? 'purple' : 'green'} fontSize="sm">
{isPastDate ? '历史数据' : '未来事件'}
</Badge>
{ztDetail && (
<Badge colorScheme="red" fontSize="sm">
涨停 {ztDetail.total_stocks || 0}
</Badge>
)}
{events?.length > 0 && (
<Badge colorScheme="blue" fontSize="sm">
事件 {events.length}
</Badge>
)}
</HStack>
</VStack>
</HStack>
</ModalHeader>
<ModalCloseButton color="whiteAlpha.600" _hover={{ color: 'white' }} size="lg" />
<ModalBody py={6} px={6} className="hero-panel-modal">
{loading ? (
<Center h="300px">
<Spin size="large" tip="加载中..." />
</Center>
) : (
<Tabs
defaultActiveKey={isPastDate ? 'zt' : 'event'}
size="large"
tabBarStyle={{ marginBottom: 24 }}
>
{/* 涨停分析 Tab */}
<TabPane
tab={
<span style={{ fontSize: '16px' }}>
<FireOutlined style={{ marginRight: 8 }} />
涨停分析 ({ztDetail?.total_stocks || 0})
</span>
}
key="zt"
disabled={!ztDetail}
>
{(sectorList.length > 0 || stockList.length > 0) ? (
<VStack spacing={4} align="stretch">
{/* 热门关键词 */}
{hotKeywords.length > 0 && (
<Box
p={3}
bg="rgba(255, 215, 0, 0.05)"
borderRadius="lg"
border="1px solid rgba(255, 215, 0, 0.15)"
>
<HStack spacing={2} mb={2}>
<FireOutlined style={{ color: '#FFD700' }} />
<Text fontSize="sm" fontWeight="bold" color="gold">
今日热词
</Text>
</HStack>
<Space wrap size={[6, 6]}>
{hotKeywords.map((kw, idx) => (
<Tag
key={kw.name}
color={idx < 3 ? 'gold' : idx < 6 ? 'orange' : 'default'}
style={{
fontSize: idx < 3 ? '14px' : '13px',
fontWeight: idx < 3 ? 'bold' : 'normal',
}}
>
{kw.name}
</Tag>
))}
</Space>
</Box>
)}
{/* 视图切换按钮 */}
<HStack justify="space-between" align="center">
<HStack spacing={2}>
<Button
size="small"
type={ztViewMode === 'sector' ? 'primary' : 'default'}
onClick={() => setZtViewMode('sector')}
icon={<TagsOutlined />}
>
按板块 ({sectorList.length})
</Button>
<Button
size="small"
type={ztViewMode === 'stock' ? 'primary' : 'default'}
onClick={() => setZtViewMode('stock')}
icon={<StockOutlined />}
>
按个股 ({stockList.length})
</Button>
</HStack>
<Text fontSize="sm" color="whiteAlpha.600">
{ztDetail?.total_stocks || 0} 只涨停
</Text>
</HStack>
{/* 板块视图 */}
{ztViewMode === 'sector' && (
<Table
dataSource={sectorList}
columns={sectorColumns}
rowKey="name"
size="small"
pagination={false}
scroll={{ y: 350 }}
/>
)}
{/* 个股视图 */}
{ztViewMode === 'stock' && (
<Table
dataSource={stockList}
columns={ztStockColumns}
rowKey="scode"
size="small"
pagination={false}
scroll={{ x: 800, y: 350 }}
/>
)}
</VStack>
) : (
<Center h="200px">
<VStack>
<FireOutlined style={{ fontSize: 48, color: '#666' }} />
<AntText type="secondary" style={{ fontSize: 16 }}>暂无涨停数据</AntText>
</VStack>
</Center>
)}
</TabPane>
{/* 未来事件 Tab */}
<TabPane
tab={
<span style={{ fontSize: '16px' }}>
<CalendarOutlined style={{ marginRight: 8 }} />
未来事件 ({events?.length || 0})
</span>
}
key="event"
disabled={!events?.length}
>
{events?.length > 0 ? (
<Table
dataSource={events}
columns={eventColumns}
rowKey="id"
size="small"
pagination={false}
scroll={{ x: 900, y: 420 }}
/>
) : (
<Center h="200px">
<VStack>
<CalendarOutlined style={{ fontSize: 48, color: '#666' }} />
<AntText type="secondary" style={{ fontSize: 16 }}>暂无事件数据</AntText>
</VStack>
</Center>
)}
</TabPane>
</Tabs>
)}
</ModalBody>
</ModalContent>
</Modal>
{/* 内容详情抽屉 */}
<Drawer
isOpen={detailDrawerVisible}
placement="right"
size="lg"
onClose={() => setDetailDrawerVisible(false)}
>
<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)"
>
<DrawerCloseButton color="whiteAlpha.600" />
<DrawerHeader borderBottom="1px solid rgba(255,215,0,0.2)" color="white">
{selectedContent?.title}
</DrawerHeader>
<DrawerBody py={6}>
<Box
className="markdown-content"
color="whiteAlpha.900"
fontSize="md"
lineHeight="1.8"
sx={{
'& p': { mb: 4 },
'& h1, & h2, & h3': { color: 'gold', mb: 3 },
'& ul, & ol': { pl: 6 },
'& li': { mb: 2 },
}}
>
<ReactMarkdown>
{typeof selectedContent?.content === 'string'
? selectedContent.content
: selectedContent?.content?.data
? selectedContent.content.data.map(item => item.sentence || '').join('\n\n')
: '暂无内容'}
</ReactMarkdown>
<Text fontSize="sm" color="whiteAlpha.500" mt={6} fontStyle="italic">
(AI合成内容)
</Text>
</Box>
</DrawerBody>
</DrawerContent>
</Drawer>
{/* 相关股票弹窗 */}
<Modal
isOpen={stocksDrawerVisible}
onClose={() => {
setStocksDrawerVisible(false);
setExpandedReasons({});
}}
size="6xl"
scrollBehavior="inside"
>
<ModalOverlay bg="blackAlpha.700" backdropFilter={GLASS_BLUR.sm} />
<ModalContent
maxW="1100px"
maxH="85vh"
bg="linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)"
border="1px solid rgba(255,215,0,0.2)"
borderRadius="2xl"
>
<ModalHeader borderBottom="1px solid rgba(255,215,0,0.2)" py={4}>
<HStack spacing={3}>
<StockOutlined style={{ color: '#FFD700', fontSize: '20px' }} />
<VStack align="start" spacing={0}>
<Text fontSize="lg" color="white">相关股票</Text>
{selectedEventTitle && (
<Text fontSize="sm" color="whiteAlpha.600" fontWeight="normal" noOfLines={1} maxW="800px">
{selectedEventTitle}
</Text>
)}
</VStack>
<Badge colorScheme="blue" ml={2}>
{selectedEventStocks?.length || 0}
</Badge>
{stockQuotesLoading && <Spin size="small" />}
</HStack>
</ModalHeader>
<ModalCloseButton color="whiteAlpha.600" />
<ModalBody py={4} className="hero-panel-modal">
{selectedEventStocks && selectedEventStocks.length > 0 ? (
<Box maxH="calc(85vh - 120px)" overflowY="auto">
<Table
dataSource={selectedEventStocks}
columns={stockColumns}
rowKey={(record) => record.code}
size="middle"
pagination={false}
/>
</Box>
) : (
<Center h="200px">
<Text color="whiteAlpha.500">暂无相关股票</Text>
</Center>
)}
</ModalBody>
</ModalContent>
</Modal>
{/* K线图弹窗 */}
{selectedKlineStock && (
<KLineChartModal
isOpen={klineModalVisible}
onClose={() => {
setKlineModalVisible(false);
setSelectedKlineStock(null);
}}
stock={selectedKlineStock}
eventTime={selectedEventTime}
size="5xl"
/>
)}
</>
);
};
/**
* 综合日历组件(无左侧面板,点击弹窗)
*/
const CombinedCalendar = () => {
const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectedDate, setSelectedDate] = useState(null);
// 涨停数据
const [ztDatesData, setZtDatesData] = useState([]);
const [ztDailyDetails, setZtDailyDetails] = useState({});
const [selectedZtDetail, setSelectedZtDetail] = useState(null);
// 投资日历数据
const [eventCounts, setEventCounts] = useState([]);
const [selectedEvents, setSelectedEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [detailLoading, setDetailLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
// 加载涨停 dates.json
useEffect(() => {
const loadZtDatesData = async () => {
try {
const response = await fetch('/data/zt/dates.json');
if (response.ok) {
const data = await response.json();
setZtDatesData(data.dates || []);
}
} catch (error) {
console.error('Failed to load zt dates.json:', error);
}
};
loadZtDatesData();
}, []);
// 加载投资日历事件数量
useEffect(() => {
const loadEventCounts = async () => {
try {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth() + 1;
const response = await eventService.calendar.getEventCounts(year, month);
if (response.success) {
setEventCounts(response.data || []);
}
} catch (error) {
console.error('Failed to load event counts:', error);
} finally {
setLoading(false);
}
};
loadEventCounts();
}, [currentMonth]);
// 获取当月涨停板块详情
useEffect(() => {
const loadMonthZtDetails = async () => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const details = {};
const promises = [];
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}${String(month + 1).padStart(2, '0')}${String(day).padStart(2, '0')}`;
const hasData = ztDatesData.some(d => d.date === dateStr);
if (hasData && !ztDailyDetails[dateStr]) {
promises.push(
fetch(`/data/zt/daily/${dateStr}.json`)
.then(res => res.ok ? res.json() : null)
.then(data => {
if (data && data.sector_data) {
let maxSector = '';
let maxCount = 0;
Object.entries(data.sector_data).forEach(([sector, info]) => {
if (info.count > maxCount) {
maxCount = info.count;
maxSector = sector;
}
});
details[dateStr] = { top_sector: maxSector, fullData: data };
}
})
.catch(() => null)
);
}
}
if (promises.length > 0) {
await Promise.all(promises);
setZtDailyDetails(prev => ({ ...prev, ...details }));
}
};
if (ztDatesData.length > 0) {
loadMonthZtDetails();
}
}, [currentMonth, ztDatesData]);
// 构建日期数据映射
const ztDataMap = useMemo(() => {
const map = new Map();
ztDatesData.forEach(d => {
const detail = ztDailyDetails[d.date] || {};
map.set(d.date, {
date: d.date,
count: d.count,
top_sector: detail.top_sector,
});
});
return map;
}, [ztDatesData, ztDailyDetails]);
const eventCountMap = useMemo(() => {
const map = new Map();
eventCounts.forEach(d => {
map.set(d.date, d.count);
});
return map;
}, [eventCounts]);
// 生成日历天数
const days = useMemo(() => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
const result = [];
for (let i = 0; i < startingDayOfWeek; i++) {
result.push(null);
}
for (let i = 1; i <= daysInMonth; i++) {
result.push(new Date(year, month, i));
}
return result;
}, [currentMonth]);
// 预计算日历格子数据
const calendarCellsData = useMemo(() => {
const today = new Date();
const todayStr = today.toDateString();
const selectedDateStr = selectedDate?.toDateString();
return days.map((date, index) => {
if (!date) {
return { key: `empty-${index}`, date: null };
}
const ztDateStr = formatDateStr(date);
const eventDateStr = dayjs(date).format('YYYY-MM-DD');
const ztData = ztDataMap.get(ztDateStr) || null;
const eventCount = eventCountMap.get(eventDateStr) || 0;
const dayOfWeek = date.getDay();
const prevDate = new Date(date);
prevDate.setDate(prevDate.getDate() - 1);
const previousZtData = ztDataMap.get(formatDateStr(prevDate)) || null;
return {
key: ztDateStr || `day-${index}`,
date,
ztData,
eventCount,
previousZtData,
isSelected: date.toDateString() === selectedDateStr,
isToday: date.toDateString() === todayStr,
isWeekend: dayOfWeek === 0 || dayOfWeek === 6,
};
});
}, [days, ztDataMap, eventCountMap, selectedDate]);
// 处理日期点击 - 打开弹窗
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 handlePrevMonth = useCallback(() => {
setCurrentMonth(prev => new Date(prev.getFullYear(), prev.getMonth() - 1));
}, []);
const handleNextMonth = useCallback(() => {
setCurrentMonth(prev => new Date(prev.getFullYear(), prev.getMonth() + 1));
}, []);
return (
<>
<Box
bg="rgba(15, 15, 22, 0.6)"
backdropFilter={GLASS_BLUR.md}
borderRadius="20px"
border="1px solid rgba(212, 175, 55, 0.2)"
p={5}
position="relative"
overflow="hidden"
>
{/* 顶部装饰条 */}
<Box
position="absolute"
top={0}
left={0}
right={0}
h="3px"
bgGradient="linear(to-r, transparent, #D4AF37, #F4D03F, #D4AF37, transparent)"
animation="shimmer 3s linear infinite"
backgroundSize="200% 100%"
/>
{/* 月份导航 */}
<HStack justify="space-between" mb={4}>
<IconButton
icon={<ChevronLeft size={22} />}
variant="ghost"
size="md"
color={textColors.secondary}
_hover={{ color: goldColors.primary, bg: 'rgba(255, 255, 255, 0.05)' }}
onClick={handlePrevMonth}
aria-label="上个月"
/>
<HStack spacing={3}>
<Icon as={Calendar} boxSize={5} color={goldColors.primary} />
<Text fontSize="xl" fontWeight="bold" color={textColors.primary}>
{currentMonth.getFullYear()}{MONTH_NAMES[currentMonth.getMonth()]}
</Text>
<Text fontSize="sm" color={textColors.muted}>
综合日历
</Text>
</HStack>
<IconButton
icon={<ChevronRight size={22} />}
variant="ghost"
size="md"
color={textColors.secondary}
_hover={{ color: goldColors.primary, bg: 'rgba(255, 255, 255, 0.05)' }}
onClick={handleNextMonth}
aria-label="下个月"
/>
</HStack>
{/* 星期标题 */}
<SimpleGrid columns={7} spacing={2} mb={3}>
{WEEK_DAYS.map((day, idx) => (
<Text
key={day}
textAlign="center"
fontSize="md"
fontWeight="600"
color={idx === 0 || idx === 6 ? 'orange.300' : textColors.secondary}
>
{day}
</Text>
))}
</SimpleGrid>
{/* 日历格子 */}
{loading ? (
<Center h="300px">
<Spinner size="xl" color={goldColors.primary} thickness="3px" />
</Center>
) : (
<SimpleGrid columns={7} spacing={2}>
{calendarCellsData.map((cellData) => (
<CalendarCell
key={cellData.key}
date={cellData.date}
ztData={cellData.ztData}
eventCount={cellData.eventCount}
previousZtData={cellData.previousZtData}
isSelected={cellData.isSelected}
isToday={cellData.isToday}
isWeekend={cellData.isWeekend}
onClick={handleDateClick}
/>
))}
</SimpleGrid>
)}
{/* 图例 */}
<HStack spacing={5} mt={4} justify="center" flexWrap="wrap">
<HStack spacing={2}>
<Icon as={Flame} boxSize={4} color="purple.400" />
<Text fontSize="sm" color={textColors.muted}>涨停80</Text>
</HStack>
<HStack spacing={2}>
<Icon as={Flame} boxSize={4} color="red.400" />
<Text fontSize="sm" color={textColors.muted}>涨停60</Text>
</HStack>
<HStack spacing={2}>
<Icon as={Flame} boxSize={4} color="orange.400" />
<Text fontSize="sm" color={textColors.muted}>涨停40</Text>
</HStack>
<HStack spacing={2}>
<Icon as={FileText} boxSize={4} color="#22c55e" />
<Text fontSize="sm" color={textColors.muted}>未来事件</Text>
</HStack>
</HStack>
</Box>
{/* 详情弹窗 */}
<DetailModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
selectedDate={selectedDate}
ztDetail={selectedZtDetail}
events={selectedEvents}
loading={detailLoading}
/>
</>
);
};
/**
* 使用说明弹窗组件
*/
const InfoModal = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<HStack
spacing={1.5}
px={3}
py={1.5}
bg="rgba(255,215,0,0.08)"
border="1px solid rgba(255,215,0,0.2)"
borderRadius="full"
cursor="pointer"
transition="all 0.2s"
_hover={{
bg: 'rgba(255,215,0,0.15)',
borderColor: 'rgba(255,215,0,0.4)',
transform: 'scale(1.02)',
}}
onClick={onOpen}
>
<Icon as={Info} color="gold" boxSize={4} />
<Text fontSize="sm" color="gold" fontWeight="medium">
使用说明
</Text>
</HStack>
<Modal isOpen={isOpen} onClose={onClose} size="lg" isCentered motionPreset="slideInBottom">
<ModalOverlay bg="blackAlpha.700" backdropFilter={GLASS_BLUR.sm} />
<ModalContent
bg="linear-gradient(135deg, rgba(15,15,30,0.98) 0%, rgba(25,25,50,0.98) 100%)"
border="1px solid rgba(255,215,0,0.3)"
borderRadius="2xl"
boxShadow="0 25px 80px rgba(0,0,0,0.8)"
maxW="550px"
mx={4}
>
<ModalHeader borderBottom="1px solid rgba(255,215,0,0.2)" pb={4}>
<HStack spacing={3}>
<Box p={2} bg="rgba(255,215,0,0.15)" borderRadius="lg" border="1px solid rgba(255,215,0,0.3)">
<Icon as={Info} color="gold" boxSize={5} />
</Box>
<Text fontSize="xl" fontWeight="bold" bgGradient="linear(to-r, #FFD700, #FFA500)" bgClip="text">
事件中心使用指南
</Text>
</HStack>
</ModalHeader>
<ModalCloseButton color="whiteAlpha.600" _hover={{ color: 'white' }} />
<ModalBody py={6} px={6}>
<VStack align="stretch" spacing={5}>
<Box>
<Text fontSize="md" fontWeight="bold" color="whiteAlpha.900" mb={2}>📅 综合日历</Text>
<Text fontSize="md" color="whiteAlpha.800" lineHeight="1.8">
日历同时展示<Text as="span" color="purple.300" fontWeight="bold">历史涨停数据</Text>
<Text as="span" color="green.300" fontWeight="bold">未来事件</Text>
点击日期查看详细信息
</Text>
</Box>
<Box>
<Text fontSize="md" fontWeight="bold" color="whiteAlpha.900" mb={2}>🔥 涨停板块</Text>
<Text fontSize="md" color="whiteAlpha.800" lineHeight="1.8">
点击历史日期查看当日涨停板块排行涨停数量涨停股票代码帮助理解市场主线
</Text>
</Box>
<Box>
<Text fontSize="md" fontWeight="bold" color="whiteAlpha.900" mb={2}>📊 未来事件</Text>
<Text fontSize="md" color="whiteAlpha.800" lineHeight="1.8">
点击未来日期查看事件详情包括<Text as="span" color="cyan.300" fontWeight="bold">背景分析</Text>
<Text as="span" color="orange.300" fontWeight="bold">未来推演</Text>
<Text as="span" color="green.300" fontWeight="bold">相关股票</Text>
</Text>
</Box>
<Box pt={3} borderTop="1px solid rgba(255,215,0,0.2)">
<Text fontSize="md" color="yellow.300" textAlign="center" fontWeight="medium">
💡 颜色越深表示涨停数越多 · 绿色标记表示有未来事件
</Text>
</Box>
</VStack>
</ModalBody>
</ModalContent>
</Modal>
</>
);
};
/**
* 顶部说明面板主组件
*/
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 (
<Card
bg={gradientBg}
borderColor={borderColor}
borderWidth="1px"
boxShadow="0 20px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,215,0,0.1) inset"
mb={6}
overflow="hidden"
position="relative"
>
{/* 装饰性光晕 */}
<Box
position="absolute"
top="-50%"
right="-30%"
width="600px"
height="600px"
borderRadius="full"
bg="radial-gradient(circle, rgba(255,215,0,0.08) 0%, transparent 70%)"
pointerEvents="none"
filter="blur(40px)"
/>
<Box
position="absolute"
bottom="-40%"
left="-20%"
width="400px"
height="400px"
borderRadius="full"
bg="radial-gradient(circle, rgba(100,150,255,0.05) 0%, transparent 70%)"
pointerEvents="none"
filter="blur(50px)"
/>
<CardBody p={{ base: 4, md: 6 }}>
{/* 标题行 */}
<Flex align="center" justify="space-between" mb={5} wrap="wrap" gap={3}>
<HStack spacing={4}>
<Heading size="xl">
<Text
bgGradient="linear(to-r, #FFD700, #FFA500, #FFD700)"
bgClip="text"
backgroundSize="200% 100%"
animation="shimmer 3s linear infinite"
fontWeight="extrabold"
>
事件中心
</Text>
</Heading>
<InfoModal />
{isInTradingTime() && (
<HStack
spacing={2}
px={3}
py={1.5}
borderRadius="full"
bg="rgba(0,218,60,0.1)"
border="1px solid rgba(0,218,60,0.3)"
>
<Box
w="8px"
h="8px"
borderRadius="full"
bg="#00da3c"
animation="pulse 1.5s infinite"
boxShadow="0 0 10px #00da3c"
/>
<Text fontSize="sm" color="#00da3c" fontWeight="bold">
交易中
</Text>
</HStack>
)}
</HStack>
</Flex>
{/* 综合日历 */}
<CombinedCalendar />
</CardBody>
</Card>
);
};
export default HeroPanel;