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:
@@ -292,6 +292,132 @@ export const loadAllStocks = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 加载自选股实时行情
|
||||
* 用于统一行情刷新,两个面板共用
|
||||
*/
|
||||
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 };
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 切换自选股状态
|
||||
*/
|
||||
@@ -359,6 +485,15 @@ const stockSlice = createSlice({
|
||||
// 自选股列表 [{ 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: [],
|
||||
|
||||
@@ -370,6 +505,9 @@ const stockSlice = createSlice({
|
||||
historicalEvents: false,
|
||||
chainAnalysis: false,
|
||||
watchlist: false,
|
||||
watchlistQuotes: false,
|
||||
followingEvents: false,
|
||||
eventComments: false,
|
||||
allStocks: false
|
||||
},
|
||||
|
||||
@@ -517,6 +655,18 @@ const stockSlice = createSlice({
|
||||
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;
|
||||
@@ -563,6 +713,47 @@ const stockSlice = createSlice({
|
||||
.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, 需要重新加载关注事件列表');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user