diff --git a/src/components/InvestmentCalendar/index.js b/src/components/InvestmentCalendar/index.js index 9d5366fc..a84a9c1f 100644 --- a/src/components/InvestmentCalendar/index.js +++ b/src/components/InvestmentCalendar/index.js @@ -1,5 +1,7 @@ // src/components/InvestmentCalendar/index.js -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { loadWatchlist, toggleWatchlist } from '@store/slices/stockSlice'; import { Card, Calendar, Badge, Modal, Table, Tabs, Tag, Button, List, Spin, Empty, Drawer, Typography, Divider, Space, Tooltip, message, Alert @@ -24,6 +26,10 @@ const { TabPane } = Tabs; const { Text, Title, Paragraph } = Typography; const InvestmentCalendar = () => { + // Redux 状态 + const dispatch = useDispatch(); + const reduxWatchlist = useSelector(state => state.stock.watchlist); + // 权限控制 const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription(); const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); @@ -45,7 +51,6 @@ const InvestmentCalendar = () => { const [selectedStock, setSelectedStock] = useState(null); const [selectedEventTime, setSelectedEventTime] = useState(null); // 记录事件时间 const [followingIds, setFollowingIds] = useState([]); // 正在处理关注的事件ID列表 - const [addingToWatchlist, setAddingToWatchlist] = useState({}); // 正在添加到自选的股票代码 const [expandedReasons, setExpandedReasons] = useState({}); // 跟踪每个股票关联理由的展开状态 // 加载月度事件统计 @@ -174,10 +179,29 @@ const InvestmentCalendar = () => { } }; + // 使用 ref 确保只加载一次自选股 + const watchlistLoadedRef = useRef(false); + + // 组件挂载时加载自选股列表(仅加载一次) + useEffect(() => { + if (!watchlistLoadedRef.current) { + watchlistLoadedRef.current = true; + dispatch(loadWatchlist()); + } + }, [dispatch]); + useEffect(() => { loadEventCounts(currentMonth); }, [currentMonth, loadEventCounts]); + // 检查股票是否已在自选中 + const isStockInWatchlist = useCallback((stockCode) => { + const sixDigitCode = getSixDigitCode(stockCode); + return reduxWatchlist.some(item => + getSixDigitCode(item.stock_code) === sixDigitCode + ); + }, [reduxWatchlist]); + // 自定义日期单元格渲染(Ant Design 5.x API) const cellRender = (current, info) => { // 只处理日期单元格,月份单元格返回默认 @@ -384,42 +408,35 @@ const InvestmentCalendar = () => { } }; - // 添加单只股票到自选(支持新旧格式) + // 添加单只股票到自选(乐观更新,无需 loading 状态) const addSingleToWatchlist = async (stock) => { // 兼容新旧格式 const code = stock.code || stock[0]; const name = stock.name || stock[1]; const stockCode = getSixDigitCode(code); - setAddingToWatchlist(prev => ({ ...prev, [stockCode]: true })); + // 检查是否已在自选中 + if (isStockInWatchlist(code)) { + message.info(`${name} 已在自选中`); + return; + } try { - const response = await fetch('/api/account/watchlist', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ - stock_code: stockCode, // 使用六位代码 - stock_name: name // 股票名称 - }) - }); + // 乐观更新:dispatch 后 Redux 立即更新状态,UI 立即响应 + await dispatch(toggleWatchlist({ + stockCode, + stockName: name, + isInWatchlist: false // false 表示添加 + })).unwrap(); - const data = await response.json(); - if (data.success) { - message.success(`已将 ${name}(${stockCode}) 添加到自选`); - } else { - message.error(data.error || '添加失败'); - } + message.success(`已将 ${name}(${stockCode}) 添加到自选`); } catch (error) { + // 失败时 Redux 会自动回滚状态 logger.error('InvestmentCalendar', 'addSingleToWatchlist', error, { stockCode, stockName: name }); message.error('添加失败,请重试'); - } finally { - setAddingToWatchlist(prev => ({ ...prev, [stockCode]: false })); } }; @@ -796,17 +813,16 @@ const InvestmentCalendar = () => { key: 'action', width: 100, render: (_, record) => { - const stockCode = getSixDigitCode(record.code); - const isAdding = addingToWatchlist[stockCode] || false; + const inWatchlist = isStockInWatchlist(record.code); return ( ); } diff --git a/src/store/slices/stockSlice.js b/src/store/slices/stockSlice.js index 37622694..b6ba2714 100644 --- a/src/store/slices/stockSlice.js +++ b/src/store/slices/stockSlice.js @@ -440,9 +440,10 @@ const stockSlice = createSlice({ state.loading.allStocks = false; }) - // ===== toggleWatchlist ===== - .addCase(toggleWatchlist.fulfilled, (state, action) => { - const { stockCode, stockName, isInWatchlist } = action.payload; + // ===== toggleWatchlist(乐观更新)===== + // pending: 立即更新状态 + .addCase(toggleWatchlist.pending, (state, action) => { + const { stockCode, stockName, isInWatchlist } = action.meta.arg; if (isInWatchlist) { // 移除 state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode); @@ -453,6 +454,25 @@ const stockSlice = createSlice({ state.watchlist.push({ stock_code: stockCode, stock_name: stockName }); } } + }) + // rejected: 回滚状态 + .addCase(toggleWatchlist.rejected, (state, action) => { + const { stockCode, stockName, isInWatchlist } = action.meta.arg; + // 回滚:与 pending 操作相反 + if (isInWatchlist) { + // 之前移除了,现在加回来 + const exists = state.watchlist.some(item => item.stock_code === stockCode); + if (!exists) { + state.watchlist.push({ stock_code: stockCode, stock_name: stockName }); + } + } else { + // 之前添加了,现在移除 + state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode); + } + }) + // fulfilled: 乐观更新模式下状态已在 pending 更新,这里无需操作 + .addCase(toggleWatchlist.fulfilled, () => { + // 状态已在 pending 时更新 }); } });