mirror of
https://github.com/massbug/judge4c.git
synced 2025-07-05 00:20:51 +00:00
423 lines
12 KiB
TypeScript
423 lines
12 KiB
TypeScript
import {
|
||
Users,
|
||
BookOpen,
|
||
CheckCircle,
|
||
Clock,
|
||
TrendingUp,
|
||
AlertCircle,
|
||
BarChart3,
|
||
Target,
|
||
Activity,
|
||
GraduationCapIcon,
|
||
} from "lucide-react";
|
||
import Link from "next/link";
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from "@/components/ui/card";
|
||
import prisma from "@/lib/prisma";
|
||
import { auth } from "@/lib/auth";
|
||
import { redirect } from "next/navigation";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Progress } from "@/components/ui/progress";
|
||
|
||
interface Stats {
|
||
totalUsers?: number;
|
||
totalProblems?: number;
|
||
totalSubmissions?: number;
|
||
totalStudents?: number;
|
||
completedProblems?: number;
|
||
}
|
||
|
||
interface Activity {
|
||
type: string;
|
||
title: string;
|
||
description: string;
|
||
time: Date;
|
||
status?: string;
|
||
}
|
||
|
||
export default async function DashboardPage() {
|
||
const session = await auth();
|
||
const user = session?.user;
|
||
|
||
if (!user) {
|
||
redirect("/sign-in");
|
||
}
|
||
|
||
// 获取用户的完整信息
|
||
const fullUser = await prisma.user.findUnique({
|
||
where: { id: user.id },
|
||
select: { id: true, name: true, email: true, image: true, role: true },
|
||
});
|
||
|
||
if (!fullUser) {
|
||
redirect("/sign-in");
|
||
}
|
||
|
||
// 根据用户角色获取不同的统计数据
|
||
let stats: Stats = {};
|
||
let recentActivity: Activity[] = [];
|
||
|
||
if (fullUser.role === "ADMIN") {
|
||
// 管理员统计
|
||
const [totalUsers, totalProblems, totalSubmissions, recentUsers] =
|
||
await Promise.all([
|
||
prisma.user.count(),
|
||
prisma.problem.count(),
|
||
prisma.submission.count(),
|
||
prisma.user.findMany({
|
||
take: 5,
|
||
orderBy: { createdAt: "desc" },
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
email: true,
|
||
role: true,
|
||
createdAt: true,
|
||
},
|
||
}),
|
||
]);
|
||
|
||
stats = { totalUsers, totalProblems, totalSubmissions };
|
||
recentActivity = recentUsers.map((user) => ({
|
||
type: "新用户注册",
|
||
title: user.name || user.email,
|
||
description: `角色: ${user.role}`,
|
||
time: user.createdAt,
|
||
}));
|
||
} else if (fullUser.role === "TEACHER") {
|
||
// 教师统计
|
||
const [totalStudents, totalProblems, totalSubmissions, recentSubmissions] =
|
||
await Promise.all([
|
||
prisma.user.count({ where: { role: "GUEST" } }),
|
||
prisma.problem.count({ where: { isPublished: true } }),
|
||
prisma.submission.count(),
|
||
prisma.submission.findMany({
|
||
take: 5,
|
||
orderBy: { createdAt: "desc" },
|
||
include: {
|
||
user: { select: { name: true, email: true } },
|
||
problem: {
|
||
select: {
|
||
displayId: true,
|
||
localizations: {
|
||
where: { type: "TITLE", locale: "zh" },
|
||
select: { content: true },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}),
|
||
]);
|
||
|
||
stats = { totalStudents, totalProblems, totalSubmissions };
|
||
recentActivity = recentSubmissions.map((sub) => ({
|
||
type: "学生提交",
|
||
title: `${sub.user.name || sub.user.email} 提交了题目 ${
|
||
sub.problem.displayId
|
||
}`,
|
||
description:
|
||
sub.problem.localizations[0]?.content || `题目${sub.problem.displayId}`,
|
||
time: sub.createdAt,
|
||
status: sub.status,
|
||
}));
|
||
} else {
|
||
// 学生统计
|
||
const [
|
||
totalProblems,
|
||
completedProblems,
|
||
totalSubmissions,
|
||
recentSubmissions,
|
||
] = await Promise.all([
|
||
prisma.problem.count({ where: { isPublished: true } }),
|
||
prisma.submission.count({
|
||
where: {
|
||
userId: user.id,
|
||
status: "AC",
|
||
},
|
||
}),
|
||
prisma.submission.count({ where: { userId: user.id } }),
|
||
prisma.submission.findMany({
|
||
where: { userId: user.id },
|
||
take: 5,
|
||
orderBy: { createdAt: "desc" },
|
||
include: {
|
||
problem: {
|
||
select: {
|
||
displayId: true,
|
||
localizations: {
|
||
where: { type: "TITLE", locale: "zh" },
|
||
select: { content: true },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}),
|
||
]);
|
||
|
||
stats = { totalProblems, completedProblems, totalSubmissions };
|
||
recentActivity = recentSubmissions.map((sub) => ({
|
||
type: "我的提交",
|
||
title: `题目 ${sub.problem.displayId}`,
|
||
description:
|
||
sub.problem.localizations[0]?.content || `题目${sub.problem.displayId}`,
|
||
time: sub.createdAt,
|
||
status: sub.status,
|
||
}));
|
||
}
|
||
|
||
const getRoleConfig = () => {
|
||
switch (fullUser.role) {
|
||
case "ADMIN":
|
||
return {
|
||
title: "系统管理后台",
|
||
description: "管理整个系统的用户、题目和统计数据",
|
||
stats: [
|
||
{
|
||
label: "总用户数",
|
||
value: stats.totalUsers,
|
||
icon: Users,
|
||
color: "text-blue-600",
|
||
},
|
||
{
|
||
label: "总题目数",
|
||
value: stats.totalProblems,
|
||
icon: BookOpen,
|
||
color: "text-green-600",
|
||
},
|
||
{
|
||
label: "总提交数",
|
||
value: stats.totalSubmissions,
|
||
icon: Activity,
|
||
color: "text-purple-600",
|
||
},
|
||
],
|
||
actions: [
|
||
{
|
||
label: "管理员管理",
|
||
href: "/dashboard/management",
|
||
icon: Target,
|
||
},
|
||
{
|
||
label: "用户管理",
|
||
href: "/dashboard/usermanagement/guest",
|
||
icon: Users,
|
||
},
|
||
{
|
||
label: "教师管理",
|
||
href: "/dashboard/usermanagement/teacher",
|
||
icon: GraduationCapIcon,
|
||
},
|
||
{
|
||
label: "题目管理",
|
||
href: "/dashboard/usermanagement/problem",
|
||
icon: BookOpen,
|
||
},
|
||
],
|
||
};
|
||
case "TEACHER":
|
||
return {
|
||
title: "教师教学平台",
|
||
description: "查看学生学习情况,管理教学资源",
|
||
stats: [
|
||
{
|
||
label: "学生数量",
|
||
value: stats.totalStudents,
|
||
icon: Users,
|
||
color: "text-blue-600",
|
||
},
|
||
{
|
||
label: "题目数量",
|
||
value: stats.totalProblems,
|
||
icon: BookOpen,
|
||
color: "text-green-600",
|
||
},
|
||
{
|
||
label: "提交数量",
|
||
value: stats.totalSubmissions,
|
||
icon: Activity,
|
||
color: "text-purple-600",
|
||
},
|
||
],
|
||
actions: [
|
||
{
|
||
label: "用户管理",
|
||
href: "/dashboard/usermanagement/guest",
|
||
icon: Users,
|
||
},
|
||
{
|
||
label: "题目管理",
|
||
href: "/dashboard/usermanagement/problem",
|
||
icon: BookOpen,
|
||
},
|
||
{
|
||
label: "完成情况",
|
||
href: "/dashboard/teacher/dashboard",
|
||
icon: BarChart3,
|
||
},
|
||
],
|
||
};
|
||
default:
|
||
return {
|
||
title: "我的学习中心",
|
||
description: "继续您的编程学习之旅",
|
||
stats: [
|
||
{
|
||
label: "总题目数",
|
||
value: stats.totalProblems,
|
||
icon: BookOpen,
|
||
color: "text-blue-600",
|
||
},
|
||
{
|
||
label: "已完成",
|
||
value: stats.completedProblems,
|
||
icon: CheckCircle,
|
||
color: "text-green-600",
|
||
},
|
||
{
|
||
label: "提交次数",
|
||
value: stats.totalSubmissions,
|
||
icon: Activity,
|
||
color: "text-purple-600",
|
||
},
|
||
],
|
||
actions: [
|
||
{
|
||
label: "我的进度",
|
||
href: "/dashboard/student/dashboard",
|
||
icon: TrendingUp,
|
||
},
|
||
{ label: "开始做题", href: "/problemset", icon: BookOpen },
|
||
{ label: "个人设置", href: "/dashboard/management", icon: Target },
|
||
],
|
||
};
|
||
}
|
||
};
|
||
|
||
const config = getRoleConfig();
|
||
const completionRate =
|
||
fullUser.role === "GUEST"
|
||
? (stats.totalProblems || 0) > 0
|
||
? ((stats.completedProblems || 0) / (stats.totalProblems || 1)) * 100
|
||
: 0
|
||
: 0;
|
||
|
||
return (
|
||
<div className="space-y-6 p-6">
|
||
{/* 欢迎区域 */}
|
||
<div className="space-y-2">
|
||
<h1 className="text-3xl font-bold tracking-tight">{config.title}</h1>
|
||
<p className="text-muted-foreground">{config.description}</p>
|
||
<div className="flex items-center gap-2">
|
||
<Badge variant="secondary">{fullUser.role}</Badge>
|
||
<span className="text-sm text-muted-foreground">
|
||
欢迎回来,{fullUser.name || fullUser.email}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 统计卡片 */}
|
||
<div className="grid gap-4 md:grid-cols-3">
|
||
{config.stats.map((stat, index) => (
|
||
<Card key={index}>
|
||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||
<CardTitle className="text-sm font-medium">
|
||
{stat.label}
|
||
</CardTitle>
|
||
<stat.icon className={`h-4 w-4 ${stat.color}`} />
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="text-2xl font-bold">{stat.value}</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
|
||
{/* 学生进度条 */}
|
||
{fullUser.role === "GUEST" && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Target className="h-5 w-5" />
|
||
学习进度
|
||
</CardTitle>
|
||
<CardDescription>
|
||
已完成 {stats.completedProblems || 0} / {stats.totalProblems || 0}{" "}
|
||
道题目
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Progress value={completionRate} className="w-full" />
|
||
<p className="mt-2 text-sm text-muted-foreground">
|
||
完成率: {completionRate.toFixed(1)}%
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 快速操作 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>快速操作</CardTitle>
|
||
<CardDescription>常用功能快速访问</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid gap-4 md:grid-cols-4">
|
||
{config.actions.map((action, index) => (
|
||
<Link key={index} href={action.href}>
|
||
<Button variant="outline" className="w-full justify-start">
|
||
<action.icon className="mr-2 h-4 w-4" />
|
||
{action.label}
|
||
</Button>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 最近活动 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>最近活动</CardTitle>
|
||
<CardDescription>查看最新的系统活动</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-4">
|
||
{recentActivity.length > 0 ? (
|
||
recentActivity.map((activity, index) => (
|
||
<div key={index} className="flex items-center space-x-4">
|
||
<div className="flex-shrink-0">
|
||
{activity.status === "AC" ? (
|
||
<CheckCircle className="h-5 w-5 text-green-500" />
|
||
) : activity.status ? (
|
||
<AlertCircle className="h-5 w-5 text-yellow-500" />
|
||
) : (
|
||
<Clock className="h-5 w-5 text-gray-500" />
|
||
)}
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium">{activity.title}</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{activity.description}
|
||
</p>
|
||
</div>
|
||
<div className="text-sm text-muted-foreground">
|
||
{new Date(activity.time).toLocaleDateString()}
|
||
</div>
|
||
</div>
|
||
))
|
||
) : (
|
||
<p className="text-sm text-muted-foreground">暂无活动</p>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|