import { useState, useEffect, useCallback, Suspense } from 'react'; import PageLoader from '../components/PageLoader'; import { Outlet, useNavigate, useSearchParams, useLocation } from 'react-router-dom'; import { Moon, Sun, Search, Plus } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { useAuth } from '../hooks/useAuth'; import { useTheme, useMediaQuery } from '../hooks/useTheme'; import { api } from '../api/client'; import type { Board, PostItem, Notification, OnlineStats, ForumStats } from '../api/types'; import { getCachedBoards, getCachedStats, setCachedBoards, setCachedStats } from '../utils/layoutCache'; import Sidebar, { isNeutralSidebarRoute } from '../components/Sidebar'; import RightPanel from '../components/RightPanel'; import { useForumLimits } from '../hooks/useForumLimits'; import { buildHomeUrl, parseFeedSort } from '../components/FeedSortBar'; import { notify } from '@/lib/notify'; export default function MainLayout() { const { user, loading: authLoading, logout } = useAuth(); const { theme, toggle } = useTheme(); const isMobile = useMediaQuery('(max-width: 768px)'); const nav = useNavigate(); const loc = useLocation(); const [params] = useSearchParams(); const isCompose = loc.pathname.startsWith('/compose') || /\/post\/\d+\/edit$/.test(loc.pathname); const [boards, setBoards] = useState(() => getCachedBoards()); const [stats, setStats] = useState(() => getCachedStats()); const [layoutReady, setLayoutReady] = useState(() => getCachedBoards().length > 0 || !!getCachedStats()); const [hot, setHot] = useState([]); const [notifications, setNotifications] = useState([]); const [online, setOnline] = useState(null); const [boardId, setBoardId] = useState(Number(params.get('board')) || 0); const [keyword, setKeyword] = useState(params.get('keyword') || ''); const feedSort = parseFeedSort(params.get('sort')); const { limits: forumLimits } = useForumLimits(); useEffect(() => { setBoardId(Number(params.get('board')) || 0); }, [params]); useEffect(() => { setKeyword(params.get('keyword') || ''); }, [params]); const refreshBoards = useCallback(() => { Promise.all([ api.boards().then(d => { const next = d.boards ?? []; setBoards(next); setCachedBoards(next); return next; }).catch(() => [] as Board[]), api.stats().then(next => { setStats(next); setCachedStats(next); return next; }).catch(() => null), ]).finally(() => setLayoutReady(true)); }, []); const refreshOnline = useCallback(() => { api.online().then(d => { setOnline({ count: d.count ?? 0, members: d.members ?? 0, guests: d.guests ?? 0, users: Array.isArray(d.users) ? d.users : [], }); }).catch(() => {}); }, []); useEffect(() => { refreshBoards(); api.hotPosts().then(d => setHot(Array.isArray(d.posts) ? d.posts : [])).catch(() => {}); api.notifications().then(d => setNotifications(Array.isArray(d.notifications) ? d.notifications : [])).catch(() => {}); refreshOnline(); api.presence().catch(() => {}); const onlineTimer = setInterval(refreshOnline, 30000); const presenceTimer = setInterval(() => api.presence().catch(() => {}), 60000); const onRefresh = () => refreshBoards(); window.addEventListener('boards-refresh', onRefresh); return () => { clearInterval(onlineTimer); clearInterval(presenceTimer); window.removeEventListener('boards-refresh', onRefresh); }; }, [refreshBoards, refreshOnline]); const doSearch = () => { const kw = keyword.trim(); if (!kw) { nav('/'); return; } const len = [...kw].length; if (forumLimits.search_keyword_min > 0 && len < forumLimits.search_keyword_min) { notify.warning(`搜索关键词至少 ${forumLimits.search_keyword_min} 个字`); return; } if (forumLimits.search_keyword_max > 0 && len > forumLimits.search_keyword_max) { notify.warning(`搜索关键词最多 ${forumLimits.search_keyword_max} 个字`); return; } nav(`/?keyword=${encodeURIComponent(kw)}`); }; const userInitial = user?.nickname?.charAt(0) || '?'; const mobileActiveBoard = isNeutralSidebarRoute(loc.pathname) ? -1 : boardId; return (
{!isCompose && (
setKeyword(e.target.value)} maxLength={forumLimits.search_keyword_max > 0 ? forumLimits.search_keyword_max : undefined} onKeyDown={e => e.key === 'Enter' && doSearch()} /> {keyword && ( )}
)}
{!isCompose && ( )}
{authLoading ? ( ) : user ? ( nav('/profile')}>个人中心 nav('/favorites')}>我的收藏 {user.role === 'admin' && ( <> nav('/admin/dashboard')}>管理后台 )} logout().then(() => nav('/login'))}> 退出登录 ) : ( )}
{!isCompose && ( )}
{isMobile && !isCompose && (
{ setBoardId(0); nav(buildHomeUrl(0, feedSort)); }} >全部 {boards.map(b => ( { setBoardId(b.id); nav(buildHomeUrl(b.id, feedSort)); }} >{b.name} ))}
)} }>
{!isCompose && ( )}
); } export type LayoutCtx = { boardId: number; keyword: string; setBoardId: (id: number) => void; boards: Board[]; stats: ForumStats | null; layoutReady: boolean; refreshBoards: () => void; isMobile: boolean; };