mirror of
https://github.com/massbug/judge4c.git
synced 2025-07-04 07:40:51 +00:00
feat(admin): add prototype admin panel
- 新增管理员布局、仪表盘、侧边栏等组件 - 实现用户数量、问题数量等统计显示 - 添加 LSP 连接状态组件- 创建统计卡片组件用于展示各类数据 - 开发网格组件以支持灵活的布局
This commit is contained in:
parent
941f1a74fa
commit
aaa8629e76
21
src/app/(app)/admin/layout.tsx
Normal file
21
src/app/(app)/admin/layout.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { ReactNode } from "react";
|
||||
import { AdminSidebar } from "@/components/admin/sidebar";
|
||||
import { Header } from "@/components/header";
|
||||
|
||||
interface AdminLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function AdminLayout({ children }: AdminLayoutProps) {
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<AdminSidebar />
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/*<Header />*/}
|
||||
<main className="flex-1 p-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
27
src/app/(app)/admin/page.tsx
Normal file
27
src/app/(app)/admin/page.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { AdminSidebar } from "@/components/admin/sidebar";
|
||||
import { Header } from "@/components/header";
|
||||
import { AdminDashboard } from "@/components/admin/dashboard";
|
||||
import AdminLayout from "@/app/(app)/admin/layout";
|
||||
import type { ReactElement } from "react";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
export default async function AdminPage(): Promise<ReactElement> {
|
||||
const [userCount, problemCount] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.problem.count(),
|
||||
]);
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<div className="flex min-h-screen">
|
||||
<AdminSidebar />
|
||||
<div className="flex-1 flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1 p-6">
|
||||
<AdminDashboard userCount={userCount} problemCount={problemCount} />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
@ -5,6 +5,8 @@ import { getLocale } from "next-intl/server";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { SettingsDialog } from "@/components/settings-dialog";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Judge4c",
|
||||
@ -29,9 +31,13 @@ export default async function RootLayout({ children }: RootLayoutProps) {
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<SessionProvider>
|
||||
<SidebarProvider>
|
||||
<div className="w-full">{children}</div>
|
||||
<SettingsDialog />
|
||||
<Toaster position="top-right" />
|
||||
</SidebarProvider>
|
||||
</SessionProvider>
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
|
56
src/components/admin/dashboard.tsx
Normal file
56
src/components/admin/dashboard.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { Grid } from "@/components/ui/grid";
|
||||
import StatCard from "./stat-card";
|
||||
import { LspStatus } from "./lsp-status";
|
||||
import { ProblemsetTable } from "@/features/problemset/components/table";
|
||||
import { SubmissionTable } from "@/features/problems/submission/components/table";
|
||||
import prisma from "@/lib/prisma";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface AdminDashboardProps {
|
||||
userCount?: number;
|
||||
problemCount?: number;
|
||||
}
|
||||
|
||||
export const AdminDashboard = async ({
|
||||
userCount = 0,
|
||||
problemCount = 0
|
||||
}: AdminDashboardProps) => {
|
||||
// 获取统计数据显示
|
||||
const [usersCount, problemsCount, submissionsCount] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.problem.count(),
|
||||
prisma.submission.count(),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">仪表盘</h2>
|
||||
<p className="text-muted-foreground">
|
||||
系统运行状态概览
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Grid cols={3} gap={6}>
|
||||
<StatCard title="用户数量" value={userCount || usersCount} />
|
||||
<StatCard title="问题数量" value={problemCount || problemsCount} />
|
||||
<StatCard title="测评次数" value={submissionsCount} />
|
||||
</Grid>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<LspStatus />
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">最近提交记录</h3>
|
||||
{/* 临时使用空字符串作为占位符,实际应从数据库获取最新问题ID */}
|
||||
<SubmissionTable problemId="" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">问题列表</h3>
|
||||
<ProblemsetTable />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
10
src/components/admin/lsp-status.tsx
Normal file
10
src/components/admin/lsp-status.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { LspConnectionIndicator } from "@/features/problems/code/components/toolbar/controls/lsp-connection-indicator";
|
||||
|
||||
export const LspStatus = () => {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-semibold mb-2">LSP 连接状态</h3>
|
||||
<LspConnectionIndicator />
|
||||
</div>
|
||||
);
|
||||
};
|
119
src/components/admin/sidebar.tsx
Normal file
119
src/components/admin/sidebar.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
"use client"
|
||||
import { useSession } from "next-auth/react";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarRail,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { NavMain } from "@/components/nav-main";
|
||||
import { NavProjects } from "@/components/nav-projects";
|
||||
import { TeamSwitcher } from "@/components/team-switcher";
|
||||
import { NavUser } from "@/components/nav-user";
|
||||
import {
|
||||
AudioWaveform,
|
||||
Bot,
|
||||
Command,
|
||||
Frame,
|
||||
GalleryVerticalEnd,
|
||||
Map,
|
||||
PieChart,
|
||||
Settings2,
|
||||
SquareTerminal,
|
||||
} from "lucide-react";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
|
||||
|
||||
// 如果 adminData.teams 没有在别处定义,请取消注释下面的代码并提供实际值
|
||||
/*
|
||||
const teams = [
|
||||
// 在这里放置你的团队数据
|
||||
];
|
||||
*/
|
||||
|
||||
// 创建图标映射
|
||||
type LucideIconMap = {
|
||||
[key: string]: typeof AudioWaveform;
|
||||
};
|
||||
|
||||
const lucideIconMap: LucideIconMap = {
|
||||
AudioWaveform,
|
||||
Bot,
|
||||
Command,
|
||||
Frame,
|
||||
GalleryVerticalEnd,
|
||||
Map,
|
||||
PieChart,
|
||||
Settings2,
|
||||
SquareTerminal,
|
||||
};
|
||||
|
||||
const adminData = {
|
||||
// teams: [
|
||||
// {
|
||||
// name: "Admin Team",
|
||||
// logo: GalleryVerticalEnd,
|
||||
// plan: "Enterprise",
|
||||
// },
|
||||
// ],
|
||||
navMain: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/admin",
|
||||
icon: Settings2,
|
||||
items: [
|
||||
{
|
||||
title: "Overview",
|
||||
url: "/admin",
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
url: "/admin/users",
|
||||
},
|
||||
{
|
||||
title: "Problems",
|
||||
url: "/admin/problems",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
name: "System Monitoring",
|
||||
url: "/admin/monitoring",
|
||||
icon: PieChart,
|
||||
},
|
||||
{
|
||||
name: "Admin Tools",
|
||||
url: "/admin/tools",
|
||||
icon: Command,
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
export const AdminSidebar = ({ ...props }: React.ComponentProps<typeof Sidebar>) => {
|
||||
const { data: session } = useSession();
|
||||
const user = {
|
||||
name: session?.user?.name || "Admin",
|
||||
email: session?.user?.email || "admin@example.com",
|
||||
avatar: session?.user?.avatar || ""
|
||||
};
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
{/*<SidebarHeader>*/}
|
||||
{/* <TeamSwitcher teams={adminData.teams} />*/}
|
||||
{/*</SidebarHeader>*/}
|
||||
<SidebarContent>
|
||||
<NavMain items={adminData.navMain} />
|
||||
<NavProjects projects={adminData.projects} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={user} />
|
||||
</SidebarFooter>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
);
|
||||
};
|
21
src/components/admin/stat-card.tsx
Normal file
21
src/components/admin/stat-card.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: number;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function StatCard({ title, value, icon }: StatCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
{icon && <div className="text-muted-foreground">{icon}</div>}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value.toLocaleString()}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
47
src/components/ui/grid.tsx
Normal file
47
src/components/ui/grid.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const gridVariants = cva(
|
||||
"grid",
|
||||
{
|
||||
variants: {
|
||||
cols: {
|
||||
1: "grid-cols-1",
|
||||
2: "grid-cols-2",
|
||||
3: "grid-cols-3",
|
||||
4: "grid-cols-4",
|
||||
},
|
||||
gap: {
|
||||
0: "gap-0",
|
||||
2: "gap-2",
|
||||
4: "gap-4",
|
||||
6: "gap-6",
|
||||
8: "gap-8",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
cols: 1,
|
||||
gap: 4,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type GridProps = React.HTMLAttributes<HTMLDivElement> &
|
||||
VariantProps<typeof gridVariants> & {
|
||||
as?: React.ElementType
|
||||
}
|
||||
|
||||
const Grid = React.forwardRef<HTMLDivElement, GridProps>(
|
||||
({ className, cols, gap, as: Comp = "div", ...props }, ref) => {
|
||||
return (
|
||||
<Comp
|
||||
className={cn(gridVariants({ cols, gap }), className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export { Grid }
|
Loading…
Reference in New Issue
Block a user