# 个人中心页面业务流程分析文档 > **文档创建日期**: 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 ├─ 显示加载动画 │ └─ + "加载个人中心数据..." └─ 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: 查看关注的事件详情 ``` 用户点击关注事件卡片中的某个事件标题 ↓ ↓ 路由跳转到事件详情页面 └─ 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 = true`
`selectedDate` 设置 | 打开添加计划 Modal,表单为空 | - | | **保存计划成功** | `isAddOpen = false` | 关闭 Modal,显示 Toast 提示,日历刷新显示新事件 | API 7 | | **删除日历事件** | 确认对话框 → `events` 更新 | 二次确认,Toast 提示,日历移除该事件标记 | API 8 | | **切换到"我的复盘" Tab** | Tab `index` 变化 | 显示复盘列表内容,隐藏计划列表 | - | | **点击"新建复盘"** | `isOpen = true`
`editingItem = null`
`formData.type = 'review'` | 打开编辑 Modal,标题为"新建复盘记录" | - | | **保存复盘成功** | `isOpen = false` | 关闭 Modal,Toast 提示,列表新增卡片 | API 10 | | **点击"编辑"按钮** | `isOpen = true`
`editingItem` 更新
`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 并行请求 ```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 {/* 桌面端: 左侧 1 列 + 右侧 2 列 */} {/* 移动端: 单列布局 */} ``` **适配**: - 桌面端 (≥992px): 两栏布局 - 移动端 (<992px): 单列布局 #### 2.3 空状态引导 ```javascript {watchlist.length === 0 ? (
暂无自选股
) : ( // 自选股列表 )} ``` **引导**: - 提示用户当前状态 - 提供明确的行动按钮 #### 2.4 实时行情颜色编码 ```javascript 0 ? 'red' : 'green'}> {changePercent > 0 ? '+' : ''}{changePercent.toFixed(2)}% ``` **符合习惯**: - 国内市场: 红涨绿跌 - 自动添加 "+" 号 #### 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 // ✅ 好的设计 // ❌ 避免的设计 ``` **优势**: - 组件更独立 - 易于维护和测试 #### 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 | --- **文档结束** 如有问题或需要进一步说明,请参考源代码或联系开发团队。