- fetchEventStocks: 新增 skipIfNoAccess 参数 - fetchHistoricalEvents: 新增 skipIfNoAccess 参数 - fetchChainAnalysis: 新增 skipIfNoAccess 参数 - 通过检查 subscription 状态判断是否跳过请求
813 lines
25 KiB
JavaScript
813 lines
25 KiB
JavaScript
// src/store/slices/stockSlice.js
|
||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||
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 ====================
|
||
|
||
/**
|
||
* 获取事件相关股票(Redux 缓存)
|
||
* @param {Object} params
|
||
* @param {string} params.eventId - 事件ID
|
||
* @param {boolean} params.forceRefresh - 是否强制刷新
|
||
* @param {boolean} params.skipIfNoAccess - 如果无权限则跳过请求(会员过期场景)
|
||
*/
|
||
export const fetchEventStocks = createAsyncThunk(
|
||
'stock/fetchEventStocks',
|
||
async ({ eventId, forceRefresh = false, skipIfNoAccess = false }, { getState }) => {
|
||
logger.debug('stockSlice', 'fetchEventStocks', { eventId, forceRefresh, skipIfNoAccess });
|
||
|
||
// 检查订阅状态,如果会员过期则跳过请求
|
||
if (skipIfNoAccess) {
|
||
const subscriptionInfo = getState().subscription?.info;
|
||
const isExpired = subscriptionInfo?.type !== 'free' && !subscriptionInfo?.is_active;
|
||
if (isExpired) {
|
||
logger.debug('stockSlice', '会员已过期,跳过 fetchEventStocks 请求', { eventId });
|
||
return { eventId, stocks: [], skipped: true };
|
||
}
|
||
}
|
||
|
||
// Redux 状态缓存
|
||
if (!forceRefresh) {
|
||
const cached = getState().stock.eventStocksCache[eventId];
|
||
if (cached && cached.length > 0) {
|
||
logger.debug('stockSlice', 'Redux 缓存命中', { eventId });
|
||
return { eventId, stocks: cached };
|
||
}
|
||
}
|
||
|
||
// API 请求
|
||
const res = await eventService.getRelatedStocks(eventId);
|
||
if (res.success && res.data) {
|
||
logger.debug('stockSlice', 'API 请求成功', {
|
||
eventId,
|
||
stockCount: res.data.length
|
||
});
|
||
return { eventId, stocks: res.data };
|
||
}
|
||
|
||
throw new Error(res.error || '获取股票数据失败');
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 获取股票行情
|
||
*/
|
||
export const fetchStockQuotes = createAsyncThunk(
|
||
'stock/fetchStockQuotes',
|
||
async ({ codes, eventTime }) => {
|
||
logger.debug('stockSlice', 'fetchStockQuotes', {
|
||
codeCount: codes.length,
|
||
eventTime
|
||
});
|
||
|
||
const quotes = await stockService.getQuotes(codes, eventTime);
|
||
return quotes;
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 获取事件详情(Redux 缓存)
|
||
*/
|
||
export const fetchEventDetail = createAsyncThunk(
|
||
'stock/fetchEventDetail',
|
||
async ({ eventId, forceRefresh = false }, { getState }) => {
|
||
logger.debug('stockSlice', 'fetchEventDetail', { eventId });
|
||
|
||
// Redux 缓存
|
||
if (!forceRefresh) {
|
||
const cached = getState().stock.eventDetailsCache[eventId];
|
||
if (cached) {
|
||
logger.debug('stockSlice', 'Redux 缓存命中 - eventDetail', { eventId });
|
||
return { eventId, detail: cached };
|
||
}
|
||
}
|
||
|
||
// API 请求
|
||
const res = await eventService.getEventDetail(eventId);
|
||
if (res.success && res.data) {
|
||
return { eventId, detail: res.data };
|
||
}
|
||
|
||
throw new Error(res.error || '获取事件详情失败');
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 获取历史事件对比(Redux 缓存)
|
||
* @param {Object} params
|
||
* @param {string} params.eventId - 事件ID
|
||
* @param {boolean} params.forceRefresh - 是否强制刷新
|
||
* @param {boolean} params.skipIfNoAccess - 如果无权限则跳过请求(会员过期场景)
|
||
*/
|
||
export const fetchHistoricalEvents = createAsyncThunk(
|
||
'stock/fetchHistoricalEvents',
|
||
async ({ eventId, forceRefresh = false, skipIfNoAccess = false }, { getState }) => {
|
||
logger.debug('stockSlice', 'fetchHistoricalEvents', { eventId, skipIfNoAccess });
|
||
|
||
// 检查订阅状态,如果会员过期则跳过请求
|
||
if (skipIfNoAccess) {
|
||
const subscriptionInfo = getState().subscription?.info;
|
||
const isExpired = subscriptionInfo?.type !== 'free' && !subscriptionInfo?.is_active;
|
||
if (isExpired) {
|
||
logger.debug('stockSlice', '会员已过期,跳过 fetchHistoricalEvents 请求', { eventId });
|
||
return { eventId, events: [], skipped: true };
|
||
}
|
||
}
|
||
|
||
// Redux 缓存
|
||
if (!forceRefresh) {
|
||
const cached = getState().stock.historicalEventsCache[eventId];
|
||
if (cached) {
|
||
return { eventId, events: cached };
|
||
}
|
||
}
|
||
|
||
// API 请求
|
||
const res = await eventService.getHistoricalEvents(eventId);
|
||
if (res.success && res.data) {
|
||
return { eventId, events: res.data };
|
||
}
|
||
|
||
return { eventId, events: [] };
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 获取传导链分析(Redux 缓存)
|
||
* @param {Object} params
|
||
* @param {string} params.eventId - 事件ID
|
||
* @param {boolean} params.forceRefresh - 是否强制刷新
|
||
* @param {boolean} params.skipIfNoAccess - 如果无权限则跳过请求(会员过期场景)
|
||
*/
|
||
export const fetchChainAnalysis = createAsyncThunk(
|
||
'stock/fetchChainAnalysis',
|
||
async ({ eventId, forceRefresh = false, skipIfNoAccess = false }, { getState }) => {
|
||
logger.debug('stockSlice', 'fetchChainAnalysis', { eventId, skipIfNoAccess });
|
||
|
||
// 检查订阅状态,如果会员过期则跳过请求
|
||
if (skipIfNoAccess) {
|
||
const subscriptionInfo = getState().subscription?.info;
|
||
const isExpired = subscriptionInfo?.type !== 'free' && !subscriptionInfo?.is_active;
|
||
if (isExpired) {
|
||
logger.debug('stockSlice', '会员已过期,跳过 fetchChainAnalysis 请求', { eventId });
|
||
return { eventId, analysis: null, skipped: true };
|
||
}
|
||
}
|
||
|
||
// Redux 缓存
|
||
if (!forceRefresh) {
|
||
const cached = getState().stock.chainAnalysisCache[eventId];
|
||
if (cached) {
|
||
return { eventId, analysis: cached };
|
||
}
|
||
}
|
||
|
||
// API 请求
|
||
const res = await eventService.getTransmissionChainAnalysis(eventId);
|
||
if (res.success && res.data) {
|
||
return { eventId, analysis: res.data };
|
||
}
|
||
|
||
return { eventId, analysis: null };
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 获取超预期得分
|
||
*/
|
||
export const fetchExpectationScore = createAsyncThunk(
|
||
'stock/fetchExpectationScore',
|
||
async ({ eventId }) => {
|
||
logger.debug('stockSlice', 'fetchExpectationScore', { eventId });
|
||
|
||
if (eventService.getExpectationScore) {
|
||
const res = await eventService.getExpectationScore(eventId);
|
||
if (res.success && res.data) {
|
||
return { eventId, score: res.data.score };
|
||
}
|
||
}
|
||
|
||
return { eventId, score: null };
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 加载用户自选股列表(包含完整信息)
|
||
* 缓存策略:Redux 内存缓存 → localStorage 持久缓存(7天) → API 请求
|
||
*/
|
||
export const loadWatchlist = createAsyncThunk(
|
||
'stock/loadWatchlist',
|
||
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'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.data) {
|
||
// 返回完整的股票信息,而不仅仅是 stock_code
|
||
const watchlistData = data.data.map(item => ({
|
||
stock_code: item.stock_code,
|
||
stock_name: item.stock_name,
|
||
}));
|
||
|
||
// 保存到 localStorage 缓存
|
||
saveWatchlistToCache(watchlistData);
|
||
|
||
logger.debug('stockSlice', '自选股列表加载成功', {
|
||
count: watchlistData.length
|
||
});
|
||
return watchlistData;
|
||
}
|
||
|
||
return [];
|
||
} catch (error) {
|
||
logger.error('stockSlice', 'loadWatchlist', error);
|
||
return [];
|
||
}
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 加载全部股票列表(用于前端模糊搜索)
|
||
*/
|
||
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 [];
|
||
}
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 加载自选股实时行情
|
||
* 用于统一行情刷新,两个面板共用
|
||
*/
|
||
export const loadWatchlistQuotes = createAsyncThunk(
|
||
'stock/loadWatchlistQuotes',
|
||
async () => {
|
||
logger.debug('stockSlice', 'loadWatchlistQuotes');
|
||
|
||
try {
|
||
const apiBase = getApiBase();
|
||
const response = await fetch(`${apiBase}/api/account/watchlist/realtime`, {
|
||
credentials: 'include',
|
||
cache: 'no-store'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success && Array.isArray(data.data)) {
|
||
logger.debug('stockSlice', '自选股行情加载成功', { count: data.data.length });
|
||
return data.data;
|
||
}
|
||
|
||
return [];
|
||
} catch (error) {
|
||
logger.error('stockSlice', 'loadWatchlistQuotes', error);
|
||
return [];
|
||
}
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 加载关注事件列表
|
||
* 用于统一关注事件数据源,两个面板共用
|
||
*/
|
||
export const loadFollowingEvents = createAsyncThunk(
|
||
'stock/loadFollowingEvents',
|
||
async () => {
|
||
logger.debug('stockSlice', 'loadFollowingEvents');
|
||
|
||
try {
|
||
const apiBase = getApiBase();
|
||
const response = await fetch(`${apiBase}/api/account/events/following`, {
|
||
credentials: 'include',
|
||
cache: 'no-store'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success && Array.isArray(data.data)) {
|
||
// 合并重复的事件(用最新的数据)
|
||
const eventMap = new Map();
|
||
for (const evt of data.data) {
|
||
if (evt && evt.id) {
|
||
eventMap.set(evt.id, evt);
|
||
}
|
||
}
|
||
const merged = Array.from(eventMap.values());
|
||
// 按创建时间降序排列
|
||
if (merged.length > 0 && merged[0].created_at) {
|
||
merged.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
|
||
} else {
|
||
merged.sort((a, b) => (b.id || 0) - (a.id || 0));
|
||
}
|
||
logger.debug('stockSlice', '关注事件列表加载成功', { count: merged.length });
|
||
return merged;
|
||
}
|
||
|
||
return [];
|
||
} catch (error) {
|
||
logger.error('stockSlice', 'loadFollowingEvents', error);
|
||
return [];
|
||
}
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 加载用户评论列表
|
||
*/
|
||
export const loadEventComments = createAsyncThunk(
|
||
'stock/loadEventComments',
|
||
async () => {
|
||
logger.debug('stockSlice', 'loadEventComments');
|
||
|
||
try {
|
||
const apiBase = getApiBase();
|
||
const response = await fetch(`${apiBase}/api/account/events/posts`, {
|
||
credentials: 'include',
|
||
cache: 'no-store'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success && Array.isArray(data.data)) {
|
||
logger.debug('stockSlice', '用户评论列表加载成功', { count: data.data.length });
|
||
return data.data;
|
||
}
|
||
|
||
return [];
|
||
} catch (error) {
|
||
logger.error('stockSlice', 'loadEventComments', error);
|
||
return [];
|
||
}
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 切换关注事件状态(关注/取消关注)
|
||
*/
|
||
export const toggleFollowEvent = createAsyncThunk(
|
||
'stock/toggleFollowEvent',
|
||
async ({ eventId, isFollowing }) => {
|
||
logger.debug('stockSlice', 'toggleFollowEvent', { eventId, isFollowing });
|
||
|
||
const apiBase = getApiBase();
|
||
const response = await fetch(`${apiBase}/api/events/${eventId}/follow`, {
|
||
method: 'POST',
|
||
credentials: 'include'
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (!response.ok || data.success === false) {
|
||
throw new Error(data.error || '操作失败');
|
||
}
|
||
|
||
return { eventId, isFollowing };
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 切换自选股状态
|
||
*/
|
||
export const toggleWatchlist = createAsyncThunk(
|
||
'stock/toggleWatchlist',
|
||
async ({ stockCode, stockName, isInWatchlist }) => {
|
||
logger.debug('stockSlice', 'toggleWatchlist', {
|
||
stockCode,
|
||
stockName,
|
||
isInWatchlist
|
||
});
|
||
|
||
const apiBase = getApiBase();
|
||
let response;
|
||
|
||
if (isInWatchlist) {
|
||
// 移除自选股
|
||
response = await fetch(`${apiBase}/api/account/watchlist/${stockCode}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include'
|
||
});
|
||
} else {
|
||
// 添加自选股
|
||
response = await fetch(`${apiBase}/api/account/watchlist`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'include',
|
||
body: JSON.stringify({ stock_code: stockCode, stock_name: stockName })
|
||
});
|
||
}
|
||
|
||
const data = await response.json();
|
||
if (!data.success) {
|
||
throw new Error(data.error || '操作失败');
|
||
}
|
||
|
||
return { stockCode, stockName, isInWatchlist };
|
||
}
|
||
);
|
||
|
||
// ==================== Slice ====================
|
||
|
||
const stockSlice = createSlice({
|
||
name: 'stock',
|
||
initialState: {
|
||
// 事件相关股票缓存 { [eventId]: stocks[] }
|
||
eventStocksCache: {},
|
||
|
||
// 股票行情 { [stockCode]: quote }
|
||
quotes: {},
|
||
|
||
// 事件详情缓存 { [eventId]: detail }
|
||
eventDetailsCache: {},
|
||
|
||
// 历史事件缓存 { [eventId]: events[] }
|
||
historicalEventsCache: {},
|
||
|
||
// 传导链分析缓存 { [eventId]: analysis }
|
||
chainAnalysisCache: {},
|
||
|
||
// 超预期得分缓存 { [eventId]: score }
|
||
expectationScores: {},
|
||
|
||
// 自选股列表 [{ stock_code, stock_name }]
|
||
watchlist: [],
|
||
|
||
// 自选股实时行情 [{ stock_code, stock_name, price, change_percent, ... }]
|
||
watchlistQuotes: [],
|
||
|
||
// 关注事件列表 [{ id, title, event_type, ... }]
|
||
followingEvents: [],
|
||
|
||
// 用户评论列表 [{ id, content, event_id, ... }]
|
||
eventComments: [],
|
||
|
||
// 全部股票列表(用于前端模糊搜索)[{ code, name }]
|
||
allStocks: [],
|
||
|
||
// 加载状态
|
||
loading: {
|
||
stocks: false,
|
||
quotes: false,
|
||
eventDetail: false,
|
||
historicalEvents: false,
|
||
chainAnalysis: false,
|
||
watchlist: false,
|
||
watchlistQuotes: false,
|
||
followingEvents: false,
|
||
eventComments: false,
|
||
allStocks: false
|
||
},
|
||
|
||
// 错误信息
|
||
error: null
|
||
},
|
||
reducers: {
|
||
/**
|
||
* 更新单个股票行情
|
||
*/
|
||
updateQuote: (state, action) => {
|
||
const { stockCode, quote } = action.payload;
|
||
state.quotes[stockCode] = quote;
|
||
},
|
||
|
||
/**
|
||
* 批量更新股票行情
|
||
*/
|
||
updateQuotes: (state, action) => {
|
||
state.quotes = { ...state.quotes, ...action.payload };
|
||
},
|
||
|
||
/**
|
||
* 清空行情数据
|
||
*/
|
||
clearQuotes: (state) => {
|
||
state.quotes = {};
|
||
},
|
||
|
||
/**
|
||
* 清空指定事件的缓存
|
||
*/
|
||
clearEventCache: (state, action) => {
|
||
const { eventId } = action.payload;
|
||
delete state.eventStocksCache[eventId];
|
||
delete state.eventDetailsCache[eventId];
|
||
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) => {
|
||
builder
|
||
// ===== fetchEventStocks =====
|
||
.addCase(fetchEventStocks.pending, (state) => {
|
||
state.loading.stocks = true;
|
||
state.error = null;
|
||
})
|
||
.addCase(fetchEventStocks.fulfilled, (state, action) => {
|
||
const { eventId, stocks } = action.payload;
|
||
state.eventStocksCache[eventId] = stocks;
|
||
state.loading.stocks = false;
|
||
})
|
||
.addCase(fetchEventStocks.rejected, (state, action) => {
|
||
state.loading.stocks = false;
|
||
state.error = action.error.message;
|
||
})
|
||
|
||
// ===== fetchStockQuotes =====
|
||
.addCase(fetchStockQuotes.pending, (state) => {
|
||
state.loading.quotes = true;
|
||
})
|
||
.addCase(fetchStockQuotes.fulfilled, (state, action) => {
|
||
state.quotes = { ...state.quotes, ...action.payload };
|
||
state.loading.quotes = false;
|
||
})
|
||
.addCase(fetchStockQuotes.rejected, (state) => {
|
||
state.loading.quotes = false;
|
||
})
|
||
|
||
// ===== fetchEventDetail =====
|
||
.addCase(fetchEventDetail.pending, (state) => {
|
||
state.loading.eventDetail = true;
|
||
})
|
||
.addCase(fetchEventDetail.fulfilled, (state, action) => {
|
||
const { eventId, detail } = action.payload;
|
||
state.eventDetailsCache[eventId] = detail;
|
||
state.loading.eventDetail = false;
|
||
})
|
||
.addCase(fetchEventDetail.rejected, (state) => {
|
||
state.loading.eventDetail = false;
|
||
})
|
||
|
||
// ===== fetchHistoricalEvents =====
|
||
.addCase(fetchHistoricalEvents.pending, (state) => {
|
||
state.loading.historicalEvents = true;
|
||
})
|
||
.addCase(fetchHistoricalEvents.fulfilled, (state, action) => {
|
||
const { eventId, events } = action.payload;
|
||
state.historicalEventsCache[eventId] = events;
|
||
state.loading.historicalEvents = false;
|
||
})
|
||
.addCase(fetchHistoricalEvents.rejected, (state) => {
|
||
state.loading.historicalEvents = false;
|
||
})
|
||
|
||
// ===== fetchChainAnalysis =====
|
||
.addCase(fetchChainAnalysis.pending, (state) => {
|
||
state.loading.chainAnalysis = true;
|
||
})
|
||
.addCase(fetchChainAnalysis.fulfilled, (state, action) => {
|
||
const { eventId, analysis } = action.payload;
|
||
state.chainAnalysisCache[eventId] = analysis;
|
||
state.loading.chainAnalysis = false;
|
||
})
|
||
.addCase(fetchChainAnalysis.rejected, (state) => {
|
||
state.loading.chainAnalysis = false;
|
||
})
|
||
|
||
// ===== fetchExpectationScore =====
|
||
.addCase(fetchExpectationScore.fulfilled, (state, action) => {
|
||
const { eventId, score } = action.payload;
|
||
state.expectationScores[eventId] = score;
|
||
})
|
||
|
||
// ===== loadWatchlist =====
|
||
.addCase(loadWatchlist.pending, (state) => {
|
||
state.loading.watchlist = true;
|
||
})
|
||
.addCase(loadWatchlist.fulfilled, (state, action) => {
|
||
state.watchlist = action.payload;
|
||
state.loading.watchlist = false;
|
||
})
|
||
.addCase(loadWatchlist.rejected, (state) => {
|
||
state.loading.watchlist = false;
|
||
})
|
||
|
||
// ===== loadWatchlistQuotes =====
|
||
.addCase(loadWatchlistQuotes.pending, (state) => {
|
||
state.loading.watchlistQuotes = true;
|
||
})
|
||
.addCase(loadWatchlistQuotes.fulfilled, (state, action) => {
|
||
state.watchlistQuotes = action.payload;
|
||
state.loading.watchlistQuotes = false;
|
||
})
|
||
.addCase(loadWatchlistQuotes.rejected, (state) => {
|
||
state.loading.watchlistQuotes = 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(乐观更新)=====
|
||
// pending: 立即更新状态
|
||
.addCase(toggleWatchlist.pending, (state, action) => {
|
||
const { stockCode, stockName, isInWatchlist } = action.meta.arg;
|
||
if (isInWatchlist) {
|
||
// 移除
|
||
state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode);
|
||
} else {
|
||
// 添加
|
||
const exists = state.watchlist.some(item => item.stock_code === stockCode);
|
||
if (!exists) {
|
||
state.watchlist.push({ stock_code: stockCode, stock_name: stockName });
|
||
}
|
||
}
|
||
})
|
||
// rejected: 回滚状态
|
||
.addCase(toggleWatchlist.rejected, (state, action) => {
|
||
const { stockCode, stockName, isInWatchlist } = action.meta.arg;
|
||
// 回滚:与 pending 操作相反
|
||
if (isInWatchlist) {
|
||
// 之前移除了,现在加回来
|
||
const exists = state.watchlist.some(item => item.stock_code === stockCode);
|
||
if (!exists) {
|
||
state.watchlist.push({ stock_code: stockCode, stock_name: stockName });
|
||
}
|
||
} else {
|
||
// 之前添加了,现在移除
|
||
state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode);
|
||
}
|
||
})
|
||
// fulfilled: 同步更新 localStorage 缓存
|
||
.addCase(toggleWatchlist.fulfilled, (state) => {
|
||
// 状态已在 pending 时更新,这里同步到 localStorage
|
||
saveWatchlistToCache(state.watchlist);
|
||
})
|
||
|
||
// ===== loadFollowingEvents =====
|
||
.addCase(loadFollowingEvents.pending, (state) => {
|
||
state.loading.followingEvents = true;
|
||
})
|
||
.addCase(loadFollowingEvents.fulfilled, (state, action) => {
|
||
state.followingEvents = action.payload;
|
||
state.loading.followingEvents = false;
|
||
})
|
||
.addCase(loadFollowingEvents.rejected, (state) => {
|
||
state.loading.followingEvents = false;
|
||
})
|
||
|
||
// ===== loadEventComments =====
|
||
.addCase(loadEventComments.pending, (state) => {
|
||
state.loading.eventComments = true;
|
||
})
|
||
.addCase(loadEventComments.fulfilled, (state, action) => {
|
||
state.eventComments = action.payload;
|
||
state.loading.eventComments = false;
|
||
})
|
||
.addCase(loadEventComments.rejected, (state) => {
|
||
state.loading.eventComments = false;
|
||
})
|
||
|
||
// ===== toggleFollowEvent(乐观更新)=====
|
||
// pending: 立即更新状态
|
||
.addCase(toggleFollowEvent.pending, (state, action) => {
|
||
const { eventId, isFollowing } = action.meta.arg;
|
||
if (isFollowing) {
|
||
// 当前已关注,取消关注 → 移除
|
||
state.followingEvents = state.followingEvents.filter(evt => evt.id !== eventId);
|
||
}
|
||
// 添加关注的情况需要事件完整数据,不在这里处理
|
||
})
|
||
// rejected: 回滚状态(仅取消关注需要回滚)
|
||
.addCase(toggleFollowEvent.rejected, (state, action) => {
|
||
// 取消关注失败时,需要刷新列表恢复数据
|
||
// 由于没有原始事件数据,这里只能触发重新加载
|
||
logger.warn('stockSlice', 'toggleFollowEvent rejected, 需要重新加载关注事件列表');
|
||
});
|
||
}
|
||
});
|
||
|
||
export const {
|
||
updateQuote,
|
||
updateQuotes,
|
||
clearQuotes,
|
||
clearEventCache,
|
||
optimisticAddWatchlist,
|
||
optimisticRemoveWatchlist
|
||
} = stockSlice.actions;
|
||
|
||
export default stockSlice.reducer;
|