refactor(HeroPanel): 主组件重构 & 删除废弃文件

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 <noreply@anthropic.com>
This commit is contained in:
zdl
2026-01-15 11:44:09 +08:00
parent a426fb22b6
commit 69f587ad08
4 changed files with 423 additions and 3548 deletions

View File

@@ -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 (
<Box
@@ -165,30 +208,6 @@ const StockChangeIndicators = ({
const renderScoreIndicator = (label, score) => {
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);

View File

@@ -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` | 主线时间线视图 |
### Hookshooks/
@@ -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/` 原子组件目录

View File

@@ -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 }) => (
<Box
bg="rgba(0,0,0,0.3)"
borderRadius="lg"
p={3}
border="1px solid"
borderColor="rgba(255,215,0,0.15)"
_hover={{ borderColor: 'rgba(255,215,0,0.3)', transform: 'translateY(-2px)' }}
transition="all 0.2s"
>
<HStack spacing={2} mb={1}>
<Box color={color} fontSize="md">
{icon}
</Box>
<Text fontSize="xs" color="gray.400" fontWeight="medium">
{label}
</Text>
</HStack>
<Text fontSize="xl" fontWeight="bold" color={color}>
{value}
</Text>
{subText && (
<Text fontSize="xs" color="gray.500" mt={1}>
{subText}
</Text>
)}
{trend !== undefined && (
<HStack spacing={1} mt={1}>
<StatArrow type={trend >= 0 ? 'increase' : 'decrease'} />
<Text fontSize="xs" color={trend >= 0 ? '#FF4D4F' : '#52C41A'}>
{Math.abs(trend).toFixed(1)}%
</Text>
</HStack>
)}
{progress !== undefined && (
<Progress
value={progress}
size="xs"
colorScheme={progress >= 60 ? 'green' : progress >= 40 ? 'yellow' : 'red'}
mt={2}
borderRadius="full"
bg="rgba(255,255,255,0.1)"
/>
)}
</Box>
);
/**
* 热门事件列表项
*/
const TopEventItem = ({ event, rank }) => (
<HStack
spacing={2}
py={1.5}
px={2}
bg="rgba(0,0,0,0.2)"
borderRadius="md"
_hover={{ bg: 'rgba(255,215,0,0.1)' }}
transition="all 0.2s"
>
<Badge
colorScheme={rank === 1 ? 'yellow' : rank === 2 ? 'gray' : 'orange'}
fontSize="xs"
px={2}
borderRadius="full"
>
{rank}
</Badge>
<Tooltip label={event.title} placement="top" hasArrow>
<Text
fontSize="xs"
color="gray.200"
flex="1"
noOfLines={1}
cursor="default"
>
{event.title}
</Text>
</Tooltip>
<Text
fontSize="xs"
fontWeight="bold"
color={getChgColor(event.max_chg)}
>
{formatChg(event.max_chg)}
</Text>
</HStack>
);
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 (
<Box
bg="linear-gradient(180deg, rgba(25, 32, 55, 0.95) 0%, rgba(17, 24, 39, 0.98) 100%)"
borderRadius="xl"
p={4}
border="1px solid"
borderColor="rgba(255, 215, 0, 0.15)"
minH="400px"
>
<Center h="350px">
<Spinner size="lg" color="yellow.400" thickness="3px" />
</Center>
</Box>
);
}
if (error || !stats) {
return (
<Box
bg="linear-gradient(180deg, rgba(25, 32, 55, 0.95) 0%, rgba(17, 24, 39, 0.98) 100%)"
borderRadius="xl"
p={4}
border="1px solid"
borderColor="rgba(255, 215, 0, 0.15)"
minH="400px"
>
<Center h="350px">
<VStack spacing={2}>
<Text color="gray.400">暂无数据</Text>
<Text fontSize="xs" color="gray.500">{error}</Text>
</VStack>
</Center>
</Box>
);
}
const { summary, topPerformers = [] } = stats;
return (
<Box
bg="linear-gradient(180deg, rgba(25, 32, 55, 0.95) 0%, rgba(17, 24, 39, 0.98) 100%)"
borderRadius="xl"
p={4}
border="1px solid"
borderColor="rgba(255, 215, 0, 0.15)"
position="relative"
overflow="hidden"
h="100%"
>
{/* 背景装饰 */}
<Box
position="absolute"
top="-50%"
right="-30%"
w="300px"
h="300px"
bg="radial-gradient(circle, rgba(255,215,0,0.08) 0%, transparent 70%)"
pointerEvents="none"
/>
{/* 标题 */}
<HStack spacing={2} mb={4}>
<Box
w="4px"
h="20px"
bg="linear-gradient(180deg, #FFD700, #FFA500)"
borderRadius="full"
/>
<Text
fontSize="md"
fontWeight="bold"
color="white"
letterSpacing="wide"
>
事件有效性统计
</Text>
<Badge
colorScheme="yellow"
variant="subtle"
fontSize="xs"
px={2}
>
近30天
</Badge>
</HStack>
<VStack spacing={4} align="stretch">
{/* 核心指标 - 2列网格 */}
<Box
display="grid"
gridTemplateColumns="repeat(2, 1fr)"
gap={3}
>
<StatCard
label="事件总数"
value={summary?.totalEvents || 0}
icon={<FireOutlined />}
color="#FFD700"
subText="活跃事件"
/>
<StatCard
label="正向率"
value={`${(summary?.positiveRate || 0).toFixed(1)}%`}
icon={<CheckCircleOutlined />}
color={summary?.positiveRate >= 50 ? '#52C41A' : '#FF4D4F'}
progress={summary?.positiveRate || 0}
/>
<StatCard
label="平均涨幅"
value={formatChg(summary?.avgChg)}
icon={<RiseOutlined />}
color={getChgColor(summary?.avgChg)}
subText="关联股票"
/>
<StatCard
label="最大涨幅"
value={formatChg(summary?.maxChg)}
icon={<ThunderboltOutlined />}
color="#FF4D4F"
subText="单事件最佳"
/>
</Box>
{/* 评分指标 */}
<Box
display="grid"
gridTemplateColumns="repeat(2, 1fr)"
gap={3}
>
<StatCard
label="投资价值"
value={(summary?.avgInvestScore || 0).toFixed(0)}
icon={<StarOutlined />}
color="#F59E0B"
progress={summary?.avgInvestScore || 0}
subText="平均评分"
/>
<StatCard
label="超预期"
value={(summary?.avgSurpriseScore || 0).toFixed(0)}
icon={<TrophyOutlined />}
color="#8B5CF6"
progress={summary?.avgSurpriseScore || 0}
subText="惊喜程度"
/>
</Box>
{/* 分割线 */}
<Divider borderColor="rgba(255,215,0,0.1)" />
{/* TOP表现事件 */}
<Box>
<HStack spacing={2} mb={3}>
<TrophyOutlined style={{ color: '#FFD700', fontSize: '14px' }} />
<Text fontSize="sm" fontWeight="bold" color="gray.300">
TOP 表现事件
</Text>
</HStack>
<VStack spacing={2} align="stretch">
{topPerformers.slice(0, 5).map((event, idx) => (
<TopEventItem key={event.id || idx} event={event} rank={idx + 1} />
))}
{topPerformers.length === 0 && (
<Text fontSize="xs" color="gray.500" textAlign="center" py={2}>
暂无数据
</Text>
)}
</VStack>
</Box>
</VStack>
</Box>
);
};
export default EventEffectivenessStats;

File diff suppressed because it is too large Load Diff