diff --git a/src/hooks/useWatchlist.js b/src/hooks/useWatchlist.js index 32eb7356..1945ce6d 100644 --- a/src/hooks/useWatchlist.js +++ b/src/hooks/useWatchlist.js @@ -1,16 +1,19 @@ // src/hooks/useWatchlist.js -// 自选股管理自定义 Hook +// 自选股管理自定义 Hook(导航栏专用,与 Redux 状态同步) -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import { useToast } from '@chakra-ui/react'; import { logger } from '../utils/logger'; import { getApiBase } from '../utils/apiConfig'; +import { toggleWatchlist as toggleWatchlistAction } from '../store/slices/stockSlice'; const WATCHLIST_PAGE_SIZE = 10; /** - * 自选股管理 Hook + * 自选股管理 Hook(导航栏专用) * 提供自选股加载、分页、移除等功能 + * 监听 Redux 中的 watchlist 变化,自动刷新行情数据 * * @returns {{ * watchlistQuotes: Array, @@ -19,14 +22,24 @@ const WATCHLIST_PAGE_SIZE = 10; * setWatchlistPage: Function, * WATCHLIST_PAGE_SIZE: number, * loadWatchlistQuotes: Function, - * handleRemoveFromWatchlist: Function + * handleRemoveFromWatchlist: Function, + * followingEvents: Array * }} */ export const useWatchlist = () => { const toast = useToast(); + const dispatch = useDispatch(); const [watchlistQuotes, setWatchlistQuotes] = useState([]); const [watchlistLoading, setWatchlistLoading] = useState(false); const [watchlistPage, setWatchlistPage] = useState(1); + const [followingEvents, setFollowingEvents] = useState([]); + + // 从 Redux 获取自选股列表(用于监听变化) + const reduxWatchlist = useSelector(state => state.stock.watchlist); + + // 用于跟踪上一次的 watchlist 长度,避免初始加载时重复请求 + const prevWatchlistLengthRef = useRef(reduxWatchlist?.length || 0); + const isInitialMount = useRef(true); // 加载自选股实时行情 const loadWatchlistQuotes = useCallback(async () => { @@ -58,35 +71,74 @@ export const useWatchlist = () => { } }, []); - // 从自选股移除 + // 监听 Redux watchlist 变化,自动刷新行情数据 + useEffect(() => { + // 跳过初始挂载(初始加载由 HomeNavbar 触发) + if (isInitialMount.current) { + isInitialMount.current = false; + prevWatchlistLengthRef.current = reduxWatchlist?.length || 0; + return; + } + + const currentLength = reduxWatchlist?.length || 0; + const prevLength = prevWatchlistLengthRef.current; + + // 只有当 watchlist 长度发生变化时才刷新 + if (currentLength !== prevLength) { + logger.debug('useWatchlist', 'Redux watchlist 变化,刷新行情', { + prevLength, + currentLength + }); + prevWatchlistLengthRef.current = currentLength; + + // 延迟一小段时间再刷新,确保后端数据已更新 + const timer = setTimeout(() => { + loadWatchlistQuotes(); + }, 300); + + return () => clearTimeout(timer); + } + }, [reduxWatchlist, loadWatchlistQuotes]); + + // 从自选股移除(同时更新 Redux 和本地状态) const handleRemoveFromWatchlist = useCallback(async (stockCode) => { try { - const base = getApiBase(); - const resp = await fetch(base + `/api/account/watchlist/${stockCode}`, { - method: 'DELETE', - credentials: 'include' + // 找到股票名称 + const stockItem = watchlistQuotes.find(item => { + const normalize6 = (code) => { + const m = String(code || '').match(/(\d{6})/); + return m ? m[1] : String(code || ''); + }; + return normalize6(item.stock_code) === normalize6(stockCode); }); - const data = await resp.json().catch(() => ({})); - if (resp.ok && data && data.success !== false) { - setWatchlistQuotes((prev) => { - const normalize6 = (code) => { - const m = String(code || '').match(/(\d{6})/); - return m ? m[1] : String(code || ''); - }; - const target = normalize6(stockCode); - const updated = (prev || []).filter((x) => normalize6(x.stock_code) !== target); - const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / WATCHLIST_PAGE_SIZE)); - setWatchlistPage((p) => Math.min(p, newMaxPage)); - return updated; - }); - toast({ title: '已从自选股移除', status: 'info', duration: 1500 }); - } else { - toast({ title: '移除失败', status: 'error', duration: 2000 }); - } + const stockName = stockItem?.stock_name || ''; + + // 通过 Redux action 移除(会同步更新 Redux 状态) + await dispatch(toggleWatchlistAction({ + stockCode, + stockName, + isInWatchlist: true // 表示当前在自选股中,需要移除 + })).unwrap(); + + // 更新本地状态(立即响应 UI) + setWatchlistQuotes((prev) => { + const normalize6 = (code) => { + const m = String(code || '').match(/(\d{6})/); + return m ? m[1] : String(code || ''); + }; + const target = normalize6(stockCode); + const updated = (prev || []).filter((x) => normalize6(x.stock_code) !== target); + const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / WATCHLIST_PAGE_SIZE)); + setWatchlistPage((p) => Math.min(p, newMaxPage)); + return updated; + }); + + toast({ title: '已从自选股移除', status: 'info', duration: 1500 }); } catch (e) { - toast({ title: '网络错误,移除失败', status: 'error', duration: 2000 }); + logger.error('useWatchlist', '移除自选股失败', e); + toast({ title: e.message || '移除失败', status: 'error', duration: 2000 }); } - }, [toast]); + }, [dispatch, watchlistQuotes, toast]); return { watchlistQuotes, @@ -95,6 +147,7 @@ export const useWatchlist = () => { setWatchlistPage, WATCHLIST_PAGE_SIZE, loadWatchlistQuotes, - handleRemoveFromWatchlist + handleRemoveFromWatchlist, + followingEvents }; };