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:
@@ -3,20 +3,39 @@
|
||||
* - 使用 axios 请求
|
||||
* - 懒加载策略
|
||||
* - 自动取消请求
|
||||
* - 自选股状态与 Redux 全局状态同步
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
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,
|
||||
WatchlistItem,
|
||||
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;
|
||||
/** 是否自动加载股票信息 */
|
||||
@@ -27,17 +46,28 @@ interface UseCompanyDataOptions {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
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
|
||||
const toast = useToast();
|
||||
@@ -76,47 +106,7 @@ export const useCompanyData = ({
|
||||
}, [stockCode]);
|
||||
|
||||
/**
|
||||
* 加载自选股状态(优化:只检查单个股票,避免加载整个列表)
|
||||
*/
|
||||
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]);
|
||||
|
||||
/**
|
||||
* 切换自选股状态
|
||||
* 切换自选股状态(使用 Redux action,自动同步全局状态)
|
||||
*/
|
||||
const toggleWatchlist = useCallback(async () => {
|
||||
if (!stockCode) {
|
||||
@@ -129,27 +119,31 @@ export const useCompanyData = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setWatchlistLoading(true);
|
||||
|
||||
try {
|
||||
if (isInWatchlist) {
|
||||
// 移除自选
|
||||
await axios.delete(`/api/account/watchlist/${stockCode}`);
|
||||
setIsInWatchlist(false);
|
||||
toast({ title: '已从自选移除', status: 'info', duration: 1500 });
|
||||
} else {
|
||||
// 添加自选
|
||||
await axios.post('/api/account/watchlist', { stock_code: stockCode });
|
||||
setIsInWatchlist(true);
|
||||
toast({ title: '已加入自选', status: 'success', duration: 1500 });
|
||||
// 使用 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 });
|
||||
} finally {
|
||||
setWatchlistLoading(false);
|
||||
}
|
||||
}, [stockCode, isAuthenticated, isInWatchlist, toast]);
|
||||
}, [stockCode, stockInfo?.stock_name, isAuthenticated, isInWatchlist, toast, dispatch]);
|
||||
|
||||
/**
|
||||
* 刷新股票信息
|
||||
@@ -169,12 +163,13 @@ export const useCompanyData = ({
|
||||
};
|
||||
}, [autoLoadStockInfo, loadStockInfo]);
|
||||
|
||||
// 自动加载自选股状态
|
||||
// 自动加载自选股列表(从 Redux)
|
||||
useEffect(() => {
|
||||
if (autoLoadWatchlist) {
|
||||
loadWatchlistStatus();
|
||||
if (autoLoadWatchlist && isAuthenticated && watchlist.length === 0) {
|
||||
// 只有当 Redux 中没有数据时才加载
|
||||
dispatch(loadWatchlist());
|
||||
}
|
||||
}, [autoLoadWatchlist, loadWatchlistStatus]);
|
||||
}, [autoLoadWatchlist, isAuthenticated, watchlist.length, dispatch]);
|
||||
|
||||
return {
|
||||
stockInfo,
|
||||
|
||||
Reference in New Issue
Block a user