From e8a9a6f180340ada84ab04761d12dd54d4d0048f Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Fri, 5 Dec 2025 17:21:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=82=A1=E7=A5=A8=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=AE=A1=E7=90=86=E8=BF=81=E7=A7=BB=E5=88=B0=20Redux?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E7=B1=BB=E5=9E=8B=E5=8C=96=20Hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stockSlice: 新增 loadAllStocks action(带缓存检查) - stockSlice: watchlist 结构升级为 { stock_code, stock_name }[] - store/hooks.ts: 新增 useAppDispatch, useAppSelector 类型化 hooks - stockService: 移除 getAllStocks(已迁移到 Redux) - mock: 股票搜索支持模糊匹配 + 相关性排序 --- src/mocks/handlers/stock.js | 38 ++++++++++++---- src/services/stockService.js | 45 +------------------ src/store/hooks.ts | 15 +++++++ src/store/index.js | 5 +++ src/store/slices/stockSlice.js | 82 +++++++++++++++++++++++++++++----- 5 files changed, 122 insertions(+), 63 deletions(-) create mode 100644 src/store/hooks.ts diff --git a/src/mocks/handlers/stock.js b/src/mocks/handlers/stock.js index eb3ef037..c8219b70 100644 --- a/src/mocks/handlers/stock.js +++ b/src/mocks/handlers/stock.js @@ -123,12 +123,12 @@ const generateStockList = () => { // 股票相关的 Handlers export const stockHandlers = [ - // 搜索股票(个股中心页面使用) + // 搜索股票(个股中心页面使用)- 支持模糊搜索 http.get('/api/stocks/search', async ({ request }) => { await delay(200); const url = new URL(request.url); - const query = url.searchParams.get('q') || ''; + const query = (url.searchParams.get('q') || '').toLowerCase().trim(); const limit = parseInt(url.searchParams.get('limit') || '10'); console.log('[Mock Stock] 搜索股票:', { query, limit }); @@ -136,22 +136,44 @@ export const stockHandlers = [ const stocks = generateStockList(); // 如果没有搜索词,返回空结果 - if (!query.trim()) { + if (!query) { return HttpResponse.json({ success: true, data: [] }); } - // 过滤匹配的股票 - const results = stocks.filter(s => - s.code.includes(query) || s.name.includes(query) - ).slice(0, limit); + // 模糊搜索:代码 + 名称(不区分大小写) + const results = stocks.filter(s => { + const code = s.code.toLowerCase(); + const name = s.name.toLowerCase(); + return code.includes(query) || name.includes(query); + }); + + // 按相关性排序:完全匹配 > 开头匹配 > 包含匹配 + results.sort((a, b) => { + const aCode = a.code.toLowerCase(); + const bCode = b.code.toLowerCase(); + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + + // 计算匹配分数 + const getScore = (code, name) => { + if (code === query || name === query) return 100; // 完全匹配 + if (code.startsWith(query)) return 80; // 代码开头 + if (name.startsWith(query)) return 60; // 名称开头 + if (code.includes(query)) return 40; // 代码包含 + if (name.includes(query)) return 20; // 名称包含 + return 0; + }; + + return getScore(bCode, bName) - getScore(aCode, aName); + }); // 返回格式化数据 return HttpResponse.json({ success: true, - data: results.map(s => ({ + data: results.slice(0, limit).map(s => ({ stock_code: s.code, stock_name: s.name, market: s.code.startsWith('6') ? 'SH' : 'SZ', diff --git a/src/services/stockService.js b/src/services/stockService.js index 5b1b4681..593d168a 100644 --- a/src/services/stockService.js +++ b/src/services/stockService.js @@ -1,52 +1,11 @@ // src/services/stockService.js -// 股票数据服务 - -import { logger } from '../utils/logger'; - -const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || ''; +// 股票数据服务 - 模糊搜索工具函数 +// 注意: getAllStocks 已迁移到 Redux (stockSlice.loadAllStocks) /** * 股票数据服务 */ export const stockService = { - /** - * 获取所有股票列表 - * @returns {Promise<{success: boolean, data: Array<{code: string, name: string}>}>} - */ - async getAllStocks() { - try { - const response = await fetch(`${API_BASE_URL}/api/stocklist`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include' - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - - logger.debug('stockService', 'getAllStocks 成功', { - count: data?.length || 0 - }); - - return { - success: true, - data: data || [] - }; - } catch (error) { - logger.error('stockService', 'getAllStocks', error); - return { - success: false, - data: [], - error: error.message - }; - } - }, - /** * 模糊搜索股票(匹配 code 或 name) * @param {string} query - 搜索关键词 diff --git a/src/store/hooks.ts b/src/store/hooks.ts new file mode 100644 index 00000000..2cf94e3b --- /dev/null +++ b/src/store/hooks.ts @@ -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; +export type AppDispatch = typeof store.dispatch; + +// 类型化的 hooks +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/src/store/index.js b/src/store/index.js index 191439a2..26a796b0 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -63,4 +63,9 @@ export const injectReducer = (key, reducer) => { store.replaceReducer(createRootReducer()); }; +/** + * @typedef {typeof store.dispatch} AppDispatch + * @typedef {ReturnType} RootState + */ + export default store; diff --git a/src/store/slices/stockSlice.js b/src/store/slices/stockSlice.js index 0b6982f4..37622694 100644 --- a/src/store/slices/stockSlice.js +++ b/src/store/slices/stockSlice.js @@ -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 + // 自选股列表 [{ 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 }); } } });