Merge branch 'feature_2025/251209_stock_pref' into feature_bugfix/251217_stock
* feature_2025/251209_stock_pref: (133 commits) chore(StockQuoteCard): 删除未使用的 mockData.ts refactor(marketService): 移除 apiRequest 包装函数,统一使用 axios.get docs(Company): 添加 API 接口清单到 STRUCTURE.md refactor(Company): 提取共享的 useStockSearch Hook fix(hooks): 添加 AbortController 解决竞态条件问题 fix(SubTabContainer): 修复 Tab 懒加载失效问题 chore(CompanyOverview): 移除未使用的 CompanyOverviewData 类型定义 fix(CompanyOverview): 修复 useBasicInfo 重复调用问题 refactor(Company): fetch 请求迁移至 axios docs(Company): 更新 STRUCTURE.md 添加数据下沉优化记录 refactor(StockQuoteCard): 数据下沉优化,Props 从 11 个精简为 4 个 feat(StockQuoteCard): 新增内部数据获取 hooks fix(MarketDataView): 添加缺失的 VStack 导入 fix(MarketDataView): loading 背景色改为深色与整体一致 refactor(Company): 统一所有 Tab 的 loading 状态组件 style(ForecastReport): 详细数据表格 UI 优化 style(ForecastReport): 盈利预测图表优化 fix(ValueChainCard): 视图切换按钮始终靠右显示 refactor(CompanyOverview): 优化多个面板显示逻辑 style(DetailTable): 简化布局,标题+表格无嵌套 ...
This commit is contained in:
@@ -4,6 +4,56 @@ import { eventService, stockService } from '../../services/eventService';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { getApiBase } from '../../utils/apiConfig';
|
||||
|
||||
// ==================== Watchlist 缓存配置 ====================
|
||||
const WATCHLIST_CACHE_KEY = 'watchlist_cache';
|
||||
const WATCHLIST_CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7天
|
||||
|
||||
/**
|
||||
* 从 localStorage 读取自选股缓存
|
||||
*/
|
||||
const loadWatchlistFromCache = () => {
|
||||
try {
|
||||
const cached = localStorage.getItem(WATCHLIST_CACHE_KEY);
|
||||
if (!cached) return null;
|
||||
|
||||
const { data, timestamp } = JSON.parse(cached);
|
||||
const now = Date.now();
|
||||
|
||||
// 检查缓存是否过期(7天)
|
||||
if (now - timestamp > WATCHLIST_CACHE_DURATION) {
|
||||
localStorage.removeItem(WATCHLIST_CACHE_KEY);
|
||||
logger.debug('stockSlice', '自选股缓存已过期');
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug('stockSlice', '自选股 localStorage 缓存命中', {
|
||||
count: data?.length || 0,
|
||||
age: Math.round((now - timestamp) / 1000 / 60) + '分钟前'
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
logger.error('stockSlice', 'loadWatchlistFromCache', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存自选股到 localStorage
|
||||
*/
|
||||
const saveWatchlistToCache = (data) => {
|
||||
try {
|
||||
localStorage.setItem(WATCHLIST_CACHE_KEY, JSON.stringify({
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
logger.debug('stockSlice', '自选股已缓存到 localStorage', {
|
||||
count: data?.length || 0
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('stockSlice', 'saveWatchlistToCache', error);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== Async Thunks ====================
|
||||
|
||||
/**
|
||||
@@ -153,13 +203,28 @@ export const fetchExpectationScore = createAsyncThunk(
|
||||
|
||||
/**
|
||||
* 加载用户自选股列表(包含完整信息)
|
||||
* 缓存策略:Redux 内存缓存 → localStorage 持久缓存(7天) → API 请求
|
||||
*/
|
||||
export const loadWatchlist = createAsyncThunk(
|
||||
'stock/loadWatchlist',
|
||||
async () => {
|
||||
async (_, { getState }) => {
|
||||
logger.debug('stockSlice', 'loadWatchlist');
|
||||
|
||||
try {
|
||||
// 1. 先检查 Redux 内存缓存
|
||||
const reduxCached = getState().stock.watchlist;
|
||||
if (reduxCached && reduxCached.length > 0) {
|
||||
logger.debug('stockSlice', 'Redux watchlist 缓存命中', { count: reduxCached.length });
|
||||
return reduxCached;
|
||||
}
|
||||
|
||||
// 2. 再检查 localStorage 持久缓存(7天有效期)
|
||||
const localCached = loadWatchlistFromCache();
|
||||
if (localCached && localCached.length > 0) {
|
||||
return localCached;
|
||||
}
|
||||
|
||||
// 3. 缓存无效,调用 API
|
||||
const apiBase = getApiBase();
|
||||
const response = await fetch(`${apiBase}/api/account/watchlist`, {
|
||||
credentials: 'include'
|
||||
@@ -172,6 +237,10 @@ export const loadWatchlist = createAsyncThunk(
|
||||
stock_code: item.stock_code,
|
||||
stock_name: item.stock_name,
|
||||
}));
|
||||
|
||||
// 保存到 localStorage 缓存
|
||||
saveWatchlistToCache(watchlistData);
|
||||
|
||||
logger.debug('stockSlice', '自选股列表加载成功', {
|
||||
count: watchlistData.length
|
||||
});
|
||||
@@ -340,6 +409,26 @@ const stockSlice = createSlice({
|
||||
delete state.historicalEventsCache[eventId];
|
||||
delete state.chainAnalysisCache[eventId];
|
||||
delete state.expectationScores[eventId];
|
||||
},
|
||||
|
||||
/**
|
||||
* 乐观更新:添加自选股(同步)
|
||||
*/
|
||||
optimisticAddWatchlist: (state, action) => {
|
||||
const { stockCode, stockName } = action.payload;
|
||||
// 避免重复添加
|
||||
const exists = state.watchlist.some(item => item.stock_code === stockCode);
|
||||
if (!exists) {
|
||||
state.watchlist.push({ stock_code: stockCode, stock_name: stockName || '' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 乐观更新:移除自选股(同步)
|
||||
*/
|
||||
optimisticRemoveWatchlist: (state, action) => {
|
||||
const { stockCode } = action.payload;
|
||||
state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode);
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
@@ -470,9 +559,10 @@ const stockSlice = createSlice({
|
||||
state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode);
|
||||
}
|
||||
})
|
||||
// fulfilled: 乐观更新模式下状态已在 pending 更新,这里无需操作
|
||||
.addCase(toggleWatchlist.fulfilled, () => {
|
||||
// 状态已在 pending 时更新
|
||||
// fulfilled: 同步更新 localStorage 缓存
|
||||
.addCase(toggleWatchlist.fulfilled, (state) => {
|
||||
// 状态已在 pending 时更新,这里同步到 localStorage
|
||||
saveWatchlistToCache(state.watchlist);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -481,7 +571,9 @@ export const {
|
||||
updateQuote,
|
||||
updateQuotes,
|
||||
clearQuotes,
|
||||
clearEventCache
|
||||
clearEventCache,
|
||||
optimisticAddWatchlist,
|
||||
optimisticRemoveWatchlist
|
||||
} = stockSlice.actions;
|
||||
|
||||
export default stockSlice.reducer;
|
||||
|
||||
Reference in New Issue
Block a user