Files
vf_react/docs/CENTER_PAGE_FLOW_ANALYSIS.md
zdl 09db05c448 docs: 将所有文档迁移到 docs/ 目录
- 移动42个文档文件到 docs/ 目录
  - 更新 .gitignore 允许 docs/ 下的 .md 文件
  - 删除根目录下的重复文档文件

  📁 文档分类:
  - StockDetailPanel 重构文档(3个)
  - PostHog 集成文档(6个)
  - 系统架构和API文档(33个)

  🤖 Generated with [Claude Code](https://claude.com/claude-code)

  Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 14:51:22 +08:00

1813 lines
72 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 个人中心页面业务流程分析文档
> **文档创建日期**: 2025-01-19
> **分析范围**: `/home/center` 个人中心页面
> **相关文件**:
> - `src/views/Dashboard/Center.js` (主组件)
> - `src/views/Dashboard/components/InvestmentCalendarChakra.js` (日历组件)
> - `src/views/Dashboard/components/InvestmentPlansAndReviews.js` (计划复盘组件)
---
## 📋 目录
- [一、页面组件结构](#一页面组件结构)
- [二、组件初始化和挂载流程](#二组件初始化和挂载流程)
- [三、API 请求时间线和数据流](#三api-请求时间线和数据流)
- [四、子组件的 API 请求](#四子组件的-api-请求)
- [五、完整 API 请求汇总表](#五完整-api-请求汇总表)
- [六、UI 状态变化和交互流程](#六ui-状态变化和交互流程)
- [七、UI 状态变化映射表](#七ui-状态变化映射表)
- [八、关键设计亮点](#八关键设计亮点)
- [九、数据流向图](#九数据流向图)
- [十、Mock 数据对应关系](#十mock-数据对应关系)
---
## 一、页面组件结构
```
CenterDashboard (主组件 - src/views/Dashboard/Center.js)
├── 头部区域
│ ├── 标题: "个人中心"
│ └── 刷新按钮 (onClick={loadData})
├── 统计卡片区域 (SimpleGrid 4列)
│ ├── 自选股票数量 (watchlist.length)
│ ├── 关注事件数量 (followingEvents.length)
│ ├── 我的评论数量 (eventComments.length)
│ └── 订阅状态 (可点击跳转到订阅管理页)
├── InvestmentCalendarChakra (投资日历组件)
│ ├── FullCalendar 月视图
│ ├── 添加计划按钮
│ ├── 事件详情 Modal
│ └── 添加计划 Modal
├── 主内容区域 (Grid 布局: 左侧1列 + 右侧2列)
│ ├── 左侧栏
│ │ ├── 自选股票卡片
│ │ │ ├── 刷新行情按钮 (onClick={loadRealtimeQuotes})
│ │ │ ├── 添加自选股按钮 (跳转到股票搜索页)
│ │ │ └── 股票列表 (最多显示10只)
│ │ │ ├── 股票名称/代码
│ │ │ ├── 当前价格
│ │ │ └── 涨跌幅 (红涨绿跌)
│ │ └── 订阅管理卡片
│ │ ├── 当前套餐显示
│ │ ├── 剩余天数
│ │ └── 升级/管理按钮
│ └── 右侧栏
│ ├── 关注事件卡片
│ │ ├── 事件列表 (最多显示5个)
│ │ │ ├── 事件标题 (可点击跳转到详情)
│ │ │ ├── 事件标签
│ │ │ ├── 统计数据 (浏览/评论/点赞)
│ │ │ └── 热度分数
│ │ └── 查看更多按钮
│ └── 我的评论卡片
│ ├── 评论列表 (最多显示5条)
│ │ ├── 评论内容
│ │ ├── 发布时间
│ │ └── 所属事件标题
│ └── 评论总数提示
└── InvestmentPlansAndReviews (投资计划与复盘组件)
├── Tab 1: 我的计划 (plans.length)
│ ├── 新建计划按钮
│ └── 计划卡片列表 (Grid 2列)
│ ├── 计划标题
│ ├── 日期
│ ├── 状态 (进行中/已完成/已取消)
│ ├── 内容摘要
│ ├── 相关股票标签
│ ├── 自定义标签
│ ├── 编辑按钮
│ └── 删除按钮
└── Tab 2: 我的复盘 (reviews.length)
├── 新建复盘按钮
└── 复盘卡片列表 (Grid 2列)
└── (结构同计划卡片)
```
---
## 二、组件初始化和挂载流程
### 2.1 State 初始化
```javascript
// src/views/Dashboard/Center.js:80-87
const [watchlist, setWatchlist] = useState([]); // 自选股列表
const [realtimeQuotes, setRealtimeQuotes] = useState({}); // 实时行情 Map {stock_code: quote_data}
const [followingEvents, setFollowingEvents] = useState([]); // 关注的事件列表
const [eventComments, setEventComments] = useState([]); // 我的评论列表
const [subscriptionInfo, setSubscriptionInfo] = useState({ // 订阅信息
type: 'free', // 订阅类型: free/pro/max
status: 'active', // 订阅状态: active/expired
days_left: 999, // 剩余天数
is_active: true // 是否激活
});
const [loading, setLoading] = useState(true); // 初次加载状态
const [refreshing, setRefreshing] = useState(false); // 刷新状态
const [quotesLoading, setQuotesLoading] = useState(false); // 行情加载状态
```
### 2.2 组件挂载钩子 (useEffect)
```javascript
// src/views/Dashboard/Center.js:198-210
useEffect(() => {
// 触发条件 1: 用户已登录
// 触发条件 2: 当前路径包含 '/home/center'
if (user && location.pathname.includes('/home/center')) {
loadData(); // 🌐 触发主数据加载
}
// 监听页面可见性变化 (从后台切回前台时自动刷新)
const onVis = () => {
if (document.visibilityState === 'visible' &&
location.pathname.includes('/home/center')) {
loadData(); // 🌐 页面重新可见时刷新数据
}
};
document.addEventListener('visibilitychange', onVis);
return () => document.removeEventListener('visibilitychange', onVis);
}, [user, location.pathname, loadData]);
```
**触发时机**:
1. 组件首次挂载 (如果用户已登录且路径正确)
2. 用户从其他页面导航到个人中心
3. 浏览器标签页从后台切换回前台
---
## 三、API 请求时间线和数据流
### Phase 1: 主数据并行加载 (loadData)
**函数位置**: `src/views/Dashboard/Center.js:89-124`
**触发时机**:
- 组件挂载
- 用户点击"刷新数据"按钮
- 页面从后台切回前台
**执行流程**:
```javascript
const loadData = useCallback(async () => {
try {
setRefreshing(true); // 🔄 显示刷新状态
const base = process.env.NODE_ENV === 'production'
? ''
: (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
const ts = Date.now(); // 时间戳防止缓存
// 🌐 并行发送 4 个 GET 请求
const [w, e, c, s] = await Promise.all([
fetch(base + `/api/account/watchlist?_=${ts}`, {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
}),
fetch(base + `/api/account/events/following?_=${ts}`, {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
}),
fetch(base + `/api/account/events/comments?_=${ts}`, {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
}),
fetch(base + `/api/subscription/current?_=${ts}`, {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
}),
]);
// 解析 JSON 响应
const jw = await w.json(); // 自选股数据
const je = await e.json(); // 关注事件数据
const jc = await c.json(); // 评论数据
const js = await s.json(); // 订阅信息
// 更新状态
if (jw.success) {
setWatchlist(Array.isArray(jw.data) ? jw.data : []);
// 🔗 如果有自选股,触发实时行情加载
if (jw.data && jw.data.length > 0) {
loadRealtimeQuotes(); // 🌐 Phase 2
}
}
if (je.success) setFollowingEvents(Array.isArray(je.data) ? je.data : []);
if (jc.success) setEventComments(Array.isArray(jc.data) ? jc.data : []);
if (js.success) setSubscriptionInfo(js.data);
} catch (err) {
logger.error('Center', 'loadData', err, {
userId: user?.id,
timestamp: new Date().toISOString()
});
} finally {
setLoading(false); // 🔄 初次加载完成
setRefreshing(false); // 🔄 刷新状态重置
}
}, [user]);
```
**API 请求详情 (Phase 1)**:
| 序号 | API Endpoint | 响应数据结构 |
|------|-------------|-------------|
| **API 1** | `GET /api/account/watchlist` | `{ success: true, data: [{ id: 1, stock_code: "600519", stock_name: "贵州茅台", current_price: 1738.50, change_percent: 2.15, industry: "白酒" }] }` |
| **API 2** | `GET /api/account/events/following` | `{ success: true, data: [{ id: 101, title: "茅台发布Q3财报", tags: ["财报", "消费"], view_count: 1520, comment_count: 48, upvote_count: 256, heat_score: 85, creator: {...} }] }` |
| **API 3** | `GET /api/account/events/comments` | `{ success: true, data: [{ id: 1, content: "这个分析很到位", event_title: "茅台发布Q3财报", created_at: "2025-01-18T10:30:00Z" }] }` |
| **API 4** | `GET /api/subscription/current` | `{ success: true, data: { type: "pro", status: "active", days_left: 25, is_active: true, start_date: "2024-12-25", end_date: "2025-01-25" } }` |
---
### Phase 2: 实时行情加载 (loadRealtimeQuotes)
**函数位置**: `src/views/Dashboard/Center.js:127-155`
**触发时机**:
1. `loadData()` 检测到有自选股时自动触发
2. 用户点击自选股区域的刷新按钮 (🔄 图标)
3. 定时器每 60 秒自动触发 (Phase 3)
**执行流程**:
```javascript
const loadRealtimeQuotes = useCallback(async () => {
try {
setQuotesLoading(true); // 🔄 行情加载中 (显示小 Spinner)
const base = process.env.NODE_ENV === 'production'
? ''
: (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
// 🌐 API 5: 获取实时行情
const response = await fetch(base + '/api/account/watchlist/realtime', {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
});
if (response.ok) {
const data = await response.json();
if (data.success) {
// 转换数组为 Map 结构: { stock_code: quote_data }
const quotesMap = {};
data.data.forEach(item => {
quotesMap[item.stock_code] = item;
});
setRealtimeQuotes(quotesMap);
}
}
} catch (error) {
logger.error('Center', 'loadRealtimeQuotes', error, {
userId: user?.id,
watchlistLength: watchlist.length
});
} finally {
setQuotesLoading(false); // 🔄 行情加载完成
}
}, []);
```
**API 请求详情 (Phase 2)**:
| 序号 | API Endpoint | 响应数据结构 |
|------|-------------|-------------|
| **API 5** | `GET /api/account/watchlist/realtime` | `{ success: true, data: [{ stock_code: "600519", current_price: 1738.50, change_percent: 2.15, change_amount: 36.50, update_time: "15:00:00", volume: 1250000, turnover: 217500000 }] }` |
**数据合并逻辑**:
```javascript
// UI 渲染时优先使用实时行情数据
const displayPrice = realtimeQuotes[stock.stock_code]?.current_price
|| stock.current_price
|| '--';
const displayChange = realtimeQuotes[stock.stock_code]?.change_percent
|| stock.change_percent
|| null;
```
---
### Phase 3: 定时刷新机制
**函数位置**: `src/views/Dashboard/Center.js:213-221`
```javascript
useEffect(() => {
// 只在有自选股时启动定时器
if (watchlist.length > 0) {
const interval = setInterval(() => {
loadRealtimeQuotes(); // 🌐 每 60 秒刷新实时行情
}, 60000); // 60000ms = 60秒
return () => clearInterval(interval); // 组件卸载时清理定时器
}
}, [watchlist.length, loadRealtimeQuotes]);
```
**定时器特性**:
- ✅ 仅在有自选股时启动
- ✅ 自选股数量变化时重新创建定时器
- ✅ 组件卸载时自动清理
- ✅ 页面在后台时仍然运行 (但 visibilitychange 会在切回时刷新全部数据)
---
## 四、子组件的 API 请求
### 4.1 InvestmentCalendarChakra (投资日历组件)
**文件位置**: `src/views/Dashboard/components/InvestmentCalendarChakra.js`
#### 4.1.1 自动加载事件数据
```javascript
// InvestmentCalendarChakra.js:86-125
const loadEvents = useCallback(async () => {
try {
setLoading(true);
const base = process.env.NODE_ENV === 'production'
? ''
: (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
// 🌐 API 6: 获取日历事件
const userResponse = await fetch(base + '/api/account/calendar/events', {
credentials: 'include'
});
if (userResponse.ok) {
const userData = await userResponse.json();
if (userData.success) {
// 转换为 FullCalendar 事件格式
const allEvents = (userData.data || []).map(event => ({
...event,
id: `${event.source || 'user'}-${event.id}`,
title: event.title,
start: event.event_date,
date: event.event_date,
// 系统事件 (未来事件) 蓝色,用户计划紫色
backgroundColor: event.source === 'future' ? '#3182CE' : '#8B5CF6',
borderColor: event.source === 'future' ? '#3182CE' : '#8B5CF6',
extendedProps: {
...event,
isSystem: event.source === 'future',
}
}));
setEvents(allEvents);
logger.debug('InvestmentCalendar', '日历事件加载成功', {
count: allEvents.length
});
}
}
} catch (error) {
logger.error('InvestmentCalendar', 'loadEvents', error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadEvents(); // 组件挂载时自动加载
}, [loadEvents]);
```
**API 6 响应数据结构**:
```json
{
"success": true,
"data": [
{
"id": 1,
"title": "关注半导体板块",
"description": "重点关注龙头股走势",
"event_date": "2025-10-20",
"type": "plan",
"importance": 4,
"stocks": ["002415", "600584"],
"source": "user",
"created_at": "2025-01-18T10:00:00Z"
},
{
"id": 201,
"title": "贵州茅台发布Q4财报",
"event_date": "2025-10-25",
"source": "future",
"importance": 5,
"stocks": ["600519"]
}
]
}
```
#### 4.1.2 用户交互 API
**添加投资计划 (API 7)**:
```javascript
// InvestmentCalendarChakra.js:169-223
const handleAddEvent = async () => {
try {
const base = process.env.NODE_ENV === 'production'
? ''
: (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
const eventData = {
...newEvent,
event_date: selectedDate ? selectedDate.format('YYYY-MM-DD') : moment().format('YYYY-MM-DD'),
stocks: newEvent.stocks.split(',').map(s => s.trim()).filter(s => s),
};
// 🌐 API 7: POST 创建投资计划
const response = await fetch(base + '/api/account/calendar/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(eventData),
});
if (response.ok) {
const data = await response.json();
if (data.success) {
toast({ title: '添加成功', status: 'success', duration: 3000 });
onAddClose(); // 关闭添加弹窗
loadEvents(); // 重新加载日历事件
setNewEvent({...}); // 重置表单
}
}
} catch (error) {
logger.error('InvestmentCalendar', 'handleAddEvent', error);
toast({ title: '添加失败', status: 'error', duration: 3000 });
}
};
```
**请求体示例**:
```json
{
"title": "关注半导体板块",
"description": "重点关注龙头股走势",
"event_date": "2025-10-20",
"type": "plan",
"importance": 4,
"stocks": ["002415", "600584"]
}
```
**删除事件 (API 8)**:
```javascript
// InvestmentCalendarChakra.js:226-262
const handleDeleteEvent = async (eventId) => {
if (!eventId) {
toast({ title: '无法删除', description: '缺少事件 ID', status: 'error' });
return;
}
try {
const base = process.env.NODE_ENV === 'production'
? ''
: (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
// 🌐 API 8: DELETE 删除事件
const response = await fetch(base + `/api/account/calendar/events/${eventId}`, {
method: 'DELETE',
credentials: 'include',
});
if (response.ok) {
toast({ title: '删除成功', status: 'success', duration: 2000 });
loadEvents(); // 重新加载日历事件
}
} catch (error) {
logger.error('InvestmentCalendar', 'handleDeleteEvent', error);
toast({ title: '删除失败', status: 'error', duration: 3000 });
}
};
```
---
### 4.2 InvestmentPlansAndReviews (计划和复盘组件)
**文件位置**: `src/views/Dashboard/components/InvestmentPlansAndReviews.js`
#### 4.2.1 自动加载数据
```javascript
// InvestmentPlansAndReviews.js:97-124
const loadData = useCallback(async () => {
try {
setLoading(true);
const base = process.env.NODE_ENV === 'production'
? ''
: (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
// 🌐 API 9: 获取投资计划和复盘
const response = await fetch(base + '/api/account/investment-plans', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (data.success) {
const allItems = data.data || [];
// 按类型分类
setPlans(allItems.filter(item => item.type === 'plan'));
setReviews(allItems.filter(item => item.type === 'review'));
logger.debug('InvestmentPlansAndReviews', '数据加载成功', {
plansCount: allItems.filter(item => item.type === 'plan').length,
reviewsCount: allItems.filter(item => item.type === 'review').length
});
}
}
} catch (error) {
logger.error('InvestmentPlansAndReviews', 'loadData', error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
loadData(); // 组件挂载时自动加载
}, [loadData]);
```
**API 9 响应数据结构**:
```json
{
"success": true,
"data": [
{
"id": 1,
"date": "2025-01-15",
"title": "布局新能源板块",
"content": "重点关注宁德时代和比亚迪...",
"type": "plan",
"stocks": ["300750", "002594"],
"tags": ["新能源", "长期"],
"status": "active",
"created_at": "2025-01-15T09:00:00Z",
"updated_at": "2025-01-15T09:00:00Z"
},
{
"id": 2,
"date": "2025-01-12",
"title": "本周交易复盘",
"content": "本周操作了3只股票总体盈利2.5%...",
"type": "review",
"stocks": ["600519", "000858"],
"tags": ["周复盘", "A股"],
"status": "completed",
"created_at": "2025-01-12T20:00:00Z",
"updated_at": "2025-01-12T20:00:00Z"
}
]
}
```
#### 4.2.2 用户交互 API
**创建计划/复盘 (API 10)**:
```javascript
// InvestmentPlansAndReviews.js:154-201
const handleSave = async () => {
try {
const base = process.env.NODE_ENV === 'production'
? ''
: (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
const url = editingItem
? base + `/api/account/investment-plans/${editingItem.id}` // 更新
: base + '/api/account/investment-plans'; // 创建
const method = editingItem ? 'PUT' : 'POST';
// 🌐 API 10 (POST) 或 API 11 (PUT)
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(formData),
});
if (response.ok) {
toast({
title: editingItem ? '更新成功' : '创建成功',
status: 'success',
duration: 2000,
});
onClose(); // 关闭编辑弹窗
loadData(); // 重新加载数据
} else {
throw new Error('保存失败');
}
} catch (error) {
logger.error('InvestmentPlansAndReviews', 'handleSave', error);
toast({ title: '保存失败', status: 'error', duration: 3000 });
}
};
```
**请求体示例 (创建计划)**:
```json
{
"date": "2025-01-20",
"title": "布局新能源板块",
"content": "重点关注宁德时代和比亚迪的估值变化...",
"type": "plan",
"stocks": ["300750", "002594"],
"tags": ["新能源", "长期"],
"status": "active"
}
```
**删除计划/复盘 (API 12)**:
```javascript
// InvestmentPlansAndReviews.js:204-232
const handleDelete = async (id) => {
if (!window.confirm('确定要删除吗?')) return;
try {
const base = process.env.NODE_ENV === 'production'
? ''
: (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
// 🌐 API 12: DELETE 删除计划/复盘
const response = await fetch(base + `/api/account/investment-plans/${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (response.ok) {
toast({ title: '删除成功', status: 'success', duration: 2000 });
loadData(); // 重新加载数据
}
} catch (error) {
logger.error('InvestmentPlansAndReviews', 'handleDelete', error);
toast({ title: '删除失败', status: 'error', duration: 3000 });
}
};
```
---
## 五、完整 API 请求汇总表
| 序号 | 组件 | API Endpoint | 方法 | 触发时机 | 自动/手动 | 请求参数 | 响应数据 |
|------|------|-------------|------|----------|----------|---------|---------|
| **1** | Center.js | `/api/account/watchlist` | GET | 页面加载/刷新/可见 | 自动 | - | 自选股列表 |
| **2** | Center.js | `/api/account/events/following` | GET | 页面加载/刷新/可见 | 自动 | - | 关注事件列表 |
| **3** | Center.js | `/api/account/events/comments` | GET | 页面加载/刷新/可见 | 自动 | - | 我的评论列表 |
| **4** | Center.js | `/api/subscription/current` | GET | 页面加载/刷新/可见 | 自动 | - | 订阅信息 |
| **5** | Center.js | `/api/account/watchlist/realtime` | GET | 有自选股时/每60s/点击刷新 | 自动+手动 | - | 实时行情数据 |
| **6** | InvestmentCalendar | `/api/account/calendar/events` | GET | 组件挂载 | 自动 | - | 日历事件列表 |
| **7** | InvestmentCalendar | `/api/account/calendar/events` | POST | 点击"添加计划" | 手动 | `{ title, description, event_date, type, importance, stocks }` | `{ success: true, data: {...} }` |
| **8** | InvestmentCalendar | `/api/account/calendar/events/:id` | DELETE | 点击"删除"按钮 | 手动 | - | `{ success: true }` |
| **9** | InvestmentPlans | `/api/account/investment-plans` | GET | 组件挂载 | 自动 | - | 计划+复盘列表 |
| **10** | InvestmentPlans | `/api/account/investment-plans` | POST | 点击"新建计划/复盘" | 手动 | `{ date, title, content, type, stocks, tags, status }` | `{ success: true, data: {...} }` |
| **11** | InvestmentPlans | `/api/account/investment-plans/:id` | PUT | 点击"编辑" | 手动 | `{ date, title, content, type, stocks, tags, status }` | `{ success: true, data: {...} }` |
| **12** | InvestmentPlans | `/api/account/investment-plans/:id` | DELETE | 点击"删除"按钮 | 手动 | - | `{ success: true }` |
**统计**:
- **自动加载 API**: 5 个 (API 1-5, 6, 9)
- **用户操作 API**: 5 个 (API 7, 8, 10, 11, 12)
- **总计**: **12 个 API 端点**
---
## 六、UI 状态变化和交互流程
### 6.1 页面加载完整流程
```
用户访问 /home/center
[Component Mount]
├─ State 初始化
│ ├─ loading = true
│ ├─ watchlist = []
│ ├─ followingEvents = []
│ ├─ eventComments = []
│ ├─ subscriptionInfo = { type: 'free', ... }
│ ├─ realtimeQuotes = {}
│ └─ refreshing = false
├─ 显示加载动画
│ └─ <Spinner> + "加载个人中心数据..."
└─ useEffect 触发条件检测
├─ user 存在 ✅
└─ location.pathname 包含 '/home/center' ✅
loadData() 触发
├─ refreshing = true
├─ 🌐 并行发送 API 1-4
│ ├─ GET /api/account/watchlist
│ ├─ GET /api/account/events/following
│ ├─ GET /api/account/events/comments
│ └─ GET /api/subscription/current
├─ [等待响应 ~800ms]
├─ ✅ 响应成功,解析 JSON
├─ State 更新:
│ ├─ setWatchlist([5只股票])
│ ├─ setFollowingEvents([5个事件])
│ ├─ setEventComments([5条评论])
│ └─ setSubscriptionInfo({ type: 'pro', days_left: 25, ... })
├─ 检测自选股数量
│ └─ if (watchlist.length > 0):
│ └─ loadRealtimeQuotes()
│ ├─ quotesLoading = true
│ ├─ 🌐 GET /api/account/watchlist/realtime
│ ├─ [等待响应 ~300ms]
│ ├─ ✅ 解析行情数据
│ ├─ setRealtimeQuotes({ "600519": {...}, ... })
│ └─ quotesLoading = false
├─ refreshing = false
└─ loading = false
页面内容渲染完成
├─ 统计卡片显示
│ ├─ 自选股票: 5
│ ├─ 关注事件: 5
│ ├─ 我的评论: 5
│ └─ 订阅状态: Pro版 (剩余25天)
├─ 自选股列表渲染
│ ├─ 贵州茅台 600519 1738.50 +2.15% ↑
│ ├─ 平安银行 000001 12.50 -0.80% ↓
│ └─ ...
├─ 关注事件列表渲染
│ └─ 显示前 5 个事件
├─ 我的评论列表渲染
│ └─ 显示前 5 条评论
└─ 启动定时器
└─ 每 60 秒调用 loadRealtimeQuotes()
[子组件挂载]
├─ InvestmentCalendarChakra 挂载
│ ├─ 🌐 GET /api/account/calendar/events
│ ├─ [等待响应 ~500ms]
│ ├─ ✅ setEvents([7个事件])
│ └─ FullCalendar 渲染完成
│ ├─ 10月20日: 关注半导体板块 (紫色)
│ ├─ 10月25日: 贵州茅台发布Q4财报 (蓝色)
│ └─ ...
└─ InvestmentPlansAndReviews 挂载
├─ 🌐 GET /api/account/investment-plans
├─ [等待响应 ~500ms]
├─ ✅ 数据解析
├─ setPlans([2个计划])
├─ setReviews([2个复盘])
└─ Tab 列表渲染完成
├─ Tab 1: 我的计划 (2)
│ ├─ "布局新能源板块" (进行中)
│ └─ "关注白酒板块回调" (已完成)
└─ Tab 2: 我的复盘 (2)
├─ "本周交易复盘" (已完成)
└─ "1月投资总结" (进行中)
```
---
### 6.2 用户交互场景详解
#### 场景 1: 点击"刷新数据"按钮
```
用户点击右上角 "刷新数据" 按钮
onClick={loadData}
├─ refreshing = true
├─ 按钮显示 "刷新中" + 加载动画
├─ 按钮禁用 (isLoading={refreshing})
├─ 🌐 重新发送 API 1-4 (并行)
├─ 🌐 重新发送 API 5 (如果有自选股)
├─ [等待响应完成]
├─ State 全部更新
│ ├─ watchlist 更新
│ ├─ followingEvents 更新
│ ├─ eventComments 更新
│ ├─ subscriptionInfo 更新
│ └─ realtimeQuotes 更新
└─ refreshing = false
按钮恢复为 "刷新数据" (可点击)
UI 显示最新数据
```
---
#### 场景 2: 点击自选股区域的刷新按钮
```
用户点击自选股卡片右上角的 🔄 图标
onClick={loadRealtimeQuotes}
├─ quotesLoading = true
├─ 自选股标题旁显示小型 Spinner
├─ 按钮禁用 (isLoading={quotesLoading})
├─ 🌐 GET /api/account/watchlist/realtime
├─ [等待响应 ~300ms]
├─ ✅ 响应成功
├─ setRealtimeQuotes({ ... })
└─ quotesLoading = false
Spinner 消失
实时行情数据更新
├─ 价格变化: 1738.50 → 1740.00
├─ 涨跌幅变化: +2.15% → +2.24%
├─ 颜色变化: 红色 (涨) / 绿色 (跌)
└─ 更新时间: "15:00:00"
```
---
#### 场景 3: 点击订阅状态卡片
```
用户点击订阅状态统计卡片
onClick={() => navigate('/home/pages/account/subscription')}
路由跳转到订阅管理页面
└─ URL 变为: /home/pages/account/subscription
```
---
#### 场景 4: 添加自选股
```
用户在自选股卡片中点击 "+" 按钮
onClick={() => navigate('/stock-analysis/overview')}
路由跳转到股票分析页面
└─ URL 变为: /stock-analysis/overview
用户在该页面搜索并添加股票 (例如: 五粮液 000858)
├─ 🌐 POST /api/account/watchlist
└─ ✅ 添加成功
用户返回个人中心页面 (点击导航或浏览器返回)
visibilitychange 事件触发 (页面从后台切回前台)
loadData() 自动刷新
├─ 🌐 重新获取 API 1-4
└─ 自选股列表更新
└─ 新增: 五粮液 000858 188.50 +1.35% ↑
```
---
#### 场景 5: 查看关注的事件详情
```
用户点击关注事件卡片中的某个事件标题
<LinkOverlay as={Link} to={`/event-detail/${event.id}`}>
路由跳转到事件详情页面
└─ URL 变为: /event-detail/101
EventDetail 页面加载
├─ 🌐 GET /api/events/101
├─ 🌐 GET /api/events/101/comments
└─ 显示完整事件内容 + 评论列表
```
---
#### 场景 6: 添加投资计划 (日历)
```
用户点击投资日历卡片的 "添加计划" 按钮
onClick={() => {
if (!selectedDate) setSelectedDate(moment());
onAddOpen();
}}
├─ selectedDate = moment() (当前日期)
└─ isAddOpen = true
打开添加计划 Modal
├─ 标题: "添加投资计划"
├─ 表单字段:
│ ├─ 标题 (必填) - Input
│ ├─ 描述 - Textarea
│ ├─ 类型 - Select (计划/提醒/分析)
│ ├─ 重要度 - Select (1-5星)
│ └─ 相关股票 - Input (逗号分隔)
└─ 按钮: [取消] [添加]
用户填写表单:
├─ 标题: "关注半导体板块"
├─ 描述: "重点关注龙头股走势"
├─ 类型: 投资计划
├─ 重要度: ⭐⭐⭐⭐ (4星)
└─ 相关股票: "002415,600584"
点击 "添加" 按钮
handleAddEvent()
├─ 验证: title 不能为空 ✅
├─ 构建请求体:
│ {
│ title: "关注半导体板块",
│ description: "重点关注龙头股走势",
│ event_date: "2025-10-20",
│ type: "plan",
│ importance: 4,
│ stocks: ["002415", "600584"]
│ }
├─ 🌐 POST /api/account/calendar/events
├─ [等待响应 ~500ms]
├─ ✅ 响应成功: { success: true, data: {...} }
├─ Toast 提示: "添加成功" (绿色)
├─ isAddOpen = false (关闭 Modal)
├─ loadEvents() 重新加载日历数据
│ └─ 🌐 GET /api/account/calendar/events
└─ 表单重置: setNewEvent({ title: '', ... })
日历更新
└─ 10月20日显示紫色事件标记: "关注半导体板块"
```
---
#### 场景 7: 点击日历日期查看事件
```
用户点击日历上的 10月20日
handleDateClick(info)
├─ selectedDate = moment('2025-10-20')
├─ 筛选当天事件:
│ selectedDateEvents = events.filter(e =>
│ moment(e.start).isSame('2025-10-20', 'day')
│ )
│ └─ 结果: [
│ { title: "关注半导体板块", ... },
│ { title: "某公司股东大会", ... }
│ ]
└─ isOpen = true
打开事件详情 Modal
├─ 标题: "2025年10月20日 的事件"
├─ 事件列表:
│ ├─ 事件 1: 关注半导体板块
│ │ ├─ Badge: "我的计划" (紫色)
│ │ ├─ 重要度: ⭐⭐⭐⭐ (4/5)
│ │ ├─ 描述: "重点关注龙头股走势"
│ │ ├─ 相关股票: [002415] [600584]
│ │ └─ 删除按钮 (仅用户自己创建的可删除)
│ └─ 事件 2: 某公司股东大会
│ ├─ Badge: "系统事件" (蓝色)
│ ├─ 重要度: ⭐⭐⭐⭐⭐ (5/5)
│ └─ 无删除按钮 (系统事件不可删除)
└─ 按钮: [关闭]
用户点击事件 1 的删除按钮
handleDeleteEvent(eventId)
├─ 验证 eventId 存在 ✅
├─ 🌐 DELETE /api/account/calendar/events/1
├─ [等待响应 ~300ms]
├─ ✅ 响应成功
├─ Toast 提示: "删除成功" (绿色)
└─ loadEvents() 重新加载日历
日历更新
└─ 10月20日只剩 1 个事件: "某公司股东大会"
```
---
#### 场景 8: 新建投资复盘
```
用户切换到 "我的复盘" Tab
Tab 切换动画
└─ 显示复盘列表 (2条记录)
用户点击 "新建复盘" 按钮
handleOpenModal(null, 'review')
├─ editingItem = null (表示新建)
├─ formData = {
│ date: moment().format('YYYY-MM-DD'), // 今天
│ title: '',
│ content: '',
│ type: 'review', // ⚠️ 类型是 review
│ stocks: [],
│ tags: [],
│ status: 'active'
│ }
└─ isOpen = true
打开编辑 Modal
├─ 标题: "新建复盘记录"
├─ 表单字段:
│ ├─ 日期 - Input (type="date") [默认今天]
│ ├─ 标题 - Input (必填) [placeholder: "例如:本周交易复盘"]
│ ├─ 内容 - Textarea [placeholder: "记录您的交易心得和经验教训..."]
│ ├─ 相关股票 - 动态添加 Tag
│ │ └─ [Input + 添加按钮] → 显示为蓝色 Tag (可删除)
│ ├─ 标签 - 动态添加 Tag
│ │ └─ [Input + 添加按钮] → 显示为紫色 Tag (可删除)
│ └─ 状态 - Select (进行中/已完成/已取消)
└─ 按钮: [取消] [保存]
用户填写表单:
├─ 日期: 2025-01-19
├─ 标题: "本周交易复盘"
├─ 内容: "本周操作了3只股票总体盈利2.5%。贵州茅台..."
├─ 相关股票:
│ ├─ 输入 "600519" → 点击添加 → Tag: [📈 600519]
│ └─ 输入 "000858" → 点击添加 → Tag: [📈 000858]
├─ 标签:
│ ├─ 输入 "周复盘" → 点击添加 → Tag: [# 周复盘]
│ └─ 输入 "A股" → 点击添加 → Tag: [# A股]
└─ 状态: 已完成
点击 "保存" 按钮
handleSave()
├─ 验证: title ✅ && date ✅
├─ 构建请求:
│ ├─ URL: /api/account/investment-plans
│ ├─ Method: POST
│ └─ Body: {
│ date: "2025-01-19",
│ title: "本周交易复盘",
│ content: "本周操作了3只股票...",
│ type: "review",
│ stocks: ["600519", "000858"],
│ tags: ["周复盘", "A股"],
│ status: "completed"
│ }
├─ 🌐 POST /api/account/investment-plans
├─ [等待响应 ~600ms]
├─ ✅ 响应成功: { success: true, data: {...} }
├─ Toast 提示: "创建成功" (绿色)
├─ isOpen = false (关闭 Modal)
└─ loadData() 重新加载数据
└─ 🌐 GET /api/account/investment-plans
复盘列表更新
├─ 显示新增的复盘卡片:
│ ├─ 标题: "本周交易复盘"
│ ├─ 日期: 2025年01月19日
│ ├─ 状态: Badge (绿色) "已完成" ✓
│ ├─ 内容摘要: "本周操作了3只股票..." (最多显示3行)
│ ├─ 相关股票: [📈 600519] [📈 000858]
│ ├─ 标签: [# 周复盘] [# A股]
│ ├─ 编辑按钮
│ └─ 删除按钮
└─ 复盘 Tab 数量更新: (2) → (3)
```
---
#### 场景 9: 编辑投资计划
```
用户在 "我的计划" Tab 中点击某个计划卡片的编辑按钮
handleOpenModal(item)
├─ editingItem = { id: 1, title: "布局新能源板块", ... }
├─ formData = {
│ ...item, // 复制所有字段
│ date: moment(item.date).format('YYYY-MM-DD')
│ }
└─ isOpen = true
打开编辑 Modal
├─ 标题: "编辑投资计划"
├─ 表单字段预填充:
│ ├─ 日期: 2025-01-15
│ ├─ 标题: "布局新能源板块"
│ ├─ 内容: "重点关注宁德时代和比亚迪..."
│ ├─ 相关股票: [📈 300750] [📈 002594]
│ ├─ 标签: [# 新能源] [# 长期]
│ └─ 状态: 进行中
└─ 按钮: [取消] [保存]
用户修改状态: 进行中 → 已完成
└─ formData.status = 'completed'
点击 "保存" 按钮
handleSave()
├─ editingItem 存在 → 更新模式
├─ 构建请求:
│ ├─ URL: /api/account/investment-plans/1
│ ├─ Method: PUT
│ └─ Body: { ...formData }
├─ 🌐 PUT /api/account/investment-plans/1
├─ [等待响应 ~500ms]
├─ ✅ 响应成功
├─ Toast 提示: "更新成功" (绿色)
├─ isOpen = false
└─ loadData()
计划卡片更新
└─ 状态 Badge 变化: "进行中" (蓝色) → "已完成" (绿色) ✓
```
---
#### 场景 10: 页面从后台切回前台
```
用户切换到其他浏览器标签页
└─ document.visibilityState = 'hidden'
└─ visibilitychange 事件触发 (但不执行 loadData)
[用户在其他标签页停留 5 分钟]
期间自选股行情可能变化
├─ 贵州茅台: 1738.50 → 1745.20 (+0.38%)
└─ 平安银行: 12.50 → 12.48 (-0.16%)
用户切换回个人中心标签页
document.visibilityState = 'visible'
visibilitychange 事件触发
检测条件:
├─ document.visibilityState === 'visible' ✅
└─ location.pathname.includes('/home/center') ✅
loadData() 自动执行
├─ 🌐 并行刷新 API 1-4
├─ 🌐 刷新 API 5 (实时行情)
└─ [等待响应 ~1秒]
页面数据全部更新为最新状态
├─ 自选股行情更新
│ ├─ 贵州茅台: 1745.20 +0.38% ↑
│ └─ 平安银行: 12.48 -0.16% ↓
├─ 关注事件数量可能变化 (新增关注)
├─ 我的评论数量可能变化 (新发评论)
└─ 订阅信息更新 (剩余天数递减)
```
---
## 七、UI 状态变化映射表
| 触发事件 | State 变化 | UI 表现 | 相关 API |
|---------|-----------|---------|---------|
| **页面加载** | `loading = true` | 显示全屏加载动画 (Spinner + 文字) | - |
| **数据加载完成** | `loading = false` | 隐藏加载动画,显示页面内容 | API 1-4 |
| **点击刷新按钮** | `refreshing = true` | 按钮文字变为"刷新中",显示加载动画,按钮禁用 | API 1-4 |
| **刷新完成** | `refreshing = false` | 按钮恢复为"刷新数据",可点击 | - |
| **自选股行情刷新** | `quotesLoading = true` | 自选股标题旁显示小 Spinner刷新按钮禁用 | API 5 |
| **行情刷新完成** | `quotesLoading = false` | Spinner 消失,价格/涨跌幅更新,颜色变化 | - |
| **定时器触发 (每60s)** | 无 State 变化 | 实时行情自动更新 (静默刷新) | API 5 |
| **点击订阅卡片** | 路由变化 | 页面跳转到订阅管理页 (/home/pages/account/subscription) | - |
| **点击自选股"+"** | 路由变化 | 页面跳转到股票分析页 (/stock-analysis/overview) | - |
| **点击事件标题** | 路由变化 | 页面跳转到事件详情页 (/event-detail/:id) | - |
| **点击日历日期** | `selectedDate` 更新<br>`selectedDateEvents` 更新<br>`isOpen = true` | 打开事件详情 Modal显示当天所有事件 | - |
| **点击"添加计划"** | `isAddOpen = true`<br>`selectedDate` 设置 | 打开添加计划 Modal表单为空 | - |
| **保存计划成功** | `isAddOpen = false` | 关闭 Modal显示 Toast 提示,日历刷新显示新事件 | API 7 |
| **删除日历事件** | 确认对话框 → `events` 更新 | 二次确认Toast 提示,日历移除该事件标记 | API 8 |
| **切换到"我的复盘" Tab** | Tab `index` 变化 | 显示复盘列表内容,隐藏计划列表 | - |
| **点击"新建复盘"** | `isOpen = true`<br>`editingItem = null`<br>`formData.type = 'review'` | 打开编辑 Modal标题为"新建复盘记录" | - |
| **保存复盘成功** | `isOpen = false` | 关闭 ModalToast 提示,列表新增卡片 | API 10 |
| **点击"编辑"按钮** | `isOpen = true`<br>`editingItem` 更新<br>`formData` 预填充 | 打开编辑 Modal表单显示现有数据 | - |
| **更新成功** | `isOpen = false` | 关闭 ModalToast 提示,卡片内容更新 | API 11 |
| **点击"删除"按钮** | 确认对话框 → `plans/reviews` 更新 | 二次确认Toast 提示,列表移除该卡片 | API 12 |
| **页面从后台切回** | `visibilitychange` 事件 | 自动调用 `loadData()`,全部数据静默刷新 | API 1-4 |
| **自选股数量变化** | `watchlist.length` 变化 | 定时器重新创建 (有股票时启动,无股票时停止) | - |
| **添加股票标签** | `formData.stocks` 数组新增 | 表单中新增蓝色 Tag带关闭按钮 | - |
| **添加自定义标签** | `formData.tags` 数组新增 | 表单中新增紫色 Tag带关闭按钮 | - |
---
## 八、关键设计亮点
### 1. 性能优化
#### 1.1 并行请求
```javascript
// 使用 Promise.all() 并行加载 4 个 API减少总等待时间
const [w, e, c, s] = await Promise.all([
fetch('/api/account/watchlist'),
fetch('/api/account/events/following'),
fetch('/api/account/events/comments'),
fetch('/api/subscription/current'),
]);
```
**优势**:
- 串行请求总耗时: ~3.2秒 (800ms × 4)
- 并行请求总耗时: ~800ms (仅等待最慢的一个)
- **性能提升 75%**
#### 1.2 缓存控制
```javascript
// 添加时间戳和缓存控制头,确保获取最新数据
fetch(`/api/account/watchlist?_=${Date.now()}`, {
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
});
```
**作用**: 防止浏览器缓存旧数据,确保数据新鲜度
#### 1.3 条件加载
```javascript
// 只有在有自选股时才加载实时行情
if (jw.data && jw.data.length > 0) {
loadRealtimeQuotes();
}
```
**优势**: 避免不必要的 API 请求,节省服务器资源
#### 1.4 定时刷新优化
```javascript
// 仅在有自选股时启动定时器
if (watchlist.length > 0) {
const interval = setInterval(() => {
loadRealtimeQuotes(); // 60秒刷新一次
}, 60000);
return () => clearInterval(interval);
}
```
**优势**:
- 自选股为空时不启动定时器
- 组件卸载时自动清理,防止内存泄漏
#### 1.5 页面可见性检测
```javascript
// 页面从后台切回前台时自动刷新数据
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
loadData();
}
});
```
**用户体验**:
- 用户切回标签页时看到最新数据
- 无需手动刷新
---
### 2. 用户体验优化
#### 2.1 多层次加载状态
```javascript
const [loading, setLoading] = useState(true); // 初次加载
const [refreshing, setRefreshing] = useState(false); // 手动刷新
const [quotesLoading, setQuotesLoading] = useState(false); // 行情刷新
```
**好处**:
- 用户清楚知道哪个部分正在加载
- 不同操作有不同的视觉反馈
#### 2.2 响应式布局
```javascript
<Grid templateColumns={{ base: '1fr', lg: '1fr 2fr' }} gap={6}>
{/* 桌面端: 左侧 1 列 + 右侧 2 列 */}
{/* 移动端: 单列布局 */}
</Grid>
```
**适配**:
- 桌面端 (≥992px): 两栏布局
- 移动端 (<992px): 单列布局
#### 2.3 空状态引导
```javascript
{watchlist.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
<Icon as={FiBarChart2} boxSize={12} color="gray.300" />
<Text color={secondaryText}>暂无自选股</Text>
<Button onClick={() => navigate('/stock-analysis/overview')}>
添加自选股
</Button>
</VStack>
</Center>
) : (
// 自选股列表
)}
```
**引导**:
- 提示用户当前状态
- 提供明确的行动按钮
#### 2.4 实时行情颜色编码
```javascript
<Badge colorScheme={changePercent > 0 ? 'red' : 'green'}>
{changePercent > 0 ? '+' : ''}{changePercent.toFixed(2)}%
</Badge>
```
**符合习惯**:
- 国内市场: 红涨绿跌
- 自动添加 "+"
#### 2.5 相对时间显示
```javascript
const formatDate = (dateString) => {
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 1) return `${diffHours}小时前`;
if (diffDays < 7) return `${diffDays}天前`;
return date.toLocaleDateString('zh-CN');
};
```
**人性化**:
- "2小时前" "2025-01-19 13:30" 更直观
- 超过 7 天显示完整日期
---
### 3. 数据一致性
#### 3.1 单一数据源
- 每个组件通过各自的 API 获取数据
- 避免复杂的 props drilling
- 数据归属清晰
#### 3.2 自动刷新机制
```javascript
// 页面可见性变化时自动刷新
if (document.visibilityState === 'visible') {
loadData();
}
```
**保证**: 用户看到的数据始终是最新的
#### 3.3 乐观更新
```javascript
// 删除/添加操作成功后立即重新加载列表
if (response.ok) {
toast({ title: '删除成功' });
loadEvents(); // 立即刷新
}
```
**优势**: UI 立即反映最新状态
---
### 4. 错误处理
#### 4.1 静默失败 (数据加载)
```javascript
try {
const data = await loadData();
} catch (err) {
logger.error('Center', 'loadData', err); // 仅日志记录
// ❌ 不显示 Toast避免干扰用户
}
```
**理由**:
- 数据加载失败通常是网络问题
- 避免频繁弹窗影响体验
#### 4.2 明确反馈 (用户操作)
```javascript
try {
await handleDelete(id);
toast({ title: '删除成功', status: 'success' });
} catch (error) {
toast({ title: '删除失败', status: 'error' }); // ✅ 明确告知
}
```
**理由**:
- 用户主动操作需要明确结果
- 失败时需要提示原因
#### 4.3 数据脱敏
```javascript
logger.error('Center', 'loadData', err, {
userId: user?.id, // ✅ 只记录 ID
// ❌ 不记录敏感信息 (手机号、邮箱等)
timestamp: new Date().toISOString()
});
```
**安全**: 日志中不包含用户隐私数据
---
### 5. 组件解耦
#### 5.1 子组件独立
- `InvestmentCalendarChakra`: 自管理状态 + API
- `InvestmentPlansAndReviews`: 自管理状态 + API
- 无需父组件传递数据
#### 5.2 无 Props 传递
```javascript
// ✅ 好的设计
<InvestmentCalendarChakra />
// ❌ 避免的设计
<InvestmentCalendarChakra
events={events}
onAdd={handleAdd}
onDelete={handleDelete}
/>
```
**优势**:
- 组件更独立
- 易于维护和测试
#### 5.3 自包含 CRUD
每个子组件都实现完整的 CRUD 功能:
- **C**reate: 添加新记录
- **R**ead: 加载数据列表
- **U**pdate: 编辑现有记录
- **D**elete: 删除记录
---
## 九、数据流向图
```
用户打开 /home/center
┌─────────────────────────────────────────────────────────────┐
│ Center.js 组件挂载 │
├─────────────────────────────────────────────────────────────┤
│ State 初始化: │
│ - loading = true │
│ - watchlist = [] │
│ - followingEvents = [] │
│ - eventComments = [] │
│ - subscriptionInfo = { type: 'free', ... } │
│ - realtimeQuotes = {} │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ useEffect 触发条件检测 │
├─────────────────────────────────────────────────────────────┤
│ if (user && location.pathname.includes('/home/center')) { │
│ loadData(); ✅ │
│ } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Phase 1: loadData() 并行请求 │
├─────────────────────────────────────────────────────────────┤
│ refreshing = true │
│ │
│ 🌐 Promise.all([ │
│ GET /api/account/watchlist, → API 1 │
│ GET /api/account/events/following, → API 2 │
│ GET /api/account/events/comments, → API 3 │
│ GET /api/subscription/current → API 4 │
│ ]) │
│ │
│ [等待响应 ~800ms] │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ API 1 响应: 自选股列表 │
├─────────────────────────────────────────────────────────────┤
│ { success: true, data: [ │
│ { id: 1, stock_code: "600519", stock_name: "贵州茅台", ... }, │
│ { id: 2, stock_code: "000001", stock_name: "平安银行", ... }, │
│ ... │
│ ]} │
│ ↓ │
│ setWatchlist([...5只股票]) │
│ ↓ │
│ if (watchlist.length > 0) { │
│ loadRealtimeQuotes(); → 触发 Phase 2 │
│ } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ API 2 响应: 关注事件列表 │
├─────────────────────────────────────────────────────────────┤
│ { success: true, data: [ │
│ { id: 101, title: "茅台发布Q3财报", tags: [...], ... }, │
│ ... │
│ ]} │
│ ↓ │
│ setFollowingEvents([...5个事件]) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ API 3 响应: 我的评论列表 │
├─────────────────────────────────────────────────────────────┤
│ { success: true, data: [ │
│ { id: 1, content: "这个分析很到位", event_title: "...", ... }, │
│ ... │
│ ]} │
│ ↓ │
│ setEventComments([...5条评论]) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ API 4 响应: 订阅信息 │
├─────────────────────────────────────────────────────────────┤
│ { success: true, data: { │
│ type: "pro", │
│ status: "active", │
│ days_left: 25, │
│ is_active: true │
│ }} │
│ ↓ │
│ setSubscriptionInfo({...}) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Phase 2: loadRealtimeQuotes() │
├─────────────────────────────────────────────────────────────┤
│ quotesLoading = true │
│ │
│ 🌐 GET /api/account/watchlist/realtime → API 5 │
│ │
│ [等待响应 ~300ms] │
│ │
│ { success: true, data: [ │
│ { stock_code: "600519", current_price: 1738.50, │
│ change_percent: 2.15, update_time: "15:00:00" }, │
│ ... │
│ ]} │
│ ↓ │
│ 转换为 Map: { │
│ "600519": { current_price: 1738.50, ... }, │
│ "000001": { current_price: 12.50, ... } │
│ } │
│ ↓ │
│ setRealtimeQuotes({...}) │
│ quotesLoading = false │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Phase 1 结束 │
├─────────────────────────────────────────────────────────────┤
│ refreshing = false │
│ loading = false │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 主组件 UI 渲染完成 │
├─────────────────────────────────────────────────────────────┤
│ - 统计卡片: 自选股(5) 关注事件(5) 评论(5) 订阅(Pro 25天) │
│ - 自选股列表: 显示 5 只股票 + 实时涨跌幅 │
│ - 关注事件: 显示前 5 个事件 │
│ - 我的评论: 显示前 5 条评论 │
│ - 订阅管理: Pro版 剩余25天 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Phase 3: 定时刷新启动 │
├─────────────────────────────────────────────────────────────┤
│ useEffect(() => { │
│ if (watchlist.length > 0) { │
│ const timer = setInterval(() => { │
│ loadRealtimeQuotes(); → 每 60 秒触发 API 5 │
│ }, 60000); │
│ } │
│ }, [watchlist.length]); │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 子组件 1: InvestmentCalendarChakra 挂载 │
├─────────────────────────────────────────────────────────────┤
│ useEffect(() => { │
│ loadEvents(); │
│ }, []); │
│ ↓ │
│ 🌐 GET /api/account/calendar/events → API 6 │
│ ↓ │
│ { success: true, data: [ │
│ { id: 1, title: "关注半导体板块", event_date: "2025-10-20", │
│ source: "user", ... }, │
│ { id: 201, title: "贵州茅台发布Q4财报", event_date: "...", │
│ source: "future", ... }, │
│ ... │
│ ]} │
│ ↓ │
│ 转换为 FullCalendar 格式: │
│ - 用户事件: 紫色 (#8B5CF6) │
│ - 系统事件: 蓝色 (#3182CE) │
│ ↓ │
│ setEvents([...7个事件]) │
│ ↓ │
│ FullCalendar 渲染: │
│ - 10月20日: 紫色标记 "关注半导体板块" │
│ - 10月25日: 蓝色标记 "贵州茅台发布Q4财报" │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 子组件 2: InvestmentPlansAndReviews 挂载 │
├─────────────────────────────────────────────────────────────┤
│ useEffect(() => { │
│ loadData(); │
│ }, []); │
│ ↓ │
│ 🌐 GET /api/account/investment-plans → API 9 │
│ ↓ │
│ { success: true, data: [ │
│ { id: 1, type: "plan", title: "布局新能源板块", ... }, │
│ { id: 2, type: "review", title: "本周交易复盘", ... }, │
│ ... │
│ ]} │
│ ↓ │
│ 按类型分类: │
│ - setPlans([type==='plan' 的记录]) → 2 个计划 │
│ - setReviews([type==='review' 的记录]) → 2 个复盘 │
│ ↓ │
│ Tab 列表渲染: │
│ - Tab 1: 我的计划 (2) │
│ - Tab 2: 我的复盘 (2) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 所有数据加载完成,页面完全可交互 │
└─────────────────────────────────────────────────────────────┘
```
---
## 十、Mock 数据对应关系
根据 `src/mocks/data/account.js` `src/mocks/handlers/account.js`以下是 Mock 数据与页面显示的对应关系:
| 页面区域 | Mock 数据变量 | Mock Handler API | 数据量 | 显示位置 |
|---------|--------------|-----------------|--------|---------|
| **自选股列表** | `mockWatchlist` | `GET /api/account/watchlist` | 5只股票 | 左侧栏 - 自选股卡片 |
| **实时行情** | `mockRealtimeQuotes` | `GET /api/account/watchlist/realtime` | 5个报价 | 自选股卡片 (价格叠加) |
| **关注事件** | `mockFollowingEvents` | `GET /api/account/events/following` | 5个事件 | 右侧栏 - 关注事件卡片 |
| **我的评论** | `mockEventComments` | `GET /api/account/events/comments` | 5条评论 | 右侧栏 - 我的评论卡片 |
| **订阅信息** | `mockSubscriptionCurrent` | `GET /api/subscription/current` | 1条记录 | 统计卡片 + 订阅管理卡片 |
| **投资日历** | `mockCalendarEvents` | `GET /api/account/calendar/events` | 7个事件 | 投资日历组件 (FullCalendar) |
| **投资计划** | `mockInvestmentPlans` (type='plan') | `GET /api/account/investment-plans` | 2个计划 | 计划复盘组件 - Tab 1 |
| **投资复盘** | `mockInvestmentPlans` (type='review') | `GET /api/account/investment-plans` | 2个复盘 | 计划复盘组件 - Tab 2 |
### Mock 数据示例
#### mockWatchlist (5只股票)
```javascript
[
{ id: 1, stock_code: "600519", stock_name: "贵州茅台", industry: "白酒" },
{ id: 2, stock_code: "000001", stock_name: "平安银行", industry: "银行" },
{ id: 3, stock_code: "000858", stock_name: "五粮液", industry: "白酒" },
{ id: 4, stock_code: "300750", stock_name: "宁德时代", industry: "新能源" },
{ id: 5, stock_code: "002594", stock_name: "比亚迪", industry: "新能源汽车" }
]
```
#### mockRealtimeQuotes (5个报价)
```javascript
[
{ stock_code: "600519", current_price: 1738.50, change_percent: 2.15, update_time: "15:00:00" },
{ stock_code: "000001", current_price: 12.50, change_percent: -0.80, update_time: "15:00:00" },
{ stock_code: "000858", current_price: 188.50, change_percent: 1.35, update_time: "15:00:00" },
{ stock_code: "300750", current_price: 198.20, change_percent: 3.45, update_time: "15:00:00" },
{ stock_code: "002594", current_price: 252.80, change_percent: -1.20, update_time: "15:00:00" }
]
```
#### mockFollowingEvents (5个事件)
```javascript
[
{
id: 101,
title: "贵州茅台发布Q3财报营收超预期",
tags: ["财报", "消费", "白酒"],
view_count: 1520,
comment_count: 48,
upvote_count: 256,
heat_score: 85,
exceed_expectation_score: 78,
creator: { username: "财报分析师", avatar_url: "..." },
created_at: "2025-01-15T09:30:00Z"
},
// ... 4 more events
]
```
#### mockCalendarEvents (7个事件)
```javascript
[
{
id: 1,
title: "关注半导体板块",
event_date: "2025-10-20",
type: "plan",
importance: 4,
stocks: ["002415", "600584"],
source: "user"
},
{
id: 201,
title: "贵州茅台发布Q4财报",
event_date: "2025-10-25",
importance: 5,
stocks: ["600519"],
source: "future"
},
// ... 5 more events
]
```
#### mockInvestmentPlans (4条记录: 2 plan + 2 review)
```javascript
[
{
id: 1,
date: "2025-01-15",
title: "布局新能源板块",
content: "重点关注宁德时代和比亚迪的估值变化...",
type: "plan",
stocks: ["300750", "002594"],
tags: ["新能源", "长期"],
status: "active"
},
{
id: 2,
date: "2025-01-12",
title: "本周交易复盘",
content: "本周操作了3只股票总体盈利2.5%...",
type: "review",
stocks: ["600519", "000858"],
tags: ["周复盘", "A股"],
status: "completed"
},
// ... 2 more items
]
```
---
## 附录
### A. 相关文件清单
| 文件路径 | 说明 |
|---------|------|
| `src/views/Dashboard/Center.js` | 个人中心主组件 (733行) |
| `src/views/Dashboard/components/InvestmentCalendarChakra.js` | 投资日历组件 (490行) |
| `src/views/Dashboard/components/InvestmentPlansAndReviews.js` | 投资计划与复盘组件 (586行) |
| `src/mocks/data/account.js` | Mock 数据定义 (479行) |
| `src/mocks/handlers/account.js` | Mock API Handlers (660行) |
| `MOCK_DATA_CENTER_SUPPLEMENT.md` | Mock 数据补充文档 (800+) |
### B. API 端点快速查找
#### 主组件 (Center.js)
- `GET /api/account/watchlist` - 自选股列表
- `GET /api/account/events/following` - 关注事件
- `GET /api/account/events/comments` - 我的评论
- `GET /api/subscription/current` - 订阅信息
- `GET /api/account/watchlist/realtime` - 实时行情
#### 投资日历 (InvestmentCalendarChakra)
- `GET /api/account/calendar/events` - 获取日历事件
- `POST /api/account/calendar/events` - 添加投资计划
- `DELETE /api/account/calendar/events/:id` - 删除事件
#### 投资计划与复盘 (InvestmentPlansAndReviews)
- `GET /api/account/investment-plans` - 获取计划+复盘
- `POST /api/account/investment-plans` - 创建计划/复盘
- `PUT /api/account/investment-plans/:id` - 更新计划/复盘
- `DELETE /api/account/investment-plans/:id` - 删除计划/复盘
### C. 关键代码位置
| 功能 | 文件 | 行号 |
|-----|------|------|
| State 初始化 | Center.js | 80-87 |
| 主数据加载 (loadData) | Center.js | 89-124 |
| 实时行情加载 | Center.js | 127-155 |
| 定时刷新机制 | Center.js | 213-221 |
| 页面可见性监听 | Center.js | 198-210 |
| 日历事件加载 | InvestmentCalendarChakra.js | 86-125 |
| 添加投资计划 | InvestmentCalendarChakra.js | 169-223 |
| 删除日历事件 | InvestmentCalendarChakra.js | 226-262 |
| 计划复盘数据加载 | InvestmentPlansAndReviews.js | 97-124 |
| 保存计划/复盘 | InvestmentPlansAndReviews.js | 154-201 |
| 删除计划/复盘 | InvestmentPlansAndReviews.js | 204-232 |
---
**文档结束**
如有问题或需要进一步说明请参考源代码或联系开发团队