Files
vf_react/src/views/Company/hooks/useCompanyData.ts
zdl 82290e8a63 docs(useCompanyData): 添加 isInWatchlist 派生逻辑注释
说明 localStorage 缓存机制确保大多数情况下立即显示正确状态

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 12:54:32 +08:00

188 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<StockInfo | null>(null);
const [stockInfoLoading, setStockInfoLoading] = useState(false);
// Redux 状态(自选股)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dispatch = useDispatch<any>();
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<AbortController | null>(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<ApiResponse<StockInfo>>(
`/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;