refactor(watchlist): 自选股数据源统一到 Redux
- stockSlice: 新增 loadWatchlistQuotes thunk 加载自选股行情 - useWatchlist: 改用 Redux selector 获取自选股数据 - WatchlistMenu: 使用 Redux 数据源,移除本地状态管理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
// src/hooks/useWatchlist.js
|
||||
// 自选股管理自定义 Hook(导航栏专用,与 Redux 状态同步)
|
||||
// 自选股管理自定义 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 } from '../store/slices/stockSlice';
|
||||
import {
|
||||
toggleWatchlist as toggleWatchlistAction,
|
||||
loadWatchlist,
|
||||
loadWatchlistQuotes
|
||||
} from '../store/slices/stockSlice';
|
||||
|
||||
const WATCHLIST_PAGE_SIZE = 10;
|
||||
|
||||
@@ -31,20 +35,18 @@ const WATCHLIST_PAGE_SIZE = 10;
|
||||
export const useWatchlist = () => {
|
||||
const toast = useToast();
|
||||
const dispatch = useDispatch();
|
||||
const [watchlistQuotes, setWatchlistQuotes] = useState([]);
|
||||
const [watchlistLoading, setWatchlistLoading] = useState(false);
|
||||
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 获取自选股列表长度(用于监听变化)
|
||||
// 使用 length 作为依赖,避免数组引用变化导致不必要的重新渲染
|
||||
const reduxWatchlistLength = useSelector(state => state.stock.watchlist?.length || 0);
|
||||
|
||||
// 检查 Redux watchlist 是否已初始化(加载状态)
|
||||
const reduxWatchlistLoading = useSelector(state => state.stock.loading?.watchlist);
|
||||
|
||||
// 用于跟踪上一次的 watchlist 长度
|
||||
const prevWatchlistLengthRef = useRef(-1); // 初始设为 -1,确保第一次变化也能检测到
|
||||
const prevWatchlistLengthRef = useRef(-1);
|
||||
|
||||
// 初始化时加载 Redux watchlist(确保 Redux 状态被初始化)
|
||||
const hasInitializedRef = useRef(false);
|
||||
@@ -56,35 +58,11 @@ export const useWatchlist = () => {
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
// 加载自选股实时行情
|
||||
const loadWatchlistQuotes = useCallback(async () => {
|
||||
try {
|
||||
setWatchlistLoading(true);
|
||||
const base = getApiBase();
|
||||
const resp = await fetch(base + '/api/account/watchlist/realtime', {
|
||||
credentials: 'include',
|
||||
cache: 'no-store'
|
||||
});
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
if (data && data.success && Array.isArray(data.data)) {
|
||||
setWatchlistQuotes(data.data);
|
||||
logger.debug('useWatchlist', '自选股行情加载成功', { count: data.data.length });
|
||||
} else {
|
||||
setWatchlistQuotes([]);
|
||||
}
|
||||
} else {
|
||||
setWatchlistQuotes([]);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('useWatchlist', '加载自选股实时行情失败', {
|
||||
error: e.message
|
||||
});
|
||||
setWatchlistQuotes([]);
|
||||
} finally {
|
||||
setWatchlistLoading(false);
|
||||
}
|
||||
}, []);
|
||||
// 加载自选股实时行情(通过 Redux)
|
||||
const loadWatchlistQuotesFunc = useCallback(() => {
|
||||
logger.debug('useWatchlist', '触发 loadWatchlistQuotes');
|
||||
dispatch(loadWatchlistQuotes());
|
||||
}, [dispatch]);
|
||||
|
||||
// 监听 Redux watchlist 长度变化,自动刷新行情数据
|
||||
useEffect(() => {
|
||||
@@ -102,7 +80,7 @@ export const useWatchlist = () => {
|
||||
// 延迟一小段时间再刷新,确保后端数据已更新
|
||||
const timer = setTimeout(() => {
|
||||
logger.debug('useWatchlist', '执行 loadWatchlistQuotes');
|
||||
loadWatchlistQuotes();
|
||||
dispatch(loadWatchlistQuotes());
|
||||
}, 500);
|
||||
|
||||
prevWatchlistLengthRef.current = currentLength;
|
||||
@@ -111,66 +89,53 @@ export const useWatchlist = () => {
|
||||
|
||||
// 更新 ref
|
||||
prevWatchlistLengthRef.current = currentLength;
|
||||
}, [reduxWatchlistLength, loadWatchlistQuotes]);
|
||||
}, [reduxWatchlistLength, dispatch]);
|
||||
|
||||
// 添加到自选股
|
||||
// 添加到自选股(通过 Redux)
|
||||
const handleAddToWatchlist = useCallback(async (stockCode, stockName) => {
|
||||
try {
|
||||
const base = getApiBase();
|
||||
const resp = await fetch(base + '/api/account/watchlist', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ stock_code: stockCode, stock_name: stockName })
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (resp.ok && data.success) {
|
||||
// 刷新自选股列表
|
||||
loadWatchlistQuotes();
|
||||
toast({ title: '已添加至自选股', status: 'success', duration: 1500 });
|
||||
return true;
|
||||
} else {
|
||||
toast({ title: '添加失败', status: 'error', duration: 2000 });
|
||||
return false;
|
||||
}
|
||||
// 通过 Redux action 添加(乐观更新)
|
||||
await dispatch(toggleWatchlistAction({
|
||||
stockCode,
|
||||
stockName,
|
||||
isInWatchlist: false // 表示当前不在自选股中,需要添加
|
||||
})).unwrap();
|
||||
|
||||
// 刷新行情
|
||||
dispatch(loadWatchlistQuotes());
|
||||
toast({ title: '已添加至自选股', status: 'success', duration: 1500 });
|
||||
return true;
|
||||
} catch (e) {
|
||||
toast({ title: '网络错误,添加失败', status: 'error', duration: 2000 });
|
||||
logger.error('useWatchlist', '添加自选股失败', e);
|
||||
toast({ title: e.message || '添加失败', status: 'error', duration: 2000 });
|
||||
return false;
|
||||
}
|
||||
}, [toast, loadWatchlistQuotes]);
|
||||
}, [dispatch, toast]);
|
||||
|
||||
// 从自选股移除
|
||||
// 从自选股移除(通过 Redux)
|
||||
const handleRemoveFromWatchlist = useCallback(async (stockCode) => {
|
||||
try {
|
||||
// 找到股票名称
|
||||
const stockItem = watchlistQuotes.find(item => {
|
||||
const normalize6 = (code) => {
|
||||
const m = String(code || '').match(/(\d{6})/);
|
||||
return m ? m[1] : String(code || '');
|
||||
};
|
||||
return normalize6(item.stock_code) === normalize6(stockCode);
|
||||
});
|
||||
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 移除(会同步更新 Redux 状态)
|
||||
// 通过 Redux action 移除(乐观更新)
|
||||
await dispatch(toggleWatchlistAction({
|
||||
stockCode,
|
||||
stockName,
|
||||
isInWatchlist: true // 表示当前在自选股中,需要移除
|
||||
})).unwrap();
|
||||
|
||||
// 更新本地状态(立即响应 UI)
|
||||
setWatchlistQuotes((prev) => {
|
||||
const normalize6 = (code) => {
|
||||
const m = String(code || '').match(/(\d{6})/);
|
||||
return m ? m[1] : String(code || '');
|
||||
};
|
||||
const target = normalize6(stockCode);
|
||||
const updated = (prev || []).filter((x) => normalize6(x.stock_code) !== target);
|
||||
const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / WATCHLIST_PAGE_SIZE));
|
||||
setWatchlistPage((p) => Math.min(p, newMaxPage));
|
||||
return updated;
|
||||
});
|
||||
// 更新分页(如果当前页超出范围)
|
||||
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) {
|
||||
@@ -195,7 +160,7 @@ export const useWatchlist = () => {
|
||||
watchlistPage,
|
||||
setWatchlistPage,
|
||||
WATCHLIST_PAGE_SIZE,
|
||||
loadWatchlistQuotes,
|
||||
loadWatchlistQuotes: loadWatchlistQuotesFunc,
|
||||
followingEvents,
|
||||
handleAddToWatchlist,
|
||||
handleRemoveFromWatchlist,
|
||||
|
||||
Reference in New Issue
Block a user