3028 lines
90 KiB
Markdown
3028 lines
90 KiB
Markdown
# CLAUDE.md
|
||
|
||
> **🌐 语言偏好**: 请始终使用中文与用户交流,包括所有解释、分析、文档编写和代码注释。
|
||
|
||
本文件为 Claude Code (claude.ai/code) 提供在此代码库中工作的指导说明。
|
||
|
||
## 📖 文档结构导航
|
||
|
||
本文档分为以下主要章节,建议按需查阅:
|
||
|
||
**基础信息**:
|
||
- [项目概览](#项目概览) - 技术栈、项目定位
|
||
- [开发命令](#开发命令) - 常用命令速查
|
||
- [配置](#配置) - 环境变量、MSW、路径别名、Webpack
|
||
|
||
**架构设计**:
|
||
- [架构](#架构) - 应用入口流程、路由架构
|
||
- [前端目录结构详解](#前端目录结构详解) - 完整的目录说明与使用指南
|
||
- [后端架构详解](#后端架构详解) - Flask、Celery、数据库架构
|
||
|
||
**开发指南**:
|
||
- [开发工作流](#开发工作流) - 路由、组件、API、Redux 开发指南
|
||
- [常见开发任务](#常见开发任务) - 5 个详细的开发任务教程
|
||
- [技术路径与开发指南](#技术路径与开发指南) - UI 框架选型、技术栈演进、最佳实践
|
||
|
||
**技术决策与规范**:
|
||
- [UI 框架选型与使用指南](#ui-框架选型与使用指南) - Ant Design + Chakra UI 混合策略
|
||
- [架构设计原则](#架构设计原则) - 组件设计、状态管理、代码分割、API 层
|
||
- [开发规范与最佳实践](#开发规范与最佳实践) - 命名规范、Git 工作流、代码审查
|
||
|
||
**技术债务与维护**:
|
||
- [技术债务与已知问题](#技术债务与已知问题) - 技术债务跟踪、优化机会
|
||
- [更新本文档](#更新本文档) - 文档维护指南
|
||
|
||
---
|
||
|
||
## 项目概览
|
||
|
||
混合式 React 仪表板,用于金融/交易分析,采用 Flask 后端。基于 Argon Dashboard Chakra PRO 模板构建。
|
||
|
||
### 技术栈
|
||
|
||
**前端**
|
||
- **核心框架**: React 18.3.1
|
||
- **UI 组件库**: Chakra UI 2.8.2(主要) + Ant Design 5.27.4(表格/表单)
|
||
- **状态管理**: Redux Toolkit 2.9.2
|
||
- **路由**: React Router v6.30.1 配合 React.lazy() 实现代码分割
|
||
- **构建系统**: CRACO 7.1.0 + 激进的 webpack 5 优化
|
||
- **图表库**: ECharts 5.6.0、ApexCharts 3.27.3、Recharts 3.1.2、D3 7.9.0、Visx 3.12.0
|
||
- **动画**: Framer Motion 4.1.17
|
||
- **日历**: FullCalendar 5.9.0、React Big Calendar 0.33.2
|
||
- **图标**: Lucide React 0.540.0(推荐)、React Icons 4.12.0、@ant-design/icons 6.0.0
|
||
- **网络请求**: Axios 1.10.0
|
||
- **实时通信**: Socket.IO Client 4.7.4
|
||
- **数据分析**: PostHog 1.281.0
|
||
- **开发工具**: MSW (Mock Service Worker) 用于 API mocking
|
||
- **虚拟化**: @tanstack/react-virtual 3.13.12(性能优化)
|
||
- **其他**: Draft.js(富文本编辑)、React Markdown、React Quill
|
||
|
||
**后端**
|
||
- Flask + SQLAlchemy ORM
|
||
- ClickHouse(分析型数据库)+ MySQL/PostgreSQL(事务型数据库)
|
||
- Flask-SocketIO 实现 WebSocket 实时更新
|
||
- Celery + Redis 处理后台任务
|
||
- 腾讯云短信 + 微信支付集成
|
||
|
||
## 开发命令
|
||
|
||
### 前端开发
|
||
```bash
|
||
npm start # 使用 mock 数据启动(.env.mock),代理到 localhost:5001
|
||
npm run start:real # 使用真实后端启动(.env.local)
|
||
npm run start:dev # 使用开发配置启动(.env.development)
|
||
npm run start:test # 同时启动后端(app.py)和前端(.env.test)
|
||
npm run dev # 'npm start' 的别名
|
||
npm run backend # 仅启动 Flask 服务器(python app.py)
|
||
|
||
npm run build # 生产环境构建,包含 Gulp 许可证头
|
||
npm run build:analyze # 使用 webpack bundle analyzer 构建
|
||
npm test # 运行 React 测试套件(CRACO)
|
||
|
||
npm run lint:check # 检查 ESLint 规则(退出码 0)
|
||
npm run lint:fix # 自动修复 ESLint 问题
|
||
npm run clean # 删除 node_modules 和 package-lock.json
|
||
npm run reinstall # 清洁安装(运行 clean + install)
|
||
```
|
||
|
||
### 后端开发
|
||
```bash
|
||
python app.py # 主 Flask 服务器
|
||
python simulation_background_processor.py # 交易模拟的后台任务处理器
|
||
pip install -r requirements.txt # 安装 Python 依赖
|
||
```
|
||
|
||
### 部署
|
||
```bash
|
||
npm run deploy # 从本地部署(scripts/deploy-from-local.sh)
|
||
npm run rollback # 回滚到上一个版本
|
||
```
|
||
|
||
## 架构
|
||
|
||
### 应用入口流程
|
||
```
|
||
src/index.js
|
||
└── src/App.js(根组件)
|
||
├── AppProviders (src/providers/AppProviders.js)
|
||
│ ├── ReduxProvider(store 来自 src/store/)
|
||
│ ├── ChakraProvider(主题来自 src/theme/)
|
||
│ ├── NotificationProvider (src/contexts/NotificationContext.js)
|
||
│ └── AuthProvider (src/contexts/AuthContext.js)
|
||
├── AppRoutes (src/routes/index.js)
|
||
│ ├── MainLayout 路由(带导航栏/页脚)
|
||
│ └── 独立路由(认证页面、全屏视图)
|
||
└── GlobalComponents(模态框覆盖层、全局 UI)
|
||
```
|
||
|
||
### 路由架构(模块化设计)
|
||
路由采用**声明式**设计,并拆分到 `src/routes/` 中的多个文件:
|
||
|
||
- **index.js** - 主路由器(组合配置 + 渲染路由)
|
||
- **routeConfig.js** - 路由定义(路径、组件、保护模式、布局、子路由)
|
||
- **lazy-components.js** - React.lazy() 导入,用于代码分割
|
||
- **homeRoutes.js** - 嵌套的首页路由
|
||
- **constants/** - 保护模式、布局映射
|
||
- **utils/** - 路由渲染逻辑(wrapWithProtection、renderRoute)
|
||
|
||
路由保护模式(PROTECTION_MODES):
|
||
- `PUBLIC` - 无需认证
|
||
- `MODAL` - 未登录时显示认证模态框
|
||
- `REDIRECT` - 未登录时重定向到 /auth/sign-in
|
||
|
||
### 前端目录结构详解
|
||
|
||
本项目采用**功能驱动的目录结构**,按职责将代码组织到不同目录中。以下是完整的目录结构及详细说明:
|
||
|
||
#### 核心目录概览
|
||
|
||
```
|
||
src/
|
||
├── index.js # 应用入口文件
|
||
├── App.js # 根组件(组合 Providers + Routes)
|
||
│
|
||
├── providers/ # Provider 组合层
|
||
├── routes/ # 路由配置与管理
|
||
├── layouts/ # 页面布局模板
|
||
├── views/ # 页面级组件
|
||
├── components/ # 可复用 UI 组件
|
||
│
|
||
├── contexts/ # React Context 状态管理
|
||
├── store/ # Redux 全局状态管理
|
||
├── hooks/ # 自定义 React Hooks
|
||
│
|
||
├── services/ # API 服务层
|
||
├── utils/ # 工具函数库
|
||
├── constants/ # 全局常量定义
|
||
│
|
||
├── theme/ # UI 主题配置
|
||
├── assets/ # 静态资源
|
||
├── styles/ # 全局样式
|
||
│
|
||
├── mocks/ # 开发环境 Mock 数据
|
||
├── lib/ # 第三方库配置
|
||
└── variables/ # 可配置变量
|
||
```
|
||
|
||
---
|
||
|
||
#### 详细目录说明
|
||
|
||
##### 📁 `src/providers/` - Provider 组合层
|
||
|
||
**用途**: 集中管理应用的所有 Context Providers,避免在 App.js 中嵌套过深。
|
||
|
||
**核心文件**:
|
||
- `AppProviders.js` - 所有 Provider 的组合容器
|
||
|
||
**Provider 嵌套顺序**(从外到内):
|
||
```javascript
|
||
ReduxProvider // 1. Redux 状态管理(最外层)
|
||
└─ ChakraProvider // 2. Chakra UI 主题系统
|
||
└─ ConfigProvider // 3. Ant Design 主题配置
|
||
└─ NotificationProvider // 4. 通知系统
|
||
└─ AuthProvider // 5. 认证系统(最内层)
|
||
```
|
||
|
||
**何时修改**:
|
||
- 添加新的全局 Provider(如 i18n、Analytics)
|
||
- 调整 Provider 顺序(注意依赖关系)
|
||
|
||
---
|
||
|
||
##### 📁 `src/routes/` - 路由系统
|
||
|
||
**用途**: 模块化路由配置,采用声明式设计,支持懒加载和路由保护。
|
||
|
||
**核心文件**:
|
||
- `index.js` - 主路由器(渲染所有路由)
|
||
- `routeConfig.js` - **路由配置中心**(所有路由定义)
|
||
- `lazy-components.js` - React.lazy() 懒加载组件导入
|
||
- `homeRoutes.js` - 首页嵌套路由
|
||
- `constants/protectionModes.js` - 路由保护模式定义
|
||
- `utils/wrapWithProtection.js` - 路由保护逻辑
|
||
- `utils/renderRoute.js` - 路由渲染工具
|
||
|
||
**路由保护模式**:
|
||
- `PUBLIC` - 公开访问,无需登录
|
||
- `MODAL` - 未登录时显示登录模态框(不跳转)
|
||
- `REDIRECT` - 未登录时重定向到 /auth/sign-in
|
||
|
||
**添加新路由的步骤**: 参见"常见开发任务 → 如何添加新的页面路由"
|
||
|
||
**命名约定**:
|
||
- 路由路径使用 kebab-case: `portfolio`, `trading-simulation`
|
||
- 组件名使用 PascalCase: `Portfolio`, `TradingSimulation`
|
||
|
||
---
|
||
|
||
##### 📁 `src/layouts/` - 页面布局
|
||
|
||
**用途**: 定义页面的通用布局结构(导航栏、侧边栏、页脚等),复用于多个页面。
|
||
|
||
**核心文件**:
|
||
- `MainLayout.js` - 主布局(带顶部导航栏 + 侧边栏 + 页脚)
|
||
- `Auth.js` - 认证页面布局(无导航栏,纯净背景)
|
||
|
||
**布局特性**:
|
||
- 每个布局包含独立的 ErrorBoundary(错误隔离)
|
||
- 通过 `<Outlet />` 渲染子路由内容
|
||
- 布局内可访问认证状态(AuthContext)
|
||
|
||
**何时创建新布局**:
|
||
- 需要完全不同的页面结构(如打印页、全屏图表)
|
||
- 特定页面需要自定义导航栏/侧边栏
|
||
|
||
---
|
||
|
||
##### 📁 `src/views/` - 页面级组件
|
||
|
||
**用途**: 存放路由对应的页面组件,每个文件对应一个路由页面。
|
||
|
||
**组织结构**:
|
||
```
|
||
views/
|
||
├── Community/ # 社区页面(大型功能模块)
|
||
│ ├── index.js # 页面入口
|
||
│ ├── components/ # 页面专属组件
|
||
│ │ ├── EventList/
|
||
│ │ ├── EventCard/
|
||
│ │ └── StockDetailPanel/
|
||
│ ├── hooks/ # 页面专属 Hooks
|
||
│ │ └── useCommunityData.js
|
||
│ └── utils/ # 页面专属工具函数
|
||
│
|
||
├── TradingSimulation/ # 交易模拟页面
|
||
│ ├── index.js
|
||
│ ├── components/
|
||
│ └── ...
|
||
│
|
||
├── Dashboard/ # 仪表板页面
|
||
│ └── index.js
|
||
│
|
||
└── Portfolio/ # 投资组合页面(简单页面)
|
||
└── index.js
|
||
```
|
||
|
||
**命名约定**:
|
||
- 目录名使用 PascalCase: `Community`, `TradingSimulation`
|
||
- 主文件始终为 `index.js`(方便导入)
|
||
|
||
**原则**:
|
||
- 页面组件主要负责**数据获取和布局组合**,不应包含复杂的 UI 逻辑
|
||
- 复杂 UI 逻辑应拆分到 `components/` 子目录
|
||
- 页面超过 500 行时考虑拆分(参见"组件组织模式")
|
||
|
||
---
|
||
|
||
##### 📁 `src/components/` - 可复用 UI 组件
|
||
|
||
**用途**: 存放跨页面复用的通用 UI 组件(按钮、卡片、表格、模态框等)。
|
||
|
||
**组织结构**(推荐采用原子设计模式):
|
||
```
|
||
components/
|
||
├── Atoms/ # 原子组件(1-50 行)
|
||
│ ├── Button/
|
||
│ ├── Badge/
|
||
│ └── Icon/
|
||
│
|
||
├── Molecules/ # 分子组件(50-150 行)
|
||
│ ├── Card/
|
||
│ ├── StatCard/
|
||
│ └── SearchBar/
|
||
│
|
||
├── Organisms/ # 有机体组件(150-500 行)
|
||
│ ├── Navbar/
|
||
│ ├── Sidebar/
|
||
│ ├── DataTable/
|
||
│ └── FilterPanel/
|
||
│
|
||
└── Templates/ # 模板组件(可选)
|
||
└── PageTemplate/
|
||
```
|
||
|
||
**何时添加新组件**:
|
||
- 组件在 2+ 个页面中使用
|
||
- 组件具有明确的职责和边界
|
||
- 组件可独立测试
|
||
|
||
**命名约定**:
|
||
- 组件目录使用 PascalCase: `EventCard`, `StockTable`
|
||
- 每个组件目录包含:
|
||
- `index.js` - 主导出文件
|
||
- `ComponentName.js` - 实现代码(可选,简单组件直接在 index.js)
|
||
- `ComponentName.test.js` - 测试文件(可选)
|
||
|
||
**避免**:
|
||
- 将页面特定组件放在这里(应放在 `views/{PageName}/components/`)
|
||
- 过度拆分(不要为了拆分而拆分)
|
||
|
||
---
|
||
|
||
##### 📁 `src/contexts/` - React Context 状态管理
|
||
|
||
**用途**: 存放使用 React Context API 管理的跨组件状态。
|
||
|
||
**核心 Contexts**:
|
||
- `AuthContext.js` - 认证状态(用户信息、登录状态、登录/登出方法)
|
||
- `NotificationContext.js` - 通知系统(显示 Toast/Alert)
|
||
- `SidebarContext.js` - 侧边栏状态(展开/收起)
|
||
|
||
**何时使用 Context**:
|
||
- 跨层级传递数据(避免 props drilling)
|
||
- 不频繁变化的数据(主题、语言、认证状态)
|
||
- 依赖注入场景
|
||
|
||
**何时不使用 Context**:
|
||
- 频繁变化的数据 → 使用 Redux
|
||
- 服务端数据 → 使用 React Query(计划中)
|
||
- 本地 UI 状态 → 使用 useState
|
||
|
||
**Context 文件结构**:
|
||
```javascript
|
||
// AuthContext.js
|
||
export const AuthContext = createContext();
|
||
|
||
export const AuthProvider = ({ children }) => {
|
||
const [user, setUser] = useState(null);
|
||
// ...
|
||
return (
|
||
<AuthContext.Provider value={{ user, login, logout }}>
|
||
{children}
|
||
</AuthContext.Provider>
|
||
);
|
||
};
|
||
|
||
export const useAuth = () => useContext(AuthContext); // 自定义 Hook
|
||
```
|
||
|
||
---
|
||
|
||
##### 📁 `src/store/` - Redux 全局状态管理
|
||
|
||
**用途**: 使用 Redux Toolkit 管理全局状态(UI 状态、跨页面共享数据)。
|
||
|
||
**目录结构**:
|
||
```
|
||
store/
|
||
├── index.js # Store 配置(combineReducers)
|
||
└── slices/ # Redux Slices
|
||
├── authModalSlice.js # 认证模态框状态
|
||
├── posthogSlice.js # PostHog 分析配置
|
||
├── stockSlice.js # 股票数据
|
||
├── industrySlice.js # 行业/概念数据
|
||
├── subscriptionSlice.js # 用户订阅状态
|
||
└── communityDataSlice.js # 社区数据
|
||
```
|
||
|
||
**何时使用 Redux**:
|
||
- 全局 UI 状态(模态框开关、侧边栏状态)
|
||
- 需要在多个不相关组件间共享的数据
|
||
- 需要持久化的状态(与 redux-persist 结合)
|
||
- 需要中间件处理的复杂逻辑
|
||
|
||
**Slice 命名约定**:
|
||
- 文件名: `{feature}Slice.js` (如 `authModalSlice.js`)
|
||
- Slice 名称: `{feature}` (如 `name: 'authModal'`)
|
||
- Actions: 动词开头 (如 `openModal`, `closeModal`, `setUser`)
|
||
|
||
**添加新 Slice**: 参见"常见开发任务 → 如何添加新的 Redux Slice"
|
||
|
||
---
|
||
|
||
##### 📁 `src/hooks/` - 自定义 React Hooks
|
||
|
||
**用途**: 存放可复用的自定义 Hooks,封装常见逻辑模式。
|
||
|
||
**Hook 类型示例**:
|
||
```
|
||
hooks/
|
||
├── useAuth.js # 认证相关(实际导出自 AuthContext)
|
||
├── useStockData.js # 数据获取 Hook
|
||
├── useDebounce.js # 防抖 Hook
|
||
├── useIntersectionObserver.js # 懒加载/无限滚动
|
||
├── useLocalStorage.js # 本地存储持久化
|
||
├── useMediaQuery.js # 响应式查询
|
||
└── usePrevious.js # 获取前一个值
|
||
```
|
||
|
||
**命名约定**:
|
||
- 文件名: `use{PascalCase}.js` (如 `useStockData.js`)
|
||
- 必须以 `use` 开头(React Hooks 规则)
|
||
- 一个文件导出一个 Hook
|
||
|
||
**Hook 设计原则**:
|
||
- 职责单一(只做一件事)
|
||
- 返回对象或数组(不返回原始值)
|
||
- 包含完整的状态管理(loading, error, data)
|
||
|
||
**何时创建新 Hook**:
|
||
- 逻辑在 2+ 个组件中重复
|
||
- 组件逻辑可以独立测试
|
||
- 需要封装浏览器 API(localStorage, IntersectionObserver)
|
||
|
||
---
|
||
|
||
##### 📁 `src/services/` - API 服务层
|
||
|
||
**用途**: 封装所有后端 API 调用,提供统一的接口供组件调用。
|
||
|
||
**组织结构**(按业务领域划分):
|
||
```
|
||
services/
|
||
├── authService.js # 认证相关 API(登录、注册、登出)
|
||
├── stockService.js # 股票数据 API
|
||
├── portfolioService.js # 投资组合 API
|
||
├── communityService.js # 社区内容 API
|
||
├── tradingService.js # 交易模拟 API
|
||
└── userService.js # 用户信息 API
|
||
```
|
||
|
||
**服务函数命名约定**:
|
||
- `fetch{Entity}` - 获取数据 (如 `fetchStockData`)
|
||
- `create{Entity}` - 创建记录 (如 `createOrder`)
|
||
- `update{Entity}` - 更新记录 (如 `updateProfile`)
|
||
- `delete{Entity}` - 删除记录 (如 `deletePost`)
|
||
|
||
**标准 API 调用结构**:
|
||
```javascript
|
||
// stockService.js
|
||
import axios from 'axios';
|
||
import { getApiBase } from '@utils/apiConfig';
|
||
|
||
const api = axios.create({
|
||
baseURL: getApiBase(),
|
||
timeout: 10000,
|
||
});
|
||
|
||
export const fetchStockData = async (stockCode) => {
|
||
try {
|
||
const response = await api.get(`/api/stocks/${stockCode}`);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('fetchStockData error:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
```
|
||
|
||
**原则**:
|
||
- 组件不应直接调用 axios(通过 service 层)
|
||
- 每个 service 函数应包含错误处理
|
||
- 使用 `getApiBase()` 获取 API 基础 URL(支持 Mock 模式)
|
||
|
||
---
|
||
|
||
##### 📁 `src/utils/` - 工具函数库
|
||
|
||
**用途**: 存放纯函数工具、格式化工具、计算逻辑等不依赖 React 的通用代码。
|
||
|
||
**核心工具文件**:
|
||
```
|
||
utils/
|
||
├── apiConfig.js # API 配置(getApiBase, isMockMode)
|
||
├── priceFormatters.js # 价格/数字格式化函数
|
||
├── dateFormatters.js # 日期格式化函数
|
||
├── logger.js # 统一日志工具
|
||
├── validators.js # 表单验证函数
|
||
├── calculations.js # 金融计算函数
|
||
└── string.js # 字符串处理函数
|
||
```
|
||
|
||
**命名约定**:
|
||
- 文件名: `camelCase.js` (如 `priceFormatters.js`)
|
||
- 函数名: `camelCase` (如 `formatPrice`, `calculateProfit`)
|
||
- 导出方式: 命名导出 (如 `export const formatPrice = ...`)
|
||
|
||
**工具函数特点**:
|
||
- **纯函数**: 相同输入始终返回相同输出,无副作用
|
||
- **可测试**: 易于编写单元测试
|
||
- **可复用**: 多处使用的逻辑
|
||
|
||
**示例**:
|
||
```javascript
|
||
// priceFormatters.js
|
||
export const formatPrice = (price, currency = '¥') => {
|
||
return `${currency}${price.toFixed(2)}`;
|
||
};
|
||
|
||
export const formatPercent = (value, decimals = 2) => {
|
||
return `${(value * 100).toFixed(decimals)}%`;
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
##### 📁 `src/constants/` - 全局常量定义
|
||
|
||
**用途**: 存放全局常量、枚举值、配置对象等不可变数据。
|
||
|
||
**常见常量文件**:
|
||
```
|
||
constants/
|
||
├── animations.js # 动画配置(duration、easing)
|
||
├── routes.js # 路由路径常量
|
||
├── apiEndpoints.js # API 端点常量
|
||
├── colors.js # 颜色常量(补充主题)
|
||
├── tradingConfig.js # 交易相关配置
|
||
└── errorMessages.js # 错误消息常量
|
||
```
|
||
|
||
**命名约定**:
|
||
- 常量名使用 SCREAMING_SNAKE_CASE: `API_BASE_URL`, `MAX_RETRY_COUNT`
|
||
- 配置对象使用 camelCase: `animationConfig`, `chartColors`
|
||
|
||
**示例**:
|
||
```javascript
|
||
// animations.js
|
||
export const ANIMATION_DURATION = {
|
||
FAST: 150,
|
||
NORMAL: 300,
|
||
SLOW: 500,
|
||
};
|
||
|
||
export const EASING = {
|
||
EASE_IN_OUT: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||
EASE_OUT: 'cubic-bezier(0, 0, 0.2, 1)',
|
||
};
|
||
```
|
||
|
||
**原则**:
|
||
- 避免魔法数字/字符串(在代码中硬编码)
|
||
- 方便统一修改(单一数据源)
|
||
- 提高代码可读性
|
||
|
||
---
|
||
|
||
##### 📁 `src/theme/` - UI 主题配置
|
||
|
||
**用途**: 定义 Chakra UI 主题配置(颜色、字体、间距、组件样式等)。
|
||
|
||
**核心文件**:
|
||
```
|
||
theme/
|
||
├── theme.js # 主题主文件(导出完整主题)
|
||
├── foundations/ # 基础样式(颜色、字体、间距)
|
||
│ ├── colors.js
|
||
│ ├── typography.js
|
||
│ └── spacing.js
|
||
└── components/ # 组件级主题覆盖
|
||
├── button.js
|
||
├── card.js
|
||
└── modal.js
|
||
```
|
||
|
||
**主题配置示例**:
|
||
```javascript
|
||
// theme.js
|
||
import { extendTheme } from '@chakra-ui/react';
|
||
import { colors } from './foundations/colors';
|
||
import { ButtonStyles as Button } from './components/button';
|
||
|
||
const theme = extendTheme({
|
||
colors,
|
||
fonts: {
|
||
heading: `'Open Sans', sans-serif`,
|
||
body: `'Raleway', sans-serif`,
|
||
},
|
||
components: {
|
||
Button,
|
||
},
|
||
});
|
||
|
||
export default theme;
|
||
```
|
||
|
||
**何时修改主题**:
|
||
- 添加自定义颜色/渐变
|
||
- 统一修改所有 Button/Card 的默认样式
|
||
- 配置深色模式
|
||
|
||
---
|
||
|
||
##### 📁 `src/assets/` - 静态资源
|
||
|
||
**用途**: 存放图片、字体、图标等静态文件。
|
||
|
||
**组织结构**:
|
||
```
|
||
assets/
|
||
├── img/ # 图片资源
|
||
│ ├── logo.png
|
||
│ ├── backgrounds/
|
||
│ └── illustrations/
|
||
├── icons/ # SVG 图标
|
||
└── fonts/ # 自定义字体文件(如需要)
|
||
```
|
||
|
||
**使用方式**:
|
||
```javascript
|
||
import logo from '@assets/img/logo.png';
|
||
|
||
<img src={logo} alt="Logo" />
|
||
```
|
||
|
||
---
|
||
|
||
##### 📁 `src/mocks/` - 开发环境 Mock 数据
|
||
|
||
**用途**: 使用 MSW (Mock Service Worker) 拦截 API 请求,返回 mock 数据,实现前后端分离开发。
|
||
|
||
**目录结构**:
|
||
```
|
||
mocks/
|
||
├── browser.js # MSW 初始化(浏览器环境)
|
||
├── handlers/ # API Mock Handlers(按领域划分)
|
||
│ ├── index.js # 汇总所有 handlers
|
||
│ ├── auth.js # 认证相关 API mock
|
||
│ ├── stock.js # 股票数据 API mock
|
||
│ ├── portfolio.js # 投资组合 API mock
|
||
│ └── community.js # 社区内容 API mock
|
||
└── data/ # Mock 数据文件
|
||
├── stocks.json
|
||
├── users.json
|
||
└── events.json
|
||
```
|
||
|
||
**启用 Mock 模式**:
|
||
```bash
|
||
# 方式 1: 使用 .env.mock 配置(默认)
|
||
npm start
|
||
|
||
# 方式 2: 手动设置环境变量
|
||
REACT_APP_ENABLE_MOCK=true npm start
|
||
```
|
||
|
||
**添加新 Mock Handler**: 参见"常见开发任务 → 如何添加新的 MSW Mock Handler"
|
||
|
||
**原则**:
|
||
- Mock 数据应尽量模拟真实数据结构
|
||
- 使用 MSW 的 `http.get/post/put/delete` 定义 handlers
|
||
- Handler 中可模拟延迟、错误响应(测试加载/错误状态)
|
||
|
||
---
|
||
|
||
##### 📁 其他目录
|
||
|
||
**`src/lib/`** - 第三方库配置
|
||
- 存放第三方库的自定义配置(如 PostHog、Analytics)
|
||
|
||
**`src/styles/`** - 全局样式
|
||
- 存放全局 CSS/SCSS 文件(如 reset.css、global.scss)
|
||
|
||
**`src/variables/`** - 可配置变量
|
||
- 存放可通过环境变量覆盖的配置(如 API URLs、Feature Flags)
|
||
|
||
---
|
||
|
||
#### 目录结构最佳实践
|
||
|
||
**1. 就近原则(Co-location)**
|
||
- 将相关文件放在一起(组件、样式、测试、工具函数)
|
||
- 示例: `EventCard/` 目录包含 `EventCard.js`、`EventCard.test.js`、`utils.js`
|
||
|
||
**2. 单一职责**
|
||
- 每个目录只负责一个领域(不要混合业务逻辑和 UI 组件)
|
||
- 示例: `services/` 只包含 API 调用,不包含 UI 组件
|
||
|
||
**3. 避免深层嵌套**
|
||
- 目录层级不超过 4 层(超过则考虑重构)
|
||
- 示例: `src/views/Community/components/EventCard/` 已经是 4 层
|
||
|
||
**4. 命名一致性**
|
||
- 组件: PascalCase (`EventCard`)
|
||
- 文件: camelCase (`priceFormatters.js`)
|
||
- 常量: SCREAMING_SNAKE_CASE (`API_BASE_URL`)
|
||
|
||
**5. 导出规范**
|
||
- 每个目录包含 `index.js` 作为主导出文件
|
||
- 使用命名导出 (`export const ...`) 而非默认导出(除组件外)
|
||
|
||
---
|
||
|
||
#### 查找文件指南
|
||
|
||
**如何快速找到想要修改的文件?**
|
||
|
||
| 需求 | 目录 | 示例 |
|
||
|------|------|------|
|
||
| 修改页面布局 | `src/layouts/` | `MainLayout.js` |
|
||
| 修改某个页面 | `src/views/{PageName}/` | `Community/index.js` |
|
||
| 修改可复用组件 | `src/components/` | `DataTable/index.js` |
|
||
| 修改 API 调用 | `src/services/` | `stockService.js` |
|
||
| 修改工具函数 | `src/utils/` | `priceFormatters.js` |
|
||
| 修改全局状态 | `src/store/slices/` | `stockSlice.js` |
|
||
| 添加新路由 | `src/routes/routeConfig.js` | - |
|
||
| 修改主题颜色 | `src/theme/foundations/colors.js` | - |
|
||
| 添加 Mock API | `src/mocks/handlers/` | `stock.js` |
|
||
|
||
**使用路径别名快速导入**:
|
||
```javascript
|
||
// ✅ 使用别名(推荐)
|
||
import { EventCard } from '@components/EventCard';
|
||
import { formatPrice } from '@utils/priceFormatters';
|
||
import { fetchStockData } from '@services/stockService';
|
||
|
||
// ❌ 使用相对路径(不推荐)
|
||
import { EventCard } from '../../../components/EventCard';
|
||
import { formatPrice } from '../../utils/priceFormatters';
|
||
```
|
||
|
||
### 后端架构详解
|
||
|
||
本项目后端采用 **Flask 微服务架构**,结合多种数据库和消息队列,实现高性能金融数据处理和实时通信。
|
||
|
||
#### 后端目录结构
|
||
|
||
```
|
||
项目根目录/
|
||
├── app.py # 主 Flask 应用(Web 服务器 + API 路由)
|
||
├── simulation_background_processor.py # Celery 后台处理器(交易模拟任务)
|
||
├── concept_api.py # 概念/行业分析独立 API 服务
|
||
│
|
||
├── wechat_pay.py # 微信支付业务逻辑
|
||
├── wechat_pay_config.py # 微信支付配置(商户号、密钥等)
|
||
│
|
||
├── tdays.csv # 交易日历数据(A 股交易日)
|
||
├── requirements.txt # Python 依赖包清单
|
||
│
|
||
├── models/ # 数据库模型定义(SQLAlchemy)
|
||
│ ├── user.py
|
||
│ ├── stock.py
|
||
│ └── transaction.py
|
||
│
|
||
├── services/ # 业务逻辑服务层
|
||
│ ├── stock_service.py
|
||
│ ├── trading_service.py
|
||
│ └── notification_service.py
|
||
│
|
||
├── utils/ # 后端工具函数
|
||
│ ├── db_utils.py # 数据库连接工具
|
||
│ ├── clickhouse_client.py # ClickHouse 客户端封装
|
||
│ └── date_utils.py # 日期处理工具
|
||
│
|
||
└── config/ # 配置文件
|
||
├── development.py
|
||
├── production.py
|
||
└── test.py
|
||
```
|
||
|
||
---
|
||
|
||
#### 核心组件详解
|
||
|
||
##### 1️⃣ `app.py` - 主 Flask 应用
|
||
|
||
**职责**: Web 服务器、API 路由、认证、会话管理、WebSocket 通信
|
||
|
||
**核心功能**:
|
||
- **Flask 应用初始化**: 配置 CORS、Session、Logging
|
||
- **API 路由定义**: RESTful API 端点(`/api/stocks`, `/api/portfolio`, `/api/community`)
|
||
- **认证系统**: Flask-Login 集成(登录、登出、权限校验)
|
||
- **WebSocket 服务**: Flask-SocketIO 实现实时推送(股票行情、事件通知)
|
||
- **数据库连接**: SQLAlchemy ORM + ClickHouse Client
|
||
- **交易日历加载**: 启动时从 `tdays.csv` 加载 A 股交易日数据到全局变量
|
||
|
||
**技术栈**:
|
||
- **Flask** - 轻量级 Web 框架
|
||
- **Flask-Login** - 用户会话管理
|
||
- **Flask-SocketIO** - WebSocket 实时通信
|
||
- **Flask-CORS** - 跨域资源共享配置
|
||
- **SQLAlchemy** - ORM(对象关系映射)
|
||
|
||
**API 路由示例**:
|
||
```python
|
||
# 获取股票数据
|
||
@app.route('/api/stocks/<stock_code>', methods=['GET'])
|
||
@login_required
|
||
def get_stock_data(stock_code):
|
||
# 从 ClickHouse 查询历史数据
|
||
data = clickhouse_client.query(f"SELECT * FROM stocks WHERE code = '{stock_code}'")
|
||
return jsonify(data)
|
||
|
||
# 创建交易订单
|
||
@app.route('/api/orders', methods=['POST'])
|
||
@login_required
|
||
def create_order():
|
||
data = request.json
|
||
order = Order(user_id=current_user.id, stock_code=data['stock_code'], ...)
|
||
db.session.add(order)
|
||
db.session.commit()
|
||
return jsonify({'order_id': order.id})
|
||
```
|
||
|
||
**WebSocket 事件示例**:
|
||
```python
|
||
# 客户端订阅股票行情
|
||
@socketio.on('subscribe_stock')
|
||
def handle_subscribe(data):
|
||
stock_code = data['stock_code']
|
||
join_room(f'stock_{stock_code}')
|
||
emit('subscribed', {'stock_code': stock_code})
|
||
|
||
# 服务端推送行情更新
|
||
def push_quote_update(stock_code, quote_data):
|
||
socketio.emit('stock_quote', quote_data, room=f'stock_{stock_code}')
|
||
```
|
||
|
||
**交易日历加载**:
|
||
```python
|
||
# 全局变量存储交易日
|
||
trading_days = []
|
||
|
||
# 启动时加载 tdays.csv
|
||
def load_trading_days():
|
||
global trading_days
|
||
with open('tdays.csv', 'r') as f:
|
||
trading_days = [line.strip() for line in f.readlines()]
|
||
|
||
# 判断是否为交易日
|
||
def is_trading_day(date_str):
|
||
return date_str in trading_days
|
||
```
|
||
|
||
---
|
||
|
||
##### 2️⃣ `simulation_background_processor.py` - Celery 后台处理器
|
||
|
||
**职责**: 处理长时间运行的后台任务(交易模拟、报表生成、数据同步)
|
||
|
||
**为什么需要后台处理器?**
|
||
- 交易模拟需要执行数百次历史回测计算(耗时 10-60 秒)
|
||
- 避免阻塞 Flask 主线程(影响其他 API 响应)
|
||
- 支持任务队列、重试、失败处理
|
||
|
||
**技术栈**:
|
||
- **Celery** - 分布式任务队列
|
||
- **Redis** - 消息代理(Broker)和结果存储(Backend)
|
||
- **ClickHouse** - 高性能查询历史股票数据
|
||
|
||
**任务示例**:
|
||
```python
|
||
from celery import Celery
|
||
|
||
# Celery 实例
|
||
celery_app = Celery('tasks', broker='redis://localhost:6379/0')
|
||
|
||
# 交易模拟任务
|
||
@celery_app.task(bind=True)
|
||
def run_simulation(self, user_id, strategy_config):
|
||
"""
|
||
执行交易模拟任务
|
||
:param user_id: 用户 ID
|
||
:param strategy_config: 策略配置(买入条件、卖出条件、资金管理)
|
||
:return: 模拟结果(收益率、最大回撤、交易明细)
|
||
"""
|
||
try:
|
||
# 1. 从 ClickHouse 获取历史数据
|
||
stock_data = clickhouse_client.query(...)
|
||
|
||
# 2. 执行回测计算
|
||
for i in range(len(stock_data)):
|
||
# 模拟交易逻辑
|
||
if should_buy(stock_data[i]):
|
||
buy(stock_data[i])
|
||
if should_sell(stock_data[i]):
|
||
sell(stock_data[i])
|
||
|
||
# 3. 计算绩效指标
|
||
result = {
|
||
'total_return': calculate_return(),
|
||
'max_drawdown': calculate_drawdown(),
|
||
'trades': get_trade_history(),
|
||
}
|
||
|
||
# 4. 保存结果到 MySQL
|
||
save_simulation_result(user_id, result)
|
||
|
||
return result
|
||
except Exception as e:
|
||
# 失败重试(最多 3 次)
|
||
self.retry(exc=e, countdown=60, max_retries=3)
|
||
```
|
||
|
||
**从 Flask 调用 Celery 任务**:
|
||
```python
|
||
# app.py
|
||
@app.route('/api/simulations', methods=['POST'])
|
||
@login_required
|
||
def start_simulation():
|
||
strategy = request.json
|
||
|
||
# 异步提交任务到 Celery
|
||
task = run_simulation.delay(current_user.id, strategy)
|
||
|
||
return jsonify({
|
||
'task_id': task.id,
|
||
'status': 'pending',
|
||
'message': '模拟任务已提交,请稍后查看结果'
|
||
})
|
||
|
||
# 查询任务状态
|
||
@app.route('/api/simulations/<task_id>', methods=['GET'])
|
||
def get_simulation_status(task_id):
|
||
task = run_simulation.AsyncResult(task_id)
|
||
|
||
if task.state == 'PENDING':
|
||
response = {'status': 'pending', 'progress': 0}
|
||
elif task.state == 'SUCCESS':
|
||
response = {'status': 'completed', 'result': task.result}
|
||
elif task.state == 'FAILURE':
|
||
response = {'status': 'failed', 'error': str(task.info)}
|
||
|
||
return jsonify(response)
|
||
```
|
||
|
||
**启动 Celery Worker**:
|
||
```bash
|
||
# 启动 worker(监听任务队列)
|
||
celery -A simulation_background_processor worker --loglevel=info
|
||
|
||
# 启动 Flower(Celery 监控工具)
|
||
celery -A simulation_background_processor flower
|
||
```
|
||
|
||
---
|
||
|
||
##### 3️⃣ `concept_api.py` - 概念/行业分析 API
|
||
|
||
**职责**: 独立的概念板块分析服务(涨停分析、热点行业、资金流向)
|
||
|
||
**为什么独立部署?**
|
||
- 概念分析计算密集(需要遍历数千只股票)
|
||
- 独立扩展性(可部署到多台服务器)
|
||
- 服务隔离(不影响主应用性能)
|
||
|
||
**部署架构**:
|
||
```
|
||
前端请求 → Nginx 反向代理
|
||
├─ /api → Flask 主应用 (端口 5001)
|
||
└─ /concept-api → Concept API (端口 6801)
|
||
```
|
||
|
||
**API 示例**:
|
||
```python
|
||
from flask import Flask, jsonify
|
||
import clickhouse_driver
|
||
|
||
app = Flask(__name__)
|
||
|
||
# 获取涨停股票列表
|
||
@app.route('/limit_up', methods=['GET'])
|
||
def get_limit_up_stocks():
|
||
"""
|
||
获取当日涨停股票列表,按概念板块分组
|
||
"""
|
||
query = """
|
||
SELECT
|
||
stock_code,
|
||
stock_name,
|
||
concept,
|
||
close_price,
|
||
change_percent
|
||
FROM stock_daily
|
||
WHERE trade_date = today()
|
||
AND change_percent >= 9.9
|
||
ORDER BY concept, change_percent DESC
|
||
"""
|
||
|
||
data = clickhouse_client.query(query)
|
||
|
||
# 按概念分组
|
||
result = {}
|
||
for row in data:
|
||
concept = row['concept']
|
||
if concept not in result:
|
||
result[concept] = []
|
||
result[concept].append(row)
|
||
|
||
return jsonify(result)
|
||
|
||
if __name__ == '__main__':
|
||
app.run(host='0.0.0.0', port=6801)
|
||
```
|
||
|
||
**前端调用**(通过代理):
|
||
```javascript
|
||
// craco.config.js 配置代理
|
||
proxy: {
|
||
'/concept-api': {
|
||
target: 'http://49.232.185.254:6801',
|
||
pathRewrite: { '^/concept-api': '' },
|
||
}
|
||
}
|
||
|
||
// 前端调用
|
||
fetch('/concept-api/limit_up')
|
||
.then(res => res.json())
|
||
.then(data => console.log(data));
|
||
```
|
||
|
||
---
|
||
|
||
##### 4️⃣ 微信支付集成
|
||
|
||
**文件**:
|
||
- `wechat_pay.py` - 微信支付业务逻辑(下单、查询、退款)
|
||
- `wechat_pay_config.py` - 配置(商户号、API 密钥、证书路径)
|
||
|
||
**核心功能**:
|
||
- **统一下单**: 创建支付订单(JSAPI、Native、H5)
|
||
- **支付回调**: 接收微信支付结果通知
|
||
- **订单查询**: 查询订单支付状态
|
||
- **退款处理**: 发起退款请求
|
||
|
||
**示例代码**:
|
||
```python
|
||
# wechat_pay.py
|
||
from wechatpy.pay import WeChatPay
|
||
from wechat_pay_config import MCHID, API_KEY, CERT_PATH
|
||
|
||
# 初始化微信支付客户端
|
||
wechat_pay = WeChatPay(
|
||
appid='your_appid',
|
||
api_key=API_KEY,
|
||
mch_id=MCHID,
|
||
mch_cert=CERT_PATH,
|
||
)
|
||
|
||
# 创建订单
|
||
def create_order(user_id, amount, description):
|
||
result = wechat_pay.order.create(
|
||
trade_type='JSAPI',
|
||
body=description,
|
||
total_fee=int(amount * 100), # 单位:分
|
||
notify_url='https://yourdomain.com/api/wechat/callback',
|
||
user_id=user_id,
|
||
)
|
||
return result
|
||
|
||
# 支付回调处理
|
||
@app.route('/api/wechat/callback', methods=['POST'])
|
||
def wechat_callback():
|
||
xml_data = request.data
|
||
data = wechat_pay.parse_payment_result(xml_data)
|
||
|
||
if data['return_code'] == 'SUCCESS':
|
||
# 更新订单状态
|
||
order_id = data['out_trade_no']
|
||
update_order_status(order_id, 'paid')
|
||
|
||
return wechat_pay.reply('OK', True)
|
||
```
|
||
|
||
---
|
||
|
||
##### 5️⃣ 数据库架构
|
||
|
||
**多数据库策略**:
|
||
|
||
| 数据库 | 用途 | 特点 | 示例表 |
|
||
|--------|------|------|--------|
|
||
| **ClickHouse** | 时序数据存储 | OLAP(列式存储、查询速度快) | `stock_daily`(日线数据)<br>`stock_minute`(分钟数据)<br>`concept_daily`(概念板块数据) |
|
||
| **MySQL/PostgreSQL** | 事务数据存储 | OLTP(ACID 保证、关系型) | `users`(用户表)<br>`orders`(订单表)<br>`portfolios`(持仓表)<br>`subscriptions`(订阅表) |
|
||
| **Redis** | 缓存 + 消息队列 | 内存数据库(高速读写) | 股票行情缓存<br>用户 Session<br>Celery 任务队列 |
|
||
|
||
**ClickHouse 使用场景**:
|
||
```python
|
||
# 查询某只股票的历史数据(100 万行数据,查询耗时 < 100ms)
|
||
query = """
|
||
SELECT
|
||
trade_date,
|
||
open, high, low, close, volume
|
||
FROM stock_daily
|
||
WHERE stock_code = '600000.SH'
|
||
AND trade_date >= '2020-01-01'
|
||
ORDER BY trade_date
|
||
"""
|
||
|
||
data = clickhouse_client.query(query)
|
||
```
|
||
|
||
**MySQL 使用场景**:
|
||
```python
|
||
# 创建订单(事务保证)
|
||
@app.route('/api/orders', methods=['POST'])
|
||
def create_order():
|
||
try:
|
||
# 开启事务
|
||
order = Order(user_id=user_id, stock_code=stock_code, ...)
|
||
db.session.add(order)
|
||
|
||
# 扣减账户余额
|
||
account = Account.query.get(user_id)
|
||
account.balance -= order.total_amount
|
||
|
||
# 提交事务
|
||
db.session.commit()
|
||
return jsonify({'success': True})
|
||
except Exception as e:
|
||
# 回滚事务
|
||
db.session.rollback()
|
||
return jsonify({'error': str(e)}), 500
|
||
```
|
||
|
||
---
|
||
|
||
#### 后端技术栈详解
|
||
|
||
**核心框架**:
|
||
- **Flask 2.x** - 轻量级 Web 框架,易于扩展
|
||
- **Flask-Login** - 用户认证与会话管理
|
||
- **Flask-SocketIO** - WebSocket 实时通信(基于 Socket.IO 协议)
|
||
- **Flask-CORS** - 跨域资源共享配置
|
||
|
||
**数据库与 ORM**:
|
||
- **SQLAlchemy** - Python ORM,支持 MySQL/PostgreSQL
|
||
- **ClickHouse Driver** - ClickHouse Python 客户端
|
||
- **Redis-py** - Redis Python 客户端
|
||
|
||
**后台任务处理**:
|
||
- **Celery** - 分布式任务队列
|
||
- **Redis** - Celery 消息代理(Broker)
|
||
|
||
**第三方服务**:
|
||
- **微信支付 SDK** - `wechatpy`
|
||
- **腾讯云短信 SDK** - 发送验证码、通知
|
||
|
||
---
|
||
|
||
#### 后端开发最佳实践
|
||
|
||
**1. API 设计规范**:
|
||
```python
|
||
# ✅ RESTful 风格
|
||
GET /api/stocks # 获取股票列表
|
||
GET /api/stocks/:id # 获取单个股票
|
||
POST /api/stocks # 创建股票
|
||
PUT /api/stocks/:id # 更新股票
|
||
DELETE /api/stocks/:id # 删除股票
|
||
|
||
# ✅ 统一响应格式
|
||
{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": { ... }
|
||
}
|
||
|
||
# ❌ 避免使用动词命名
|
||
POST /api/getStockData # 不推荐
|
||
POST /api/createNewOrder # 不推荐
|
||
```
|
||
|
||
**2. 错误处理**:
|
||
```python
|
||
# 统一错误处理
|
||
@app.errorhandler(Exception)
|
||
def handle_error(e):
|
||
if isinstance(e, ValidationError):
|
||
return jsonify({'error': str(e)}), 400
|
||
elif isinstance(e, Unauthorized):
|
||
return jsonify({'error': 'Unauthorized'}), 401
|
||
else:
|
||
logger.error(f'Unhandled exception: {e}')
|
||
return jsonify({'error': 'Internal server error'}), 500
|
||
```
|
||
|
||
**3. 数据库连接管理**:
|
||
```python
|
||
# 使用连接池
|
||
engine = create_engine(
|
||
'mysql://user:pass@localhost/db',
|
||
pool_size=10,
|
||
max_overflow=20,
|
||
pool_recycle=3600,
|
||
)
|
||
|
||
# 请求结束后自动关闭连接
|
||
@app.teardown_appcontext
|
||
def shutdown_session(exception=None):
|
||
db.session.remove()
|
||
```
|
||
|
||
**4. 性能优化**:
|
||
- **数据库索引**: 为常用查询字段添加索引
|
||
- **查询优化**: 避免 N+1 查询,使用 `joinedload`
|
||
- **缓存策略**: 使用 Redis 缓存热点数据(股票行情、用户信息)
|
||
- **异步任务**: 耗时操作使用 Celery 异步处理
|
||
|
||
---
|
||
|
||
#### 后端部署架构
|
||
|
||
**生产环境部署**:
|
||
```
|
||
Nginx (反向代理 + 负载均衡)
|
||
├─ Gunicorn (WSGI 服务器) × 4 进程
|
||
│ └─ Flask 应用 (app.py)
|
||
│
|
||
├─ Celery Worker × 2 进程
|
||
│ └─ 后台任务处理
|
||
│
|
||
├─ Redis (缓存 + 消息队列)
|
||
├─ MySQL (事务数据)
|
||
└─ ClickHouse (时序数据)
|
||
```
|
||
|
||
**启动命令**:
|
||
```bash
|
||
# 启动 Flask 应用(Gunicorn)
|
||
gunicorn -w 4 -b 0.0.0.0:5001 app:app
|
||
|
||
# 启动 Celery Worker
|
||
celery -A simulation_background_processor worker --loglevel=info
|
||
|
||
# 启动 Concept API
|
||
python concept_api.py
|
||
```
|
||
|
||
**监控与日志**:
|
||
- **日志**: 使用 Python `logging` 模块记录日志
|
||
- **监控**: Celery Flower 监控任务状态
|
||
- **性能**: 使用 APM 工具(如 New Relic、Datadog)
|
||
|
||
---
|
||
|
||
## 配置
|
||
|
||
### 环境文件
|
||
```
|
||
.env.mock - Mock 模式(默认):MSW 拦截所有 API 调用,无需后端
|
||
.env.development - 开发模式:连接到开发后端
|
||
.env.test - 测试模式:用于 'npm run start:test'(后端 + 前端一起)
|
||
.env.production - 生产环境构建配置
|
||
```
|
||
|
||
**关键环境变量:**
|
||
- `REACT_APP_ENABLE_MOCK=true` - 启用 MSW mocking
|
||
- `REACT_APP_API_URL` - 后端 URL(空字符串 = 使用相对路径或 MSW)
|
||
|
||
### MSW (Mock Service Worker) 设置
|
||
MSW 用于开发期间的 API mocking:
|
||
|
||
1. **激活方式**:在 env 文件中设置 `REACT_APP_ENABLE_MOCK=true`
|
||
2. **Worker 文件**:`public/mockServiceWorker.js`(自动生成)
|
||
3. **Handlers**:`src/mocks/handlers/`(按领域组织:auth、stock、company 等)
|
||
4. **模式**:`onUnhandledRequest: 'warn'` - 未处理的请求会传递到后端
|
||
|
||
当 MSW 激活时,开发服务器代理被禁用(MSW 优先拦截)。
|
||
|
||
### 路径别名 (craco.config.js)
|
||
所有别名解析到 `src/` 子目录:
|
||
```
|
||
@/ → src/
|
||
@assets/ → src/assets/
|
||
@components/ → src/components/
|
||
@constants/ → src/constants/
|
||
@contexts/ → src/contexts/
|
||
@data/ → src/data/
|
||
@hooks/ → src/hooks/
|
||
@layouts/ → src/layouts/
|
||
@lib/ → src/lib/
|
||
@mocks/ → src/mocks/
|
||
@providers/ → src/providers/
|
||
@routes/ → src/routes/
|
||
@services/ → src/services/
|
||
@store/ → src/store/
|
||
@styles/ → src/styles/
|
||
@theme/ → src/theme/
|
||
@utils/ → src/utils/
|
||
@variables/ → src/variables/
|
||
@views/ → src/views/
|
||
```
|
||
|
||
### Webpack 优化 (craco.config.js)
|
||
**性能特性:**
|
||
- 文件系统缓存(重新构建速度提升 50-80%)
|
||
- 按库激进的代码分割:
|
||
- `react-vendor` - React 核心(优先级 30)
|
||
- `charts-lib` - echarts、d3、apexcharts、recharts(优先级 25)
|
||
- `chakra-ui` - Chakra UI + Emotion(优先级 23)
|
||
- `antd-lib` - Ant Design(优先级 22)
|
||
- `three-lib` - Three.js(优先级 20)
|
||
- `calendar-lib` - moment、date-fns、FullCalendar(优先级 18)
|
||
- 从构建中移除 ESLint 插件(速度提升 20-30%)
|
||
- 启用 Babel 缓存
|
||
- moment locale 剥离(IgnorePlugin)
|
||
- Source maps:生产环境禁用,开发环境使用 `eval-cheap-module-source-map`
|
||
|
||
**开发服务器:**
|
||
- 端口 3000(prestart 时杀死现有进程)
|
||
- 代理(当 MSW 禁用时):`/api` → `http://49.232.185.254:5001`
|
||
- Bundle 分析器:`ANALYZE=true npm run build:analyze`
|
||
|
||
### 构建流程
|
||
1. `npm run build` 使用 CRACO + webpack 优化编译
|
||
2. Gulp 任务(`gulp licenses`)为 JS/HTML 添加 Creative Tim 许可证头
|
||
3. 输出:`build/` 目录
|
||
|
||
**Node 兼容性:**
|
||
```bash
|
||
NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096'
|
||
```
|
||
|
||
## 开发工作流
|
||
|
||
### 路由开发
|
||
添加新路由的步骤:
|
||
|
||
1. 在 `src/routes/lazy-components.js` 中添加 lazy import
|
||
2. 在 `src/routes/routeConfig.js` 中添加路由配置:
|
||
- `path` - URL 路径
|
||
- `component` - 来自 lazyComponents
|
||
- `protection` - MODAL/REDIRECT/PUBLIC
|
||
- `layout` - 'main'(带导航栏)或 'none'(全屏)
|
||
3. 路由自动使用 Suspense + ErrorBoundary 渲染(由 PageTransitionWrapper 处理)
|
||
|
||
### 组件组织模式
|
||
基于最近的重构(详见 README.md):
|
||
|
||
**原子设计模式:**
|
||
- **Atoms(原子)** - 基础 UI 元素(按钮、徽章、输入框)
|
||
- **Molecules(分子)** - 原子的组合(卡片、表单)
|
||
- **Organisms(有机体)** - 复杂组件(列表、面板)
|
||
|
||
**大型组件示例结构(1000+ 行):**
|
||
```
|
||
src/views/Community/components/
|
||
├── EventCard/
|
||
│ ├── index.js - 智能包装器(路由紧凑 vs 详细视图)
|
||
│ ├── CompactEventCard.js - 紧凑视图
|
||
│ ├── DetailedEventCard.js - 详细视图
|
||
│ ├── EventTimeline.js - 原子组件
|
||
│ ├── EventImportanceBadge.js - 原子组件
|
||
│ └── ...
|
||
```
|
||
|
||
**工具提取:**
|
||
- 将可复用逻辑提取到 `src/utils/`(如 `priceFormatters.js`)
|
||
- 将共享常量提取到 `src/constants/`(如 `animations.js`)
|
||
|
||
### API 集成
|
||
**服务层**(`src/services/`):
|
||
- 使用 `src/utils/apiConfig.js` 中的 `getApiBase()` 获取基础 URL
|
||
- 示例:`${getApiBase()}/api/endpoint`
|
||
- 在 mock 模式下,MSW 拦截;在开发/生产环境下,请求后端
|
||
|
||
**添加新的 API 端点:**
|
||
1. 在 `src/services/` 中添加服务函数(或在组件中内联)
|
||
2. 如果使用 MSW:在 `src/mocks/handlers/{domain}.js` 中添加 handler
|
||
3. 在 `src/mocks/handlers/index.js` 中导入 handler
|
||
|
||
### Redux 状态管理
|
||
**现有 slices**(`src/store/slices/`):
|
||
- `authModalSlice` - 认证模态框状态
|
||
- `posthogSlice` - PostHog 分析
|
||
- `stockSlice` - 股票数据
|
||
- `industrySlice` - 行业/概念数据
|
||
- `subscriptionSlice` - 用户订阅
|
||
- `communityDataSlice` - 社区内容
|
||
|
||
**添加新 slice:**
|
||
1. 创建 `src/store/slices/yourSlice.js`
|
||
2. 在 `src/store/index.js` 中导入并添加
|
||
3. 通过 `useSelector` 访问,通过 `useDispatch` 派发
|
||
|
||
---
|
||
|
||
### 常见开发任务
|
||
|
||
本节提供常见开发任务的详细步骤和代码示例,帮助快速上手项目开发。
|
||
|
||
#### 1. 如何添加新的 MSW Mock Handler
|
||
|
||
**使用场景**: 开发新功能时需要 mock API 接口
|
||
|
||
**步骤**:
|
||
|
||
1. 在 `src/mocks/handlers/` 创建新的 handler 文件
|
||
2. 定义 HTTP handlers
|
||
3. 在 `src/mocks/handlers/index.js` 中导入并导出
|
||
|
||
**完整示例**:
|
||
|
||
```javascript
|
||
// ✅ 步骤 1: 创建 src/mocks/handlers/portfolio.js
|
||
import { http, HttpResponse } from 'msw';
|
||
|
||
export const portfolioHandlers = [
|
||
// GET 请求示例
|
||
http.get('/api/portfolio/:userId', ({ params }) => {
|
||
return HttpResponse.json({
|
||
userId: params.userId,
|
||
holdings: [
|
||
{ stockCode: '600000', shares: 1000, costPrice: 10.50 },
|
||
{ stockCode: '000001', shares: 2000, costPrice: 15.80 },
|
||
],
|
||
totalValue: 42100.00,
|
||
});
|
||
}),
|
||
|
||
// POST 请求示例
|
||
http.post('/api/portfolio/buy', async ({ request }) => {
|
||
const body = await request.json();
|
||
return HttpResponse.json({
|
||
success: true,
|
||
orderId: 'ORDER_' + Date.now(),
|
||
stockCode: body.stockCode,
|
||
shares: body.shares,
|
||
});
|
||
}),
|
||
|
||
// 模拟错误响应
|
||
http.get('/api/portfolio/error', () => {
|
||
return HttpResponse.json(
|
||
{ error: '服务器错误' },
|
||
{ status: 500 }
|
||
);
|
||
}),
|
||
|
||
// 模拟延迟响应(测试加载状态)
|
||
http.get('/api/portfolio/slow', async () => {
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
return HttpResponse.json({ data: 'slow response' });
|
||
}),
|
||
];
|
||
```
|
||
|
||
```javascript
|
||
// ✅ 步骤 2: 在 src/mocks/handlers/index.js 中导入
|
||
import { authHandlers } from './auth';
|
||
import { stockHandlers } from './stock';
|
||
import { portfolioHandlers } from './portfolio'; // 新增
|
||
|
||
export const handlers = [
|
||
...authHandlers,
|
||
...stockHandlers,
|
||
...portfolioHandlers, // 新增
|
||
// ... 其他 handlers
|
||
];
|
||
```
|
||
|
||
**测试 Mock Handler**:
|
||
|
||
```bash
|
||
# 启动 Mock 模式
|
||
npm start
|
||
|
||
# 查看浏览器控制台,应该看到:
|
||
# [MSW] Mocking enabled
|
||
# [MSW] GET /api/portfolio/123 (200)
|
||
```
|
||
|
||
#### 2. 如何添加新的 Redux Slice
|
||
|
||
**使用场景**: 添加需要全局共享的状态(UI 状态、缓存数据)
|
||
|
||
**步骤**:
|
||
|
||
1. 在 `src/store/slices/` 创建新 slice
|
||
2. 在 `src/store/index.js` 中注册 reducer
|
||
3. 在组件中使用
|
||
|
||
**完整示例**:
|
||
|
||
```javascript
|
||
// ✅ 步骤 1: 创建 src/store/slices/portfolioSlice.js
|
||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||
import { fetchPortfolio } from '@services/portfolioService';
|
||
|
||
// 异步 thunk(用于 API 调用)
|
||
export const fetchPortfolioData = createAsyncThunk(
|
||
'portfolio/fetchData',
|
||
async (userId, { rejectWithValue }) => {
|
||
try {
|
||
const data = await fetchPortfolio(userId);
|
||
return data;
|
||
} catch (error) {
|
||
return rejectWithValue(error.response?.data || '加载失败');
|
||
}
|
||
}
|
||
);
|
||
|
||
const portfolioSlice = createSlice({
|
||
name: 'portfolio',
|
||
initialState: {
|
||
holdings: [],
|
||
totalValue: 0,
|
||
loading: false,
|
||
error: null,
|
||
},
|
||
reducers: {
|
||
// 同步 action
|
||
addHolding: (state, action) => {
|
||
state.holdings.push(action.payload);
|
||
},
|
||
removeHolding: (state, action) => {
|
||
state.holdings = state.holdings.filter(
|
||
h => h.stockCode !== action.payload
|
||
);
|
||
},
|
||
clearPortfolio: (state) => {
|
||
state.holdings = [];
|
||
state.totalValue = 0;
|
||
},
|
||
},
|
||
extraReducers: (builder) => {
|
||
// 异步 action 的状态处理
|
||
builder
|
||
.addCase(fetchPortfolioData.pending, (state) => {
|
||
state.loading = true;
|
||
state.error = null;
|
||
})
|
||
.addCase(fetchPortfolioData.fulfilled, (state, action) => {
|
||
state.loading = false;
|
||
state.holdings = action.payload.holdings;
|
||
state.totalValue = action.payload.totalValue;
|
||
})
|
||
.addCase(fetchPortfolioData.rejected, (state, action) => {
|
||
state.loading = false;
|
||
state.error = action.payload;
|
||
});
|
||
},
|
||
});
|
||
|
||
export const { addHolding, removeHolding, clearPortfolio } = portfolioSlice.actions;
|
||
export default portfolioSlice.reducer;
|
||
```
|
||
|
||
```javascript
|
||
// ✅ 步骤 2: 在 src/store/index.js 中注册
|
||
import { configureStore } from '@reduxjs/toolkit';
|
||
import authModalReducer from './slices/authModalSlice';
|
||
import portfolioReducer from './slices/portfolioSlice'; // 新增
|
||
|
||
export const store = configureStore({
|
||
reducer: {
|
||
authModal: authModalReducer,
|
||
portfolio: portfolioReducer, // 新增
|
||
// ... 其他 reducers
|
||
},
|
||
});
|
||
```
|
||
|
||
```javascript
|
||
// ✅ 步骤 3: 在组件中使用
|
||
import { useSelector, useDispatch } from 'react-redux';
|
||
import { fetchPortfolioData, addHolding } from '@store/slices/portfolioSlice';
|
||
|
||
const PortfolioComponent = () => {
|
||
const dispatch = useDispatch();
|
||
const { holdings, totalValue, loading, error } = useSelector(
|
||
state => state.portfolio
|
||
);
|
||
|
||
useEffect(() => {
|
||
// 异步获取数据
|
||
dispatch(fetchPortfolioData('user123'));
|
||
}, [dispatch]);
|
||
|
||
const handleAddStock = () => {
|
||
// 同步更新状态
|
||
dispatch(addHolding({
|
||
stockCode: '600519',
|
||
shares: 100,
|
||
costPrice: 1800.00,
|
||
}));
|
||
};
|
||
|
||
if (loading) return <Spinner />;
|
||
if (error) return <Alert status="error">{error}</Alert>;
|
||
|
||
return (
|
||
<Box>
|
||
<Text>总市值:¥{totalValue}</Text>
|
||
{holdings.map(h => (
|
||
<Text key={h.stockCode}>
|
||
{h.stockCode} - {h.shares}股
|
||
</Text>
|
||
))}
|
||
<Button onClick={handleAddStock}>添加持仓</Button>
|
||
</Box>
|
||
);
|
||
};
|
||
```
|
||
|
||
#### 3. 如何添加新的页面路由
|
||
|
||
**使用场景**: 添加新的功能页面
|
||
|
||
**步骤**:
|
||
|
||
1. 在 `src/views/` 创建页面组件
|
||
2. 在 `src/routes/lazy-components.js` 添加懒加载导入
|
||
3. 在 `src/routes/routeConfig.js` 添加路由配置
|
||
|
||
**完整示例**:
|
||
|
||
```javascript
|
||
// ✅ 步骤 1: 创建 src/views/Portfolio/index.js
|
||
import React from 'react';
|
||
import { Box, Heading, SimpleGrid } from '@chakra-ui/react';
|
||
|
||
export const Portfolio = () => {
|
||
return (
|
||
<Box p={8}>
|
||
<Heading mb={6}>我的投资组合</Heading>
|
||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6}>
|
||
{/* 组件内容 */}
|
||
</SimpleGrid>
|
||
</Box>
|
||
);
|
||
};
|
||
```
|
||
|
||
```javascript
|
||
// ✅ 步骤 2: 在 src/routes/lazy-components.js 添加
|
||
import { lazy } from 'react';
|
||
|
||
export const lazyComponents = {
|
||
// ... 现有组件
|
||
Community: lazy(() => import('@views/Community')),
|
||
Portfolio: lazy(() => import('@views/Portfolio')), // 新增
|
||
};
|
||
```
|
||
|
||
```javascript
|
||
// ✅ 步骤 3: 在 src/routes/routeConfig.js 添加配置
|
||
import { lazyComponents } from './lazy-components';
|
||
import { PROTECTION_MODES } from './constants/protectionModes';
|
||
|
||
export const routeConfig = [
|
||
// ... 现有路由
|
||
{
|
||
path: 'portfolio', // URL: /portfolio
|
||
component: lazyComponents.Portfolio,
|
||
protection: PROTECTION_MODES.REDIRECT, // 需要登录,否则跳转
|
||
layout: 'main', // 使用主布局(带导航栏)
|
||
meta: {
|
||
title: '投资组合',
|
||
description: '查看我的投资组合和持仓',
|
||
},
|
||
},
|
||
];
|
||
```
|
||
|
||
**路由保护模式说明**:
|
||
|
||
```javascript
|
||
// PUBLIC - 无需登录
|
||
{
|
||
path: 'about',
|
||
protection: PROTECTION_MODES.PUBLIC,
|
||
// 任何人都可以访问
|
||
}
|
||
|
||
// MODAL - 未登录时显示登录模态框,不跳转
|
||
{
|
||
path: 'community',
|
||
protection: PROTECTION_MODES.MODAL,
|
||
// 未登录用户可以看页面,点击某些操作会弹出登录框
|
||
}
|
||
|
||
// REDIRECT - 未登录时跳转到登录页
|
||
{
|
||
path: 'portfolio',
|
||
protection: PROTECTION_MODES.REDIRECT,
|
||
// 未登录用户会被重定向到 /auth/sign-in
|
||
}
|
||
```
|
||
|
||
**添加嵌套路由**(子路由):
|
||
|
||
```javascript
|
||
// 父路由
|
||
{
|
||
path: 'portfolio',
|
||
component: null, // 使用 Outlet 渲染子路由
|
||
protection: PROTECTION_MODES.REDIRECT,
|
||
layout: 'main',
|
||
children: [
|
||
{
|
||
path: '', // /portfolio
|
||
component: lazyComponents.PortfolioOverview,
|
||
},
|
||
{
|
||
path: 'holdings', // /portfolio/holdings
|
||
component: lazyComponents.PortfolioHoldings,
|
||
},
|
||
{
|
||
path: 'transactions', // /portfolio/transactions
|
||
component: lazyComponents.PortfolioTransactions,
|
||
},
|
||
],
|
||
}
|
||
```
|
||
|
||
#### 4. 如何实现实时数据更新(WebSocket)
|
||
|
||
**使用场景**: 股票行情、账户资金、动态消息等实时数据
|
||
|
||
**步骤**:
|
||
|
||
1. 创建 WebSocket 服务
|
||
2. 在组件中订阅事件
|
||
|
||
**完整示例**:
|
||
|
||
```javascript
|
||
// ✅ 步骤 1: 创建 src/services/websocketService.js
|
||
import io from 'socket.io-client';
|
||
import { getApiBase } from '@utils/apiConfig';
|
||
import { logger } from '@utils/logger';
|
||
|
||
class WebSocketService {
|
||
constructor() {
|
||
this.socket = null;
|
||
this.listeners = new Map();
|
||
}
|
||
|
||
// 连接 WebSocket
|
||
connect() {
|
||
if (this.socket?.connected) {
|
||
logger.info('WebSocket', '已连接,跳过重复连接');
|
||
return;
|
||
}
|
||
|
||
this.socket = io(getApiBase(), {
|
||
transports: ['websocket'],
|
||
reconnection: true,
|
||
reconnectionDelay: 1000,
|
||
reconnectionAttempts: 5,
|
||
});
|
||
|
||
this.socket.on('connect', () => {
|
||
logger.info('WebSocket', '连接成功');
|
||
});
|
||
|
||
this.socket.on('disconnect', () => {
|
||
logger.warn('WebSocket', '连接断开');
|
||
});
|
||
|
||
this.socket.on('error', (error) => {
|
||
logger.error('WebSocket', '连接错误', error);
|
||
});
|
||
}
|
||
|
||
// 断开连接
|
||
disconnect() {
|
||
if (this.socket) {
|
||
this.socket.disconnect();
|
||
this.socket = null;
|
||
}
|
||
}
|
||
|
||
// 订阅事件
|
||
subscribe(event, callback) {
|
||
if (!this.socket) this.connect();
|
||
|
||
this.socket.on(event, callback);
|
||
logger.debug('WebSocket', `订阅事件: ${event}`);
|
||
|
||
// 存储回调引用,方便取消订阅
|
||
if (!this.listeners.has(event)) {
|
||
this.listeners.set(event, new Set());
|
||
}
|
||
this.listeners.get(event).add(callback);
|
||
}
|
||
|
||
// 取消订阅
|
||
unsubscribe(event, callback) {
|
||
if (this.socket) {
|
||
this.socket.off(event, callback);
|
||
}
|
||
|
||
const callbacks = this.listeners.get(event);
|
||
if (callbacks) {
|
||
callbacks.delete(callback);
|
||
if (callbacks.size === 0) {
|
||
this.listeners.delete(event);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 发送消息
|
||
emit(event, data) {
|
||
if (this.socket?.connected) {
|
||
this.socket.emit(event, data);
|
||
} else {
|
||
logger.warn('WebSocket', '未连接,无法发送消息');
|
||
}
|
||
}
|
||
}
|
||
|
||
export const wsService = new WebSocketService();
|
||
```
|
||
|
||
```javascript
|
||
// ✅ 步骤 2: 在组件中使用
|
||
import { useEffect, useState } from 'react';
|
||
import { Box, Text } from '@chakra-ui/react';
|
||
import { wsService } from '@services/websocketService';
|
||
|
||
const StockQuoteComponent = ({ stockCode }) => {
|
||
const [quote, setQuote] = useState(null);
|
||
|
||
useEffect(() => {
|
||
// 订阅实时行情
|
||
const handleQuoteUpdate = (data) => {
|
||
if (data.stockCode === stockCode) {
|
||
setQuote(data);
|
||
}
|
||
};
|
||
|
||
wsService.subscribe('stock_quote', handleQuoteUpdate);
|
||
|
||
// 请求订阅特定股票
|
||
wsService.emit('subscribe_stock', { stockCode });
|
||
|
||
// 清理:取消订阅
|
||
return () => {
|
||
wsService.unsubscribe('stock_quote', handleQuoteUpdate);
|
||
wsService.emit('unsubscribe_stock', { stockCode });
|
||
};
|
||
}, [stockCode]);
|
||
|
||
return (
|
||
<Box>
|
||
<Text>股票代码:{stockCode}</Text>
|
||
<Text>当前价格:{quote?.price || '--'}</Text>
|
||
<Text>涨跌幅:{quote?.change || '--'}%</Text>
|
||
</Box>
|
||
);
|
||
};
|
||
```
|
||
|
||
**使用自定义 Hook 封装**:
|
||
|
||
```javascript
|
||
// src/hooks/useStockQuote.js
|
||
import { useState, useEffect } from 'react';
|
||
import { wsService } from '@services/websocketService';
|
||
|
||
export const useStockQuote = (stockCode) => {
|
||
const [quote, setQuote] = useState(null);
|
||
const [connected, setConnected] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (!stockCode) return;
|
||
|
||
const handleQuote = (data) => {
|
||
if (data.stockCode === stockCode) {
|
||
setQuote(data);
|
||
}
|
||
};
|
||
|
||
const handleConnect = () => setConnected(true);
|
||
const handleDisconnect = () => setConnected(false);
|
||
|
||
wsService.subscribe('stock_quote', handleQuote);
|
||
wsService.subscribe('connect', handleConnect);
|
||
wsService.subscribe('disconnect', handleDisconnect);
|
||
wsService.emit('subscribe_stock', { stockCode });
|
||
|
||
return () => {
|
||
wsService.unsubscribe('stock_quote', handleQuote);
|
||
wsService.unsubscribe('connect', handleConnect);
|
||
wsService.unsubscribe('disconnect', handleDisconnect);
|
||
wsService.emit('unsubscribe_stock', { stockCode });
|
||
};
|
||
}, [stockCode]);
|
||
|
||
return { quote, connected };
|
||
};
|
||
|
||
// 使用
|
||
const MyComponent = () => {
|
||
const { quote, connected } = useStockQuote('600000');
|
||
|
||
return (
|
||
<Box>
|
||
<Badge colorScheme={connected ? 'green' : 'red'}>
|
||
{connected ? '已连接' : '未连接'}
|
||
</Badge>
|
||
<Text>价格:{quote?.price}</Text>
|
||
</Box>
|
||
);
|
||
};
|
||
```
|
||
|
||
#### 5. 如何调试 API 请求(Mock vs Real)
|
||
|
||
**切换 Mock/Real 模式**:
|
||
|
||
```bash
|
||
# ✅ Mock 模式(MSW 拦截所有请求)
|
||
npm start
|
||
# 使用 .env.mock,REACT_APP_ENABLE_MOCK=true
|
||
|
||
# ✅ 真实后端模式
|
||
npm run start:real
|
||
# 使用 .env.local,REACT_APP_ENABLE_MOCK=false
|
||
# 代理到 http://49.232.185.254:5001
|
||
|
||
# ✅ 开发模式(可自定义配置)
|
||
npm run start:dev
|
||
# 使用 .env.development
|
||
```
|
||
|
||
**查看 MSW 拦截的请求**:
|
||
|
||
打开浏览器控制台(F12),查看以下日志:
|
||
|
||
```
|
||
[MSW] Mocking enabled (warn mode)
|
||
[MSW] GET /api/stocks/600000 → 200 (mock data)
|
||
[MSW] POST /api/portfolio/buy → 200 (mock data)
|
||
```
|
||
|
||
**调试技巧**:
|
||
|
||
```javascript
|
||
// src/utils/apiConfig.js
|
||
export const getApiBase = () => {
|
||
const isMock = process.env.REACT_APP_ENABLE_MOCK === 'true';
|
||
const apiUrl = process.env.REACT_APP_API_URL;
|
||
|
||
// 添加调试日志
|
||
console.group('🔧 API 配置');
|
||
console.log('Mock 模式:', isMock ? 'ON ✅' : 'OFF ❌');
|
||
console.log('API URL:', apiUrl || '默认后端');
|
||
console.log('环境:', process.env.NODE_ENV);
|
||
console.groupEnd();
|
||
|
||
// ...
|
||
};
|
||
```
|
||
|
||
**使用 Redux DevTools 调试状态**:
|
||
|
||
1. 安装 Chrome 插件:[Redux DevTools](https://chrome.google.com/webstore/detail/redux-devtools)
|
||
2. 打开浏览器控制台 → Redux 标签页
|
||
3. 查看 Actions、State Diff、State Tree
|
||
|
||
**使用 React DevTools 调试组件**:
|
||
|
||
1. 安装 Chrome 插件:[React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools)
|
||
2. 打开浏览器控制台 → Components 标签页
|
||
3. 查看组件树、Props、Hooks 状态
|
||
|
||
**使用 Network 面板调试 API**:
|
||
|
||
1. 打开浏览器控制台(F12)→ Network 标签页
|
||
2. 筛选 XHR/Fetch 请求
|
||
3. 查看请求头、响应体、状态码
|
||
|
||
**测试 API 错误处理**:
|
||
|
||
```javascript
|
||
// 在 MSW handler 中模拟错误
|
||
http.get('/api/test-error', () => {
|
||
return HttpResponse.json(
|
||
{ error: '服务器错误' },
|
||
{ status: 500 }
|
||
);
|
||
});
|
||
|
||
// 测试组件是否正确处理错误
|
||
try {
|
||
const data = await fetchData();
|
||
} catch (error) {
|
||
console.error('API 错误:', error);
|
||
showNotification('数据加载失败', 'error');
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 重要说明
|
||
|
||
### 代码分割策略
|
||
重量级页面进行懒加载以减小初始 bundle 大小:
|
||
- Community、TradingSimulation、Company 页面使用 `React.lazy()`
|
||
- Webpack 将大型库拆分为独立 chunks
|
||
- 使用 `npm run build:analyze` 检查 bundle 大小
|
||
|
||
### 错误边界
|
||
- **布局级**:每个布局(MainLayout、Auth)都有自己的 ErrorBoundary
|
||
- **页面级**:PageTransitionWrapper 为每个路由包裹 ErrorBoundary
|
||
- **策略**:错误被隔离,防止整个应用崩溃
|
||
|
||
### PostHog 分析
|
||
- 在 Redux 中初始化(`posthogSlice`)
|
||
- 在 `App.js` 中应用启动时配置
|
||
- 用于用户行为追踪
|
||
|
||
### 性能考虑
|
||
- **大型组件**:如果组件超过 ~500 行,考虑重构(见 README.md)
|
||
- **重渲染**:对昂贵操作使用 `React.memo`、`useMemo`、`useCallback`
|
||
- **Bundle 大小**:使用 webpack-bundle-analyzer 监控
|
||
- **缓存**:Webpack 文件系统缓存显著加速重新构建
|
||
|
||
---
|
||
|
||
## 技术路径与开发指南
|
||
|
||
本章节定义了项目的技术方向、架构原则和开发标准。所有开发工作都应遵循这些指南。
|
||
|
||
### UI 框架选型与使用指南
|
||
|
||
#### 架构决策:混合 UI 框架策略
|
||
|
||
**决策时间**: 2025-01-07
|
||
**决策内容**: Ant Design 60% + Chakra UI 40% 混合架构
|
||
|
||
**选型理由**:
|
||
|
||
本项目是**金融交易分析平台**,需要同时满足:
|
||
1. 企业级数据展示能力(复杂表格、表单、日历)
|
||
2. 现代化视觉设计(统计卡片、图表容器、主题切换)
|
||
3. 高开发效率(减少自定义代码)
|
||
4. 灵活的主题系统(深色/浅色模式)
|
||
|
||
经过评估,单一 UI 框架无法同时满足所有需求,因此采用**混合策略**:
|
||
|
||
**Ant Design (60%)** - 数据组件为主
|
||
- 复杂数据表格(Table - 排序、筛选、分页、虚拟滚动)
|
||
- 高级表单组件(Form、InputNumber、Cascader、DatePicker)
|
||
- 日历组件(Calendar - 事件标记、自定义单元格)
|
||
- 抽屉和模态框(Drawer、Modal - 大尺寸、多Tab)
|
||
- 企业级交互组件(Tooltip、Popover、Dropdown)
|
||
|
||
**Chakra UI (40%)** - 视觉与布局为主
|
||
- 全局布局系统(SimpleGrid、Stack、Flex、Box)
|
||
- 统计卡片(Stat - 支持渐变、动画、主题切换)
|
||
- 图表容器(Box、Card - 灵活样式定制)
|
||
- 主题系统(useColorModeValue - 深色模式)
|
||
- 通用 UI 组件(Button、Badge、Tag - 可选)
|
||
|
||
#### 组件选择决策指南
|
||
|
||
**何时使用 Chakra UI**:
|
||
```
|
||
✅ 页面布局(SimpleGrid、Stack、Flex、Box)
|
||
✅ 统计卡片(Stat - 带渐变背景、动画效果)
|
||
✅ 图表容器(Box、Card - 需要灵活样式)
|
||
✅ 主题切换(useColorModeValue、useTheme)
|
||
✅ 需要 CSS-in-JS 动态定制的组件
|
||
✅ 响应式设计(breakpoints: base、md、lg)
|
||
```
|
||
|
||
**何时使用 Ant Design**:
|
||
```
|
||
✅ 数据表格(需要排序、筛选、分页、虚拟滚动)
|
||
✅ 复杂表单(多字段、联动计算、实时验证)
|
||
✅ 数字输入(金融场景、格式化、单位转换)
|
||
✅ 日期选择(禁用日期、范围选择、自定义渲染)
|
||
✅ 级联选择器(多级联动、搜索、异步加载)
|
||
✅ 日历组件(事件标记、自定义单元格、月份切换)
|
||
✅ 大尺寸抽屉/模态框(900px+、多Tab、额外操作区)
|
||
✅ 企业级交互(Tooltip、Popover、Dropdown、Menu)
|
||
```
|
||
|
||
#### 混合使用最佳实践
|
||
|
||
**1. 主题统一**
|
||
|
||
使用 Chakra UI 的主题系统作为主色调,通过 Ant Design 的 `ConfigProvider` 同步样式:
|
||
|
||
```javascript
|
||
// src/providers/AppProviders.js
|
||
import { ConfigProvider, theme as antdTheme } from 'antd';
|
||
import { ChakraProvider, useColorMode, useTheme } from '@chakra-ui/react';
|
||
|
||
function AppProviders({ children }) {
|
||
const { colorMode } = useColorMode();
|
||
const chakraTheme = useTheme();
|
||
|
||
// 🎨 将 Chakra 主题色同步到 Ant Design
|
||
const antdCustomTheme = {
|
||
token: {
|
||
colorPrimary: chakraTheme.colors.blue[500], // 主色
|
||
colorLink: chakraTheme.colors.blue[600], // 链接色
|
||
colorSuccess: chakraTheme.colors.green[500], // 成功色
|
||
colorWarning: chakraTheme.colors.orange[500], // 警告色
|
||
colorError: chakraTheme.colors.red[500], // 错误色
|
||
borderRadius: 8, // 圆角
|
||
fontSize: 14, // 字体大小
|
||
},
|
||
algorithm: colorMode === 'dark'
|
||
? antdTheme.darkAlgorithm
|
||
: antdTheme.defaultAlgorithm,
|
||
};
|
||
|
||
return (
|
||
<ChakraProvider theme={chakraTheme}>
|
||
<ConfigProvider theme={antdCustomTheme}>
|
||
{children}
|
||
</ConfigProvider>
|
||
</ChakraProvider>
|
||
);
|
||
}
|
||
```
|
||
|
||
**2. 布局策略**
|
||
|
||
外层使用 Chakra UI 布局,内层嵌入 Ant Design 数据组件:
|
||
|
||
```jsx
|
||
// ✅ 推荐:Chakra UI 容器 + Ant Design 数据
|
||
import { Box, Heading } from '@chakra-ui/react';
|
||
import { Table } from 'antd';
|
||
|
||
<Box p={6} bg={useColorModeValue('white', 'gray.800')} borderRadius="lg" boxShadow="md">
|
||
<Heading size="md" mb={4}>交易历史</Heading> {/* Chakra 标题 */}
|
||
<Table {/* Ant Design 表格 */}
|
||
columns={columns}
|
||
dataSource={data}
|
||
pagination={{ pageSize: 50 }}
|
||
/>
|
||
</Box>
|
||
|
||
// ❌ 避免:全用 Ant Design 做布局(失去主题控制)
|
||
<Card title="交易历史"> {/* 无法使用 useColorModeValue */}
|
||
<Table ... />
|
||
</Card>
|
||
```
|
||
|
||
**3. 样式冲突处理**
|
||
|
||
如果出现样式冲突,使用 Chakra UI 的 `sx` prop 覆盖 Ant Design 样式:
|
||
|
||
```jsx
|
||
import { Box, useColorModeValue } from '@chakra-ui/react';
|
||
import { Table } from 'antd';
|
||
|
||
<Box
|
||
sx={{
|
||
// 覆盖 Ant Design Table 的默认样式
|
||
'.ant-table': {
|
||
backgroundColor: 'transparent',
|
||
color: useColorModeValue('gray.800', 'white'),
|
||
},
|
||
'.ant-table-thead > tr > th': {
|
||
backgroundColor: useColorModeValue('gray.50', 'gray.700'),
|
||
color: useColorModeValue('gray.700', 'gray.200'),
|
||
borderColor: useColorModeValue('gray.200', 'gray.600'),
|
||
},
|
||
'.ant-table-tbody > tr > td': {
|
||
borderColor: useColorModeValue('gray.200', 'gray.600'),
|
||
},
|
||
'.ant-table-tbody > tr:hover > td': {
|
||
backgroundColor: useColorModeValue('gray.50', 'gray.750'),
|
||
},
|
||
}}
|
||
>
|
||
<Table columns={columns} dataSource={data} />
|
||
</Box>
|
||
```
|
||
|
||
**4. 图标统一**
|
||
|
||
优先使用 `lucide-react`(已安装),减少对多个图标库的依赖:
|
||
|
||
```jsx
|
||
// ✅ 推荐:lucide-react(体积小、一致性好)
|
||
import { Star, TrendingUp, Calendar } from 'lucide-react';
|
||
import { Icon } from '@chakra-ui/react';
|
||
|
||
<Icon as={Star} boxSize={5} color="yellow.400" />
|
||
<Icon as={TrendingUp} boxSize={5} color="green.500" />
|
||
|
||
// ⚠️ 仅在 Ant Design 组件内部使用 @ant-design/icons
|
||
import { StarFilled, ArrowUpOutlined } from '@ant-design/icons';
|
||
|
||
<Table
|
||
columns={[
|
||
{
|
||
title: '自选',
|
||
render: () => <StarFilled style={{ color: '#faad14' }} />
|
||
}
|
||
]}
|
||
/>
|
||
```
|
||
|
||
#### 实际应用案例
|
||
|
||
**案例 1: 交易面板**
|
||
```jsx
|
||
// 布局: Chakra UI (Box, Stack, SimpleGrid)
|
||
// 表单: Ant Design (Form, InputNumber)
|
||
// 按钮: Chakra UI (Button)
|
||
// 统计: Chakra UI (Stat)
|
||
|
||
<Box p={6} bg={cardBg} borderRadius="lg">
|
||
<SimpleGrid columns={{ base: 1, md: 2, lg: 4 }} spacing={6} mb={6}>
|
||
{/* Chakra UI 统计卡片 */}
|
||
<Stat>
|
||
<StatLabel>账户余额</StatLabel>
|
||
<StatNumber>¥123,456.78</StatNumber>
|
||
<StatHelpText>
|
||
<StatArrow type="increase" />
|
||
+23.45%
|
||
</StatHelpText>
|
||
</Stat>
|
||
</SimpleGrid>
|
||
|
||
{/* Ant Design 表单 */}
|
||
<Form layout="vertical">
|
||
<Form.Item label="交易数量">
|
||
<InputNumber
|
||
step={100}
|
||
formatter={value => `${value} 股`}
|
||
parser={value => value.replace(' 股', '')}
|
||
/>
|
||
</Form.Item>
|
||
</Form>
|
||
|
||
{/* Chakra UI 按钮 */}
|
||
<Stack direction="row" spacing={4}>
|
||
<Button colorScheme="blue" size="lg">买入</Button>
|
||
<Button colorScheme="red" size="lg">卖出</Button>
|
||
</Stack>
|
||
</Box>
|
||
```
|
||
|
||
**案例 2: 板块详情(涨停分析)**
|
||
```jsx
|
||
// 容器: Chakra UI (Card, Box)
|
||
// 折叠面板: Ant Design (Collapse) - 数据量大
|
||
// 表格: Ant Design (Table) - 需要虚拟滚动
|
||
// 标签: Chakra UI (Badge, Tag)
|
||
|
||
<Box p={6}>
|
||
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4} mb={6}>
|
||
{/* Chakra UI 统计卡片 */}
|
||
<Card>
|
||
<CardBody>
|
||
<Stat>
|
||
<StatLabel>涨停股票数</StatLabel>
|
||
<StatNumber>156</StatNumber>
|
||
</Stat>
|
||
</CardBody>
|
||
</Card>
|
||
</SimpleGrid>
|
||
|
||
{/* Ant Design 折叠面板 + 表格 */}
|
||
<Collapse>
|
||
{sectors.map(sector => (
|
||
<Collapse.Panel
|
||
header={
|
||
<Flex justify="space-between">
|
||
<Text>{sector.name}</Text>
|
||
<Badge colorScheme="red">{sector.count} 只</Badge>
|
||
</Flex>
|
||
}
|
||
>
|
||
<Table
|
||
columns={columns}
|
||
dataSource={sector.stocks}
|
||
pagination={false}
|
||
scroll={{ y: 300 }}
|
||
/>
|
||
</Collapse.Panel>
|
||
))}
|
||
</Collapse>
|
||
</Box>
|
||
```
|
||
|
||
**案例 3: 投资日历**
|
||
```jsx
|
||
// 布局: Chakra UI (SimpleGrid)
|
||
// 日历: Ant Design (Calendar) - 自定义渲染
|
||
// 抽屉: Ant Design (Drawer) - 900px宽
|
||
// 统计卡片: Chakra UI (Stat)
|
||
|
||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
|
||
{/* Ant Design 日历 */}
|
||
<Box>
|
||
<Calendar
|
||
dateCellRender={date => {
|
||
const count = getEventCount(date);
|
||
return (
|
||
<Badge colorScheme="blue" fontSize="xs">
|
||
{count} 条
|
||
</Badge>
|
||
);
|
||
}}
|
||
onSelect={onDateSelect}
|
||
/>
|
||
</Box>
|
||
|
||
{/* Chakra UI 统计区域 */}
|
||
<SimpleGrid columns={2} spacing={4}>
|
||
<Stat>
|
||
<StatLabel>本月事件</StatLabel>
|
||
<StatNumber>245</StatNumber>
|
||
</Stat>
|
||
</SimpleGrid>
|
||
</SimpleGrid>
|
||
|
||
{/* Ant Design 抽屉 */}
|
||
<Drawer
|
||
width={900}
|
||
title="事件详情"
|
||
placement="right"
|
||
onClose={onClose}
|
||
open={visible}
|
||
>
|
||
{/* 内容 */}
|
||
</Drawer>
|
||
```
|
||
|
||
#### 性能优化建议
|
||
|
||
1. **按需加载**: 使用 webpack 代码分割(已在 craco.config.js 配置)
|
||
2. **Tree Shaking**: 从 Ant Design 按需导入组件
|
||
```javascript
|
||
// ✅ 推荐:按需导入
|
||
import { Table, Form, Input } from 'antd';
|
||
|
||
// ❌ 避免:全量导入
|
||
import antd from 'antd';
|
||
```
|
||
3. **样式隔离**: Ant Design 组件包裹在独立的 Box 中,避免全局样式污染
|
||
4. **虚拟化**: 大型列表使用 Ant Design Table 的 `virtual` 属性
|
||
5. **懒加载**: 图表组件使用 React.lazy() 按需加载
|
||
|
||
#### Bundle 大小监控
|
||
|
||
**当前配置**(craco.config.js):
|
||
- `chakra-ui` chunk: ~150-200KB gzipped
|
||
- `antd-lib` chunk: ~200-250KB gzipped
|
||
- **总计**: ~400KB gzipped
|
||
|
||
**优化目标**: 保持 < 500KB gzipped
|
||
|
||
**监控命令**:
|
||
```bash
|
||
npm run build:analyze # 打开 webpack bundle analyzer
|
||
```
|
||
|
||
**优化机会**:
|
||
- 减少 `@ant-design/icons` 使用,改用 `lucide-react`
|
||
- 按需加载 Ant Design 组件(避免全量导入)
|
||
- 考虑移除未使用的 Chakra UI 组件
|
||
|
||
#### 技术债务说明
|
||
|
||
**为什么不迁移到单一框架?**
|
||
|
||
**经过评估(2025-01-07)**:
|
||
- **迁移成本**:
|
||
- Ant Design Table → @tanstack/react-table: 5-7 天
|
||
- Ant Design Form → 自定义表单: 3-5 天
|
||
- Ant Design Calendar → react-big-calendar: 2-3 天
|
||
- **总计**: 10-15 天工作量
|
||
|
||
- **ROI 分析**:
|
||
- Bundle 减少: ~200KB
|
||
- 开发体验: 下降(需要更多自定义代码)
|
||
- 维护成本: 上升(自定义组件需要维护)
|
||
- 视觉一致性: 提升(单一设计语言)
|
||
|
||
- **结论**: 当前混合方案的性价比更高,暂不迁移
|
||
|
||
**未来演进方向**:
|
||
|
||
**短期(3-6 个月)**:
|
||
- 优化 Ant Design 组件样式,与 Chakra UI 主题更统一
|
||
- 减少 `@ant-design/icons` 使用,改用 `lucide-react`
|
||
- 统一按钮、Badge、Tag 组件(优先使用 Chakra UI)
|
||
|
||
**中期(6-12 个月)**:
|
||
- 监控两个框架的使用率(通过 webpack-bundle-analyzer)
|
||
- 如果 Chakra UI 使用率 < 10%,考虑完全移除
|
||
- 如果 Ant Design 使用率 < 10%,考虑完全移除
|
||
- 观察社区动态,评估新的 UI 框架(Radix UI + Tailwind CSS)
|
||
|
||
**长期(12+ 个月)**:
|
||
- 如果 React 19 Server Components 成熟,重新评估 UI 框架选型
|
||
- 考虑自建设计系统(基于 Radix UI Primitives)
|
||
|
||
---
|
||
|
||
### 技术栈决策与演进路线
|
||
|
||
#### 当前技术选型及理由
|
||
|
||
**前端框架栈:**
|
||
- **React 18.3.1**: 成熟的生态系统、并发渲染特性、广泛的社区支持
|
||
- **Chakra UI 2.8.2**: 主UI库,优先考虑可访问性、主题定制能力强、基于 Emotion 的样式系统
|
||
- **Ant Design**: 辅助UI库,用于复杂的数据密集型组件(表格、表单),弥补 Chakra UI 的不足
|
||
- **Redux Toolkit**: 可预测的全局状态管理、优秀的 DevTools 集成、完善的 TypeScript 支持
|
||
|
||
**构建与开发工具:**
|
||
- **CRACO**: 允许在不 eject 的情况下自定义 webpack 配置
|
||
- **MSW (Mock Service Worker)**: 在网络层进行 API mock,提供更真实的开发体验,加快开发迭代
|
||
- **Webpack 5**: 利用文件系统缓存,重新构建速度提升 50-80%
|
||
|
||
**图表库(多库策略):**
|
||
- **ApexCharts**: 主要用于金融/交易图表(K线图、折线图)
|
||
- **ECharts**: 用于复杂的交互式可视化和大数据集渲染
|
||
- **Recharts**: 轻量级选项,用于仪表板中的简单图表
|
||
- **D3.js**: 底层控制,用于自定义可视化
|
||
- **选型理由**: 不同图表类型有不同的最优库,多库策略提供灵活性
|
||
|
||
**后端技术栈:**
|
||
- **Flask + SQLAlchemy**: Python 生态系统与金融/数据科学工作流契合度高
|
||
- **ClickHouse**: 针对时序分析查询优化(股票数据、交易指标)
|
||
- **MySQL/PostgreSQL**: 传统关系型数据库,用于事务性数据,提供 ACID 保证
|
||
- **Celery + Redis**: 处理长时间运行的后台任务(交易模拟、报表生成)
|
||
|
||
#### 计划中的技术演进
|
||
|
||
**短期(未来 3-6 个月):**
|
||
- **React Query / TanStack Query**: 逐步从 Redux 迁移到 React Query 管理服务端状态
|
||
- 优势: 自动缓存、后台自动刷新、乐观更新
|
||
- 策略: 从新功能开始,然后迁移现有 API 调用
|
||
- Redux 将继续用于客户端状态(UI 状态、模态框等)
|
||
|
||
- **Vite 迁移**: 考虑从 CRACO/webpack 迁移到 Vite,提升开发体验
|
||
- 优势: HMR 速度提升 10-50 倍,配置更简单
|
||
- 风险: 可能需要大量重构构建配置
|
||
- 决策节点: React Query 迁移稳定后
|
||
|
||
**中期(6-12 个月):**
|
||
- **React 19**: 生态系统稳定后升级
|
||
- 关注点: Chakra UI 和 Ant Design 的兼容性
|
||
- 新特性利用: Server Components(如适用)、改进的 Suspense
|
||
|
||
- **TypeScript 迁移**: 逐步为关键路径添加 TypeScript
|
||
- 优先级: API 服务层 → Redux slices → 共享组件 → 视图组件
|
||
- 新文件使用 `.ts`/`.tsx`,遗留代码保持 `.js`/`.jsx`(渐进式迁移)
|
||
|
||
**长期(12+ 个月):**
|
||
- **Monorepo 结构**: 如果项目规模扩大,考虑 Nx 或 Turborepo
|
||
- **微前端架构**: 如果团队规模扩大,考虑模块联邦(Module Federation)
|
||
|
||
#### 废弃/避免使用的技术
|
||
|
||
- **Moment.js**: 正在逐步淘汰,改用 `date-fns` 或原生 `Intl.DateTimeFormat`
|
||
- 当前已配置 webpack `IgnorePlugin` 以移除 locale 文件
|
||
- 新的日期处理代码请使用 `date-fns`
|
||
|
||
- **jQuery**: 新代码中禁止使用(与 React 虚拟 DOM 冲突)
|
||
|
||
- **Class 组件**: 所有新组件必须使用函数式组件 + Hooks
|
||
|
||
### 架构设计原则
|
||
|
||
#### 1. 组件设计原则
|
||
|
||
**原子设计层级:**
|
||
```
|
||
原子组件 (Atoms, 1-50 行)
|
||
↓ 组合成
|
||
分子组件 (Molecules, 50-150 行)
|
||
↓ 组合成
|
||
有机体组件 (Organisms, 150-500 行)
|
||
↓ 组合成
|
||
模板/视图 (Templates/Views, 500+ 行,但主要是组合)
|
||
```
|
||
|
||
**单一职责原则:**
|
||
- 每个组件应该只做一件事,并做好
|
||
- 如果组件超过 500 行,很可能做了太多事情 → 需要重构
|
||
- 将逻辑提取到自定义 hooks、工具函数或子组件中
|
||
|
||
**组合优于配置:**
|
||
```jsx
|
||
// 推荐: 可组合
|
||
<Card>
|
||
<CardHeader>标题</CardHeader>
|
||
<CardBody>内容</CardBody>
|
||
</Card>
|
||
|
||
// 避免: 过多的 props
|
||
<Card title="标题" body="内容" showBorder={true} theme="dark" />
|
||
```
|
||
|
||
**就近原则(Co-location):**
|
||
```
|
||
ComponentName/
|
||
├── index.js - 主导出文件
|
||
├── ComponentName.js - 实现代码
|
||
├── ComponentName.test.js - 测试
|
||
├── hooks/ - 组件专用 hooks
|
||
│ └── useComponentLogic.js
|
||
├── utils.js - 组件专用工具函数
|
||
└── constants.js - 组件专用常量
|
||
```
|
||
|
||
#### 2. 状态管理决策树
|
||
|
||
使用此决策树确定状态应该存储在哪里:
|
||
|
||
```
|
||
是否是服务端数据(来自 API)?
|
||
├─ 是 → 使用 React Query(或暂时使用 Redux,后续迁移)
|
||
│
|
||
└─ 否 → 是否需要在多个不相关的组件间共享?
|
||
├─ 是 → 是否是 UI 状态(模态框、侧边栏开关等)?
|
||
│ ├─ 是 → Redux (authModalSlice, sidebarSlice 等)
|
||
│ └─ 否 → React Context (AuthContext, NotificationContext)
|
||
│
|
||
└─ 否 → 是否需要在父组件和多个子组件间共享?
|
||
├─ 是 → 状态提升到共同父组件,通过 props 传递
|
||
└─ 否 → 本地组件状态 (useState, useReducer)
|
||
```
|
||
|
||
**状态管理规则:**
|
||
- **Redux**: 全局 UI 状态、跨切面关注点、需要在路由切换间保持的状态
|
||
- **Context**: 依赖注入(主题、认证)、避免属性透传,适合稳定值
|
||
- **组件状态**: 表单输入、开关、本地 UI 状态(下拉菜单、标签页)
|
||
- **React Query**(未来): 所有服务端状态、API 缓存、数据变更
|
||
|
||
**性能规则:**
|
||
- 如果 Context 值频繁变化,拆分为多个 Context 或使用 Redux
|
||
- 在 Context 边界使用 `React.memo` 防止不必要的重渲染
|
||
|
||
#### 3. 代码分割策略
|
||
|
||
**路由级分割(主要策略):**
|
||
- 所有主要视图在 `src/routes/lazy-components.js` 中使用 `React.lazy()`
|
||
- 重量级页面(Community、TradingSimulation、Company)始终懒加载
|
||
- 认证页面可以懒加载(不在关键路径上)
|
||
|
||
**库级分割(webpack):**
|
||
- 已在 `craco.config.js` 中配置
|
||
- 大型库独立打包:charts、Three.js、Chakra、Ant Design
|
||
- 关键规则: 保持初始 bundle < 500KB gzipped
|
||
|
||
**组件级分割(选择性使用):**
|
||
- 仅用于真正重量级且条件渲染的组件(如 3D 可视化)
|
||
```jsx
|
||
const HeavyChart = lazy(() => import('./HeavyChart'));
|
||
// 仅在需要时渲染
|
||
{showChart && <Suspense fallback={<Spinner />}><HeavyChart /></Suspense>}
|
||
```
|
||
|
||
#### 4. API 层设计模式
|
||
|
||
**服务层架构:**
|
||
```
|
||
组件 (Component)
|
||
↓ 调用
|
||
自定义 Hook (useStockData)
|
||
↓ 使用
|
||
服务函数 (src/services/stockService.js)
|
||
↓ 调用
|
||
Axios 实例(配置了 baseURL、拦截器)
|
||
↓ 请求
|
||
后端 API 或 MSW Mock
|
||
```
|
||
|
||
**API 服务模板:**
|
||
```javascript
|
||
// src/services/exampleService.js
|
||
import axios from 'axios';
|
||
import { getApiBase } from '@utils/apiConfig';
|
||
|
||
const api = axios.create({
|
||
baseURL: getApiBase(),
|
||
timeout: 10000,
|
||
});
|
||
|
||
// 请求拦截器(添加认证 token)
|
||
api.interceptors.request.use(config => {
|
||
const token = localStorage.getItem('token');
|
||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||
return config;
|
||
});
|
||
|
||
// 响应拦截器(全局错误处理)
|
||
api.interceptors.response.use(
|
||
response => response.data,
|
||
error => {
|
||
// 全局错误处理
|
||
console.error('API Error:', error);
|
||
return Promise.reject(error);
|
||
}
|
||
);
|
||
|
||
export const fetchExample = async (id) => {
|
||
return api.get(`/api/example/${id}`);
|
||
};
|
||
```
|
||
|
||
**MSW Mock Handler 模板:**
|
||
```javascript
|
||
// src/mocks/handlers/example.js
|
||
import { http, HttpResponse } from 'msw';
|
||
|
||
export const exampleHandlers = [
|
||
http.get('/api/example/:id', ({ params }) => {
|
||
return HttpResponse.json({
|
||
id: params.id,
|
||
data: 'mock data',
|
||
});
|
||
}),
|
||
];
|
||
```
|
||
|
||
#### 5. 错误处理架构
|
||
|
||
**三层错误边界策略:**
|
||
```
|
||
App.js(根组件)
|
||
↓ 捕获致命错误(显示降级 UI,记录到监控服务)
|
||
布局 (MainLayout, AuthLayout)
|
||
↓ 捕获布局特定错误(显示错误页面,允许重试)
|
||
页面(通过 PageTransitionWrapper 包裹的各个路由)
|
||
↓ 捕获页面特定错误(显示错误消息,允许导航)
|
||
```
|
||
|
||
**API 错误处理模式:**
|
||
```javascript
|
||
try {
|
||
const data = await fetchData();
|
||
// 处理成功情况
|
||
} catch (error) {
|
||
if (error.response?.status === 401) {
|
||
// 跳转到登录页
|
||
} else if (error.response?.status === 403) {
|
||
// 显示权限错误
|
||
} else {
|
||
// 显示通用错误
|
||
showNotification('数据加载失败', 'error');
|
||
}
|
||
}
|
||
```
|
||
|
||
### 开发规范与最佳实践
|
||
|
||
#### 1. 组件开发规范
|
||
|
||
**文件命名约定:**
|
||
- 组件: `PascalCase.js` (如 `EventCard.js`)
|
||
- 工具函数: `camelCase.js` (如 `priceFormatters.js`)
|
||
- 常量: `SCREAMING_SNAKE_CASE.js` 或 `camelCase.js` (如 `API_ENDPOINTS.js`, `animations.js`)
|
||
- Hooks: `useCamelCase.js` (如 `useStockData.js`)
|
||
|
||
**组件文件结构:**
|
||
```javascript
|
||
// 1. 导入(分组并排序)
|
||
import React, { useState, useEffect } from 'react';
|
||
import { Box, Text } from '@chakra-ui/react';
|
||
import { useDispatch } from 'react-redux';
|
||
import PropTypes from 'prop-types';
|
||
import { CustomComponent } from '@components/CustomComponent';
|
||
import { useCustomHook } from '@hooks/useCustomHook';
|
||
import { helperFunction } from './utils';
|
||
|
||
// 2. 常量(组件专用)
|
||
const DEFAULT_VALUE = 10;
|
||
|
||
// 3. 组件定义
|
||
export const MyComponent = ({ prop1, prop2 }) => {
|
||
// 3a. Hooks(按类型分组)
|
||
const dispatch = useDispatch();
|
||
const [state, setState] = useState(DEFAULT_VALUE);
|
||
const customValue = useCustomHook();
|
||
|
||
// 3b. 副作用
|
||
useEffect(() => {
|
||
// 副作用逻辑
|
||
}, []);
|
||
|
||
// 3c. 事件处理器
|
||
const handleClick = () => {
|
||
setState(prev => prev + 1);
|
||
};
|
||
|
||
// 3d. 渲染逻辑
|
||
return (
|
||
<Box>
|
||
<Text>{state}</Text>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
// 4. PropTypes(或 TypeScript 类型)
|
||
MyComponent.propTypes = {
|
||
prop1: PropTypes.string.isRequired,
|
||
prop2: PropTypes.number,
|
||
};
|
||
|
||
// 5. 默认 Props
|
||
MyComponent.defaultProps = {
|
||
prop2: 0,
|
||
};
|
||
```
|
||
|
||
**Props 设计指南:**
|
||
- 在组件参数中使用对象解构
|
||
- 必需的 props 在前,可选的 props 在后
|
||
- 布尔类型 props 使用 `is`、`has`、`should` 前缀(如 `isLoading`、`hasError`)
|
||
- 回调函数使用 `onEventName` 格式(如 `onClick`、`onSubmit`)
|
||
- 避免传递整个对象,只传递需要的几个属性
|
||
|
||
**性能优化检查清单:**
|
||
- [ ] 昂贵的计算是否用 `useMemo` 包裹?
|
||
- [ ] 传递给子组件的回调函数是否用 `useCallback` 包裹?
|
||
- [ ] 列表项是否使用唯一且稳定的 `key` props(不是数组索引)?
|
||
- [ ] 大型列表是否虚拟化(react-window 或 react-virtuoso)?
|
||
- [ ] 频繁接收 prop 更新的组件是否用 `React.memo` 包裹?
|
||
- [ ] 图片是否懒加载(`loading="lazy"`)?
|
||
|
||
#### 2. Hooks 开发规范
|
||
|
||
**自定义 Hook 规则:**
|
||
- 必须以 `use` 前缀开头
|
||
- 应该在多个组件间可复用
|
||
- 应该处理一个特定关注点(数据获取、表单状态等)
|
||
- 应该返回对象或数组(不直接返回原始值)
|
||
|
||
**自定义 Hook 模板:**
|
||
```javascript
|
||
// src/hooks/useStockData.js
|
||
import { useState, useEffect } from 'react';
|
||
import { fetchStockData } from '@services/stockService';
|
||
|
||
export const useStockData = (stockId) => {
|
||
const [data, setData] = useState(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState(null);
|
||
|
||
useEffect(() => {
|
||
if (!stockId) return;
|
||
|
||
let isMounted = true;
|
||
setLoading(true);
|
||
|
||
fetchStockData(stockId)
|
||
.then(result => {
|
||
if (isMounted) {
|
||
setData(result);
|
||
setError(null);
|
||
}
|
||
})
|
||
.catch(err => {
|
||
if (isMounted) {
|
||
setError(err);
|
||
}
|
||
})
|
||
.finally(() => {
|
||
if (isMounted) setLoading(false);
|
||
});
|
||
|
||
return () => {
|
||
isMounted = false;
|
||
};
|
||
}, [stockId]);
|
||
|
||
return { data, loading, error };
|
||
};
|
||
```
|
||
|
||
#### 3. Git 工作流与提交规范
|
||
|
||
**分支命名约定:**
|
||
```
|
||
feature/简短描述 - 新功能
|
||
fix/bug描述 - Bug 修复
|
||
refactor/组件名称 - 代码重构
|
||
docs/变更内容 - 文档更新
|
||
chore/任务描述 - 维护任务
|
||
```
|
||
|
||
**提交信息格式:**
|
||
```
|
||
<类型>(<范围>): <主题>
|
||
|
||
<正文>
|
||
|
||
<脚注>
|
||
```
|
||
|
||
**类型:**
|
||
- `feat`: 新功能
|
||
- `fix`: Bug 修复
|
||
- `refactor`: 既不修复 Bug 也不添加功能的代码更改
|
||
- `style`: 代码风格更改(格式化、缺少分号等)
|
||
- `docs`: 文档更改
|
||
- `test`: 添加或更新测试
|
||
- `chore`: 维护任务(依赖、构建配置等)
|
||
|
||
**示例:**
|
||
```
|
||
feat(community): 添加按日期范围筛选事件功能
|
||
|
||
实现了日期范围选择器用于筛选事件。
|
||
添加了 Redux slice 用于筛选状态管理。
|
||
|
||
Closes #123
|
||
|
||
---
|
||
|
||
fix(trading): 修正模拟交易中的利润计算
|
||
|
||
修复了导致盈亏显示不正确的浮点精度问题。
|
||
|
||
---
|
||
|
||
refactor(components): 将 EventCard 拆分为原子组件
|
||
|
||
将 1200 行的 EventCard 拆分为:
|
||
- CompactEventCard
|
||
- DetailedEventCard
|
||
- EventTimeline (原子组件)
|
||
- EventImportanceBadge (原子组件)
|
||
```
|
||
|
||
**代码审查检查清单:**
|
||
- [ ] 代码是否遵循组件结构指南?
|
||
- [ ] 是否有超过 500 行应该拆分的组件?
|
||
- [ ] 状态管理是否合适(Redux vs Context vs Local)?
|
||
- [ ] 是否存在性能问题(不必要的重渲染、缺少记忆化)?
|
||
- [ ] 新功能是否有错误边界?
|
||
- [ ] 错误处理是否全面(API 错误、边界情况)?
|
||
- [ ] 新的 API 是否在 MSW 中 mock 以便开发?
|
||
- [ ] 是否定义了 PropTypes 或 TypeScript 类型?
|
||
- [ ] 代码是否具有可访问性(ARIA 标签、键盘导航)?
|
||
- [ ] 是否有 console 错误或警告?
|
||
|
||
### 技术债务与已知问题
|
||
|
||
本节跟踪技术债务、性能问题和计划中的重构工作。发现或解决问题时请更新此列表。
|
||
|
||
#### 高优先级技术债务
|
||
|
||
**1. Redux 到 React Query 迁移**
|
||
- **状态**: 已计划
|
||
- **影响**: 高 - 影响所有 API 调用
|
||
- **时间线**: 2025 年 Q2 开始
|
||
- **描述**: 将服务端状态从 Redux 迁移到 React Query,以获得更好的缓存、自动重新获取和减少样板代码
|
||
- **受影响文件**: `src/store/slices/*Slice.js`、整个应用中的 API 服务调用
|
||
- **迁移策略**:
|
||
1. 在 `AppProviders.js` 中设置 React Query provider
|
||
2. 创建 `src/queries/` 目录存放 query hooks
|
||
3. 一次迁移一个领域(从 Community 或 Stock 数据开始)
|
||
4. Redux 保留用于 UI 状态(模态框、侧边栏等)
|
||
|
||
**2. 大型组件重构**
|
||
- **状态**: 进行中
|
||
- **影响**: 中 - 可维护性和可读性
|
||
- **需要重构的组件**:
|
||
- 任何超过 500 行的新组件
|
||
- 审查 `src/views/` 中的组件,寻找潜在的拆分机会
|
||
- **遵循模式**: 参见上面的"组件组织模式"章节
|
||
|
||
**3. TypeScript 渐进式迁移**
|
||
- **状态**: 已计划
|
||
- **影响**: 高 - 类型安全、减少运行时错误
|
||
- **时间线**: 2025 年 Q3 开始
|
||
- **迁移顺序**:
|
||
1. `src/utils/` (工具函数 - 最容易添加类型)
|
||
2. `src/services/` (API 层 - 高价值)
|
||
3. `src/store/` (Redux slices - 中等价值)
|
||
4. `src/components/` (共享组件)
|
||
5. `src/views/` (页面组件 - 最后)
|
||
|
||
#### 中优先级技术债务
|
||
|
||
**4. Webpack 到 Vite 迁移**
|
||
- **状态**: 评估中
|
||
- **影响**: 高 - 开发体验
|
||
- **好处**: HMR 速度提升 10-50 倍,配置更简单
|
||
- **风险**: 可能需要大量重构构建配置、CRACO 自定义配置
|
||
- **决策节点**: React Query 迁移完成后
|
||
|
||
**5. Moment.js 替换**
|
||
- **状态**: 进行中(已配置移除 locale 文件)
|
||
- **影响**: 低 - bundle 大小减少(约 20KB)
|
||
- **策略**: 新代码中用 `date-fns` 替换 `moment()`,现有代码逐步迁移
|
||
- **使用 moment.js 的文件**: 搜索 `import moment` 以识别
|
||
|
||
**6. 统一错误处理**
|
||
- **状态**: 需要改进
|
||
- **影响**: 中 - 用户体验、调试
|
||
- **问题**:
|
||
- 部分 API 调用缺少错误处理
|
||
- 错误边界使用不一致
|
||
- 某些地方的错误消息对用户不友好
|
||
- **行动项**:
|
||
- 审计所有 API 调用的错误处理
|
||
- 为所有主要视图添加错误边界
|
||
- 标准化错误消息显示(NotificationContext)
|
||
|
||
#### 低优先级技术债务
|
||
|
||
**7. 测试覆盖率**
|
||
- **状态**: 不足
|
||
- **影响**: 中 - 变更信心
|
||
- **当前覆盖率**: 未知(需要测量)
|
||
- **目标**: 关键路径 60%+ 覆盖率
|
||
- **优先区域**:
|
||
1. 工具函数(易于测试、高价值)
|
||
2. Redux slices(状态管理逻辑)
|
||
3. 自定义 hooks(可复用逻辑)
|
||
4. 关键 UI 流程(认证、交易模拟)
|
||
|
||
**8. Bundle 大小优化**
|
||
- **状态**: 持续监控中
|
||
- **影响**: 低 - 加载时间
|
||
- **当前大小**: 使用 `npm run build:analyze` 检查
|
||
- **优化机会**:
|
||
- 移除未使用的依赖
|
||
- Tree-shake 大型库(lodash → lodash-es)
|
||
- 考虑懒加载 Three.js 和其他重量级库
|
||
|
||
#### 已知问题与限制
|
||
|
||
**性能:**
|
||
- **Community 视图中的大型事件列表**: 如果列表超过 100 项,考虑虚拟化(react-window)
|
||
- **图表重渲染**: ApexCharts 在大数据集时可能较慢;考虑节流数据更新
|
||
|
||
**浏览器兼容性:**
|
||
- **不支持 IE11**: 现代 JavaScript 特性需要 polyfills 才能支持旧浏览器
|
||
- **Safari 兼容性**: 某些 Chakra UI 动画在 Safari 中行为可能不同
|
||
|
||
**移动端响应式:**
|
||
- **交易模拟**: 复杂表格在移动端未完全优化
|
||
- **图表**: 可能需要改进触摸事件处理
|
||
|
||
**数据/API:**
|
||
- **实时 WebSocket**: 连接断开时不总是能优雅处理;需添加重连逻辑
|
||
- **ClickHouse 查询**: 大日期范围的某些分析查询可能较慢;考虑分页或聚合
|
||
|
||
---
|
||
|
||
## 项目变更历史
|
||
|
||
本节记录项目的重要变更、重构和架构调整,帮助理解项目演进过程。
|
||
|
||
> **📝 页面级变更历史**: 特定页面的详细变更历史和技术文档已迁移到各自的文档中:
|
||
> - **Community 页面**: [docs/Community.md](./docs/Community.md) - 页面架构、组件结构、数据流、变更历史
|
||
> - **Agent 系统**: [AGENT_INTEGRATION_COMPLETE.md](./AGENT_INTEGRATION_COMPLETE.md) - Agent 系统集成完整说明
|
||
> - **其他页面**: 根据需要创建独立的页面文档
|
||
|
||
### 2025-11-07: Agent 系统集成到 mcp_server.py
|
||
|
||
**影响范围**: 后端 MCP 服务器 + 前端 Agent 聊天功能
|
||
|
||
**集成成果**:
|
||
- 将独立的 Agent 系统完全集成到 `mcp_server.py` 中
|
||
- 使用 **Kimi (kimi-k2-thinking)** 进行计划制定和推理
|
||
- 使用 **DeepMoney (本地部署)** 进行新闻总结
|
||
- 实现三阶段智能分析流程(计划→执行→总结)
|
||
- 前端使用 ChatInterfaceV2 + 可折叠卡片展示执行过程
|
||
- **无需运行多个脚本**,所有功能集成在单一服务中
|
||
|
||
**技术要点**:
|
||
- 新增 `MCPAgentIntegrated` 类(991-1367行)
|
||
- 新增 `/agent/chat` API 端点
|
||
- 新增特殊工具 `summarize_news`(使用 DeepMoney)
|
||
- Kimi 使用 `reasoning_content` 字段记录思考过程
|
||
- 自动替换占位符("前面的新闻数据" → 实际数据)
|
||
|
||
**前端组件**:
|
||
- `ChatInterfaceV2.js` - 新版聊天界面
|
||
- `PlanCard.js` - 执行计划展示
|
||
- `StepResultCard.js` - 步骤结果卡片(可折叠)
|
||
- 路由:`/agent-chat`
|
||
|
||
**详细文档**: 参见 [AGENT_INTEGRATION_COMPLETE.md](./AGENT_INTEGRATION_COMPLETE.md)
|
||
|
||
### 2025-10-30: EventList.js 组件化重构
|
||
|
||
**影响范围**: Community 页面核心组件
|
||
|
||
**重构成果**:
|
||
- 将 1095 行的 `EventList.js` 拆分为 497 行主组件 + 10 个子组件
|
||
- 代码行数减少 **54.6%** (598 行)
|
||
- 创建了 7 个原子组件 (Atoms) 和 2 个组合组件 (Molecules)
|
||
|
||
**新增组件**:
|
||
- `EventCard/` - 统一入口,智能路由紧凑/详细模式
|
||
- `CompactEventCard.js` - 紧凑模式事件卡片
|
||
- `DetailedEventCard.js` - 详细模式事件卡片
|
||
- 7 个原子组件: EventTimeline, EventImportanceBadge, EventStats, EventFollowButton, EventPriceDisplay, EventDescription, EventHeader
|
||
|
||
**新增工具函数**:
|
||
- `src/utils/priceFormatters.js` - 价格格式化工具 (getPriceChangeColor, formatPriceChange, PriceArrow)
|
||
- `src/constants/animations.js` - 动画常量 (pulseAnimation, fadeIn, slideInUp)
|
||
|
||
**优势**: 提高了代码可维护性、可复用性、可测试性和性能
|
||
|
||
**详细文档**: 参见 [docs/Community.md](./docs/Community.md)
|
||
|
||
---
|
||
|
||
## 更新本文档
|
||
|
||
本 CLAUDE.md 文件是一个持续更新的文档。在以下情况下应更新它:
|
||
- 添加新的架构模式或指南
|
||
- 发现新的最佳实践
|
||
- 解决或添加技术债务项
|
||
- 做出重要的技术决策
|
||
- 进行重要的代码清理或重构
|
||
|
||
所有开发人员应在入职时审查本文档,并在做出架构决策时参考它。
|
||
|
||
**文档维护指南**:
|
||
- 重要变更应记录在"项目变更历史"章节
|
||
- 技术决策应记录在"技术路径与开发指南"章节
|
||
- 开发规范更新应记录在"开发规范与最佳实践"章节 |