From 69f587ad08dc02932f76ad6765ec53a39e8d0391 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Thu, 15 Jan 2026 11:44:09 +0800 Subject: [PATCH] =?UTF-8?q?refactor(HeroPanel):=20=E4=B8=BB=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E9=87=8D=E6=9E=84=20&=20=E5=88=A0=E9=99=A4=E5=BA=9F?= =?UTF-8?q?=E5=BC=83=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HeroPanel 重构: - 从 3000+ 行精简到 219 行,代码提取到子模块 - 导入模块化组件:CombinedCalendar, DetailModal - 导入样式:animations.css StockChangeIndicators 修复: - 修复 React Hooks 规则违规:useColorModeValue 移至组件顶层 - 颜色选择函数不再调用 Hook,只做值选择 其他: - 删除废弃的 EventEffectivenessStats.js - 更新 Community/README.md 目录结构文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/StockChangeIndicators.js | 139 +- src/views/Community/README.md | 233 +- .../components/EventEffectivenessStats.js | 362 -- src/views/Community/components/HeroPanel.js | 3237 +---------------- 4 files changed, 423 insertions(+), 3548 deletions(-) delete mode 100644 src/views/Community/components/EventEffectivenessStats.js diff --git a/src/components/StockChangeIndicators.js b/src/components/StockChangeIndicators.js index 68f4970b..b5179fd7 100644 --- a/src/components/StockChangeIndicators.js +++ b/src/components/StockChangeIndicators.js @@ -5,7 +5,6 @@ import React from 'react'; import { Flex, Box, Text, useColorModeValue } from '@chakra-ui/react'; import { ChevronUp, ChevronDown } from 'lucide-react'; import { TbArrowBigUpFilled, TbArrowBigDownFilled } from 'react-icons/tb'; -import { getChangeColor } from '../utils/colorUtils'; /** * 股票涨跌幅指标组件(3个指标:平均超额、最大超额、超预期得分) @@ -27,50 +26,95 @@ const StockChangeIndicators = ({ const isComfortable = size === 'comfortable'; const isDefault = size === 'default'; - // 根据涨跌幅获取数字颜色(动态深浅) + // ============ 在组件顶层调用所有 useColorModeValue ============ + // 涨跌幅数字颜色 + const colors = { + // 灰色系(null 值、0 值) + grayText: useColorModeValue('gray.700', 'gray.400'), + grayTextAlt: useColorModeValue('gray.600', 'gray.400'), + grayBg: useColorModeValue('gray.50', 'gray.800'), + grayBorder: useColorModeValue('gray.200', 'gray.700'), + // 红色系(上涨) + redHigh: useColorModeValue('red.600', 'red.300'), + redMid: useColorModeValue('red.500', 'red.300'), + redLow: useColorModeValue('red.400', 'red.200'), + redBg: useColorModeValue('red.50', 'red.900'), + redBorder: useColorModeValue('red.200', 'red.700'), + redScore: useColorModeValue('red.600', 'red.400'), + // 绿色系(下跌) + greenHigh: useColorModeValue('green.600', 'green.300'), + greenMid: useColorModeValue('green.500', 'green.300'), + greenLow: useColorModeValue('green.400', 'green.200'), + greenBg: useColorModeValue('green.50', 'green.900'), + greenBorder: useColorModeValue('green.200', 'green.700'), + // 橙色系(中等分数) + orangeScore: useColorModeValue('orange.600', 'orange.400'), + orangeBg: useColorModeValue('orange.50', 'orange.900'), + orangeBorder: useColorModeValue('orange.200', 'orange.700'), + // 蓝色系(低分数) + blueScore: useColorModeValue('blue.600', 'blue.400'), + blueBg: useColorModeValue('blue.50', 'blue.900'), + blueBorder: useColorModeValue('blue.200', 'blue.700'), + }; + + // 标签颜色 + const labelColor = colors.grayTextAlt; + + // ============ 颜色选择函数(不调用 Hook,只做选择)============ + // 根据涨跌幅获取数字颜色 const getNumberColor = (value) => { - if (value == null) { - return useColorModeValue('gray.700', 'gray.400'); - } + if (value == null) return colors.grayText; + if (value === 0) return colors.grayTextAlt; - // 使用动态颜色函数 - return getChangeColor(value); + const absValue = Math.abs(value); + if (value > 0) { + if (absValue >= 3) return colors.redHigh; + if (absValue >= 1) return colors.redMid; + return colors.redLow; + } else { + if (absValue >= 3) return colors.greenHigh; + if (absValue >= 1) return colors.greenMid; + return colors.greenLow; + } }; - // 根据涨跌幅获取背景色(永远比文字色浅) + // 根据涨跌幅获取背景色 const getBgColor = (value) => { - if (value == null) { - return useColorModeValue('gray.50', 'gray.800'); - } - - // 0值使用中性灰色背景 - if (value === 0) { - return useColorModeValue('gray.50', 'gray.800'); - } - - // 统一背景色:上涨红色系,下跌绿色系 - return value > 0 - ? useColorModeValue('red.50', 'red.900') - : useColorModeValue('green.50', 'green.900'); + if (value == null || value === 0) return colors.grayBg; + return value > 0 ? colors.redBg : colors.greenBg; }; - // 根据涨跌幅获取边框色(比背景深,比文字浅) + // 根据涨跌幅获取边框色 const getBorderColor = (value) => { - if (value == null) { - return useColorModeValue('gray.200', 'gray.700'); - } - - // 0值使用中性灰色边框 - if (value === 0) { - return useColorModeValue('gray.200', 'gray.700'); - } - - // 统一边框色:上涨红色系,下跌绿色系 - return value > 0 - ? useColorModeValue('red.200', 'red.700') - : useColorModeValue('green.200', 'green.700'); + if (value == null || value === 0) return colors.grayBorder; + return value > 0 ? colors.redBorder : colors.greenBorder; }; + // 根据分数获取颜色 + const getScoreColor = (score) => { + if (score >= 60) return colors.redScore; + if (score >= 40) return colors.orangeScore; + if (score >= 20) return colors.blueScore; + return colors.grayTextAlt; + }; + + // 根据分数获取背景色 + const getScoreBgColor = (score) => { + if (score >= 60) return colors.redBg; + if (score >= 40) return colors.orangeBg; + if (score >= 20) return colors.blueBg; + return colors.grayBg; + }; + + // 根据分数获取边框色 + const getScoreBorderColor = (score) => { + if (score >= 60) return colors.redBorder; + if (score >= 40) return colors.orangeBorder; + if (score >= 20) return colors.blueBorder; + return colors.grayBorder; + }; + + // ============ 渲染函数 ============ // 渲染单个指标 const renderIndicator = (label, value) => { if (value == null) return null; @@ -81,7 +125,6 @@ const StockChangeIndicators = ({ const numberColor = getNumberColor(value); const bgColor = getBgColor(value); const borderColor = getBorderColor(value); - const labelColor = useColorModeValue('gray.600', 'gray.400'); return ( { if (score == null) return null; - const labelColor = useColorModeValue('gray.600', 'gray.400'); - - // 根据分数确定颜色:>=60红色,>=40橙色,>=20蓝色,其他灰色 - const getScoreColor = (s) => { - if (s >= 60) return useColorModeValue('red.600', 'red.400'); - if (s >= 40) return useColorModeValue('orange.600', 'orange.400'); - if (s >= 20) return useColorModeValue('blue.600', 'blue.400'); - return useColorModeValue('gray.600', 'gray.400'); - }; - - const getScoreBgColor = (s) => { - if (s >= 60) return useColorModeValue('red.50', 'red.900'); - if (s >= 40) return useColorModeValue('orange.50', 'orange.900'); - if (s >= 20) return useColorModeValue('blue.50', 'blue.900'); - return useColorModeValue('gray.50', 'gray.800'); - }; - - const getScoreBorderColor = (s) => { - if (s >= 60) return useColorModeValue('red.200', 'red.700'); - if (s >= 40) return useColorModeValue('orange.200', 'orange.700'); - if (s >= 20) return useColorModeValue('blue.200', 'blue.700'); - return useColorModeValue('gray.200', 'gray.700'); - }; - const scoreColor = getScoreColor(score); const bgColor = getScoreBgColor(score); const borderColor = getScoreBorderColor(score); diff --git a/src/views/Community/README.md b/src/views/Community/README.md index 0952464d..796cb470 100644 --- a/src/views/Community/README.md +++ b/src/views/Community/README.md @@ -6,34 +6,90 @@ ``` src/views/Community/ -├── index.js # 页面入口 -├── components/ # 组件目录 -│ ├── SearchFilters/ # 搜索筛选模块 +├── index.js # 页面入口 +├── components/ # 组件目录 +│ ├── SearchFilters/ # 搜索筛选模块 │ │ ├── CompactSearchBox.js -│ │ ├── CompactSearchBox.css │ │ ├── TradingTimeFilter.js │ │ └── index.js -│ ├── EventCard/ # 事件卡片模块 -│ │ ├── atoms/ # 原子组件 +│ ├── EventCard/ # 事件卡片模块 +│ │ ├── atoms/ # 原子组件 +│ │ │ ├── EventDescription.js +│ │ │ ├── EventEngagement.js +│ │ │ ├── EventFollowButton.js +│ │ │ ├── EventHeader.js +│ │ │ ├── EventImportanceBadge.js +│ │ │ ├── EventPriceDisplay.js +│ │ │ ├── EventStats.js +│ │ │ ├── EventTimeline.js +│ │ │ ├── ImportanceBadge.js +│ │ │ ├── ImportanceStamp.js +│ │ │ ├── KeywordsCarousel.js +│ │ │ └── index.js +│ │ ├── CompactEventCard.js +│ │ ├── DetailedEventCard.js +│ │ ├── DynamicNewsEventCard.js +│ │ ├── HorizontalDynamicNewsEventCard.js +│ │ ├── MiniEventCard.js │ │ └── index.js -│ ├── HotEvents/ # 热点事件模块 -│ │ ├── HotEvents.js -│ │ ├── HotEvents.css -│ │ ├── HotEventsSection.js -│ │ └── index.js -│ ├── DynamicNews/ # 动态新闻模块 -│ │ ├── layouts/ +│ ├── DynamicNews/ # 动态新闻模块 │ │ ├── hooks/ +│ │ │ ├── usePagination.js +│ │ │ └── index.js +│ │ ├── layouts/ +│ │ │ ├── MainlineTimelineView.js +│ │ │ ├── VerticalModeLayout.js +│ │ │ └── index.js +│ │ ├── constants.js +│ │ ├── DynamicNewsCard.js +│ │ ├── EventDetailScrollPanel.js +│ │ ├── EventScrollList.js +│ │ ├── ModeToggleButtons.js +│ │ ├── PaginationControl.js │ │ └── index.js -│ ├── EventDetailModal/ # 事件详情弹窗模块 +│ ├── EventDetailModal/ # 事件详情弹窗模块 │ │ ├── EventDetailModal.tsx -│ │ ├── EventDetailModal.less │ │ └── index.ts -│ └── HeroPanel.js # 英雄面板(独立组件) -└── hooks/ # 页面级 Hooks +│ ├── HeroPanel/ # 英雄面板模块(重构版) +│ │ ├── columns/ # 表格列定义 +│ │ │ ├── index.js # 统一导出 +│ │ │ ├── renderers.js # 通用渲染器 +│ │ │ ├── stockColumns.js # 事件关联股票列 +│ │ │ ├── sectorColumns.js # 涨停板块列 +│ │ │ ├── ztStockColumns.js # 涨停个股列 +│ │ │ └── eventColumns.js # 未来事件列 +│ │ ├── components/ # 子组件 +│ │ │ ├── DetailModal/ # 详情弹窗子模块 +│ │ │ │ ├── EventsTabView.js +│ │ │ │ ├── RelatedEventsModal.js +│ │ │ │ ├── SectorStocksModal.js +│ │ │ │ ├── ZTSectorView.js +│ │ │ │ ├── ZTStockListView.js +│ │ │ │ └── index.js +│ │ │ ├── CalendarCell.js # 日历单元格 +│ │ │ ├── CombinedCalendar.js # 组合日历视图 +│ │ │ ├── InfoModal.js # 信息弹窗 +│ │ │ ├── HotKeywordsCloud.js # 热门关键词云 +│ │ │ ├── ZTStatsCards.js # 涨停统计卡片 +│ │ │ └── index.js +│ │ ├── constants/ # 常量定义 +│ │ │ └── index.js +│ │ ├── hooks/ # 自定义 Hooks +│ │ │ ├── useDetailModalState.js +│ │ │ └── index.js +│ │ ├── styles/ # 样式文件 +│ │ │ └── animations.css +│ │ ├── utils/ # 工具函数 +│ │ │ └── index.js +│ │ └── index.js +│ ├── EventDailyStats.js # 事件每日统计 +│ ├── HeroPanel.js # 英雄面板(主入口) +│ ├── MarketOverviewBanner.js # 市场概览横幅 +│ └── ThemeCometChart.js # 主题彗星图 +└── hooks/ # 页面级 Hooks + ├── useCommunityEvents.js ├── useEventData.js - ├── useEventFilters.js - └── useCommunityEvents.js + └── useEventFilters.js ``` --- @@ -42,10 +98,10 @@ src/views/Community/ 路径:`components/SearchFilters/` -| 文件 | 行数 | 功能 | -|------|------|------| -| `CompactSearchBox.js` | 612 | 紧凑搜索框,集成关键词搜索、概念/行业筛选 | -| `TradingTimeFilter.js` | 491 | 交易时间筛选器,被 CompactSearchBox 引用 | +| 文件 | 功能 | +|------|------| +| `CompactSearchBox.js` | 紧凑搜索框,集成关键词搜索、概念/行业筛选 | +| `TradingTimeFilter.js` | 交易时间筛选器,被 CompactSearchBox 引用 | **使用方式**: ```javascript @@ -66,6 +122,7 @@ import { CompactSearchBox } from './components/SearchFilters'; | `DetailedEventCard.js` | 详细事件卡片(展开模式) | | `DynamicNewsEventCard.js` | 动态新闻事件卡片 | | `HorizontalDynamicNewsEventCard.js` | 水平布局新闻卡片 | +| `MiniEventCard.js` | 迷你事件卡片 | ### 原子组件(atoms/) @@ -77,6 +134,7 @@ import { CompactSearchBox } from './components/SearchFilters'; | `EventPriceDisplay.js` | 股价显示 | | `EventTimeline.js` | 事件时间线 | | `EventFollowButton.js` | 关注按钮 | +| `EventEngagement.js` | 事件互动数据 | | `EventImportanceBadge.js` | 重要性徽章 | | `ImportanceBadge.js` | 通用重要性徽章 | | `ImportanceStamp.js` | 重要性印章 | @@ -93,23 +151,7 @@ import { EventHeader, EventTimeline } from './components/EventCard/atoms'; --- -## 3. HotEvents 模块(热点事件) - -路径:`components/HotEvents/` - -| 文件 | 功能 | -|------|------| -| `HotEvents.js` | 热点事件列表渲染 | -| `HotEventsSection.js` | 热点事件区块容器 | - -**使用方式**: -```javascript -import { HotEventsSection } from './components/HotEvents'; -``` - ---- - -## 4. DynamicNews 模块(动态新闻) +## 3. DynamicNews 模块(动态新闻) 路径:`components/DynamicNews/` @@ -117,7 +159,7 @@ import { HotEventsSection } from './components/HotEvents'; | 文件 | 功能 | |------|------| -| `DynamicNewsCard.js` | 主列表容器(695行) | +| `DynamicNewsCard.js` | 主列表容器 | | `EventScrollList.js` | 事件滚动列表 | | `EventDetailScrollPanel.js` | 事件详情滚动面板 | | `ModeToggleButtons.js` | 模式切换按钮 | @@ -129,7 +171,7 @@ import { HotEventsSection } from './components/HotEvents'; | 文件 | 功能 | |------|------| | `VerticalModeLayout.js` | 垂直布局模式 | -| `VirtualizedFourRowGrid.js` | 虚拟滚动四行网格(性能优化) | +| `MainlineTimelineView.js` | 主线时间线视图 | ### Hooks(hooks/) @@ -145,14 +187,13 @@ import { usePagination } from './components/DynamicNews/hooks'; --- -## 5. EventDetailModal 模块(事件详情弹窗) +## 4. EventDetailModal 模块(事件详情弹窗) 路径:`components/EventDetailModal/` | 文件 | 功能 | |------|------| | `EventDetailModal.tsx` | 事件详情弹窗(TypeScript) | -| `EventDetailModal.less` | 弹窗样式 | **使用方式**: ```javascript @@ -161,21 +202,115 @@ import EventDetailModal from './components/EventDetailModal'; --- +## 5. HeroPanel 模块(英雄面板) + +路径:`components/HeroPanel/` + `components/HeroPanel.js` + +### 主入口 +- `HeroPanel.js` - 英雄面板主组件(首页指数K线 + 概念词云 + 日历) + +### 子模块结构 + +| 目录 | 功能 | +|------|------| +| `columns/` | 表格列定义(工厂函数模式) | +| `components/` | 子组件集合 | +| `constants/` | 常量定义(颜色、热度等级等) | +| `hooks/` | 自定义 Hooks | +| `styles/` | 样式文件(动画 CSS) | +| `utils/` | 工具函数(日期、股票代码处理等) | + +### columns/ 表格列定义 + +| 文件 | 功能 | +|------|------| +| `stockColumns.js` | 事件关联股票表格列(现价、涨跌幅、关联理由、研报引用) | +| `sectorColumns.js` | 涨停板块表格列(排名、板块名称、涨停数、涨停股票、涨停归因) | +| `ztStockColumns.js` | 涨停个股表格列(股票信息、涨停时间、连板、核心板块、涨停简报) | +| `eventColumns.js` | 未来事件表格列(时间、重要度、标题、背景、未来推演、相关股票) | +| `renderers.js` | 通用列渲染器 | + +### components/ 子组件 + +| 文件 | 功能 | +|------|------| +| `CalendarCell.js` | 日历单元格(显示涨停数/事件数热度) | +| `CombinedCalendar.js` | 组合日历视图(FullCalendar 封装) | +| `InfoModal.js` | 信息弹窗 | +| `HotKeywordsCloud.js` | 热门关键词云(今日热词展示) | +| `ZTStatsCards.js` | 涨停统计卡片(连板分布、封板时间、公告驱动) | +| `DetailModal/` | 详情弹窗子模块 | + +### DetailModal/ 详情弹窗 + +| 文件 | 功能 | +|------|------| +| `EventsTabView.js` | 事件标签页视图 | +| `RelatedEventsModal.js` | 相关事件弹窗(涨停归因详情) | +| `SectorStocksModal.js` | 板块股票弹窗 | +| `ZTSectorView.js` | 涨停板块视图 | +| `ZTStockListView.js` | 涨停股票列表视图 | + +### hooks/ 自定义 Hooks + +| 文件 | 功能 | +|------|------| +| `useDetailModalState.js` | 详情弹窗状态管理(整合 17 个状态) | + +**使用方式**: +```javascript +// 使用主组件 +import HeroPanel from './components/HeroPanel'; + +// 使用列定义工厂函数 +import { createStockColumns, createSectorColumns } from './components/HeroPanel/columns'; + +// 使用子组件 +import { HotKeywordsCloud, ZTStatsCards } from './components/HeroPanel/components'; +``` + +--- + ## 6. 独立组件 路径:`components/` -| 文件 | 行数 | 功能 | -|------|------|------| -| `HeroPanel.js` | 972 | 首页英雄面板(指数K线 + 概念词云) | +| 文件 | 功能 | +|------|------| +| `MarketOverviewBanner.js` | 市场概览横幅,展示指数行情 | +| `ThemeCometChart.js` | 主题彗星图,可视化概念热度 | +| `EventDailyStats.js` | 事件每日统计面板 | -**说明**: -- `HeroPanel.js` 使用懒加载,包含 ECharts (~600KB) +--- + +## 页面级 Hooks + +路径:`hooks/` + +| 文件 | 功能 | +|------|------| +| `useCommunityEvents.js` | 社区事件数据获取与管理 | +| `useEventData.js` | 事件数据处理 | +| `useEventFilters.js` | 事件筛选逻辑 | --- ## 更新日志 +- **2026-01-13**: HeroPanel 模块重构优化 + - 新增 `columns/` 表格列定义文件(stockColumns、sectorColumns、ztStockColumns、eventColumns) + - 新增 `HotKeywordsCloud.js` 热门关键词云组件 + - 新增 `ZTStatsCards.js` 涨停统计卡片组件 + - HeroPanel.js 从 2,299 行优化至 1,257 行(减少 45%) + - 采用工厂函数模式提取列定义,支持 useMemo 缓存 + +- **2026-01-13**: 目录结构同步更新 + - 移除已删除的 `HotEvents/` 模块 + - 新增 `HeroPanel/` 模块结构说明 + - 新增独立组件说明(MarketOverviewBanner、ThemeCometChart、EventDailyStats) + - 删除未引用组件 `EventEffectivenessStats.js` + - 完善 `EventCard/atoms/` 原子组件列表 + - **2024-12-09**: 目录结构重组 - 创建 `SearchFilters/` 模块(含 CSS) - 创建 `EventCard/atoms/` 原子组件目录 diff --git a/src/views/Community/components/EventEffectivenessStats.js b/src/views/Community/components/EventEffectivenessStats.js deleted file mode 100644 index 8ab0327f..00000000 --- a/src/views/Community/components/EventEffectivenessStats.js +++ /dev/null @@ -1,362 +0,0 @@ -/** - * EventEffectivenessStats - 事件有效性统计 - * 展示事件中心的事件有效性数据,证明系统推荐价值 - */ -import React, { useState, useEffect, useMemo, useCallback } from 'react'; -import { - Box, - Text, - VStack, - HStack, - Spinner, - Center, - useToast, - Stat, - StatLabel, - StatNumber, - StatHelpText, - StatArrow, - Progress, - Badge, - Divider, - Tooltip, - Icon, -} from '@chakra-ui/react'; -import { - TrophyOutlined, - RiseOutlined, - FireOutlined, - CheckCircleOutlined, - ThunderboltOutlined, - StarOutlined, -} from '@ant-design/icons'; -import { getApiBase } from '@utils/apiConfig'; - -/** - * 格式化涨跌幅 - */ -const formatChg = (val) => { - if (val === null || val === undefined) return '-'; - const num = parseFloat(val); - if (isNaN(num)) return '-'; - return (num >= 0 ? '+' : '') + num.toFixed(2) + '%'; -}; - -/** - * 获取涨跌幅颜色 - */ -const getChgColor = (val) => { - if (val === null || val === undefined) return 'gray.400'; - const num = parseFloat(val); - if (isNaN(num)) return 'gray.400'; - if (num > 0) return '#FF4D4F'; - if (num < 0) return '#52C41A'; - return 'gray.400'; -}; - -/** - * 数据卡片组件 - */ -const StatCard = ({ label, value, icon, color = '#FFD700', subText, trend, progress }) => ( - - - - {icon} - - - {label} - - - - {value} - - {subText && ( - - {subText} - - )} - {trend !== undefined && ( - - = 0 ? 'increase' : 'decrease'} /> - = 0 ? '#FF4D4F' : '#52C41A'}> - {Math.abs(trend).toFixed(1)}% - - - )} - {progress !== undefined && ( - = 60 ? 'green' : progress >= 40 ? 'yellow' : 'red'} - mt={2} - borderRadius="full" - bg="rgba(255,255,255,0.1)" - /> - )} - -); - -/** - * 热门事件列表项 - */ -const TopEventItem = ({ event, rank }) => ( - - - {rank} - - - - {event.title} - - - - {formatChg(event.max_chg)} - - -); - -const EventEffectivenessStats = () => { - const [loading, setLoading] = useState(true); - const [stats, setStats] = useState(null); - const [error, setError] = useState(null); - const toast = useToast(); - - const fetchStats = useCallback(async () => { - setLoading(true); - setError(null); - try { - const apiBase = getApiBase(); - const response = await fetch(`${apiBase}/api/v1/events/effectiveness-stats?days=30`); - if (!response.ok) throw new Error('获取数据失败'); - const data = await response.json(); - if (data.code === 200) { - setStats(data.data); - } else { - throw new Error(data.message || '数据格式错误'); - } - } catch (err) { - console.error('获取事件有效性统计失败:', err); - setError(err.message); - toast({ - title: '获取统计数据失败', - description: err.message, - status: 'error', - duration: 3000, - isClosable: true, - }); - } finally { - setLoading(false); - } - }, [toast]); - - useEffect(() => { - fetchStats(); - }, [fetchStats]); - - if (loading) { - return ( - -
- -
-
- ); - } - - if (error || !stats) { - return ( - -
- - 暂无数据 - {error} - -
-
- ); - } - - const { summary, topPerformers = [] } = stats; - - return ( - - {/* 背景装饰 */} - - - {/* 标题 */} - - - - 事件有效性统计 - - - 近30天 - - - - - {/* 核心指标 - 2列网格 */} - - } - color="#FFD700" - subText="活跃事件" - /> - } - color={summary?.positiveRate >= 50 ? '#52C41A' : '#FF4D4F'} - progress={summary?.positiveRate || 0} - /> - } - color={getChgColor(summary?.avgChg)} - subText="关联股票" - /> - } - color="#FF4D4F" - subText="单事件最佳" - /> - - - {/* 评分指标 */} - - } - color="#F59E0B" - progress={summary?.avgInvestScore || 0} - subText="平均评分" - /> - } - color="#8B5CF6" - progress={summary?.avgSurpriseScore || 0} - subText="惊喜程度" - /> - - - {/* 分割线 */} - - - {/* TOP表现事件 */} - - - - - TOP 表现事件 - - - - {topPerformers.slice(0, 5).map((event, idx) => ( - - ))} - {topPerformers.length === 0 && ( - - 暂无数据 - - )} - - - - - ); -}; - -export default EventEffectivenessStats; diff --git a/src/views/Community/components/HeroPanel.js b/src/views/Community/components/HeroPanel.js index fe446121..15e4fcd1 100644 --- a/src/views/Community/components/HeroPanel.js +++ b/src/views/Community/components/HeroPanel.js @@ -2,9 +2,7 @@ // 综合日历面板:融合涨停分析 + 投资日历 // 点击日期弹出详情弹窗(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 React, { useState } from "react"; import { Box, Card, @@ -13,3043 +11,32 @@ import { 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; } - } - - /* 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 ( - <> - - - - 使用说明 - - - - - - - - - - - - - 事件中心使用指南 - - - - - - - - - 📅 综合日历 - - 日历同时展示历史涨停数据和 - 未来事件, - 点击日期查看详细信息。 - - - - - 🔥 涨停板块 - - 点击历史日期,查看当日涨停板块排行、涨停数量、涨停股票代码,帮助理解市场主线。 - - - - - 📊 未来事件 - - 点击未来日期,查看事件详情,包括背景分析、 - 未来推演、 - 相关股票等。 - - - - - - 💡 颜色越深表示涨停数越多 · 绿色标记表示有未来事件 - - - - - - - - ); -}; +} from "@chakra-ui/react"; +import { CalendarOutlined, FireOutlined } from "@ant-design/icons"; +import ThemeCometChart from "./ThemeCometChart"; +import EventDailyStats from "./EventDailyStats"; +import MarketOverviewBanner from "./MarketOverviewBanner"; +// HeroPanel 模块化导入 +import "./HeroPanel/styles/animations.css"; +import { DetailModal, CombinedCalendar } from "./HeroPanel/components"; /** * 顶部说明面板主组件 */ 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)'); + const [activeTab, setActiveTab] = useState("calendar"); // "calendar" | "comet" + const gradientBg = + "linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 25%, #16213e 50%, #1a1a2e 75%, #0a0a0a 100%)"; return ( {/* 装饰性光晕 */} { filter="blur(50px)" /> - - {/* 标题行 */} - - - - - 事件中心 - - - - {isInTradingTime() && ( - - - - 交易中 - - - )} - - + + {/* 顶部通栏:事件中心概览 */} + + + - {/* AI舆情时空决策驾驶舱 - 左侧今日统计(2/5),右侧Tab切换(3/5) */} - - {/* 左侧:今日事件统计 */} - - + {/* 左侧导航+TOP10(3/10),右侧内容(7/10) */} + + {/* 左侧:导航标签 + TOP10 */} + + + {/* 财经日历标签 */} + setActiveTab("calendar")} + > + + + + 财经日历 + + + 历史涨停 · 未来事件 + + + + + {/* 连板情绪监测标签 */} + setActiveTab("comet")} + > + + + + 连板情绪监测 + + + + + {/* TOP10 排行 */} + + + + - {/* 右侧:连板情绪 / 日历 Tab 切换 */} - - + {/* 右侧:内容区域 - 根据 Tab 切换 */} + + {activeTab === "calendar" ? ( + + ) : ( + + + + )}