import { useEffect, useState, useCallback, useRef } from 'react'; import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom'; import { LayoutDashboard, FolderKanban, FileText, MessageSquare, Flag, Users, Images, Settings, ArrowLeft, Moon, Sun, Menu, X, Award, Link2, BookOpen, Globe2, Activity, } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { useAuth } from '../hooks/useAuth'; import { useTheme, useMediaQuery } from '../hooks/useTheme'; import { useOverlayA11y } from '../hooks/useOverlayA11y'; import { notify } from '@/lib/notify'; import { cn } from '@/lib/utils'; import BackToTop from '../components/BackToTop'; import { loginPath } from '../utils/authRedirect'; import { useSiteBranding } from '../hooks/useSiteBranding'; import { useNoIndexSEO } from '../hooks/usePageSEO'; import SiteBrandMark from '../components/SiteBrandMark'; import { api } from '../api/client'; type BadgeKey = 'posts' | 'comments' | 'reports' | 'links'; type NavItem = { to: string; label: string; icon: typeof LayoutDashboard; badgeKey?: BadgeKey; /** 仅社区枢纽开启时显示 */ hubOnly?: boolean; }; type NavGroup = { label: string; items: NavItem[]; }; /** 信息架构:概览 → 内容审核 → 社区 → 系统(2026 管理台常见分层) */ const NAV_GROUPS: NavGroup[] = [ { label: '概览', items: [ { to: '/admin/dashboard', label: '仪表盘', icon: LayoutDashboard }, { to: '/admin/monitor', label: '网站监控', icon: Activity }, { to: '/admin/community', label: '公网实例', icon: Globe2, hubOnly: true }, ], }, { label: '内容审核', items: [ { to: '/admin/posts', label: '帖子管理', icon: FileText, badgeKey: 'posts' }, { to: '/admin/comments', label: '评论管理', icon: MessageSquare, badgeKey: 'comments' }, { to: '/admin/reports', label: '举报管理', icon: Flag, badgeKey: 'reports' }, ], }, { label: '社区', items: [ { to: '/admin/boards', label: '板块管理', icon: FolderKanban }, { to: '/admin/pages', label: '单页管理', icon: BookOpen }, { to: '/admin/users', label: '用户管理', icon: Users }, { to: '/admin/badges', label: '徽章管理', icon: Award }, { to: '/admin/links', label: '友情链接', icon: Link2, badgeKey: 'links' }, ], }, { label: '系统', items: [ { to: '/admin/media', label: '媒体库', icon: Images }, { to: '/admin/settings', label: '系统设置', icon: Settings }, ], }, ]; type PendingCounts = { posts: number; comments: number; reports: number; links: number; }; function formatNavBadge(n: number) { if (n <= 0) return null; return n > 99 ? '99+' : String(n); } /** React 管理后台布局,与前台 SPA 风格统一 */ export default function AdminLayout() { const { user, loading } = useAuth(); const { theme, toggle } = useTheme(); const { branding } = useSiteBranding(); useNoIndexSEO('管理后台'); const isNarrow = useMediaQuery('(max-width: 768px)'); const [navOpen, setNavOpen] = useState(false); const [pending, setPending] = useState({ posts: 0, comments: 0, reports: 0, links: 0 }); const [communityHub, setCommunityHub] = useState(false); const nav = useNavigate(); const location = useLocation(); const drawerRef = useRef(null); const closeRef = useRef(null); const closeNav = useCallback(() => setNavOpen(false), []); useOverlayA11y(isNarrow && navOpen, closeNav, drawerRef, { initialFocusRef: closeRef, }); const refreshPending = useCallback(() => { api.adminDashboard() .then(d => setPending({ posts: d.pending_posts ?? 0, comments: d.pending_comments ?? 0, reports: d.pending_reports ?? 0, links: d.pending_friend_links ?? 0, })) .catch(() => {}); }, []); useEffect(() => { if (loading || !user || user.role !== 'admin') return; api.adminSettings() .then(s => setCommunityHub(!!s.community?.hub_enabled)) .catch(() => setCommunityHub(false)); }, [loading, user]); useEffect(() => { if (loading || !user || user.role !== 'admin') return; refreshPending(); const onRefresh = () => refreshPending(); window.addEventListener('admin-pending-refresh', onRefresh); const timer = window.setInterval(refreshPending, 60_000); return () => { window.removeEventListener('admin-pending-refresh', onRefresh); window.clearInterval(timer); }; }, [loading, user, refreshPending]); useEffect(() => { if (loading) return; if (!user) { nav(loginPath('/admin/dashboard')); return; } if (user.role !== 'admin') { notify.warning('需要管理员权限'); nav('/'); } }, [user, loading, nav]); useEffect(() => { if (!user || user.role !== 'admin') return; refreshPending(); }, [user, location.pathname, refreshPending]); useEffect(() => { if (!isNarrow) setNavOpen(false); }, [isNarrow]); useEffect(() => { if (!(isNarrow && navOpen)) return; const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, [isNarrow, navOpen]); if (loading) { return
; } if (!user || user.role !== 'admin') return null; const renderNav = () => ( NAV_GROUPS.map(group => (
{group.label}
{group.items.filter(item => !item.hubOnly || communityHub).map(({ to, label, icon: Icon, badgeKey }) => { const badge = badgeKey ? formatNavBadge(pending[badgeKey]) : null; return ( cn('admin-nav-item', isActive && 'active')} onClick={closeNav} > {label} {badge && ( {badge} )} ); })}
)) ); return (
{isNarrow && ( )}
{branding.name}
管理后台
{user.nickname}
{!isNarrow && ( )}
{isNarrow && navOpen && (
)} ); } /** 管理页就绪状态(鉴权由 AdminLayout 负责,此处不再重复跳转) */ export function useAdminGuard() { const { user, loading } = useAuth(); return { user, loading, ready: !loading && !!user && user.role === 'admin' }; }