diff --git a/src/app/(app)/admin/layout.tsx b/src/app/(app)/admin/layout.tsx new file mode 100644 index 0000000..1206a8f --- /dev/null +++ b/src/app/(app)/admin/layout.tsx @@ -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 ( +
+ +
+ {/*
*/} +
+ {children} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/(app)/admin/page.tsx b/src/app/(app)/admin/page.tsx new file mode 100644 index 0000000..b987b3a --- /dev/null +++ b/src/app/(app)/admin/page.tsx @@ -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 { + const [userCount, problemCount] = await Promise.all([ + prisma.user.count(), + prisma.problem.count(), + ]); + + return ( + +
+ +
+
+
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d50795c..4669ff6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 > -
{children}
- - + + +
{children}
+ + +
+
diff --git a/src/components/admin/dashboard.tsx b/src/components/admin/dashboard.tsx new file mode 100644 index 0000000..05f4cae --- /dev/null +++ b/src/components/admin/dashboard.tsx @@ -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 ( +
+
+

仪表盘

+

+ 系统运行状态概览 +

+
+ + + + + + + +
+ + +
+

最近提交记录

+ {/* 临时使用空字符串作为占位符,实际应从数据库获取最新问题ID */} + +
+
+ +
+

问题列表

+ +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/admin/lsp-status.tsx b/src/components/admin/lsp-status.tsx new file mode 100644 index 0000000..05045a4 --- /dev/null +++ b/src/components/admin/lsp-status.tsx @@ -0,0 +1,10 @@ +import { LspConnectionIndicator } from "@/features/problems/code/components/toolbar/controls/lsp-connection-indicator"; + +export const LspStatus = () => { + return ( +
+

LSP 连接状态

+ +
+ ); +}; \ No newline at end of file diff --git a/src/components/admin/sidebar.tsx b/src/components/admin/sidebar.tsx new file mode 100644 index 0000000..03d8e37 --- /dev/null +++ b/src/components/admin/sidebar.tsx @@ -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) => { + const { data: session } = useSession(); + const user = { + name: session?.user?.name || "Admin", + email: session?.user?.email || "admin@example.com", + avatar: session?.user?.avatar || "" + }; + + return ( + + {/**/} + {/* */} + {/**/} + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/src/components/admin/stat-card.tsx b/src/components/admin/stat-card.tsx new file mode 100644 index 0000000..75cc940 --- /dev/null +++ b/src/components/admin/stat-card.tsx @@ -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 ( + + + {title} + {icon &&
{icon}
} +
+ +
{value.toLocaleString()}
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/ui/grid.tsx b/src/components/ui/grid.tsx new file mode 100644 index 0000000..67bfa59 --- /dev/null +++ b/src/components/ui/grid.tsx @@ -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 & + VariantProps & { + as?: React.ElementType + } + +const Grid = React.forwardRef( + ({ className, cols, gap, as: Comp = "div", ...props }, ref) => { + return ( + + ) + } +) + +export { Grid } \ No newline at end of file