diff --git a/src/contexts/GlobalSidebarContext.js b/src/contexts/GlobalSidebarContext.js new file mode 100644 index 00000000..ed6078df --- /dev/null +++ b/src/contexts/GlobalSidebarContext.js @@ -0,0 +1,262 @@ +/** + * GlobalSidebarContext - 全局右侧工具栏状态管理 + * + * 管理侧边栏的展开/收起状态和数据加载 + */ + +import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react'; +import { useAuth } from './AuthContext'; +import { logger } from '@/utils/logger'; +import { getApiBase } from '@/utils/apiConfig'; + +const GlobalSidebarContext = createContext(null); + +/** + * GlobalSidebarProvider - 全局侧边栏 Provider + */ +export const GlobalSidebarProvider = ({ children }) => { + const { user } = useAuth(); + const userId = user?.id; + + // 侧边栏展开/收起状态 + const [isOpen, setIsOpen] = useState(true); + + // 数据状态 + const [watchlist, setWatchlist] = useState([]); + const [realtimeQuotes, setRealtimeQuotes] = useState({}); + const [followingEvents, setFollowingEvents] = useState([]); + const [eventComments, setEventComments] = useState([]); + const [loading, setLoading] = useState(false); + const [quotesLoading, setQuotesLoading] = useState(false); + + // 防止重复加载 + const hasLoadedRef = useRef(false); + + /** + * 切换侧边栏展开/收起 + */ + const toggle = useCallback(() => { + setIsOpen(prev => !prev); + }, []); + + /** + * 加载实时行情 + */ + const loadRealtimeQuotes = useCallback(async () => { + if (!userId) return; + + try { + setQuotesLoading(true); + const base = getApiBase(); + const response = await fetch(base + '/api/account/watchlist/realtime', { + credentials: 'include', + cache: 'no-store' + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + const quotesMap = {}; + data.data.forEach(item => { + quotesMap[item.stock_code] = item; + }); + setRealtimeQuotes(quotesMap); + } + } + } catch (error) { + logger.error('GlobalSidebar', 'loadRealtimeQuotes', error, { + userId, + timestamp: new Date().toISOString() + }); + } finally { + setQuotesLoading(false); + } + }, [userId]); + + /** + * 加载所有数据(自选股、关注事件、评论) + */ + const loadData = useCallback(async () => { + if (!userId) return; + + try { + setLoading(true); + const base = getApiBase(); + const ts = Date.now(); + + const [w, e, c] = await Promise.all([ + fetch(base + `/api/account/watchlist?_=${ts}`, { credentials: 'include', cache: 'no-store' }), + fetch(base + `/api/account/events/following?_=${ts}`, { credentials: 'include', cache: 'no-store' }), + fetch(base + `/api/account/events/posts?_=${ts}`, { credentials: 'include', cache: 'no-store' }), + ]); + + const jw = await w.json(); + const je = await e.json(); + const jc = await c.json(); + + if (jw.success) { + const watchlistData = Array.isArray(jw.data) ? jw.data : []; + setWatchlist(watchlistData); + + // 加载实时行情 + if (watchlistData.length > 0) { + loadRealtimeQuotes(); + } + } + + if (je.success) { + setFollowingEvents(Array.isArray(je.data) ? je.data : []); + } + + if (jc.success) { + setEventComments(Array.isArray(jc.data) ? jc.data : []); + } + } catch (err) { + logger.error('GlobalSidebar', 'loadData', err, { + userId, + timestamp: new Date().toISOString() + }); + } finally { + setLoading(false); + } + }, [userId, loadRealtimeQuotes]); + + /** + * 刷新数据 + */ + const refresh = useCallback(async () => { + await loadData(); + }, [loadData]); + + /** + * 取消关注股票 + */ + const unwatchStock = useCallback(async (stockCode) => { + if (!userId) return; + try { + const base = getApiBase(); + const response = await fetch(base + '/api/account/watchlist/remove', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ stock_code: stockCode }), + }); + + if (response.ok) { + // 本地更新,不用重新请求 + setWatchlist(prev => prev.filter(s => s.stock_code !== stockCode)); + setRealtimeQuotes(prev => { + const newQuotes = { ...prev }; + delete newQuotes[stockCode]; + return newQuotes; + }); + } + } catch (error) { + logger.error('GlobalSidebar', 'unwatchStock', error, { stockCode, userId }); + } + }, [userId]); + + /** + * 取消关注事件 + */ + const unfollowEvent = useCallback(async (eventId) => { + if (!userId) return; + try { + const base = getApiBase(); + const response = await fetch(base + `/api/events/${eventId}/unfollow`, { + method: 'POST', + credentials: 'include', + }); + + if (response.ok) { + // 本地更新 + setFollowingEvents(prev => prev.filter(e => e.id !== eventId)); + } + } catch (error) { + logger.error('GlobalSidebar', 'unfollowEvent', error, { eventId, userId }); + } + }, [userId]); + + // 用户登录后加载数据 + useEffect(() => { + if (user && !hasLoadedRef.current) { + console.log('[GlobalSidebar] 用户登录,加载数据'); + hasLoadedRef.current = true; + loadData(); + } + + // 用户登出时重置 + if (!user) { + hasLoadedRef.current = false; + setWatchlist([]); + setRealtimeQuotes({}); + setFollowingEvents([]); + setEventComments([]); + } + }, [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) { + const interval = setInterval(() => { + loadRealtimeQuotes(); + }, 60000); + + return () => clearInterval(interval); + } + }, [watchlist.length, loadRealtimeQuotes]); + + const value = { + // 状态 + isOpen, + toggle, + + // 数据 + watchlist, + realtimeQuotes, + followingEvents, + eventComments, + + // 加载状态 + loading, + quotesLoading, + + // 方法 + refresh, + loadRealtimeQuotes, + unwatchStock, + unfollowEvent, + }; + + return ( + + {children} + + ); +}; + +/** + * 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;