refactor: 股票数据管理迁移到 Redux,新增类型化 Hooks
- stockSlice: 新增 loadAllStocks action(带缓存检查)
- stockSlice: watchlist 结构升级为 { stock_code, stock_name }[]
- store/hooks.ts: 新增 useAppDispatch, useAppSelector 类型化 hooks
- stockService: 移除 getAllStocks(已迁移到 Redux)
- mock: 股票搜索支持模糊匹配 + 相关性排序
This commit is contained in:
15
src/store/hooks.ts
Normal file
15
src/store/hooks.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Redux Typed Hooks
|
||||
* 提供类型安全的 useDispatch 和 useSelector hooks
|
||||
*/
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type { TypedUseSelectorHook } from 'react-redux';
|
||||
import { store } from './index';
|
||||
|
||||
// 从 store 推断类型
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
// 类型化的 hooks
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
@@ -63,4 +63,9 @@ export const injectReducer = (key, reducer) => {
|
||||
store.replaceReducer(createRootReducer());
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {typeof store.dispatch} AppDispatch
|
||||
* @typedef {ReturnType<typeof store.getState>} RootState
|
||||
*/
|
||||
|
||||
export default store;
|
||||
|
||||
@@ -152,11 +152,11 @@ export const fetchExpectationScore = createAsyncThunk(
|
||||
);
|
||||
|
||||
/**
|
||||
* 加载用户自选股列表
|
||||
* 加载用户自选股列表(包含完整信息)
|
||||
*/
|
||||
export const loadWatchlist = createAsyncThunk(
|
||||
'stock/loadWatchlist',
|
||||
async (_, { getState }) => {
|
||||
async () => {
|
||||
logger.debug('stockSlice', 'loadWatchlist');
|
||||
|
||||
try {
|
||||
@@ -167,11 +167,15 @@ export const loadWatchlist = createAsyncThunk(
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
const stockCodes = data.data.map(item => item.stock_code);
|
||||
// 返回完整的股票信息,而不仅仅是 stock_code
|
||||
const watchlistData = data.data.map(item => ({
|
||||
stock_code: item.stock_code,
|
||||
stock_name: item.stock_name,
|
||||
}));
|
||||
logger.debug('stockSlice', '自选股列表加载成功', {
|
||||
count: stockCodes.length
|
||||
count: watchlistData.length
|
||||
});
|
||||
return stockCodes;
|
||||
return watchlistData;
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -182,6 +186,43 @@ export const loadWatchlist = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 加载全部股票列表(用于前端模糊搜索)
|
||||
*/
|
||||
export const loadAllStocks = createAsyncThunk(
|
||||
'stock/loadAllStocks',
|
||||
async (_, { getState }) => {
|
||||
// 检查缓存
|
||||
const cached = getState().stock.allStocks;
|
||||
if (cached && cached.length > 0) {
|
||||
logger.debug('stockSlice', 'allStocks 缓存命中', { count: cached.length });
|
||||
return cached;
|
||||
}
|
||||
|
||||
logger.debug('stockSlice', 'loadAllStocks');
|
||||
|
||||
try {
|
||||
const apiBase = getApiBase();
|
||||
const response = await fetch(`${apiBase}/api/stocklist`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
logger.debug('stockSlice', '全部股票列表加载成功', {
|
||||
count: data.length
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
logger.error('stockSlice', 'loadAllStocks', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 切换自选股状态
|
||||
*/
|
||||
@@ -219,7 +260,7 @@ export const toggleWatchlist = createAsyncThunk(
|
||||
throw new Error(data.error || '操作失败');
|
||||
}
|
||||
|
||||
return { stockCode, isInWatchlist };
|
||||
return { stockCode, stockName, isInWatchlist };
|
||||
}
|
||||
);
|
||||
|
||||
@@ -246,9 +287,12 @@ const stockSlice = createSlice({
|
||||
// 超预期得分缓存 { [eventId]: score }
|
||||
expectationScores: {},
|
||||
|
||||
// 自选股列表 Set<stockCode>
|
||||
// 自选股列表 [{ stock_code, stock_name }]
|
||||
watchlist: [],
|
||||
|
||||
// 全部股票列表(用于前端模糊搜索)[{ code, name }]
|
||||
allStocks: [],
|
||||
|
||||
// 加载状态
|
||||
loading: {
|
||||
stocks: false,
|
||||
@@ -256,7 +300,8 @@ const stockSlice = createSlice({
|
||||
eventDetail: false,
|
||||
historicalEvents: false,
|
||||
chainAnalysis: false,
|
||||
watchlist: false
|
||||
watchlist: false,
|
||||
allStocks: false
|
||||
},
|
||||
|
||||
// 错误信息
|
||||
@@ -383,16 +428,29 @@ const stockSlice = createSlice({
|
||||
state.loading.watchlist = false;
|
||||
})
|
||||
|
||||
// ===== loadAllStocks =====
|
||||
.addCase(loadAllStocks.pending, (state) => {
|
||||
state.loading.allStocks = true;
|
||||
})
|
||||
.addCase(loadAllStocks.fulfilled, (state, action) => {
|
||||
state.allStocks = action.payload;
|
||||
state.loading.allStocks = false;
|
||||
})
|
||||
.addCase(loadAllStocks.rejected, (state) => {
|
||||
state.loading.allStocks = false;
|
||||
})
|
||||
|
||||
// ===== toggleWatchlist =====
|
||||
.addCase(toggleWatchlist.fulfilled, (state, action) => {
|
||||
const { stockCode, isInWatchlist } = action.payload;
|
||||
const { stockCode, stockName, isInWatchlist } = action.payload;
|
||||
if (isInWatchlist) {
|
||||
// 移除
|
||||
state.watchlist = state.watchlist.filter(code => code !== stockCode);
|
||||
state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode);
|
||||
} else {
|
||||
// 添加
|
||||
if (!state.watchlist.includes(stockCode)) {
|
||||
state.watchlist.push(stockCode);
|
||||
const exists = state.watchlist.some(item => item.stock_code === stockCode);
|
||||
if (!exists) {
|
||||
state.watchlist.push({ stock_code: stockCode, stock_name: stockName });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user