// src/hooks/useWatchlist.js // 自选股管理自定义 Hook(与 Redux 状态同步,支持多组件共用) 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, loadWatchlist, loadWatchlistQuotes } from '../store/slices/stockSlice'; const WATCHLIST_PAGE_SIZE = 10; /** * 自选股管理 Hook(导航栏专用) * 提供自选股加载、分页、移除等功能 * 监听 Redux 中的 watchlist 变化,自动刷新行情数据 * * @returns {{ * watchlistQuotes: Array, * watchlistLoading: boolean, * watchlistPage: number, * setWatchlistPage: Function, * WATCHLIST_PAGE_SIZE: number, * loadWatchlistQuotes: Function, * followingEvents: Array, * handleAddToWatchlist: Function, * handleRemoveFromWatchlist: Function, * isInWatchlist: Function * }} */ export const useWatchlist = () => { const toast = useToast(); const dispatch = useDispatch(); const [watchlistPage, setWatchlistPage] = useState(1); const [followingEvents, setFollowingEvents] = useState([]); // 从 Redux 获取自选股数据(与 GlobalSidebar 共用) const watchlistQuotes = useSelector(state => state.stock.watchlistQuotes || []); const watchlistLoading = useSelector(state => state.stock.loading?.watchlistQuotes || false); // 从 Redux 获取自选股列表长度(用于监听变化) const reduxWatchlistLength = useSelector(state => state.stock.watchlist?.length || 0); // 用于跟踪上一次的 watchlist 长度 const prevWatchlistLengthRef = useRef(-1); // 初始化时加载 Redux watchlist(确保 Redux 状态被初始化) const hasInitializedRef = useRef(false); useEffect(() => { if (!hasInitializedRef.current) { hasInitializedRef.current = true; logger.debug('useWatchlist', '初始化 Redux watchlist'); dispatch(loadWatchlist()); } }, [dispatch]); // 加载自选股实时行情(通过 Redux) const loadWatchlistQuotesFunc = useCallback(() => { logger.debug('useWatchlist', '触发 loadWatchlistQuotes'); dispatch(loadWatchlistQuotes()); }, [dispatch]); // 监听 Redux watchlist 长度变化,自动刷新行情数据 useEffect(() => { const currentLength = reduxWatchlistLength; const prevLength = prevWatchlistLengthRef.current; // 只有当 watchlist 长度发生变化时才刷新 // prevLength = -1 表示初始状态,此时不触发刷新(由菜单打开时触发) if (prevLength !== -1 && currentLength !== prevLength) { logger.debug('useWatchlist', 'Redux watchlist 长度变化,刷新行情', { prevLength, currentLength }); // 延迟一小段时间再刷新,确保后端数据已更新 const timer = setTimeout(() => { logger.debug('useWatchlist', '执行 loadWatchlistQuotes'); dispatch(loadWatchlistQuotes()); }, 500); prevWatchlistLengthRef.current = currentLength; return () => clearTimeout(timer); } // 更新 ref prevWatchlistLengthRef.current = currentLength; }, [reduxWatchlistLength, dispatch]); // 添加到自选股(通过 Redux) const handleAddToWatchlist = useCallback(async (stockCode, stockName) => { try { // 通过 Redux action 添加(乐观更新) await dispatch(toggleWatchlistAction({ stockCode, stockName, isInWatchlist: false // 表示当前不在自选股中,需要添加 })).unwrap(); // 刷新行情 dispatch(loadWatchlistQuotes()); toast({ title: '已添加至自选股', status: 'success', duration: 1500 }); return true; } catch (e) { logger.error('useWatchlist', '添加自选股失败', e); toast({ title: e.message || '添加失败', status: 'error', duration: 2000 }); return false; } }, [dispatch, toast]); // 从自选股移除(通过 Redux) const handleRemoveFromWatchlist = useCallback(async (stockCode) => { try { // 找到股票名称 const normalize6 = (code) => { const m = String(code || '').match(/(\d{6})/); return m ? m[1] : String(code || ''); }; const stockItem = watchlistQuotes.find(item => normalize6(item.stock_code) === normalize6(stockCode) ); const stockName = stockItem?.stock_name || ''; // 通过 Redux action 移除(乐观更新) await dispatch(toggleWatchlistAction({ stockCode, stockName, isInWatchlist: true // 表示当前在自选股中,需要移除 })).unwrap(); // 更新分页(如果当前页超出范围) const newLength = watchlistQuotes.length - 1; const newMaxPage = Math.max(1, Math.ceil(newLength / WATCHLIST_PAGE_SIZE)); setWatchlistPage(p => Math.min(p, newMaxPage)); toast({ title: '已从自选股移除', status: 'info', duration: 1500 }); } catch (e) { logger.error('useWatchlist', '移除自选股失败', e); toast({ title: e.message || '移除失败', status: 'error', duration: 2000 }); } }, [dispatch, watchlistQuotes, toast]); // 判断股票是否在自选股中 const isInWatchlist = useCallback((stockCode) => { const normalize6 = (code) => { const m = String(code || '').match(/(\d{6})/); return m ? m[1] : String(code || ''); }; const target = normalize6(stockCode); return watchlistQuotes.some(item => normalize6(item.stock_code) === target); }, [watchlistQuotes]); return { watchlistQuotes, watchlistLoading, watchlistPage, setWatchlistPage, WATCHLIST_PAGE_SIZE, loadWatchlistQuotes: loadWatchlistQuotesFunc, followingEvents, handleAddToWatchlist, handleRemoveFromWatchlist, isInWatchlist }; };