72 KiB
个人中心页面业务流程分析文档
文档创建日期: 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 请求汇总表
- 六、UI 状态变化和交互流程
- 七、UI 状态变化映射表
- 八、关键设计亮点
- 九、数据流向图
- 十、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 初始化
// 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)
// 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]);
触发时机:
- 组件首次挂载 (如果用户已登录且路径正确)
- 用户从其他页面导航到个人中心
- 浏览器标签页从后台切换回前台
三、API 请求时间线和数据流
Phase 1: 主数据并行加载 (loadData)
函数位置: src/views/Dashboard/Center.js:89-124
触发时机:
- 组件挂载
- 用户点击"刷新数据"按钮
- 页面从后台切回前台
执行流程:
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
触发时机:
loadData()检测到有自选股时自动触发- 用户点击自选股区域的刷新按钮 (🔄 图标)
- 定时器每 60 秒自动触发 (Phase 3)
执行流程:
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 }] } |
数据合并逻辑:
// 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
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 自动加载事件数据
// 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 响应数据结构:
{
"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):
// 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 });
}
};
请求体示例:
{
"title": "关注半导体板块",
"description": "重点关注龙头股走势",
"event_date": "2025-10-20",
"type": "plan",
"importance": 4,
"stocks": ["002415", "600584"]
}
删除事件 (API 8):
// 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 自动加载数据
// 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 响应数据结构:
{
"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):
// 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 });
}
};
请求体示例 (创建计划):
{
"date": "2025-01-20",
"title": "布局新能源板块",
"content": "重点关注宁德时代和比亚迪的估值变化...",
"type": "plan",
"stocks": ["300750", "002594"],
"tags": ["新能源", "长期"],
"status": "active"
}
删除计划/复盘 (API 12):
// 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 更新selectedDateEvents 更新isOpen = true |
打开事件详情 Modal,显示当天所有事件 | - |
| 点击"添加计划" | isAddOpen = trueselectedDate 设置 |
打开添加计划 Modal,表单为空 | - |
| 保存计划成功 | isAddOpen = false |
关闭 Modal,显示 Toast 提示,日历刷新显示新事件 | API 7 |
| 删除日历事件 | 确认对话框 → events 更新 |
二次确认,Toast 提示,日历移除该事件标记 | API 8 |
| 切换到"我的复盘" Tab | Tab index 变化 |
显示复盘列表内容,隐藏计划列表 | - |
| 点击"新建复盘" | isOpen = trueeditingItem = nullformData.type = 'review' |
打开编辑 Modal,标题为"新建复盘记录" | - |
| 保存复盘成功 | isOpen = false |
关闭 Modal,Toast 提示,列表新增卡片 | API 10 |
| 点击"编辑"按钮 | isOpen = trueeditingItem 更新formData 预填充 |
打开编辑 Modal,表单显示现有数据 | - |
| 更新成功 | isOpen = false |
关闭 Modal,Toast 提示,卡片内容更新 | API 11 |
| 点击"删除"按钮 | 确认对话框 → plans/reviews 更新 |
二次确认,Toast 提示,列表移除该卡片 | API 12 |
| 页面从后台切回 | visibilitychange 事件 |
自动调用 loadData(),全部数据静默刷新 |
API 1-4 |
| 自选股数量变化 | watchlist.length 变化 |
定时器重新创建 (有股票时启动,无股票时停止) | - |
| 添加股票标签 | formData.stocks 数组新增 |
表单中新增蓝色 Tag,带关闭按钮 | - |
| 添加自定义标签 | formData.tags 数组新增 |
表单中新增紫色 Tag,带关闭按钮 | - |
八、关键设计亮点
1. 性能优化
1.1 并行请求
// 使用 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 缓存控制
// 添加时间戳和缓存控制头,确保获取最新数据
fetch(`/api/account/watchlist?_=${Date.now()}`, {
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
});
作用: 防止浏览器缓存旧数据,确保数据新鲜度
1.3 条件加载
// 只有在有自选股时才加载实时行情
if (jw.data && jw.data.length > 0) {
loadRealtimeQuotes();
}
优势: 避免不必要的 API 请求,节省服务器资源
1.4 定时刷新优化
// 仅在有自选股时启动定时器
if (watchlist.length > 0) {
const interval = setInterval(() => {
loadRealtimeQuotes(); // 60秒刷新一次
}, 60000);
return () => clearInterval(interval);
}
优势:
- 自选股为空时不启动定时器
- 组件卸载时自动清理,防止内存泄漏
1.5 页面可见性检测
// 页面从后台切回前台时自动刷新数据
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
loadData();
}
});
用户体验:
- 用户切回标签页时看到最新数据
- 无需手动刷新
2. 用户体验优化
2.1 多层次加载状态
const [loading, setLoading] = useState(true); // 初次加载
const [refreshing, setRefreshing] = useState(false); // 手动刷新
const [quotesLoading, setQuotesLoading] = useState(false); // 行情刷新
好处:
- 用户清楚知道哪个部分正在加载
- 不同操作有不同的视觉反馈
2.2 响应式布局
<Grid templateColumns={{ base: '1fr', lg: '1fr 2fr' }} gap={6}>
{/* 桌面端: 左侧 1 列 + 右侧 2 列 */}
{/* 移动端: 单列布局 */}
</Grid>
适配:
- 桌面端 (≥992px): 两栏布局
- 移动端 (<992px): 单列布局
2.3 空状态引导
{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 实时行情颜色编码
<Badge colorScheme={changePercent > 0 ? 'red' : 'green'}>
{changePercent > 0 ? '+' : ''}{changePercent.toFixed(2)}%
</Badge>
符合习惯:
- 国内市场: 红涨绿跌
- 自动添加 "+" 号
2.5 相对时间显示
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 自动刷新机制
// 页面可见性变化时自动刷新
if (document.visibilityState === 'visible') {
loadData();
}
保证: 用户看到的数据始终是最新的
3.3 乐观更新
// 删除/添加操作成功后立即重新加载列表
if (response.ok) {
toast({ title: '删除成功' });
loadEvents(); // 立即刷新
}
优势: UI 立即反映最新状态
4. 错误处理
4.1 静默失败 (数据加载)
try {
const data = await loadData();
} catch (err) {
logger.error('Center', 'loadData', err); // 仅日志记录
// ❌ 不显示 Toast,避免干扰用户
}
理由:
- 数据加载失败通常是网络问题
- 避免频繁弹窗影响体验
4.2 明确反馈 (用户操作)
try {
await handleDelete(id);
toast({ title: '删除成功', status: 'success' });
} catch (error) {
toast({ title: '删除失败', status: 'error' }); // ✅ 明确告知
}
理由:
- 用户主动操作需要明确结果
- 失败时需要提示原因
4.3 数据脱敏
logger.error('Center', 'loadData', err, {
userId: user?.id, // ✅ 只记录 ID
// ❌ 不记录敏感信息 (手机号、邮箱等)
timestamp: new Date().toISOString()
});
安全: 日志中不包含用户隐私数据
5. 组件解耦
5.1 子组件独立
InvestmentCalendarChakra: 自管理状态 + APIInvestmentPlansAndReviews: 自管理状态 + API- 无需父组件传递数据
5.2 无 Props 传递
// ✅ 好的设计
<InvestmentCalendarChakra />
// ❌ 避免的设计
<InvestmentCalendarChakra
events={events}
onAdd={handleAdd}
onDelete={handleDelete}
/>
优势:
- 组件更独立
- 易于维护和测试
5.3 自包含 CRUD
每个子组件都实现完整的 CRUD 功能:
- Create: 添加新记录
- Read: 加载数据列表
- Update: 编辑现有记录
- Delete: 删除记录
九、数据流向图
用户打开 /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只股票)
[
{ 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个报价)
[
{ 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个事件)
[
{
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个事件)
[
{
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)
[
{
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 |
文档结束
如有问题或需要进一步说明,请参考源代码或联系开发团队。