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:
@@ -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);
|
||||
|
||||
@@ -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/` 原子组件目录
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user