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 { NextIntlClientProvider } from "next-intl";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { SettingsDialog } from "@/components/settings-dialog";
|
import { SettingsDialog } from "@/components/settings-dialog";
|
||||||
|
import { SessionProvider } from "next-auth/react";
|
||||||
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Judge4c",
|
title: "Judge4c",
|
||||||
@ -29,9 +31,13 @@ export default async function RootLayout({ children }: RootLayoutProps) {
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<div className="w-full">{children}</div>
|
<SessionProvider>
|
||||||
<SettingsDialog />
|
<SidebarProvider>
|
||||||
<Toaster position="top-right" />
|
<div className="w-full">{children}</div>
|
||||||
|
<SettingsDialog />
|
||||||
|
<Toaster position="top-right" />
|
||||||
|
</SidebarProvider>
|
||||||
|
</SessionProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</body>
|
</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