f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
105 lines
3.5 KiB
TypeScript
105 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter, usePathname } from "next/navigation";
|
|
import { useAuthStore } from "@/stores/auth";
|
|
import { Header } from "@/components/layout/header";
|
|
import Link from "next/link";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
BarChart3,
|
|
TrendingUp,
|
|
AppWindow,
|
|
CheckCircle,
|
|
Users,
|
|
Bot,
|
|
ClipboardList,
|
|
ShieldCheck,
|
|
Menu,
|
|
X,
|
|
} from "lucide-react";
|
|
import type { LucideIcon } from "lucide-react";
|
|
|
|
const adminNavItems: { href: string; label: string; icon: LucideIcon }[] = [
|
|
{ href: "/dashboard", label: "数据总览", icon: BarChart3 },
|
|
{ href: "/analytics", label: "使用分析", icon: TrendingUp },
|
|
{ href: "/apps", label: "应用管理", icon: AppWindow },
|
|
{ href: "/reviews", label: "审核队列", icon: CheckCircle },
|
|
{ href: "/users", label: "人员管理", icon: Users },
|
|
{ href: "/models", label: "模型管理", icon: Bot },
|
|
{ href: "/audit", label: "审计日志", icon: ClipboardList },
|
|
{ href: "/security", label: "安全管理", icon: ShieldCheck },
|
|
];
|
|
|
|
export default function AdminLayout({ children }: { children: React.ReactNode }) {
|
|
const user = useAuthStore((s) => s.user);
|
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
|
const isLoading = useAuthStore((s) => s.isLoading);
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!isLoading && (!isAuthenticated || !["admin", "super_admin"].includes(user?.role || ""))) {
|
|
router.replace("/store");
|
|
}
|
|
}, [isAuthenticated, isLoading, user, router]);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-screen items-center justify-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isAuthenticated) return null;
|
|
|
|
return (
|
|
<div className="flex min-h-screen flex-col">
|
|
<Header />
|
|
<div className="flex flex-1 relative">
|
|
{/* 手机端侧边栏切换按钮 */}
|
|
<button
|
|
className="md:hidden fixed bottom-4 right-4 z-50 p-3 rounded-full bg-primary text-primary-foreground shadow-lg"
|
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
|
>
|
|
{sidebarOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
</button>
|
|
|
|
{/* 手机端遮罩层 */}
|
|
{sidebarOpen && (
|
|
<div
|
|
className="fixed inset-0 z-40 bg-black/40 md:hidden"
|
|
onClick={() => setSidebarOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
<aside
|
|
className={`fixed inset-y-[3.5rem] left-0 z-50 w-56 border-r bg-background transition-transform duration-200 md:static md:inset-y-0 md:translate-x-0 ${
|
|
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
|
}`}
|
|
>
|
|
<nav className="p-3 space-y-1">
|
|
{adminNavItems.map((item) => (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
onClick={() => setSidebarOpen(false)}
|
|
className={cn(
|
|
"flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors",
|
|
pathname === item.href ? "bg-primary text-primary-foreground" : "hover:bg-muted",
|
|
)}
|
|
>
|
|
<item.icon className="h-4 w-4" />
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
</aside>
|
|
<main className="flex-1 p-3 md:p-6 min-w-0">{children}</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|