/** * Company 页面数据加载 Hook * - 使用 axios 请求 * - 懒加载策略 * - 自动取消请求 * - 自选股状态与 Redux 全局状态同步 */ import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useToast } from '@chakra-ui/react'; import { useSelector, useDispatch } from 'react-redux'; import axios from '@utils/axiosConfig'; import { logger } from '@utils/logger'; import { useAuth } from '@contexts/AuthContext'; import { toggleWatchlist as reduxToggleWatchlist, loadWatchlist } from '@store/slices/stockSlice'; import type { StockInfo, UseCompanyDataReturn, ApiResponse, } from '../types'; // Store 类型(因为 store 是 JS 文件,这里内联定义) interface WatchlistItem { stock_code: string; stock_name: string; } interface StockState { watchlist: WatchlistItem[]; loading: { watchlist: boolean; }; } interface RootState { stock: StockState; } interface UseCompanyDataOptions { stockCode: string; /** 是否自动加载股票信息 */ autoLoadStockInfo?: boolean; /** 是否自动加载自选股状态 */ autoLoadWatchlist?: boolean; } /** * Company 页面数据管理 Hook * * 自选股状态现在从 Redux 全局状态读取,确保与导航栏等其他组件同步 */ export const useCompanyData = ({ stockCode, autoLoadStockInfo = true, autoLoadWatchlist = true, }: UseCompanyDataOptions): UseCompanyDataReturn => { // 本地状态(仅股票信息) const [stockInfo, setStockInfo] = useState(null); const [stockInfoLoading, setStockInfoLoading] = useState(false); // Redux 状态(自选股) // eslint-disable-next-line @typescript-eslint/no-explicit-any const dispatch = useDispatch(); const watchlist = useSelector((state: RootState) => state.stock.watchlist); const watchlistLoading = useSelector((state: RootState) => state.stock.loading.watchlist); // 从 Redux watchlist 中派生当前股票的自选状态 // 注意:当 watchlist 正在加载时,保持之前的状态(避免闪烁) const isInWatchlist = useMemo(() => { // 如果正在加载且 watchlist 为空,暂时返回 false // localStorage 缓存会很快返回,所以大多数情况下不会看到错误状态 return watchlist.some((item) => item.stock_code === stockCode); }, [watchlist, stockCode]); // Hooks const toast = useToast(); const { isAuthenticated } = useAuth(); // AbortController 用于取消请求 const abortControllerRef = useRef(null); /** * 加载股票基本信息 */ const loadStockInfo = useCallback(async () => { if (!stockCode || stockCode.length !== 6) return; // 取消之前的请求 abortControllerRef.current?.abort(); abortControllerRef.current = new AbortController(); setStockInfoLoading(true); try { const { data } = await axios.get>( `/api/financial/stock-info/${stockCode}`, { signal: abortControllerRef.current.signal } ); if (data.success && data.data) { setStockInfo(data.data); } } catch (error: any) { if (error.name === 'CanceledError') return; logger.error('useCompanyData', 'loadStockInfo', error, { stockCode }); } finally { setStockInfoLoading(false); } }, [stockCode]); /** * 切换自选股状态(使用 Redux action,自动同步全局状态) */ const toggleWatchlist = useCallback(async () => { if (!stockCode) { toast({ title: '无效的股票代码', status: 'error', duration: 2000 }); return; } if (!isAuthenticated) { toast({ title: '请先登录后再加入自选', status: 'warning', duration: 2000 }); return; } try { // 使用 Redux action,状态会自动同步到全局 // @ts-expect-error stockSlice 是 JS 文件,TypeScript 无法推断 thunk 参数类型 const result = await dispatch(reduxToggleWatchlist({ stockCode, stockName: stockInfo?.stock_name || '', isInWatchlist, })); // 检查是否成功(rejected action 会有 error 属性) if (result.error) { throw new Error(result.error.message || '操作失败'); } // 显示提示 toast({ title: isInWatchlist ? '已从自选移除' : '已加入自选', status: isInWatchlist ? 'info' : 'success', duration: 1500, }); } catch (error: any) { logger.error('useCompanyData', 'toggleWatchlist', error, { stockCode }); toast({ title: '操作失败,请稍后重试', status: 'error', duration: 2000 }); } }, [stockCode, stockInfo?.stock_name, isAuthenticated, isInWatchlist, toast, dispatch]); /** * 刷新股票信息 */ const refreshStockInfo = useCallback(async () => { await loadStockInfo(); }, [loadStockInfo]); // 自动加载股票信息 useEffect(() => { if (autoLoadStockInfo) { loadStockInfo(); } return () => { abortControllerRef.current?.abort(); }; }, [autoLoadStockInfo, loadStockInfo]); // 自动加载自选股列表(从 Redux) useEffect(() => { if (autoLoadWatchlist && isAuthenticated && watchlist.length === 0) { // 只有当 Redux 中没有数据时才加载 dispatch(loadWatchlist()); } }, [autoLoadWatchlist, isAuthenticated, watchlist.length, dispatch]); return { stockInfo, stockInfoLoading, isInWatchlist, watchlistLoading, toggleWatchlist, refreshStockInfo, }; }; export default useCompanyData;