- useFollowingEvents: 改用 Redux selector 获取关注事件 - GlobalSidebarContext: 移除本地 followingEvents 状态,使用 Redux - 侧边栏和导航栏共享同一数据源,保持状态同步 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
217 lines
6.1 KiB
JavaScript
217 lines
6.1 KiB
JavaScript
/**
|
||
* GlobalSidebarContext - 全局右侧工具栏状态管理
|
||
*
|
||
* 管理侧边栏的展开/收起状态和数据加载
|
||
* 自选股和关注事件数据都从 Redux 获取,与导航栏共用数据源
|
||
*/
|
||
|
||
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
|
||
import { useSelector, useDispatch } from 'react-redux';
|
||
import { useAuth } from './AuthContext';
|
||
import { logger } from '@/utils/logger';
|
||
import {
|
||
loadWatchlist,
|
||
loadWatchlistQuotes,
|
||
toggleWatchlist,
|
||
loadFollowingEvents,
|
||
loadEventComments,
|
||
toggleFollowEvent
|
||
} from '@/store/slices/stockSlice';
|
||
|
||
const GlobalSidebarContext = createContext(null);
|
||
|
||
/**
|
||
* GlobalSidebarProvider - 全局侧边栏 Provider
|
||
*/
|
||
export const GlobalSidebarProvider = ({ children }) => {
|
||
const { user } = useAuth();
|
||
const userId = user?.id;
|
||
const dispatch = useDispatch();
|
||
|
||
// 侧边栏展开/收起状态(默认折叠)
|
||
const [isOpen, setIsOpen] = useState(false);
|
||
|
||
// 从 Redux 获取自选股数据(与导航栏共用)
|
||
const watchlist = useSelector(state => state.stock.watchlist || []);
|
||
const watchlistQuotes = useSelector(state => state.stock.watchlistQuotes || []);
|
||
const watchlistLoading = useSelector(state => state.stock.loading?.watchlist);
|
||
const quotesLoading = useSelector(state => state.stock.loading?.watchlistQuotes);
|
||
|
||
// 将 watchlistQuotes 数组转换为 { stock_code: quote } 格式(兼容现有组件)
|
||
const realtimeQuotes = React.useMemo(() => {
|
||
const quotesMap = {};
|
||
watchlistQuotes.forEach(item => {
|
||
quotesMap[item.stock_code] = item;
|
||
});
|
||
return quotesMap;
|
||
}, [watchlistQuotes]);
|
||
|
||
// 从 Redux 获取关注事件数据(与导航栏共用)
|
||
const followingEvents = useSelector(state => state.stock.followingEvents || []);
|
||
const eventComments = useSelector(state => state.stock.eventComments || []);
|
||
const eventsLoading = useSelector(state => state.stock.loading?.followingEvents || false);
|
||
|
||
// 防止重复加载
|
||
const hasLoadedRef = useRef(false);
|
||
|
||
/**
|
||
* 切换侧边栏展开/收起
|
||
*/
|
||
const toggle = useCallback(() => {
|
||
setIsOpen(prev => !prev);
|
||
}, []);
|
||
|
||
/**
|
||
* 加载实时行情(通过 Redux)
|
||
*/
|
||
const loadRealtimeQuotes = useCallback(() => {
|
||
if (!userId) return;
|
||
dispatch(loadWatchlistQuotes());
|
||
}, [userId, dispatch]);
|
||
|
||
/**
|
||
* 加载所有数据(自选股和关注事件都从 Redux 获取)
|
||
*/
|
||
const loadData = useCallback(() => {
|
||
if (!userId) return;
|
||
|
||
// 自选股通过 Redux 加载
|
||
dispatch(loadWatchlist());
|
||
dispatch(loadWatchlistQuotes());
|
||
|
||
// 关注事件和评论通过 Redux 加载
|
||
dispatch(loadFollowingEvents());
|
||
dispatch(loadEventComments());
|
||
}, [userId, dispatch]);
|
||
|
||
/**
|
||
* 刷新数据
|
||
*/
|
||
const refresh = useCallback(async () => {
|
||
await loadData();
|
||
}, [loadData]);
|
||
|
||
/**
|
||
* 取消关注股票(通过 Redux)
|
||
*/
|
||
const unwatchStock = useCallback(async (stockCode) => {
|
||
if (!userId) return;
|
||
try {
|
||
// 找到股票名称
|
||
const stockItem = watchlist.find(s => s.stock_code === stockCode);
|
||
const stockName = stockItem?.stock_name || '';
|
||
|
||
// 通过 Redux action 移除(乐观更新)
|
||
await dispatch(toggleWatchlist({
|
||
stockCode,
|
||
stockName,
|
||
isInWatchlist: true // 表示当前在自选股中,需要移除
|
||
})).unwrap();
|
||
|
||
logger.debug('GlobalSidebar', 'unwatchStock 成功', { stockCode });
|
||
} catch (error) {
|
||
logger.error('GlobalSidebar', 'unwatchStock', error, { stockCode, userId });
|
||
}
|
||
}, [userId, dispatch, watchlist]);
|
||
|
||
/**
|
||
* 取消关注事件(通过 Redux)
|
||
*/
|
||
const unfollowEvent = useCallback(async (eventId) => {
|
||
if (!userId) return;
|
||
try {
|
||
// 通过 Redux action 取消关注(乐观更新)
|
||
await dispatch(toggleFollowEvent({
|
||
eventId,
|
||
isFollowing: true // 表示当前已关注,需要取消
|
||
})).unwrap();
|
||
|
||
logger.debug('GlobalSidebar', 'unfollowEvent 成功', { eventId });
|
||
} catch (error) {
|
||
logger.error('GlobalSidebar', 'unfollowEvent', error, { eventId, userId });
|
||
// 失败时重新加载列表
|
||
dispatch(loadFollowingEvents());
|
||
}
|
||
}, [userId, dispatch]);
|
||
|
||
// 用户登录后加载数据
|
||
useEffect(() => {
|
||
if (user && !hasLoadedRef.current) {
|
||
console.log('[GlobalSidebar] 用户登录,加载数据');
|
||
hasLoadedRef.current = true;
|
||
loadData();
|
||
}
|
||
|
||
// 用户登出时重置(所有状态由 Redux 管理)
|
||
if (!user) {
|
||
hasLoadedRef.current = false;
|
||
}
|
||
}, [user, loadData]);
|
||
|
||
// 页面可见性变化时刷新数据
|
||
useEffect(() => {
|
||
const onVisibilityChange = () => {
|
||
if (document.visibilityState === 'visible' && user) {
|
||
console.log('[GlobalSidebar] 页面可见,刷新数据');
|
||
loadData();
|
||
}
|
||
};
|
||
|
||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||
return () => document.removeEventListener('visibilitychange', onVisibilityChange);
|
||
}, [user, loadData]);
|
||
|
||
// 定时刷新实时行情(每分钟一次,两个面板共用)
|
||
useEffect(() => {
|
||
if (watchlist.length > 0 && userId) {
|
||
const interval = setInterval(() => {
|
||
console.log('[GlobalSidebar] 定时刷新行情');
|
||
dispatch(loadWatchlistQuotes());
|
||
}, 60000);
|
||
|
||
return () => clearInterval(interval);
|
||
}
|
||
}, [watchlist.length, userId, dispatch]);
|
||
|
||
const value = {
|
||
// 状态
|
||
isOpen,
|
||
toggle,
|
||
|
||
// 数据(watchlist 和 realtimeQuotes 从 Redux 获取)
|
||
watchlist,
|
||
realtimeQuotes,
|
||
followingEvents,
|
||
eventComments,
|
||
|
||
// 加载状态
|
||
loading: watchlistLoading || eventsLoading,
|
||
quotesLoading,
|
||
|
||
// 方法
|
||
refresh,
|
||
loadRealtimeQuotes,
|
||
unwatchStock,
|
||
unfollowEvent,
|
||
};
|
||
|
||
return (
|
||
<GlobalSidebarContext.Provider value={value}>
|
||
{children}
|
||
</GlobalSidebarContext.Provider>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* useGlobalSidebar - 获取全局侧边栏 Context
|
||
*/
|
||
export const useGlobalSidebar = () => {
|
||
const context = useContext(GlobalSidebarContext);
|
||
if (!context) {
|
||
throw new Error('useGlobalSidebar must be used within a GlobalSidebarProvider');
|
||
}
|
||
return context;
|
||
};
|
||
|
||
export default GlobalSidebarContext;
|