"use client"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; import type { LucideIcon } from "lucide-react"; import { LayoutDashboard, Users, Megaphone, Palette, ShieldCheck, ArrowLeft, ClipboardCheck, } from "lucide-react"; import type { User } from "@/lib/api"; import { apiAdminPendingCounts } from "@/lib/api"; import { canModerateAny, isAdminOrAbove, isSuperOrOwner, type RoleUserLike, } from "@/lib/roles"; import Avatar from "@/components/Avatar"; import RoleBadge from "@/components/RoleBadge"; interface NavItem { href: string; label: string; icon: LucideIcon; exact?: boolean; /** 按 /me 角色过滤可见性;最终权限以后端路由守卫为准 */ visible: (u: RoleUserLike) => boolean; /** 是否携带待审核角标 */ pendingBadge?: boolean; } // 后台导航项:exact 为精确匹配(仪表盘 /admin),其余按路径前缀匹配 const NAV_ITEMS: NavItem[] = [ { href: "/admin", label: "仪表盘", icon: LayoutDashboard, exact: true, visible: () => true }, { href: "/admin/moderation", label: "内容审核", icon: ClipboardCheck, visible: (u) => canModerateAny(u), pendingBadge: true, }, { href: "/admin/announcements", label: "公告管理", icon: Megaphone, visible: (u) => isAdminOrAbove(u.role), }, { href: "/admin/users", label: "用户权限", icon: Users, visible: (u) => isSuperOrOwner(u.role), }, { href: "/admin/appearance", label: "外观设置", icon: Palette, visible: (u) => isSuperOrOwner(u.role), }, ]; function isActive(pathname: string, href: string, exact?: boolean) { return exact ? pathname === href : pathname === href || pathname.startsWith(`${href}/`); } /** 待审核数量轮询(30 秒);仅内容审核入口可见时启用 */ function usePendingCounts(enabled: boolean) { const [counts, setCounts] = useState({ posts: 0, comments: 0 }); useEffect(() => { if (!enabled) return; let alive = true; const load = () => { apiAdminPendingCounts() .then((c) => { if (alive) setCounts(c); }) .catch(() => {}); }; load(); const t = setInterval(load, 30_000); const onVisible = () => { if (document.visibilityState === "visible") load(); }; document.addEventListener("visibilitychange", onVisible); return () => { alive = false; clearInterval(t); document.removeEventListener("visibilitychange", onVisible); }; }, [enabled]); return counts.posts + counts.comments; } function PendingBadge({ count }: { count: number }) { if (count <= 0) return null; return ( {count > 99 ? "99+" : count} ); } /** * 桌面端后台侧边栏:品牌头 + 分组导航 + 返回站点 + 身份卡。 * 选中态复用全站导航语言(--nav-active-bg 淡底 + accent 文字), * 与 j13-bcard / 顶部 pill 保持一致。 */ export function AdminSidebar({ user }: { user: User }) { const pathname = usePathname(); const items = NAV_ITEMS.filter((i) => i.visible(user)); const pendingTotal = usePendingCounts(items.some((i) => i.pendingBadge)); return (
{user.nickname}
@{user.username}