fix(Company): 自选股状态同步到 Redux 全局状态

- useCompanyData 改用 Redux stockSlice 管理自选股状态
- isInWatchlist 从 Redux watchlist 中派生,确保全局同步
- toggleWatchlist 使用 Redux action,乐观更新 + localStorage 持久化
- 移除独立的 loadWatchlistStatus API 调用,复用 Redux 缓存

修复问题:Company 页面关注按钮与导航栏等其他组件状态不同步

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-19 11:39:50 +08:00
parent 958222e75f
commit 029a61e42c

View File

@@ -3,20 +3,39 @@
* - 使用 axios 请求 * - 使用 axios 请求
* - 懒加载策略 * - 懒加载策略
* - 自动取消请求 * - 自动取消请求
* - 自选股状态与 Redux 全局状态同步
*/ */
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import { useSelector, useDispatch } from 'react-redux';
import axios from '@utils/axiosConfig'; import axios from '@utils/axiosConfig';
import { logger } from '@utils/logger'; import { logger } from '@utils/logger';
import { useAuth } from '@contexts/AuthContext'; import { useAuth } from '@contexts/AuthContext';
import { toggleWatchlist as reduxToggleWatchlist, loadWatchlist } from '@store/slices/stockSlice';
import type { import type {
StockInfo, StockInfo,
WatchlistItem,
UseCompanyDataReturn, UseCompanyDataReturn,
ApiResponse, ApiResponse,
} from '../types'; } 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 { interface UseCompanyDataOptions {
stockCode: string; stockCode: string;
/** 是否自动加载股票信息 */ /** 是否自动加载股票信息 */
@@ -27,17 +46,28 @@ interface UseCompanyDataOptions {
/** /**
* Company 页面数据管理 Hook * Company 页面数据管理 Hook
*
* 自选股状态现在从 Redux 全局状态读取,确保与导航栏等其他组件同步
*/ */
export const useCompanyData = ({ export const useCompanyData = ({
stockCode, stockCode,
autoLoadStockInfo = true, autoLoadStockInfo = true,
autoLoadWatchlist = true, autoLoadWatchlist = true,
}: UseCompanyDataOptions): UseCompanyDataReturn => { }: UseCompanyDataOptions): UseCompanyDataReturn => {
// 状态 // 本地状态(仅股票信息)
const [stockInfo, setStockInfo] = useState<StockInfo | null>(null); const [stockInfo, setStockInfo] = useState<StockInfo | null>(null);
const [stockInfoLoading, setStockInfoLoading] = useState(false); const [stockInfoLoading, setStockInfoLoading] = useState(false);
const [isInWatchlist, setIsInWatchlist] = useState(false);
const [watchlistLoading, setWatchlistLoading] = 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 中派生当前股票的自选状态
const isInWatchlist = useMemo(() => {
return watchlist.some((item) => item.stock_code === stockCode);
}, [watchlist, stockCode]);
// Hooks // Hooks
const toast = useToast(); const toast = useToast();
@@ -76,47 +106,7 @@ export const useCompanyData = ({
}, [stockCode]); }, [stockCode]);
/** /**
* 加载自选股状态(优化:只检查单个股票,避免加载整个列表 * 切换自选股状态(使用 Redux action自动同步全局状态
*/
const loadWatchlistStatus = useCallback(async () => {
if (!isAuthenticated || !stockCode) {
setIsInWatchlist(false);
return;
}
try {
const { data } = await axios.get<ApiResponse<{ is_in_watchlist: boolean }>>(
`/api/account/watchlist/check/${stockCode}`
);
if (data.success && data.data) {
setIsInWatchlist(data.data.is_in_watchlist);
} else {
setIsInWatchlist(false);
}
} catch (error: any) {
// 接口不存在时降级到原方案
if (error.response?.status === 404) {
try {
const { data: listData } = await axios.get<ApiResponse<WatchlistItem[]>>(
'/api/account/watchlist'
);
if (listData.success && Array.isArray(listData.data)) {
const codes = new Set(listData.data.map((item) => item.stock_code));
setIsInWatchlist(codes.has(stockCode));
}
} catch {
setIsInWatchlist(false);
}
} else {
logger.error('useCompanyData', 'loadWatchlistStatus', error);
setIsInWatchlist(false);
}
}
}, [stockCode, isAuthenticated]);
/**
* 切换自选股状态
*/ */
const toggleWatchlist = useCallback(async () => { const toggleWatchlist = useCallback(async () => {
if (!stockCode) { if (!stockCode) {
@@ -129,27 +119,31 @@ export const useCompanyData = ({
return; return;
} }
setWatchlistLoading(true);
try { try {
if (isInWatchlist) { // 使用 Redux action状态会自动同步到全局
// 移除自选 // @ts-expect-error stockSlice 是 JS 文件TypeScript 无法推断 thunk 参数类型
await axios.delete(`/api/account/watchlist/${stockCode}`); const result = await dispatch(reduxToggleWatchlist({
setIsInWatchlist(false); stockCode,
toast({ title: '已从自选移除', status: 'info', duration: 1500 }); stockName: stockInfo?.stock_name || '',
} else { isInWatchlist,
// 添加自选 }));
await axios.post('/api/account/watchlist', { stock_code: stockCode });
setIsInWatchlist(true); // 检查是否成功rejected action 会有 error 属性)
toast({ title: '已加入自选', status: 'success', duration: 1500 }); if (result.error) {
throw new Error(result.error.message || '操作失败');
} }
// 显示提示
toast({
title: isInWatchlist ? '已从自选移除' : '已加入自选',
status: isInWatchlist ? 'info' : 'success',
duration: 1500,
});
} catch (error: any) { } catch (error: any) {
logger.error('useCompanyData', 'toggleWatchlist', error, { stockCode }); logger.error('useCompanyData', 'toggleWatchlist', error, { stockCode });
toast({ title: '操作失败,请稍后重试', status: 'error', duration: 2000 }); toast({ title: '操作失败,请稍后重试', status: 'error', duration: 2000 });
} finally {
setWatchlistLoading(false);
} }
}, [stockCode, isAuthenticated, isInWatchlist, toast]); }, [stockCode, stockInfo?.stock_name, isAuthenticated, isInWatchlist, toast, dispatch]);
/** /**
* 刷新股票信息 * 刷新股票信息
@@ -169,12 +163,13 @@ export const useCompanyData = ({
}; };
}, [autoLoadStockInfo, loadStockInfo]); }, [autoLoadStockInfo, loadStockInfo]);
// 自动加载自选股状态 // 自动加载自选股列表(从 Redux
useEffect(() => { useEffect(() => {
if (autoLoadWatchlist) { if (autoLoadWatchlist && isAuthenticated && watchlist.length === 0) {
loadWatchlistStatus(); // 只有当 Redux 中没有数据时才加载
dispatch(loadWatchlist());
} }
}, [autoLoadWatchlist, loadWatchlistStatus]); }, [autoLoadWatchlist, isAuthenticated, watchlist.length, dispatch]);
return { return {
stockInfo, stockInfo,