feat(contexts): 新增 GlobalSidebarContext 全局侧边栏状态管理
- 管理侧边栏展开/收起状态 (isOpen, toggle) - 统一管理数据加载(自选股、关注事件、评论) - 实时行情定时刷新(每分钟) - 页面可见性变化时自动刷新数据 - 用户登录/登出时自动管理数据生命周期 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
262
src/contexts/GlobalSidebarContext.js
Normal file
262
src/contexts/GlobalSidebarContext.js
Normal file
@@ -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 (
|
||||||
|
<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;
|
||||||
Reference in New Issue
Block a user