From 0695dd2f61cfedd9c69366f848abd04dda857077 Mon Sep 17 00:00:00 2001 From: cfngc4594 Date: Sat, 21 Jun 2025 23:19:49 +0800 Subject: [PATCH] refactor: format and relocate code --- prisma/check-problem-submissions.ts | 67 -- prisma/fill-testcase-results.ts | 37 - prisma/generate-user-data.ts | 118 --- prisma/test-student-dashboard.ts | 144 ---- prisma/test-student-data-access.ts | 79 -- src/app/(app)/problemset/layout.tsx | 2 +- .../admin/problems/[problemId]/edit/page.tsx | 17 - .../(protected)/(protect-admin)/layout.tsx | 11 - .../_actions => actions}/student-dashboard.ts | 174 ++-- .../_actions => actions}/teacher-dashboard.ts | 115 +-- .../problems/[problemId]/edit/layout.tsx | 2 +- .../admin/problems/[problemId]/edit/page.tsx | 13 + src/app/(protected)/dashboard/layout.tsx | 14 +- .../management/actions/changePassword.ts | 5 +- .../management/actions/getUserInfo.ts | 3 +- .../dashboard/management/actions/index.ts | 3 +- .../management/actions/updateUserInfo.ts | 3 +- .../management/change-password/page.tsx | 47 +- .../(protected)/dashboard/management/page.tsx | 54 +- .../dashboard/management/profile/page.tsx | 110 ++- src/app/(protected)/dashboard/page.tsx | 293 +++++-- .../student/dashboard/page.tsx | 83 +- .../teacher/dashboard/page.tsx | 93 ++- .../teacher/dashboard/test-data.tsx | 22 +- .../usermanagement/_actions/problemActions.ts | 14 - .../usermanagement/_actions/userActions.ts | 46 - .../_components/GenericLayout.tsx | 10 - .../usermanagement/actions/problemActions.ts | 17 + .../usermanagement/actions/userActions.ts | 47 ++ .../dashboard/usermanagement/admin/layout.tsx | 10 +- .../dashboard/usermanagement/admin/page.tsx | 8 +- .../components/GenericLayout.tsx | 15 + .../ProtectedLayout.tsx | 11 +- .../dashboard/usermanagement/guest/layout.tsx | 16 +- .../dashboard/usermanagement/guest/page.tsx | 8 +- .../usermanagement/problem/layout.tsx | 16 +- .../dashboard/usermanagement/problem/page.tsx | 8 +- .../usermanagement/teacher/layout.tsx | 10 +- .../dashboard/usermanagement/teacher/page.tsx | 8 +- src/app/globals.css | 41 +- src/app/layout.tsx | 3 +- .../UncompletedProject/sharedialog.tsx | 10 +- .../UncompletedProject/wrongbook-dialog.tsx | 111 ++- src/components/dashboard-button.tsx | 16 + src/components/dynamic-breadcrumb.tsx | 98 ++- src/components/nav-main.tsx | 35 +- src/components/nav-projects.tsx | 81 +- src/components/nav-secondary.tsx | 17 +- src/components/nav-user.tsx | 65 +- src/components/sidebar/admin-sidebar.tsx | 34 +- src/components/sidebar/app-sidebar.tsx | 20 +- src/components/sidebar/teacher-sidebar.tsx | 30 +- src/components/user-avatar.tsx | 2 + .../admin/ui/layouts/protected-layout.tsx | 32 + .../admin/ui/views/problem-edit-view.tsx | 38 +- .../components/generic-page.tsx | 33 +- .../user-management/components/user-table.tsx | 788 +++++++++++------- src/features/user-management/config/admin.ts | 23 +- .../user-management/config/base-config.ts | 80 +- src/features/user-management/config/guest.ts | 9 +- .../user-management/config/problem.ts | 18 +- .../user-management/config/teacher.ts | 9 +- src/lib/auth.ts | 5 - tailwind.config.ts | 162 ++-- 64 files changed, 1799 insertions(+), 1714 deletions(-) delete mode 100644 prisma/check-problem-submissions.ts delete mode 100644 prisma/fill-testcase-results.ts delete mode 100644 prisma/generate-user-data.ts delete mode 100644 prisma/test-student-dashboard.ts delete mode 100644 prisma/test-student-data-access.ts delete mode 100644 src/app/(protected)/(protect-admin)/admin/problems/[problemId]/edit/page.tsx delete mode 100644 src/app/(protected)/(protect-admin)/layout.tsx rename src/app/(protected)/dashboard/{(userdashboard)/_actions => actions}/student-dashboard.ts (51%) rename src/app/(protected)/dashboard/{(userdashboard)/_actions => actions}/teacher-dashboard.ts (69%) rename src/app/(protected)/{(protect-admin) => dashboard}/admin/problems/[problemId]/edit/layout.tsx (95%) create mode 100644 src/app/(protected)/dashboard/admin/problems/[problemId]/edit/page.tsx rename src/app/(protected)/dashboard/{(userdashboard) => }/student/dashboard/page.tsx (73%) rename src/app/(protected)/dashboard/{(userdashboard) => }/teacher/dashboard/page.tsx (80%) rename src/app/(protected)/dashboard/{(userdashboard) => }/teacher/dashboard/test-data.tsx (81%) delete mode 100644 src/app/(protected)/dashboard/usermanagement/_actions/problemActions.ts delete mode 100644 src/app/(protected)/dashboard/usermanagement/_actions/userActions.ts delete mode 100644 src/app/(protected)/dashboard/usermanagement/_components/GenericLayout.tsx create mode 100644 src/app/(protected)/dashboard/usermanagement/actions/problemActions.ts create mode 100644 src/app/(protected)/dashboard/usermanagement/actions/userActions.ts create mode 100644 src/app/(protected)/dashboard/usermanagement/components/GenericLayout.tsx rename src/app/(protected)/dashboard/usermanagement/{_components => components}/ProtectedLayout.tsx (80%) create mode 100644 src/components/dashboard-button.tsx create mode 100644 src/features/admin/ui/layouts/protected-layout.tsx diff --git a/prisma/check-problem-submissions.ts b/prisma/check-problem-submissions.ts deleted file mode 100644 index b3aca04..0000000 --- a/prisma/check-problem-submissions.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { PrismaClient } from "@/generated/client"; - -const prisma = new PrismaClient(); - -async function checkProblemSubmissions() { - console.log("检查所有题目的提交记录情况..."); - - // 获取所有题目 - const problems = await prisma.problem.findMany({ - orderBy: { displayId: 'asc' } - }); - - console.log(`总题目数: ${problems.length}`); - - for (const problem of problems) { - // 统计该题目的提交记录 - const submissionCount = await prisma.submission.count({ - where: { problemId: problem.id } - }); - - // 统计该题目的完成情况 - const completedCount = await prisma.submission.count({ - where: { - problemId: problem.id, - status: "AC" - } - }); - - // 查询时包含 localizations - const problems = await prisma.problem.findMany({ - include: { - localizations: { - where: { type: "TITLE" }, - select: { content: true } - } - } - }); - - problems.forEach(problem => { - const title = problem.localizations[0]?.content || "无标题"; - console.log(`题目${problem.displayId} (${title}): ...`); - }); - - // 统计有提交记录的题目数量 - const problemsWithSubmissions = await prisma.problem.findMany({ - where: { - submissions: { - some: {} - } - } - }); - - console.log(`\n有提交记录的题目数量: ${problemsWithSubmissions.length}`); - console.log("有提交记录的题目编号:"); - problemsWithSubmissions.forEach(p => { - console.log(` ${p.displayId}`); - }); -} - -checkProblemSubmissions() - .catch((e) => { - console.error("检查数据时出错:", e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - })}; \ No newline at end of file diff --git a/prisma/fill-testcase-results.ts b/prisma/fill-testcase-results.ts deleted file mode 100644 index 56a1fff..0000000 --- a/prisma/fill-testcase-results.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { PrismaClient, Status } from "@/generated/client"; -const prisma = new PrismaClient(); - -async function fillTestcaseResults() { - const submissions = await prisma.submission.findMany(); - let count = 0; - for (const submission of submissions) { - const testcases = await prisma.testcase.findMany({ - where: { problemId: submission.problemId }, - }); - for (const testcase of testcases) { - // 检查是否已存在,避免重复 - const exists = await prisma.testcaseResult.findFirst({ - where: { - submissionId: submission.id, - testcaseId: testcase.id, - }, - }); - if (!exists) { - await prisma.testcaseResult.create({ - data: { - isCorrect: submission.status === Status.AC, - output: submission.status === Status.AC ? "正确答案" : "错误答案", - timeUsage: Math.floor(Math.random() * 1000) + 1, - memoryUsage: Math.floor(Math.random() * 128) + 1, - submissionId: submission.id, - testcaseId: testcase.id, - }, - }); - count++; - } - } - } - console.log(`已为 ${count} 个提交生成测试用例结果`); -} - -fillTestcaseResults().finally(() => prisma.$disconnect()); \ No newline at end of file diff --git a/prisma/generate-user-data.ts b/prisma/generate-user-data.ts deleted file mode 100644 index 176c219..0000000 --- a/prisma/generate-user-data.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { PrismaClient, Status, Language } from "@/generated/client"; - -const prisma = new PrismaClient(); - -async function generateUserData() { - console.log("为 student@example.com 生成测试数据..."); - - try { - // 查找用户 - const user = await prisma.user.findUnique({ - where: { email: "student@example.com" } - }); - - if (!user) { - console.log("用户不存在,创建用户..."); - const newUser = await prisma.user.create({ - data: { - name: "测试学生", - email: "student@example.com", - password: "$2b$10$SD1T/dYvKTArGdTmf8ERxuBKIONxY01/wSboRNaNsHnKZzDhps/0u", - role: "GUEST", - }, - }); - console.log("创建用户成功:", newUser); - return; - } - - console.log("找到用户:", user.name || user.email); - - // 获取所有已发布的题目 - const problems = await prisma.problem.findMany({ - where: { isPublished: true }, - select: { id: true, displayId: true, localizations: { - where: { locale: "en", type: "TITLE" }, - select: { content: true } - } } - }); - - console.log(`找到 ${problems.length} 道已发布题目`); - - // 为这个用户生成提交记录 - const submissionCount = Math.min(problems.length, 8); // 最多8道题目 - const selectedProblems = problems.slice(0, submissionCount); - - for (const problem of selectedProblems) { - // 为每道题目生成1-3次提交 - const attempts = Math.floor(Math.random() * 3) + 1; - - for (let i = 0; i < attempts; i++) { - // 60%概率AC,40%概率WA - const isAC = Math.random() < 0.6 || i === attempts - 1; // 最后一次提交更可能是AC - - const submission = await prisma.submission.create({ - data: { - language: Math.random() > 0.5 ? Language.c : Language.cpp, - content: `// ${user.name || user.email} 针对题目${problem.displayId}的第${i + 1}次提交`, - status: isAC ? Status.AC : Status.WA, - message: isAC ? "Accepted" : "Wrong Answer", - timeUsage: Math.floor(Math.random() * 1000) + 1, - memoryUsage: Math.floor(Math.random() * 128) + 1, - userId: user.id, - problemId: problem.id, - }, - }); - - // 获取题目的测试用例 - const testcases = await prisma.testcase.findMany({ - where: { problemId: problem.id } - }); - - // 为每个提交生成测试用例结果 - for (const testcase of testcases) { - await prisma.testcaseResult.create({ - data: { - isCorrect: isAC, - output: isAC ? "正确答案" : "错误答案", - timeUsage: Math.floor(Math.random() * 1000) + 1, - memoryUsage: Math.floor(Math.random() * 128) + 1, - submissionId: submission.id, - testcaseId: testcase.id, - }, - }); - } - - console.log(`题目${problem.displayId}: 第${i + 1}次提交 - ${isAC ? 'AC' : 'WA'}`); - - // 如果AC了,就不再继续提交这道题 - if (isAC) break; - } - } - - console.log("数据生成完成!"); - - // 验证生成的数据 - const userSubmissions = await prisma.submission.findMany({ - where: { userId: user.id }, - include: { - problem: { select: { displayId: true, localizations: { - where: { locale: "en", type: "TITLE" }, - select: { content: true } - } } } - } - }); - - console.log(`\n用户 ${user.name || user.email} 现在有 ${userSubmissions.length} 条提交记录:`); - userSubmissions.forEach((s, index) => { - const title = s.problem.localizations.find(l => l.content === "TITLE")?.content || "无标题"; - console.log(`${index + 1}. 题目${s.problem.displayId} (${title}) - ${s.status}`); -}); - - } catch (error) { - console.error("生成数据时出错:", error); - } finally { - await prisma.$disconnect(); - } -} - -generateUserData(); \ No newline at end of file diff --git a/prisma/test-student-dashboard.ts b/prisma/test-student-dashboard.ts deleted file mode 100644 index eb49d54..0000000 --- a/prisma/test-student-dashboard.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { PrismaClient } from "@/generated/client"; - -const prisma = new PrismaClient(); - -async function testStudentDashboard() { - console.log("测试学生仪表板数据获取..."); - - // 获取一个学生用户 - const student = await prisma.user.findFirst({ - where: { role: "GUEST" }, - select: { id: true, name: true, email: true } - }); - - if (!student) { - console.log("未找到学生用户,创建测试学生..."); - const newStudent = await prisma.user.create({ - data: { - name: "测试学生", - email: "test_student@example.com", - password: "$2b$10$SD1T/dYvKTArGdTmf8ERxuBKIONxY01/wSboRNaNsHnKZzDhps/0u", - role: "GUEST", - }, - }); - console.log(`创建学生: ${newStudent.name} (${newStudent.email})`); - } - - // 获取所有已发布的题目 - const allProblems = await prisma.problem.findMany({ - where: { isPublished: true }, - select: { - id: true, - displayId: true, - difficulty: true, - localizations: { - where: { - type: "TITLE", - locale: "en" // 或者根据需求使用其他语言 - }, - select: { - content: true - } - } - } - }); - - console.log(`总题目数: ${allProblems.length}`); - - // 获取学生的所有提交记录 - const userSubmissions = await prisma.submission.findMany({ - where: { userId: student?.id }, - include: { - problem: { - select: { - id: true, - displayId: true, - difficulty: true, - localizations: { - where: { - type: "TITLE", - locale: "en" // 或者根据需求使用其他语言 - }, - select: { - content: true - } - } - } - } - } - }); - - console.log(`学生提交记录数: ${userSubmissions.length}`); - - // 计算题目完成情况 - const completedProblems = new Set(); - const attemptedProblems = new Set(); - const wrongSubmissions = new Map(); // problemId -> count - - userSubmissions.forEach(submission => { - attemptedProblems.add(submission.problemId); - - if (submission.status === "AC") { - completedProblems.add(submission.problemId); - } else { - // 统计错误提交次数 - const currentCount = wrongSubmissions.get(submission.problemId) || 0; - wrongSubmissions.set(submission.problemId, currentCount + 1); - } - }); - - // 题目完成比例数据 - const completionData = { - total: allProblems.length, - completed: completedProblems.size, - percentage: allProblems.length > 0 ? Math.round((completedProblems.size / allProblems.length) * 100) : 0, - }; - - // 错题比例数据 - const totalSubmissions = userSubmissions.length; - const wrongSubmissionsCount = userSubmissions.filter(s => s.status !== "AC").length; - const errorData = { - total: totalSubmissions, - wrong: wrongSubmissionsCount, - percentage: totalSubmissions > 0 ? Math.round((wrongSubmissionsCount / totalSubmissions) * 100) : 0, - }; - //易错题列表(按错误次数排序) - const difficultProblems = Array.from(wrongSubmissions.entries()) - .map(([problemId, errorCount]) => { - const problem = allProblems.find(p => p.id === problemId); - // 从 localizations 获取标题(英文优先) - const title = problem?.localizations?.find(l => l.content === "TITLE")?.content || "未知题目"; - - return { - id: problem?.displayId || problemId, - title: title, // 使用从 localizations 获取的标题 - difficulty: problem?.difficulty || "未知", - errorCount: errorCount as number, - }; - }) - .sort((a, b) => b.errorCount - a.errorCount) - .slice(0, 10); - - console.log("\n=== 学生仪表板数据 ==="); - console.log(`题目完成情况: ${completionData.completed}/${completionData.total} (${completionData.percentage}%)`); - console.log(`提交正确率: ${errorData.total - errorData.wrong}/${errorData.total} (${100 - errorData.percentage}%)`); - console.log(`易错题数量: ${difficultProblems.length}`); - - if (difficultProblems.length > 0) { - console.log("\n易错题列表:"); - difficultProblems.forEach((problem, index) => { - console.log(`${index + 1}. 题目${problem.id} (${problem.title}) - ${problem.difficulty} - 错误${problem.errorCount}次`); - }); - } - - console.log("\n测试完成!"); -} - -testStudentDashboard() - .catch((e) => { - console.error("测试学生仪表板时出错:", e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); \ No newline at end of file diff --git a/prisma/test-student-data-access.ts b/prisma/test-student-data-access.ts deleted file mode 100644 index 3a9643f..0000000 --- a/prisma/test-student-data-access.ts +++ /dev/null @@ -1,79 +0,0 @@ -// import { PrismaClient } from "@/generated/client"; - -// const prisma = new PrismaClient(); - -// async function testStudentDataAccess() { -// console.log("测试学生数据访问..."); - -// try { -// // 1. 检查是否有学生用户 -// const students = await prisma.user.findMany({ -// where: { role: "GUEST" }, -// select: { id: true, name: true, email: true } -// }); -// console.log(`找到 ${students.length} 个学生用户:`); -// students.forEach(s => console.log(` - ${s.name} (${s.email})`)); - -// if (students.length === 0) { -// console.log("没有学生用户,创建测试学生..."); -// const testStudent = await prisma.user.create({ -// data: { -// name: "测试学生", -// email: "test_student@example.com", -// password: "$2b$10$SD1T/dYvKTArGdTmf8ERxuBKIONxY01/wSboRNaNsHnKZzDhps/0u", -// role: "GUEST", -// }, -// }); -// console.log(`创建学生: ${testStudent.name}`); -// } - -// // 2. 检查已发布的题目 -// const publishedProblems = await prisma.problem.findMany({ -// where: { isPublished: true }, -// select: { id: true, -// displayId: true, -// localizations: { -// where: { locale: "en", type: "TITLE" }, -// select: { content: true } -// } -// } -// }); -// console.log(`\n已发布题目数量: ${publishedProblems.length}`); -// publishedProblems.slice(0, 5).forEach((p: any) => { -// const title = p.localizations?.find((l: any) => l.type === "TITLE")?.content || "无标题"; -// console.log(` - ${p.displayId}: ${title}`); -// }); - -// // 3. 检查提交记录 -// const allSubmissions = await prisma.submission.findMany({ -// select: { id: true, userId: true, problemId: true, status: true } -// }); -// console.log(`\n总提交记录数: ${allSubmissions.length}`); - -// // 4. 检查特定学生的提交记录 -// const firstStudent = students[0] || await prisma.user.findFirst({ where: { role: "GUEST" } }); -// if (firstStudent) { -// const studentSubmissions = await prisma.submission.findMany({ -// where: { userId: firstStudent.id }, -// include: { -// problem: { select: { displayId: true, localizations: { -// where: { locale: "en", type: "TITLE" }, -// select: { content: true } -// } } } -// } -// }); -// console.log(`\n学生 ${firstStudent.name} 的提交记录数: ${studentSubmissions.length}`); -// studentSubmissions.slice(0, 3).forEach(s => { -// console.log(` - 题目${s.problem.displayId}: ${s.status}`); -// }); -// } - -// console.log("\n数据访问测试完成!"); -// } catch (error) { -// console.error("测试过程中出错:", error); -// } finally { -// await prisma.$disconnect(); -// } -// } - -// testStudentDataAccess(); \ No newline at end of file diff --git a/src/app/(app)/problemset/layout.tsx b/src/app/(app)/problemset/layout.tsx index 6866a54..0a1981f 100644 --- a/src/app/(app)/problemset/layout.tsx +++ b/src/app/(app)/problemset/layout.tsx @@ -11,4 +11,4 @@ export default function ProblemsetLayout({ children }: ProblemsetLayoutProps) { {children} ); -} \ No newline at end of file +} diff --git a/src/app/(protected)/(protect-admin)/admin/problems/[problemId]/edit/page.tsx b/src/app/(protected)/(protect-admin)/admin/problems/[problemId]/edit/page.tsx deleted file mode 100644 index 87ae266..0000000 --- a/src/app/(protected)/(protect-admin)/admin/problems/[problemId]/edit/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ProblemEditView } from "@/features/admin/ui/views/problem-edit-view"; - -// interface PageProps { -// params: Promise<{ problemId: string }>; -// } - -const Page = async ( - // { params }: PageProps -) => { - // const { problemId } = await params; - - return ; -}; - -export default Page; \ No newline at end of file diff --git a/src/app/(protected)/(protect-admin)/layout.tsx b/src/app/(protected)/(protect-admin)/layout.tsx deleted file mode 100644 index 105864e..0000000 --- a/src/app/(protected)/(protect-admin)/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { AdminProtectedLayout } from "@/features/admin/ui/layouts/admin-protected-layout"; - -interface LayoutProps { - children: React.ReactNode; -} - -const Layout = ({ children }: LayoutProps) => { - return {children}; -}; - -export default Layout; \ No newline at end of file diff --git a/src/app/(protected)/dashboard/(userdashboard)/_actions/student-dashboard.ts b/src/app/(protected)/dashboard/actions/student-dashboard.ts similarity index 51% rename from src/app/(protected)/dashboard/(userdashboard)/_actions/student-dashboard.ts rename to src/app/(protected)/dashboard/actions/student-dashboard.ts index d54485d..8880d77 100644 --- a/src/app/(protected)/dashboard/(userdashboard)/_actions/student-dashboard.ts +++ b/src/app/(protected)/dashboard/actions/student-dashboard.ts @@ -6,11 +6,11 @@ import { auth } from "@/lib/auth"; export async function getStudentDashboardData() { try { console.log("=== 开始获取学生仪表板数据 ==="); - + const session = await auth(); console.log("Session 获取成功:", !!session); console.log("Session user:", session?.user); - + if (!session?.user?.id) { console.log("用户未登录或session无效"); throw new Error("未登录"); @@ -22,7 +22,7 @@ export async function getStudentDashboardData() { // 检查用户是否存在 const currentUser = await prisma.user.findUnique({ where: { id: userId }, - select: { id: true, name: true, email: true, role: true } + select: { id: true, name: true, email: true, role: true }, }); console.log("当前用户信息:", currentUser); @@ -31,59 +31,65 @@ export async function getStudentDashboardData() { } // 获取所有已发布的题目(包含英文标题) -const allProblems = await prisma.problem.findMany({ - where: { isPublished: true }, - select: { - id: true, - displayId: true, - difficulty: true, - localizations: { - where: { - type: "TITLE", + const allProblems = await prisma.problem.findMany({ + where: { isPublished: true }, + select: { + id: true, + displayId: true, + difficulty: true, + localizations: { + where: { + type: "TITLE", + }, + }, }, - } - } -}); + }); -console.log("已发布题目数量:", allProblems.length); -console.log("题目列表:", allProblems.map(p => ({ - id: p.id, - displayId: p.displayId, - title: p.localizations[0]?.content || "无标题", - difficulty: p.difficulty -}))); + console.log("已发布题目数量:", allProblems.length); + console.log( + "题目列表:", + allProblems.map((p) => ({ + id: p.id, + displayId: p.displayId, + title: p.localizations[0]?.content || "无标题", + difficulty: p.difficulty, + })) + ); -// 获取当前学生的所有提交记录(包含题目英文标题) - const userSubmissions = await prisma.submission.findMany({ - where: { userId: userId }, - include: { - problem: { - select: { - id: true, - displayId: true, - difficulty: true, - localizations: { - where: { - type: "TITLE", - locale: "en" // 或者根据需求使用其他语言 - }, - select: { - content: true - } - } - } - } - } -}); + // 获取当前学生的所有提交记录(包含题目英文标题) + const userSubmissions = await prisma.submission.findMany({ + where: { userId: userId }, + include: { + problem: { + select: { + id: true, + displayId: true, + difficulty: true, + localizations: { + where: { + type: "TITLE", + locale: "en", // 或者根据需求使用其他语言 + }, + select: { + content: true, + }, + }, + }, + }, + }, + }); -console.log("当前用户提交记录数量:", userSubmissions.length); -console.log("提交记录详情:", userSubmissions.map(s => ({ - problemId: s.problemId, - problemDisplayId: s.problem.displayId, - title: s.problem.localizations[0]?.content || "无标题", - difficulty: s.problem.difficulty, - status: s.status -}))); + console.log("当前用户提交记录数量:", userSubmissions.length); + console.log( + "提交记录详情:", + userSubmissions.map((s) => ({ + problemId: s.problemId, + problemDisplayId: s.problem.displayId, + title: s.problem.localizations[0]?.content || "无标题", + difficulty: s.problem.difficulty, + status: s.status, + })) + ); // 计算题目完成情况 const completedProblems = new Set(); @@ -92,7 +98,7 @@ console.log("提交记录详情:", userSubmissions.map(s => ({ userSubmissions.forEach((submission) => { attemptedProblems.add(submission.problemId); - + if (submission.status === "AC") { completedProblems.add(submission.problemId); } else { @@ -110,42 +116,53 @@ console.log("提交记录详情:", userSubmissions.map(s => ({ const completionData = { total: allProblems.length, completed: completedProblems.size, - percentage: allProblems.length > 0 ? Math.round((completedProblems.size / allProblems.length) * 100) : 0, + percentage: + allProblems.length > 0 + ? Math.round((completedProblems.size / allProblems.length) * 100) + : 0, }; // 错题比例数据 - 基于已完成的题目计算 const wrongProblems = new Set(); - + // 统计在已完成的题目中,哪些题目曾经有过错误提交 userSubmissions.forEach((submission) => { - if (submission.status !== "AC" && completedProblems.has(submission.problemId)) { + if ( + submission.status !== "AC" && + completedProblems.has(submission.problemId) + ) { wrongProblems.add(submission.problemId); } }); - + const errorData = { total: completedProblems.size, // 已完成的题目总数 wrong: wrongProblems.size, // 在已完成的题目中有过错误的题目数 - percentage: completedProblems.size > 0 ? Math.round((wrongProblems.size / completedProblems.size) * 100) : 0, + percentage: + completedProblems.size > 0 + ? Math.round((wrongProblems.size / completedProblems.size) * 100) + : 0, }; // 易错题列表(按错误次数排序) - const difficultProblems = Array.from(wrongSubmissions.entries()) - .map(([problemId, errorCount]) => { - const problem = allProblems.find((p) => p.id === problemId); - - // 从 problem.localizations 中获取标题 - const title = problem?.localizations?.find((loc) => loc.type === "TITLE")?.content || "未知题目"; + const difficultProblems = Array.from(wrongSubmissions.entries()) + .map(([problemId, errorCount]) => { + const problem = allProblems.find((p) => p.id === problemId); - return { - id: problem?.displayId || problemId, - title: title, // 使用从 localizations 获取的标题 - difficulty: problem?.difficulty || "未知", - errorCount: errorCount as number, - }; - }) - .sort((a, b) => b.errorCount - a.errorCount) - .slice(0, 10); // 只显示前10个 + // 从 problem.localizations 中获取标题 + const title = + problem?.localizations?.find((loc) => loc.type === "TITLE") + ?.content || "未知题目"; + + return { + id: problem?.displayId || problemId, + title: title, // 使用从 localizations 获取的标题 + difficulty: problem?.difficulty || "未知", + errorCount: errorCount as number, + }; + }) + .sort((a, b) => b.errorCount - a.errorCount) + .slice(0, 10); // 只显示前10个 const result = { completionData, @@ -153,7 +170,10 @@ console.log("提交记录详情:", userSubmissions.map(s => ({ difficultProblems, pieChartData: [ { name: "已完成", value: completionData.completed }, - { name: "未完成", value: completionData.total - completionData.completed }, + { + name: "未完成", + value: completionData.total - completionData.completed, + }, ], errorPieChartData: [ { name: "正确", value: errorData.total - errorData.wrong }, @@ -170,6 +190,8 @@ console.log("提交记录详情:", userSubmissions.map(s => ({ return result; } catch (error) { console.error("获取学生仪表板数据失败:", error); - throw new Error(`获取数据失败: ${error instanceof Error ? error.message : '未知错误'}`); + throw new Error( + `获取数据失败: ${error instanceof Error ? error.message : "未知错误"}` + ); } -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/(userdashboard)/_actions/teacher-dashboard.ts b/src/app/(protected)/dashboard/actions/teacher-dashboard.ts similarity index 69% rename from src/app/(protected)/dashboard/(userdashboard)/_actions/teacher-dashboard.ts rename to src/app/(protected)/dashboard/actions/teacher-dashboard.ts index aeb9718..874eba3 100644 --- a/src/app/(protected)/dashboard/(userdashboard)/_actions/teacher-dashboard.ts +++ b/src/app/(protected)/dashboard/actions/teacher-dashboard.ts @@ -1,8 +1,8 @@ "use server"; import prisma from "@/lib/prisma"; -import { Locale, Status, ProblemLocalization } from "@/generated/client"; import { getLocale } from "next-intl/server"; +import { Locale, Status, ProblemLocalization } from "@/generated/client"; const getLocalizedTitle = ( localizations: ProblemLocalization[], @@ -38,32 +38,37 @@ export interface DifficultProblemData { problemDisplayId: number; } -export async function getProblemCompletionData(): Promise { +export async function getProblemCompletionData(): Promise< + ProblemCompletionData[] +> { // 获取所有提交记录,按题目分组统计 const submissions = await prisma.submission.findMany({ include: { user: true, problem: { - include:{ - localizations:true - } - } + include: { + localizations: true, + }, + }, }, }); const locale = await getLocale(); // 按题目分组统计完成情况(统计独立用户数) - const problemStats = new Map; - totalUsers: Set; - title: string; - displayId: number; - }>(); + const problemStats = new Map< + string, + { + completedUsers: Set; + totalUsers: Set; + title: string; + displayId: number; + } + >(); submissions.forEach((submission) => { - const localizations=submission.problem.localizations; - const title=getLocalizedTitle(localizations,locale as Locale); + const localizations = submission.problem.localizations; + const title = getLocalizedTitle(localizations, locale as Locale); const problemId = submission.problemId; const problemTitle = title; const problemDisplayId = submission.problem.displayId; @@ -71,9 +76,9 @@ export async function getProblemCompletionData(): Promise { - const completed = stats.completedUsers.size; - const total = stats.totalUsers.size; - - return { - problemId: problemId, - problemDisplayId: stats.displayId, - problemTitle: stats.title, - completed: completed, - uncompleted: total - completed, - total: total, - completedPercent: total > 0 ? (completed / total) * 100 : 0, - uncompletedPercent: total > 0 ? ((total - completed) / total) * 100 : 0, - }; - }); + const problemDataArray = Array.from(problemStats.entries()).map( + ([problemId, stats]) => { + const completed = stats.completedUsers.size; + const total = stats.totalUsers.size; + + return { + problemId: problemId, + problemDisplayId: stats.displayId, + problemTitle: stats.title, + completed: completed, + uncompleted: total - completed, + total: total, + completedPercent: total > 0 ? (completed / total) * 100 : 0, + uncompletedPercent: total > 0 ? ((total - completed) / total) * 100 : 0, + }; + } + ); // 按题目编号排序 - return problemDataArray.sort((a, b) => a.problemDisplayId - b.problemDisplayId); + return problemDataArray.sort( + (a, b) => a.problemDisplayId - b.problemDisplayId + ); } -export async function getDifficultProblemsData(): Promise { +export async function getDifficultProblemsData(): Promise< + DifficultProblemData[] +> { // 获取所有测试用例结果 const testcaseResults = await prisma.testcaseResult.findMany({ include: { @@ -120,8 +131,8 @@ export async function getDifficultProblemsData(): Promise; - }>(); + const problemStats = new Map< + string, + { + totalAttempts: number; + wrongAttempts: number; + title: string; + displayId: number; + users: Set; + } + >(); testcaseResults.forEach((result) => { const problemId = result.testcase.problemId; - const problemTitle = result.testcase.problem.localizations?.find( - (loc) => loc.type === "TITLE" - )?.content || "无标题"; + const problemTitle = + result.testcase.problem.localizations?.find((loc) => loc.type === "TITLE") + ?.content || "无标题"; const problemDisplayId = result.testcase.problem.displayId; const userId = result.submission.userId; const isWrong = !result.isCorrect; @@ -181,9 +195,10 @@ export async function getDifficultProblemsData(): Promise - problem.errorRate > 30 && // 错误率超过30% - problem.totalAttempts >= 3 // 至少有3次尝试 + .filter( + (problem) => + problem.errorRate > 30 && // 错误率超过30% + problem.totalAttempts >= 3 // 至少有3次尝试 ) .sort((a, b) => b.errorRate - a.errorRate) // 按错误率降序排列 .slice(0, 10); // 取前10个最难的题目 @@ -203,4 +218,4 @@ export async function getDashboardStats() { totalProblems: problemData.length, totalDifficultProblems: difficultProblems.length, }; -} \ No newline at end of file +} diff --git a/src/app/(protected)/(protect-admin)/admin/problems/[problemId]/edit/layout.tsx b/src/app/(protected)/dashboard/admin/problems/[problemId]/edit/layout.tsx similarity index 95% rename from src/app/(protected)/(protect-admin)/admin/problems/[problemId]/edit/layout.tsx rename to src/app/(protected)/dashboard/admin/problems/[problemId]/edit/layout.tsx index 0667f52..fb46c63 100644 --- a/src/app/(protected)/(protect-admin)/admin/problems/[problemId]/edit/layout.tsx +++ b/src/app/(protected)/dashboard/admin/problems/[problemId]/edit/layout.tsx @@ -16,4 +16,4 @@ const Layout = async ({ children, params }: LayoutProps) => { return {children}; }; -export default Layout; \ No newline at end of file +export default Layout; diff --git a/src/app/(protected)/dashboard/admin/problems/[problemId]/edit/page.tsx b/src/app/(protected)/dashboard/admin/problems/[problemId]/edit/page.tsx new file mode 100644 index 0000000..8f8302f --- /dev/null +++ b/src/app/(protected)/dashboard/admin/problems/[problemId]/edit/page.tsx @@ -0,0 +1,13 @@ +import { ProblemEditView } from "@/features/admin/ui/views/problem-edit-view"; + +interface PageProps { + params: Promise<{ problemId: string }>; +} + +const Page = async ({ params }: PageProps) => { + const { problemId } = await params; + + return ; +}; + +export default Page; diff --git a/src/app/(protected)/dashboard/layout.tsx b/src/app/(protected)/dashboard/layout.tsx index 119e21f..e403e75 100644 --- a/src/app/(protected)/dashboard/layout.tsx +++ b/src/app/(protected)/dashboard/layout.tsx @@ -1,16 +1,16 @@ -import { AppSidebar } from "@/components/sidebar/app-sidebar"; -import { AdminSidebar } from "@/components/sidebar/admin-sidebar"; -import { TeacherSidebar } from "@/components/sidebar/teacher-sidebar"; -import { DynamicBreadcrumb } from "@/components/dynamic-breadcrumb"; -import { Separator } from "@/components/ui/separator"; import { SidebarInset, SidebarProvider, SidebarTrigger, } from "@/components/ui/sidebar"; -import { auth } from "@/lib/auth"; import prisma from "@/lib/prisma"; +import { auth } from "@/lib/auth"; import { redirect } from "next/navigation"; +import { Separator } from "@/components/ui/separator"; +import { AppSidebar } from "@/components/sidebar/app-sidebar"; +import { AdminSidebar } from "@/components/sidebar/admin-sidebar"; +import { DynamicBreadcrumb } from "@/components/dynamic-breadcrumb"; +import { TeacherSidebar } from "@/components/sidebar/teacher-sidebar"; interface LayoutProps { children: React.ReactNode; @@ -33,7 +33,7 @@ export default async function Layout({ children }: LayoutProps) { // 获取用户的完整信息(包括角色) const fullUser = await prisma.user.findUnique({ where: { id: user.id }, - select: { id: true, name: true, email: true, image: true, role: true } + select: { id: true, name: true, email: true, image: true, role: true }, }); if (!fullUser) { diff --git a/src/app/(protected)/dashboard/management/actions/changePassword.ts b/src/app/(protected)/dashboard/management/actions/changePassword.ts index 554a2be..6e8ba92 100644 --- a/src/app/(protected)/dashboard/management/actions/changePassword.ts +++ b/src/app/(protected)/dashboard/management/actions/changePassword.ts @@ -1,9 +1,8 @@ -// changePassword.ts "use server"; +import bcrypt from "bcryptjs"; import { auth } from "@/lib/auth"; import prisma from "@/lib/prisma"; -import bcrypt from "bcryptjs"; export async function changePassword(formData: FormData) { const oldPassword = formData.get("oldPassword") as string; @@ -56,4 +55,4 @@ export async function changePassword(formData: FormData) { console.error("修改密码失败:", error); throw new Error("修改密码失败"); } -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/management/actions/getUserInfo.ts b/src/app/(protected)/dashboard/management/actions/getUserInfo.ts index 53aacab..1916740 100644 --- a/src/app/(protected)/dashboard/management/actions/getUserInfo.ts +++ b/src/app/(protected)/dashboard/management/actions/getUserInfo.ts @@ -1,4 +1,3 @@ -// getUserInfo.ts "use server"; import { auth } from "@/lib/auth"; @@ -37,4 +36,4 @@ export async function getUserInfo() { console.error("获取用户信息失败:", error); throw new Error("获取用户信息失败"); } -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/management/actions/index.ts b/src/app/(protected)/dashboard/management/actions/index.ts index 5c599e5..baf0cc7 100644 --- a/src/app/(protected)/dashboard/management/actions/index.ts +++ b/src/app/(protected)/dashboard/management/actions/index.ts @@ -1,4 +1,3 @@ -// index.ts export { getUserInfo } from "./getUserInfo"; export { updateUserInfo } from "./updateUserInfo"; -export { changePassword } from "./changePassword"; \ No newline at end of file +export { changePassword } from "./changePassword"; diff --git a/src/app/(protected)/dashboard/management/actions/updateUserInfo.ts b/src/app/(protected)/dashboard/management/actions/updateUserInfo.ts index 66edff6..e8d539b 100644 --- a/src/app/(protected)/dashboard/management/actions/updateUserInfo.ts +++ b/src/app/(protected)/dashboard/management/actions/updateUserInfo.ts @@ -1,4 +1,3 @@ -// updateUserInfo.ts "use server"; import { auth } from "@/lib/auth"; @@ -31,4 +30,4 @@ export async function updateUserInfo(formData: FormData) { console.error("更新用户信息失败:", error); throw new Error("更新用户信息失败"); } -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/management/change-password/page.tsx b/src/app/(protected)/dashboard/management/change-password/page.tsx index b635a46..e39300a 100644 --- a/src/app/(protected)/dashboard/management/change-password/page.tsx +++ b/src/app/(protected)/dashboard/management/change-password/page.tsx @@ -1,7 +1,8 @@ -// src/app/(app)/management/change-password/page.tsx "use client"; import { useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; import { changePassword } from "@/app/(protected)/dashboard/management/actions/changePassword"; export default function ChangePasswordPage() { @@ -51,40 +52,46 @@ export default function ChangePasswordPage() { setShowSuccess(true); setTimeout(() => setShowSuccess(false), 3000); } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : '修改密码失败'; + const errorMessage = + error instanceof Error ? error.message : "修改密码失败"; alert(errorMessage); } }; return (
-
+

修改密码

-
+
- setOldPassword(e.target.value)} - className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-full border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required />
- setNewPassword(e.target.value)} - className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-full border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required /> {newPassword && ( -

+

密码强度: - +   {strengthLabel}

@@ -93,25 +100,27 @@ export default function ChangePasswordPage() {
- setConfirmPassword(e.target.value)} - className="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-full border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required /> - {newPassword && confirmPassword && newPassword !== confirmPassword && ( -

密码不一致

- )} + {newPassword && + confirmPassword && + newPassword !== confirmPassword && ( +

密码不一致

+ )}
- +
@@ -123,4 +132,4 @@ export default function ChangePasswordPage() { )}
); -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/management/page.tsx b/src/app/(protected)/dashboard/management/page.tsx index 7bdf039..5545c71 100644 --- a/src/app/(protected)/dashboard/management/page.tsx +++ b/src/app/(protected)/dashboard/management/page.tsx @@ -1,58 +1,50 @@ -"use client" -import React, { useState } from "react" -import { Separator } from "@/components/ui/separator" -import ProfilePage from "./profile/page" -import ChangePasswordPage from "./change-password/page" +"use client"; + +import { cn } from "@/lib/utils"; +import React, { useState } from "react"; +import ProfilePage from "./profile/page"; +import { Button } from "@/components/ui/button"; +import ChangePasswordPage from "./change-password/page"; export default function ManagementDefaultPage() { - const [activePage, setActivePage] = useState("profile") + const [activePage, setActivePage] = useState("profile"); const renderContent = () => { switch (activePage) { case "profile": - return + return ; case "change-password": - return + return ; default: - return + return ; } - } + }; return (
{/* 顶部导航栏 */}
- - {/* 页面切换按钮 */}
- - +
{/* 主体内容 */} -
- {renderContent()} -
+
{renderContent()}
- ) -} \ No newline at end of file + ); +} diff --git a/src/app/(protected)/dashboard/management/profile/page.tsx b/src/app/(protected)/dashboard/management/profile/page.tsx index a789024..9d6e319 100644 --- a/src/app/(protected)/dashboard/management/profile/page.tsx +++ b/src/app/(protected)/dashboard/management/profile/page.tsx @@ -1,7 +1,8 @@ -// src/app/(app)/management/profile/page.tsx "use client"; import { useEffect, useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; import { getUserInfo } from "@/app/(protected)/dashboard/management/actions/getUserInfo"; import { updateUserInfo } from "@/app/(protected)/dashboard/management/actions/updateUserInfo"; @@ -34,33 +35,38 @@ export default function ProfilePage() { }, []); const handleSave = async () => { - const nameInput = document.getElementById("name") as HTMLInputElement | null; - const emailInput = document.getElementById("email") as HTMLInputElement | null; + const nameInput = document.getElementById( + "name" + ) as HTMLInputElement | null; + const emailInput = document.getElementById( + "email" + ) as HTMLInputElement | null; - if (!nameInput || !emailInput) { - alert("表单元素缺失"); - return; - } + if (!nameInput || !emailInput) { + alert("表单元素缺失"); + return; + } - const formData = new FormData(); - formData.append("name", nameInput.value); - formData.append("email", emailInput.value); + const formData = new FormData(); + formData.append("name", nameInput.value); + formData.append("email", emailInput.value); - try { - const updatedUser = await updateUserInfo(formData); - setUser(updatedUser); - setIsEditing(false); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : '更新用户信息失败'; - alert(errorMessage); - } -}; + try { + const updatedUser = await updateUserInfo(formData); + setUser(updatedUser); + setIsEditing(false); + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "更新用户信息失败"; + alert(errorMessage); + } + }; if (!user) return

加载中...

; return (
-
+

用户信息

@@ -71,82 +77,94 @@ export default function ProfilePage() {
{isEditing ? ( - ) : ( -

{user?.name || "未提供"}

+

+ {user?.name || "未提供"} +

)} -

角色:{user?.role}

-

邮箱验证时间:{user.emailVerified ? new Date(user.emailVerified).toLocaleString() : "未验证"}

+

角色:{user?.role}

+

+ 邮箱验证时间: + {user.emailVerified + ? new Date(user.emailVerified).toLocaleString() + : "未验证"} +

-
+
- -

{user.id}

+ +

{user.id}

- + {isEditing ? ( - ) : ( -

{user.email}

+

{user.email}

)}
- -

{new Date(user.createdAt).toLocaleString()}

+ +

+ {new Date(user.createdAt).toLocaleString()} +

- -

{new Date(user.updatedAt).toLocaleString()}

+ +

+ {new Date(user.updatedAt).toLocaleString()} +

{isEditing ? ( <> - - + ) : ( - + )}
); -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/page.tsx b/src/app/(protected)/dashboard/page.tsx index 16d0921..397287a 100644 --- a/src/app/(protected)/dashboard/page.tsx +++ b/src/app/(protected)/dashboard/page.tsx @@ -1,22 +1,28 @@ -import { auth } from "@/lib/auth"; -import prisma from "@/lib/prisma"; -import { redirect } from "next/navigation"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Progress } from "@/components/ui/progress"; -import { - Users, - BookOpen, - CheckCircle, - Clock, - TrendingUp, +import { + Users, + BookOpen, + CheckCircle, + Clock, + TrendingUp, AlertCircle, BarChart3, Target, - Activity + Activity, } 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; @@ -37,7 +43,7 @@ interface Activity { export default async function DashboardPage() { const session = await auth(); const user = session?.user; - + if (!user) { redirect("/sign-in"); } @@ -45,7 +51,7 @@ export default async function DashboardPage() { // 获取用户的完整信息 const fullUser = await prisma.user.findUnique({ where: { id: user.id }, - select: { id: true, name: true, email: true, image: true, role: true } + select: { id: true, name: true, email: true, image: true, role: true }, }); if (!fullUser) { @@ -58,62 +64,81 @@ export default async function DashboardPage() { 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 } - }) - ]); + 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 => ({ + recentActivity = recentUsers.map((user) => ({ type: "新用户注册", title: user.name || user.email, description: `角色: ${user.role}`, - time: user.createdAt + 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 } } - } - } - } - }) - ]); + 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 => ({ + recentActivity = recentSubmissions.map((sub) => ({ type: "学生提交", - title: `${sub.user.name || sub.user.email} 提交了题目 ${sub.problem.displayId}`, - description: sub.problem.localizations[0]?.content || `题目${sub.problem.displayId}`, + 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 + status: sub.status, })); } else { // 学生统计 - const [totalProblems, completedProblems, totalSubmissions, recentSubmissions] = await Promise.all([ + 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, + status: "AC", + }, }), prisma.submission.count({ where: { userId: user.id } }), prisma.submission.findMany({ @@ -121,23 +146,27 @@ export default async function DashboardPage() { take: 5, orderBy: { createdAt: "desc" }, include: { - problem: { - select: { + problem: { + select: { displayId: true, - localizations: { where: { type: "TITLE", locale: "zh" }, select: { content: true } } - } - } - } - }) + localizations: { + where: { type: "TITLE", locale: "zh" }, + select: { content: true }, + }, + }, + }, + }, + }), ]); stats = { totalProblems, completedProblems, totalSubmissions }; - recentActivity = recentSubmissions.map(sub => ({ + recentActivity = recentSubmissions.map((sub) => ({ type: "我的提交", title: `题目 ${sub.problem.displayId}`, - description: sub.problem.localizations[0]?.content || `题目${sub.problem.displayId}`, + description: + sub.problem.localizations[0]?.content || `题目${sub.problem.displayId}`, time: sub.createdAt, - status: sub.status + status: sub.status, })); } @@ -148,52 +177,129 @@ export default async function DashboardPage() { 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" } + { + 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/usermanagement/guest", icon: Users }, - { label: "题目管理", href: "/dashboard/usermanagement/problem", icon: BookOpen }, - { label: "管理员设置", href: "/dashboard/management", icon: Target } - ] + { + label: "用户管理", + href: "/dashboard/usermanagement/guest", + icon: Users, + }, + { + label: "题目管理", + href: "/dashboard/usermanagement/problem", + icon: BookOpen, + }, + { + label: "管理员设置", + href: "/dashboard/management", + icon: Target, + }, + ], }; 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" } + { + 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 } - ] + { + 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" } + { + 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: "/problemset", icon: BookOpen }, - { label: "我的进度", href: "/dashboard/student/dashboard", icon: TrendingUp }, - { label: "个人设置", href: "/dashboard/management", icon: Target } - ] + { + label: "我的进度", + href: "/dashboard/student/dashboard", + icon: TrendingUp, + }, + { 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; + const completionRate = + fullUser.role === "GUEST" + ? (stats.totalProblems || 0) > 0 + ? ((stats.completedProblems || 0) / (stats.totalProblems || 1)) * 100 + : 0 + : 0; return (
@@ -214,7 +320,9 @@ export default async function DashboardPage() { {config.stats.map((stat, index) => ( - {stat.label} + + {stat.label} + @@ -233,7 +341,8 @@ export default async function DashboardPage() { 学习进度 - 已完成 {stats.completedProblems || 0} / {stats.totalProblems || 0} 道题目 + 已完成 {stats.completedProblems || 0} / {stats.totalProblems || 0}{" "} + 道题目 @@ -287,7 +396,9 @@ export default async function DashboardPage() {

{activity.title}

-

{activity.description}

+

+ {activity.description} +

{new Date(activity.time).toLocaleDateString()} diff --git a/src/app/(protected)/dashboard/(userdashboard)/student/dashboard/page.tsx b/src/app/(protected)/dashboard/student/dashboard/page.tsx similarity index 73% rename from src/app/(protected)/dashboard/(userdashboard)/student/dashboard/page.tsx rename to src/app/(protected)/dashboard/student/dashboard/page.tsx index d2d9c53..2db7818 100644 --- a/src/app/(protected)/dashboard/(userdashboard)/student/dashboard/page.tsx +++ b/src/app/(protected)/dashboard/student/dashboard/page.tsx @@ -1,7 +1,5 @@ "use client"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Progress } from "@/components/ui/progress"; import { Table, TableBody, @@ -10,9 +8,11 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; import { useEffect, useState } from "react"; -import { getStudentDashboardData } from "@/app/(protected)/dashboard/(userdashboard)/_actions/student-dashboard"; +import { Progress } from "@/components/ui/progress"; +import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { getStudentDashboardData } from "@/app/(protected)/dashboard/actions/student-dashboard"; interface DashboardData { completionData: { @@ -86,13 +86,19 @@ export default function StudentDashboard() { ); } - const { completionData, errorData, difficultProblems, pieChartData, errorPieChartData } = data; + const { + completionData, + errorData, + difficultProblems, + pieChartData, + errorPieChartData, + } = data; const COLORS = ["#4CAF50", "#FFC107"]; return (

学生仪表板

- +
{/* 题目完成比例模块 */} @@ -102,8 +108,12 @@ export default function StudentDashboard() {
- 已完成题目:{completionData.completed}/{completionData.total} - {completionData.percentage}% + + 已完成题目:{completionData.completed}/{completionData.total} + + + {completionData.percentage}% +
@@ -119,9 +129,17 @@ export default function StudentDashboard() { paddingAngle={5} dataKey="value" > - {pieChartData.map((entry: { name: string; value: number }, index: number) => ( - - ))} + {pieChartData.map( + ( + entry: { name: string; value: number }, + index: number + ) => ( + + ) + )} @@ -138,7 +156,9 @@ export default function StudentDashboard() {
- 错题数量:{errorData.wrong}/{errorData.total} + + 错题数量:{errorData.wrong}/{errorData.total} + {errorData.percentage}%
@@ -155,9 +175,17 @@ export default function StudentDashboard() { paddingAngle={5} dataKey="value" > - {errorPieChartData.map((entry: { name: string; value: number }, index: number) => ( - - ))} + {errorPieChartData.map( + ( + entry: { name: string; value: number }, + index: number + ) => ( + + ) + )} @@ -188,14 +216,21 @@ export default function StudentDashboard() { - {difficultProblems.map((problem: { id: string | number; title: string; difficulty: string; errorCount: number }) => ( - - {problem.id} - {problem.title} - {problem.difficulty} - {problem.errorCount} - - ))} + {difficultProblems.map( + (problem: { + id: string | number; + title: string; + difficulty: string; + errorCount: number; + }) => ( + + {problem.id} + {problem.title} + {problem.difficulty} + {problem.errorCount} + + ) + )} ) : ( @@ -208,4 +243,4 @@ export default function StudentDashboard() {
); -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/(userdashboard)/teacher/dashboard/page.tsx b/src/app/(protected)/dashboard/teacher/dashboard/page.tsx similarity index 80% rename from src/app/(protected)/dashboard/(userdashboard)/teacher/dashboard/page.tsx rename to src/app/(protected)/dashboard/teacher/dashboard/page.tsx index 01f0ba1..407fa2c 100644 --- a/src/app/(protected)/dashboard/(userdashboard)/teacher/dashboard/page.tsx +++ b/src/app/(protected)/dashboard/teacher/dashboard/page.tsx @@ -1,10 +1,13 @@ "use client"; -import { TrendingUp } from "lucide-react"; -import { Bar, BarChart, XAxis, YAxis, LabelList, CartesianGrid } from "recharts"; -import { Button } from "@/components/ui/button"; -import { useState, useEffect } from "react"; - +import { + Bar, + BarChart, + XAxis, + YAxis, + LabelList, + CartesianGrid, +} from "recharts"; import { Card, CardContent, @@ -27,7 +30,14 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { getDashboardStats, ProblemCompletionData, DifficultProblemData } from "@/app/(protected)/dashboard/(userdashboard)/_actions/teacher-dashboard"; +import { TrendingUp } from "lucide-react"; +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { + getDashboardStats, + ProblemCompletionData, + DifficultProblemData, +} from "@/app/(protected)/dashboard/actions/teacher-dashboard"; const ITEMS_PER_PAGE = 5; // 每页显示的题目数量 @@ -45,7 +55,9 @@ const chartConfig = { export default function TeacherDashboard() { const [currentPage, setCurrentPage] = useState(1); const [chartData, setChartData] = useState([]); - const [difficultProblems, setDifficultProblems] = useState([]); + const [difficultProblems, setDifficultProblems] = useState< + DifficultProblemData[] + >([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -57,8 +69,8 @@ export default function TeacherDashboard() { setChartData(data.problemData); setDifficultProblems(data.difficultProblems); } catch (err) { - setError(err instanceof Error ? err.message : '获取数据失败'); - console.error('Failed to fetch dashboard data:', err); + setError(err instanceof Error ? err.message : "获取数据失败"); + console.error("Failed to fetch dashboard data:", err); } finally { setLoading(false); } @@ -68,7 +80,7 @@ export default function TeacherDashboard() { }, []); const totalPages = Math.ceil(chartData.length / ITEMS_PER_PAGE); - + // 获取当前页的数据 const currentPageData = chartData.slice( (currentPage - 1) * ITEMS_PER_PAGE, @@ -128,9 +140,9 @@ export default function TeacherDashboard() { barCategoryGap={20} > - `${value}%`} /> } /> - - `${value}人`} + `${value}人`} /> - - `${value}人`} + `${value}人`} /> @@ -178,7 +190,9 @@ export default function TeacherDashboard() {
{difficultProblems.length === 0 ? (
-
暂无易错题数据
+
+ 暂无易错题数据 +
) : ( @@ -236,7 +254,10 @@ export default function TeacherDashboard() { {difficultProblems.map((problem) => ( - {problem.problemDisplayId || problem.id.substring(0, 8)} + + {problem.problemDisplayId || + problem.id.substring(0, 8)} + {problem.problemTitle} {problem.problemCount} @@ -250,4 +271,4 @@ export default function TeacherDashboard() { ); -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/(userdashboard)/teacher/dashboard/test-data.tsx b/src/app/(protected)/dashboard/teacher/dashboard/test-data.tsx similarity index 81% rename from src/app/(protected)/dashboard/(userdashboard)/teacher/dashboard/test-data.tsx rename to src/app/(protected)/dashboard/teacher/dashboard/test-data.tsx index bf518d5..bee5f21 100644 --- a/src/app/(protected)/dashboard/(userdashboard)/teacher/dashboard/test-data.tsx +++ b/src/app/(protected)/dashboard/teacher/dashboard/test-data.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect } from "react"; -import { getDashboardStats } from "@/app/(protected)/dashboard/(userdashboard)/_actions/teacher-dashboard"; +import { getDashboardStats } from "@/app/(protected)/dashboard/actions/teacher-dashboard"; interface DashboardData { problemData: Array<{ @@ -37,8 +37,8 @@ export default function TestDataPage() { const result = await getDashboardStats(); setData(result); } catch (err) { - setError(err instanceof Error ? err.message : '获取数据失败'); - console.error('Failed to fetch data:', err); + setError(err instanceof Error ? err.message : "获取数据失败"); + console.error("Failed to fetch data:", err); } finally { setLoading(false); } @@ -58,7 +58,7 @@ export default function TestDataPage() { return (

数据测试页面

- +

题目完成数据

@@ -77,13 +77,17 @@ export default function TestDataPage() {

统计信息

-            {JSON.stringify({
-              totalProblems: data?.totalProblems,
-              totalDifficultProblems: data?.totalDifficultProblems,
-            }, null, 2)}
+            {JSON.stringify(
+              {
+                totalProblems: data?.totalProblems,
+                totalDifficultProblems: data?.totalDifficultProblems,
+              },
+              null,
+              2
+            )}
           
); -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/usermanagement/_actions/problemActions.ts b/src/app/(protected)/dashboard/usermanagement/_actions/problemActions.ts deleted file mode 100644 index ca7ca81..0000000 --- a/src/app/(protected)/dashboard/usermanagement/_actions/problemActions.ts +++ /dev/null @@ -1,14 +0,0 @@ -'use server' -import prisma from '@/lib/prisma' -import { revalidatePath } from 'next/cache' -import type { Problem } from '@/generated/client' - -export async function createProblem(data: Omit) { - await prisma.problem.create({ data }) - revalidatePath('/usermanagement/problem') -} - -export async function deleteProblem(id: string) { - await prisma.problem.delete({ where: { id } }) - revalidatePath('/usermanagement/problem') -} \ No newline at end of file diff --git a/src/app/(protected)/dashboard/usermanagement/_actions/userActions.ts b/src/app/(protected)/dashboard/usermanagement/_actions/userActions.ts deleted file mode 100644 index e49269f..0000000 --- a/src/app/(protected)/dashboard/usermanagement/_actions/userActions.ts +++ /dev/null @@ -1,46 +0,0 @@ -'use server' -import prisma from '@/lib/prisma' -import { revalidatePath } from 'next/cache' -import bcrypt from 'bcryptjs' -import type { User } from '@/generated/client' -import { Role } from '@/generated/client' - -type UserType = 'admin' | 'teacher' | 'guest' - -export async function createUser( - userType: UserType, - data: Omit & { password?: string } -) { - let password = data.password - if (password) { - password = await bcrypt.hash(password, 10) - } - - const role = userType.toUpperCase() as Role - await prisma.user.create({ data: { ...data, password, role } }) - revalidatePath(`/usermanagement/${userType}`) -} - -export async function updateUser( - userType: UserType, - id: string, - data: Partial> -) { - const updateData = { ...data } - - // 如果包含密码字段且不为空,则进行加密 - if (data.password && data.password.trim() !== '') { - updateData.password = await bcrypt.hash(data.password, 10) - } else { - // 如果密码为空,则从更新数据中移除密码字段,保持原密码不变 - delete updateData.password - } - - await prisma.user.update({ where: { id }, data: updateData }) - revalidatePath(`/usermanagement/${userType}`) -} - -export async function deleteUser(userType: UserType, id: string) { - await prisma.user.delete({ where: { id } }) - revalidatePath(`/usermanagement/${userType}`) -} \ No newline at end of file diff --git a/src/app/(protected)/dashboard/usermanagement/_components/GenericLayout.tsx b/src/app/(protected)/dashboard/usermanagement/_components/GenericLayout.tsx deleted file mode 100644 index a182e77..0000000 --- a/src/app/(protected)/dashboard/usermanagement/_components/GenericLayout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import ProtectedLayout from "./ProtectedLayout"; - -interface GenericLayoutProps { - children: React.ReactNode; - allowedRoles: string[]; -} - -export default function GenericLayout({ children, allowedRoles }: GenericLayoutProps) { - return {children}; -} \ No newline at end of file diff --git a/src/app/(protected)/dashboard/usermanagement/actions/problemActions.ts b/src/app/(protected)/dashboard/usermanagement/actions/problemActions.ts new file mode 100644 index 0000000..f86502a --- /dev/null +++ b/src/app/(protected)/dashboard/usermanagement/actions/problemActions.ts @@ -0,0 +1,17 @@ +"use server"; + +import prisma from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; +import type { Problem } from "@/generated/client"; + +export async function createProblem( + data: Omit +) { + await prisma.problem.create({ data }); + revalidatePath("/usermanagement/problem"); +} + +export async function deleteProblem(id: string) { + await prisma.problem.delete({ where: { id } }); + revalidatePath("/usermanagement/problem"); +} diff --git a/src/app/(protected)/dashboard/usermanagement/actions/userActions.ts b/src/app/(protected)/dashboard/usermanagement/actions/userActions.ts new file mode 100644 index 0000000..16aa684 --- /dev/null +++ b/src/app/(protected)/dashboard/usermanagement/actions/userActions.ts @@ -0,0 +1,47 @@ +"use server"; + +import bcrypt from "bcryptjs"; +import prisma from "@/lib/prisma"; +import { Role } from "@/generated/client"; +import { revalidatePath } from "next/cache"; +import type { User } from "@/generated/client"; + +type UserType = "admin" | "teacher" | "guest"; + +export async function createUser( + userType: UserType, + data: Omit & { password?: string } +) { + let password = data.password; + if (password) { + password = await bcrypt.hash(password, 10); + } + + const role = userType.toUpperCase() as Role; + await prisma.user.create({ data: { ...data, password, role } }); + revalidatePath(`/usermanagement/${userType}`); +} + +export async function updateUser( + userType: UserType, + id: string, + data: Partial> +) { + const updateData = { ...data }; + + // 如果包含密码字段且不为空,则进行加密 + if (data.password && data.password.trim() !== "") { + updateData.password = await bcrypt.hash(data.password, 10); + } else { + // 如果密码为空,则从更新数据中移除密码字段,保持原密码不变 + delete updateData.password; + } + + await prisma.user.update({ where: { id }, data: updateData }); + revalidatePath(`/usermanagement/${userType}`); +} + +export async function deleteUser(userType: UserType, id: string) { + await prisma.user.delete({ where: { id } }); + revalidatePath(`/usermanagement/${userType}`); +} diff --git a/src/app/(protected)/dashboard/usermanagement/admin/layout.tsx b/src/app/(protected)/dashboard/usermanagement/admin/layout.tsx index db50dae..711abf1 100644 --- a/src/app/(protected)/dashboard/usermanagement/admin/layout.tsx +++ b/src/app/(protected)/dashboard/usermanagement/admin/layout.tsx @@ -1,5 +1,9 @@ -import GenericLayout from "../_components/GenericLayout"; +import GenericLayout from "../components/GenericLayout"; -export default function AdminLayout({ children }: { children: React.ReactNode }) { +export default function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { return {children}; -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/usermanagement/admin/page.tsx b/src/app/(protected)/dashboard/usermanagement/admin/page.tsx index d911127..96065e8 100644 --- a/src/app/(protected)/dashboard/usermanagement/admin/page.tsx +++ b/src/app/(protected)/dashboard/usermanagement/admin/page.tsx @@ -1,6 +1,6 @@ -import GenericPage from '@/features/user-management/components/generic-page' -import { adminConfig } from '@/features/user-management/config/admin' +import { adminConfig } from "@/features/user-management/config/admin"; +import GenericPage from "@/features/user-management/components/generic-page"; export default function AdminPage() { - return -} \ No newline at end of file + return ; +} diff --git a/src/app/(protected)/dashboard/usermanagement/components/GenericLayout.tsx b/src/app/(protected)/dashboard/usermanagement/components/GenericLayout.tsx new file mode 100644 index 0000000..27a6163 --- /dev/null +++ b/src/app/(protected)/dashboard/usermanagement/components/GenericLayout.tsx @@ -0,0 +1,15 @@ +import ProtectedLayout from "./ProtectedLayout"; + +interface GenericLayoutProps { + children: React.ReactNode; + allowedRoles: string[]; +} + +export default function GenericLayout({ + children, + allowedRoles, +}: GenericLayoutProps) { + return ( + {children} + ); +} diff --git a/src/app/(protected)/dashboard/usermanagement/_components/ProtectedLayout.tsx b/src/app/(protected)/dashboard/usermanagement/components/ProtectedLayout.tsx similarity index 80% rename from src/app/(protected)/dashboard/usermanagement/_components/ProtectedLayout.tsx rename to src/app/(protected)/dashboard/usermanagement/components/ProtectedLayout.tsx index ed7830a..b3843f1 100644 --- a/src/app/(protected)/dashboard/usermanagement/_components/ProtectedLayout.tsx +++ b/src/app/(protected)/dashboard/usermanagement/components/ProtectedLayout.tsx @@ -1,5 +1,5 @@ -import { auth } from "@/lib/auth"; import prisma from "@/lib/prisma"; +import { auth } from "@/lib/auth"; import { redirect } from "next/navigation"; interface ProtectedLayoutProps { @@ -7,7 +7,10 @@ interface ProtectedLayoutProps { allowedRoles: string[]; } -export default async function ProtectedLayout({ children, allowedRoles }: ProtectedLayoutProps) { +export default async function ProtectedLayout({ + children, + allowedRoles, +}: ProtectedLayoutProps) { const session = await auth(); const userId = session?.user?.id; @@ -17,7 +20,7 @@ export default async function ProtectedLayout({ children, allowedRoles }: Protec const user = await prisma.user.findUnique({ where: { id: userId }, - select: { role: true } + select: { role: true }, }); if (!user || !allowedRoles.includes(user.role)) { @@ -25,4 +28,4 @@ export default async function ProtectedLayout({ children, allowedRoles }: Protec } return
{children}
; -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/usermanagement/guest/layout.tsx b/src/app/(protected)/dashboard/usermanagement/guest/layout.tsx index 6c084f4..1c4b421 100644 --- a/src/app/(protected)/dashboard/usermanagement/guest/layout.tsx +++ b/src/app/(protected)/dashboard/usermanagement/guest/layout.tsx @@ -1,5 +1,13 @@ -import GenericLayout from "../_components/GenericLayout"; +import GenericLayout from "../components/GenericLayout"; -export default function GuestLayout({ children }: { children: React.ReactNode }) { - return {children}; -} \ No newline at end of file +export default function GuestLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/src/app/(protected)/dashboard/usermanagement/guest/page.tsx b/src/app/(protected)/dashboard/usermanagement/guest/page.tsx index 5d8d1bc..3aa6a30 100644 --- a/src/app/(protected)/dashboard/usermanagement/guest/page.tsx +++ b/src/app/(protected)/dashboard/usermanagement/guest/page.tsx @@ -1,6 +1,6 @@ -import GenericPage from '@/features/user-management/components/generic-page' -import { guestConfig } from '@/features/user-management/config/guest' +import { guestConfig } from "@/features/user-management/config/guest"; +import GenericPage from "@/features/user-management/components/generic-page"; export default function GuestPage() { - return -} \ No newline at end of file + return ; +} diff --git a/src/app/(protected)/dashboard/usermanagement/problem/layout.tsx b/src/app/(protected)/dashboard/usermanagement/problem/layout.tsx index b4a002e..5bd5c5a 100644 --- a/src/app/(protected)/dashboard/usermanagement/problem/layout.tsx +++ b/src/app/(protected)/dashboard/usermanagement/problem/layout.tsx @@ -1,5 +1,13 @@ -import GenericLayout from "../_components/GenericLayout"; +import GenericLayout from "../components/GenericLayout"; -export default function ProblemLayout({ children }: { children: React.ReactNode }) { - return {children}; -} \ No newline at end of file +export default function ProblemLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/src/app/(protected)/dashboard/usermanagement/problem/page.tsx b/src/app/(protected)/dashboard/usermanagement/problem/page.tsx index e67a3d0..d61ac67 100644 --- a/src/app/(protected)/dashboard/usermanagement/problem/page.tsx +++ b/src/app/(protected)/dashboard/usermanagement/problem/page.tsx @@ -1,6 +1,6 @@ -import GenericPage from '@/features/user-management/components/generic-page' -import { problemConfig } from '@/features/user-management/config/problem' +import { problemConfig } from "@/features/user-management/config/problem"; +import GenericPage from "@/features/user-management/components/generic-page"; export default function ProblemPage() { - return -} \ No newline at end of file + return ; +} diff --git a/src/app/(protected)/dashboard/usermanagement/teacher/layout.tsx b/src/app/(protected)/dashboard/usermanagement/teacher/layout.tsx index 8051801..53d57cb 100644 --- a/src/app/(protected)/dashboard/usermanagement/teacher/layout.tsx +++ b/src/app/(protected)/dashboard/usermanagement/teacher/layout.tsx @@ -1,5 +1,9 @@ -import GenericLayout from "../_components/GenericLayout"; +import GenericLayout from "../components/GenericLayout"; -export default function TeacherLayout({ children }: { children: React.ReactNode }) { +export default function TeacherLayout({ + children, +}: { + children: React.ReactNode; +}) { return {children}; -} \ No newline at end of file +} diff --git a/src/app/(protected)/dashboard/usermanagement/teacher/page.tsx b/src/app/(protected)/dashboard/usermanagement/teacher/page.tsx index 9bccf6f..26393b9 100644 --- a/src/app/(protected)/dashboard/usermanagement/teacher/page.tsx +++ b/src/app/(protected)/dashboard/usermanagement/teacher/page.tsx @@ -1,6 +1,6 @@ -import GenericPage from '@/features/user-management/components/generic-page' -import { teacherConfig } from '@/features/user-management/config/teacher' +import { teacherConfig } from "@/features/user-management/config/teacher"; +import GenericPage from "@/features/user-management/components/generic-page"; export default function TeacherPage() { - return -} \ No newline at end of file + return ; +} diff --git a/src/app/globals.css b/src/app/globals.css index a257927..26923bb 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -33,13 +33,13 @@ --chart-5: 213 16% 16%; --radius: 0.5rem; --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; + --sidebar-foreground: 213 13% 6%; + --sidebar-primary: 213 13% 16%; + --sidebar-primary-foreground: 213 13% 76%; + --sidebar-accent: 0 0% 85%; + --sidebar-accent-foreground: 0 0% 25%; + --sidebar-border: 0 0% 95%; + --sidebar-ring: 213 13% 16%; } .dark { @@ -67,14 +67,14 @@ --chart-3: 216 28% 22%; --chart-4: 210 7% 28%; --chart-5: 210 20% 82%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; + --sidebar-background: 216 28% 5%; + --sidebar-foreground: 210 17% 92%; + --sidebar-primary: 210 17% 82%; + --sidebar-primary-foreground: 210 17% 22%; + --sidebar-accent: 216 28% 22%; + --sidebar-accent-foreground: 216 28% 82%; + --sidebar-border: 216 18% 12%; + --sidebar-ring: 210 17% 82%; } } @@ -119,14 +119,3 @@ code[data-theme*=" "] span { color: var(--shiki-dark); background-color: var(--shiki-dark-bg); } - - - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7726dce..d50795c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,7 +6,6 @@ import { NextIntlClientProvider } from "next-intl"; import { ThemeProvider } from "@/components/theme-provider"; import { SettingsDialog } from "@/components/settings-dialog"; - export const metadata: Metadata = { title: "Judge4c", description: @@ -38,4 +37,4 @@ export default async function RootLayout({ children }: RootLayoutProps) { ); -} \ No newline at end of file +} diff --git a/src/components/UncompletedProject/sharedialog.tsx b/src/components/UncompletedProject/sharedialog.tsx index f3fc891..c146ca5 100644 --- a/src/components/UncompletedProject/sharedialog.tsx +++ b/src/components/UncompletedProject/sharedialog.tsx @@ -1,4 +1,3 @@ -import { Button } from "@/components/ui/button" import { DialogContent, DialogDescription, @@ -6,9 +5,10 @@ import { DialogHeader, DialogTitle, DialogClose, -} from "@/components/ui/dialog" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; export function ShareDialogContent({ link }: { link: string }) { return ( @@ -35,5 +35,5 @@ export function ShareDialogContent({ link }: { link: string }) { - ) + ); } diff --git a/src/components/UncompletedProject/wrongbook-dialog.tsx b/src/components/UncompletedProject/wrongbook-dialog.tsx index 03241b3..da6b281 100644 --- a/src/components/UncompletedProject/wrongbook-dialog.tsx +++ b/src/components/UncompletedProject/wrongbook-dialog.tsx @@ -1,37 +1,54 @@ -"use client" +"use client"; -import * as React from "react" +import { + Check, + X, + Info, + AlertTriangle, + Copy, + Check as CheckIcon, +} from "lucide-react"; +import Link from "next/link"; +import * as React from "react"; import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, -} from "@/components/ui/dialog" -import { Check, X, Info, AlertTriangle, Copy, Check as CheckIcon } from "lucide-react" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import Link from "next/link" +} from "@/components/ui/dialog"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; -export function WrongbookDialog({ problems, children }: { problems: { id: string; name: string; status: string; url?: string }[]; children?: React.ReactNode }) { - const [copiedId, setCopiedId] = React.useState(null) +export function WrongbookDialog({ + problems, + children, +}: { + problems: { id: string; name: string; status: string; url?: string }[]; + children?: React.ReactNode; +}) { + const [copiedId, setCopiedId] = React.useState(null); const handleCopyLink = async (item: { id: string; url?: string }) => { - const link = `${window.location.origin}/problems/${item.id}` + const link = `${window.location.origin}/problems/${item.id}`; try { - await navigator.clipboard.writeText(link) - setCopiedId(item.id) - setTimeout(() => setCopiedId(null), 2000) // 2秒后重置状态 + await navigator.clipboard.writeText(link); + setCopiedId(item.id); + setTimeout(() => setCopiedId(null), 2000); // 2秒后重置状态 } catch (err) { - console.error('Failed to copy link:', err) + console.error("Failed to copy link:", err); } - } + }; return ( - {children ? children : ( - + {children ? ( + children + ) : ( + )} @@ -44,13 +61,18 @@ export function WrongbookDialog({ problems, children }: { problems: { id: string
- + {problems.map((item) => ( - + @@ -74,28 +99,46 @@ export function WrongbookDialog({ problems, children }: { problems: { id: string {(() => { if (item.status === "AC") { return ( - - {item.status} + + + {item.status} - ) + ); } else if (item.status === "WA") { return ( - - {item.status} + + + {item.status} - ) - } else if (["RE", "CE", "MLE", "TLE"].includes(item.status)) { + ); + } else if ( + ["RE", "CE", "MLE", "TLE"].includes(item.status) + ) { return ( - - {item.status} + + + {item.status} - ) + ); } else { return ( - - {item.status} + + + {item.status} - ) + ); } })()} @@ -107,5 +150,5 @@ export function WrongbookDialog({ problems, children }: { problems: { id: string - ) + ); } diff --git a/src/components/dashboard-button.tsx b/src/components/dashboard-button.tsx new file mode 100644 index 0000000..93b5bc8 --- /dev/null +++ b/src/components/dashboard-button.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { LayoutDashboardIcon } from "lucide-react"; +import { DropdownMenuItem } from "./ui/dropdown-menu"; + +export const DashboardButton = () => { + const router = useRouter(); + + return ( + router.push("/dashboard")}> + + Dashboard + + ); +}; diff --git a/src/components/dynamic-breadcrumb.tsx b/src/components/dynamic-breadcrumb.tsx index a52799a..4368a2f 100644 --- a/src/components/dynamic-breadcrumb.tsx +++ b/src/components/dynamic-breadcrumb.tsx @@ -1,6 +1,5 @@ -"use client" +"use client"; -import { usePathname } from "next/navigation" import { Breadcrumb, BreadcrumbItem, @@ -8,76 +7,77 @@ import { BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, -} from "@/components/ui/breadcrumb" +} from "@/components/ui/breadcrumb"; +import { usePathname } from "next/navigation"; interface BreadcrumbItem { - label: string - href?: string + label: string; + href?: string; } export function DynamicBreadcrumb() { - const pathname = usePathname() + const pathname = usePathname(); const generateBreadcrumbs = (): BreadcrumbItem[] => { - const segments = pathname.split('/').filter(Boolean) - const breadcrumbs: BreadcrumbItem[] = [] + const segments = pathname.split("/").filter(Boolean); + const breadcrumbs: BreadcrumbItem[] = []; // 添加首页 - breadcrumbs.push({ label: "首页", href: "/" }) + breadcrumbs.push({ label: "首页", href: "/" }); + + let currentPath = ""; - let currentPath = "" - segments.forEach((segment, index) => { - currentPath += `/${segment}` - + currentPath += `/${segment}`; + // 根据路径段生成标签 - let label = segment - + let label = segment; + // 路径映射 const pathMap: Record = { - 'dashboard': '仪表板', - 'management': '管理面板', - 'profile': '用户信息', - 'change-password': '修改密码', - 'problems': '题目', - 'problemset': '题目集', - 'admin': '管理后台', - 'teacher': '教师平台', - 'student': '学生平台', - 'usermanagement': '用户管理', - 'userdashboard': '用户仪表板', - 'protected': '受保护', - 'app': '应用', - 'auth': '认证', - 'sign-in': '登录', - 'sign-up': '注册', - } + dashboard: "仪表板", + management: "管理面板", + profile: "用户信息", + "change-password": "修改密码", + problems: "题目", + problemset: "题目集", + admin: "管理后台", + teacher: "教师平台", + student: "学生平台", + usermanagement: "用户管理", + userdashboard: "用户仪表板", + protected: "受保护", + app: "应用", + auth: "认证", + "sign-in": "登录", + "sign-up": "注册", + }; // 如果是数字,可能是题目ID,显示为"题目详情" if (/^\d+$/.test(segment)) { - label = "详情" + label = "详情"; } else if (pathMap[segment]) { - label = pathMap[segment] + label = pathMap[segment]; } else { // 将 kebab-case 转换为中文 label = segment - .split('-') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); } // 最后一个项目不添加链接 if (index === segments.length - 1) { - breadcrumbs.push({ label }) + breadcrumbs.push({ label }); } else { - breadcrumbs.push({ label, href: currentPath }) + breadcrumbs.push({ label, href: currentPath }); } - }) + }); - return breadcrumbs - } + return breadcrumbs; + }; - const breadcrumbs = generateBreadcrumbs() + const breadcrumbs = generateBreadcrumbs(); return ( @@ -86,13 +86,9 @@ export function DynamicBreadcrumb() {
{item.href ? ( - - {item.label} - + {item.label} ) : ( - - {item.label} - + {item.label} )} {index < breadcrumbs.length - 1 && ( @@ -102,5 +98,5 @@ export function DynamicBreadcrumb() { ))} - ) -} \ No newline at end of file + ); +} diff --git a/src/components/nav-main.tsx b/src/components/nav-main.tsx index 3481f96..8b2d99c 100644 --- a/src/components/nav-main.tsx +++ b/src/components/nav-main.tsx @@ -1,12 +1,5 @@ -"use client" +"use client"; -import { ChevronRight, type LucideIcon } from "lucide-react" - -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible" import { SidebarGroup, SidebarGroupLabel, @@ -17,21 +10,27 @@ import { SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, -} from "@/components/ui/sidebar" +} from "@/components/ui/sidebar"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { ChevronRight, type LucideIcon } from "lucide-react"; export function NavMain({ items, }: { items: { - title: string - url: string - icon: LucideIcon - isActive?: boolean + title: string; + url: string; + icon: LucideIcon; + isActive?: boolean; items?: { - title: string - url: string - }[] - }[] + title: string; + url: string; + }[]; + }[]; }) { return ( @@ -74,5 +73,5 @@ export function NavMain({ ))} - ) + ); } diff --git a/src/components/nav-projects.tsx b/src/components/nav-projects.tsx index acb0c8d..3803013 100644 --- a/src/components/nav-projects.tsx +++ b/src/components/nav-projects.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import { BookX, @@ -9,19 +9,7 @@ import { X, Info, AlertTriangle, -} from "lucide-react" -import React, { useState } from "react" -import { useRouter } from "next/navigation" -import { - Dialog, -} from "@/components/ui/dialog" - -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" +} from "lucide-react"; import { SidebarGroup, SidebarGroupLabel, @@ -30,24 +18,33 @@ import { SidebarMenuButton, SidebarMenuItem, useSidebar, -} from "@/components/ui/sidebar" -import { WrongbookDialog } from "@/components/UncompletedProject/wrongbook-dialog" -import { ShareDialogContent } from "@/components/UncompletedProject/sharedialog" +} from "@/components/ui/sidebar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Dialog } from "@/components/ui/dialog"; +import { ShareDialogContent } from "@/components/UncompletedProject/sharedialog"; +import { WrongbookDialog } from "@/components/UncompletedProject/wrongbook-dialog"; export function NavProjects({ projects, }: { projects: { - id: string - name: string - status: string - url?: string - }[] + id: string; + name: string; + status: string; + url?: string; + }[]; }) { - const { isMobile } = useSidebar() - const [shareOpen, setShareOpen] = useState(false) - const [shareLink, setShareLink] = useState("") - const router = useRouter() + const { isMobile } = useSidebar(); + const [shareOpen, setShareOpen] = useState(false); + const [shareLink, setShareLink] = useState(""); + const router = useRouter(); return ( <> @@ -59,7 +56,7 @@ export function NavProjects({ - + {item.status} - ) + ); } else if (item.status === "WA") { return ( {item.status} - ) - } else if (["RE", "CE", "MLE", "TLE"].includes(item.status)) { + ); + } else if ( + ["RE", "CE", "MLE", "TLE"].includes(item.status) + ) { return ( {item.status} - ) + ); } else { return ( {item.status} - ) + ); } })()} @@ -114,11 +113,11 @@ export function NavProjects({ > { - e.stopPropagation() + e.stopPropagation(); if (item.url) { - router.push(item.url) + router.push(item.url); } else { - router.push(`/problems/${item.id}`) + router.push(`/problems/${item.id}`); } }} > @@ -127,9 +126,11 @@ export function NavProjects({ { - e.stopPropagation() - setShareLink(`${window.location.origin}/problems/${item.id}`) - setShareOpen(true) + e.stopPropagation(); + setShareLink( + `${window.location.origin}/problems/${item.id}` + ); + setShareOpen(true); }} > @@ -153,5 +154,5 @@ export function NavProjects({ - ) -} \ No newline at end of file + ); +} diff --git a/src/components/nav-secondary.tsx b/src/components/nav-secondary.tsx index a931a7e..1d9a13c 100644 --- a/src/components/nav-secondary.tsx +++ b/src/components/nav-secondary.tsx @@ -1,23 +1,22 @@ -import * as React from "react" -import { type LucideIcon } from "lucide-react" - +import * as React from "react"; import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem, -} from "@/components/ui/sidebar" +} from "@/components/ui/sidebar"; +import { type LucideIcon } from "lucide-react"; export function NavSecondary({ items, ...props }: { items: { - title: string - url: string - icon: LucideIcon - }[] + title: string; + url: string; + icon: LucideIcon; + }[]; } & React.ComponentPropsWithoutRef) { return ( @@ -36,5 +35,5 @@ export function NavSecondary({ - ) + ); } diff --git a/src/components/nav-user.tsx b/src/components/nav-user.tsx index d915947..ad9e4aa 100644 --- a/src/components/nav-user.tsx +++ b/src/components/nav-user.tsx @@ -1,21 +1,11 @@ -"use client" +"use client"; import { - BadgeCheck, - // Bell, - ChevronsUpDown, - UserPen, - LogOut, - // Sparkles, -} from "lucide-react" -import { useRouter } from "next/navigation" -import { signOut } from "next-auth/react" - -import { - Avatar, - AvatarFallback, - AvatarImage, -} from "@/components/ui/avatar" + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar"; import { DropdownMenu, DropdownMenuContent, @@ -24,30 +14,28 @@ import { DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar" +} from "@/components/ui/dropdown-menu"; +import { signOut } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { BadgeCheck, ChevronsUpDown, UserPen, LogOut } from "lucide-react"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; export function NavUser({ user, }: { user: { - name: string - email: string - avatar: string - } + name: string; + email: string; + avatar: string; + }; }) { - const { isMobile } = useSidebar() - const router = useRouter() + const { isMobile } = useSidebar(); + const router = useRouter(); async function handleLogout() { - await signOut({ + await signOut({ callbackUrl: "/sign-in", - redirect: true + redirect: true, }); } @@ -98,26 +86,15 @@ export function NavUser({
- {/* - - - Update - - */} - {/* */} Account - router.push("/sign-in")}> + router.push("/sign-in")}> Switch User - {/* - - Notifications - */} @@ -128,5 +105,5 @@ export function NavUser({ - ) + ); } diff --git a/src/components/sidebar/admin-sidebar.tsx b/src/components/sidebar/admin-sidebar.tsx index 4525c75..26b716a 100644 --- a/src/components/sidebar/admin-sidebar.tsx +++ b/src/components/sidebar/admin-sidebar.tsx @@ -1,15 +1,6 @@ -"use client" -import { siteConfig } from "@/config/site" -import * as React from "react" -import { - LifeBuoy, - Send, - Shield, -} from "lucide-react" +"use client"; -import { NavMain } from "@/components/nav-main" -import { NavSecondary } from "@/components/nav-secondary" -import { NavUser } from "@/components/nav-user" +import * as React from "react"; import { Sidebar, SidebarContent, @@ -18,14 +9,19 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, -} from "@/components/ui/sidebar" -import { User } from "next-auth" +} from "@/components/ui/sidebar"; +import { User } from "next-auth"; +import { siteConfig } from "@/config/site"; +import { NavMain } from "@/components/nav-main"; +import { NavUser } from "@/components/nav-user"; +import { LifeBuoy, Send, Shield } from "lucide-react"; +import { NavSecondary } from "@/components/nav-secondary"; const adminData = { navMain: [ { title: "管理面板", - url: "#", + url: "/dashboard", icon: Shield, isActive: true, items: [ @@ -35,19 +31,21 @@ const adminData = { { title: "题目管理", url: "/dashboard/usermanagement/problem" }, ], }, - ], navSecondary: [ { title: "帮助", url: "/", icon: LifeBuoy }, { title: "反馈", url: siteConfig.url.repo.github, icon: Send }, ], -} +}; interface AdminSidebarProps { user: User; } -export function AdminSidebar({ user, ...props }: AdminSidebarProps & React.ComponentProps) { +export function AdminSidebar({ + user, + ...props +}: AdminSidebarProps & React.ComponentProps) { const userInfo = { name: user.name ?? "管理员", email: user.email ?? "", @@ -81,5 +79,5 @@ export function AdminSidebar({ user, ...props }: AdminSidebarProps & React.Compo - ) + ); } diff --git a/src/components/sidebar/app-sidebar.tsx b/src/components/sidebar/app-sidebar.tsx index 84af52c..fcaf119 100644 --- a/src/components/sidebar/app-sidebar.tsx +++ b/src/components/sidebar/app-sidebar.tsx @@ -1,20 +1,6 @@ "use client"; -import { siteConfig } from "@/config/site"; import * as React from "react"; -import { - // BookOpen, - Command, - LifeBuoy, - Send, - // Settings2, - SquareTerminal, -} from "lucide-react"; - -import { NavMain } from "@/components/nav-main"; -import { NavProjects } from "@/components/nav-projects"; -import { NavSecondary } from "@/components/nav-secondary"; -import { NavUser } from "@/components/nav-user"; import { Sidebar, SidebarContent, @@ -25,6 +11,12 @@ import { SidebarMenuItem, } from "@/components/ui/sidebar"; import { User } from "next-auth"; +import { siteConfig } from "@/config/site"; +import { NavMain } from "@/components/nav-main"; +import { NavUser } from "@/components/nav-user"; +import { NavProjects } from "@/components/nav-projects"; +import { NavSecondary } from "@/components/nav-secondary"; +import { Command, LifeBuoy, Send, SquareTerminal } from "lucide-react"; const data = { navMain: [ diff --git a/src/components/sidebar/teacher-sidebar.tsx b/src/components/sidebar/teacher-sidebar.tsx index 78abc3a..6338fa3 100644 --- a/src/components/sidebar/teacher-sidebar.tsx +++ b/src/components/sidebar/teacher-sidebar.tsx @@ -1,18 +1,13 @@ -"use client" -import { siteConfig } from "@/config/site" -import * as React from "react" +"use client"; + import { Command, LifeBuoy, PieChart, Send, - // Settings2, SquareTerminal, -} from "lucide-react" - -import { NavMain } from "@/components/nav-main" -import { NavSecondary } from "@/components/nav-secondary" -import { NavUser } from "@/components/nav-user" +} from "lucide-react"; +import * as React from "react"; import { Sidebar, SidebarContent, @@ -21,8 +16,12 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, -} from "@/components/ui/sidebar" -import { User } from "next-auth" +} from "@/components/ui/sidebar"; +import { User } from "next-auth"; +import { siteConfig } from "@/config/site"; +import { NavMain } from "@/components/nav-main"; +import { NavUser } from "@/components/nav-user"; +import { NavSecondary } from "@/components/nav-secondary"; const data = { navMain: [ @@ -81,13 +80,16 @@ const data = { icon: Send, }, ], -} +}; interface TeacherSidebarProps { user: User; } -export function TeacherSidebar({ user, ...props }: TeacherSidebarProps & React.ComponentProps) { +export function TeacherSidebar({ + user, + ...props +}: TeacherSidebarProps & React.ComponentProps) { const userInfo = { name: user.name ?? "", email: user.email ?? "", @@ -121,5 +123,5 @@ export function TeacherSidebar({ user, ...props }: TeacherSidebarProps & React.C - ) + ); } diff --git a/src/components/user-avatar.tsx b/src/components/user-avatar.tsx index e6c97dd..2cada3a 100644 --- a/src/components/user-avatar.tsx +++ b/src/components/user-avatar.tsx @@ -13,6 +13,7 @@ import { auth, signIn, signOut } from "@/lib/auth"; import { getTranslations } from "next-intl/server"; import { Skeleton } from "@/components/ui/skeleton"; import { SettingsButton } from "@/components/settings-button"; +import { DashboardButton } from "@/components/dashboard-button"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; const handleLogIn = async () => { @@ -88,6 +89,7 @@ const UserAvatar = async () => { + diff --git a/src/features/admin/ui/layouts/protected-layout.tsx b/src/features/admin/ui/layouts/protected-layout.tsx new file mode 100644 index 0000000..e1d6b7a --- /dev/null +++ b/src/features/admin/ui/layouts/protected-layout.tsx @@ -0,0 +1,32 @@ +import prisma from "@/lib/prisma"; +import { Role } from "@/generated/client"; +import { auth, signIn } from "@/lib/auth"; +import { redirect } from "next/navigation"; + +interface ProtectedLayoutProps { + roles: Role[]; + children: React.ReactNode; +} + +export const ProtectedLayout = async ({ + roles, + children, +}: ProtectedLayoutProps) => { + const session = await auth(); + const userId = session?.user?.id; + + if (!userId) { + await signIn(); + } + + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { role: true }, + }); + + if (!user || !roles.includes(user.role)) { + redirect("unauthorized"); + } + + return <>{children}; +}; diff --git a/src/features/admin/ui/views/problem-edit-view.tsx b/src/features/admin/ui/views/problem-edit-view.tsx index a9f6f22..57e17a3 100644 --- a/src/features/admin/ui/views/problem-edit-view.tsx +++ b/src/features/admin/ui/views/problem-edit-view.tsx @@ -1,28 +1,26 @@ -// import EditCodePanel from "@/components/creater/edit-code-panel"; -// import EditDetailPanel from "@/components/creater/edit-detail-panel"; -// import EditSolutionPanel from "@/components/creater/edit-solution-panel"; -// import EditTestcasePanel from "@/components/creater/edit-testcase-panel"; -// import EditDescriptionPanel from "@/components/creater/edit-description-panel"; -// import { ProblemEditFlexLayout } from "@/features/admin/ui/components/problem-edit-flexlayout"; +import EditCodePanel from "@/components/creater/edit-code-panel"; +import EditDetailPanel from "@/components/creater/edit-detail-panel"; +import EditSolutionPanel from "@/components/creater/edit-solution-panel"; +import EditTestcasePanel from "@/components/creater/edit-testcase-panel"; +import EditDescriptionPanel from "@/components/creater/edit-description-panel"; +import { ProblemEditFlexLayout } from "@/features/admin/ui/components/problem-edit-flexlayout"; -// interface ProblemEditViewProps { -// problemId: string; -// } +interface ProblemEditViewProps { + problemId: string; +} -export const ProblemEditView = ( - // { problemId }: ProblemEditViewProps -) => { - // const components: Record = { - // description: , - // solution: , - // detail: , - // code: , - // testcase: , - // }; +export const ProblemEditView = ({ problemId }: ProblemEditViewProps) => { + const components: Record = { + detail: , + description: , + solution: , + code: , + testcase: , + }; return (
- {/* */} +
); }; diff --git a/src/features/user-management/components/generic-page.tsx b/src/features/user-management/components/generic-page.tsx index 7063fea..090c63e 100644 --- a/src/features/user-management/components/generic-page.tsx +++ b/src/features/user-management/components/generic-page.tsx @@ -1,21 +1,24 @@ -import { UserTable } from './user-table' -import { UserConfig } from './user-table' -import prisma from '@/lib/prisma' -import type { User, Problem } from '@/generated/client' -import { Role } from '@/generated/client' +import prisma from "@/lib/prisma"; +import { UserTable } from "./user-table"; +import { Role } from "@/generated/client"; +import { UserConfig } from "./user-table"; +import type { User, Problem } from "@/generated/client"; interface GenericPageProps { - userType: 'admin' | 'teacher' | 'guest' | 'problem' - config: UserConfig + userType: "admin" | "teacher" | "guest" | "problem"; + config: UserConfig; } -export default async function GenericPage({ userType, config }: GenericPageProps) { - if (userType === 'problem') { - const data: Problem[] = await prisma.problem.findMany({}) - return +export default async function GenericPage({ + userType, + config, +}: GenericPageProps) { + if (userType === "problem") { + const data: Problem[] = await prisma.problem.findMany({}); + return ; } else { - const role = userType.toUpperCase() as Role - const data: User[] = await prisma.user.findMany({ where: { role } }) - return + const role = userType.toUpperCase() as Role; + const data: User[] = await prisma.user.findMany({ where: { role } }); + return ; } -} \ No newline at end of file +} diff --git a/src/features/user-management/components/user-table.tsx b/src/features/user-management/components/user-table.tsx index ad5710e..2235187 100644 --- a/src/features/user-management/components/user-table.tsx +++ b/src/features/user-management/components/user-table.tsx @@ -1,6 +1,25 @@ -"use client" +"use client"; -import * as React from "react" +import { + ChevronLeftIcon, + ChevronRightIcon, + ChevronsLeftIcon, + ChevronsRightIcon, + PlusIcon, + PencilIcon, + TrashIcon, + ListFilter, +} from "lucide-react"; +import { z } from "zod"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import * as React from "react"; import { ColumnDef, ColumnFiltersState, @@ -14,24 +33,15 @@ import { getPaginationRowModel, getSortedRowModel, useReactTable, -} from "@tanstack/react-table" +} from "@tanstack/react-table"; +import { toast } from "sonner"; import { - ChevronLeftIcon, - ChevronRightIcon, - ChevronsLeftIcon, - ChevronsRightIcon, - PlusIcon, - PencilIcon, - TrashIcon, - ListFilter, -} from "lucide-react" -import { toast } from "sonner" -import { useState, useEffect } from "react" -import { useForm } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { z } from "zod" -import { useRouter } from "next/navigation" - + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Dialog, DialogContent, @@ -39,89 +49,82 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from "@/components/ui/dialog" -import { Button } from "@/components/ui/button" -import { Checkbox } from "@/components/ui/checkbox" +} from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" +} from "@/components/ui/dropdown-menu"; +import { useForm } from "react-hook-form"; +import { Tabs } from "@/components/ui/tabs"; +import { useRouter } from "next/navigation"; +import { useState, useEffect } from "react"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Difficulty, Role } from "@/generated/client"; +import type { User, Problem } from "@/generated/client"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" + createUser, + updateUser, + deleteUser, +} from "@/app/(protected)/dashboard/usermanagement/actions/userActions"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" -import { - Tabs, -} from "@/components/ui/tabs" - -import { createUser, updateUser, deleteUser } from '@/app/(protected)/dashboard/usermanagement/_actions/userActions' -import { createProblem, deleteProblem } from '@/app/(protected)/dashboard/usermanagement/_actions/problemActions' -import type { User, Problem } from '@/generated/client' -import { Difficulty, Role } from '@/generated/client' + createProblem, + deleteProblem, +} from "@/app/(protected)/dashboard/usermanagement/actions/problemActions"; export interface UserConfig { - userType: string - title: string - apiPath: string + userType: string; + title: string; + apiPath: string; columns: Array<{ - key: string - label: string - sortable?: boolean - searchable?: boolean - placeholder?: string - }> + key: string; + label: string; + sortable?: boolean; + searchable?: boolean; + placeholder?: string; + }>; formFields: Array<{ - key: string - label: string - type: string - placeholder?: string - required?: boolean - options?: Array<{ value: string; label: string }> - }> + key: string; + label: string; + type: string; + placeholder?: string; + required?: boolean; + options?: Array<{ value: string; label: string }>; + }>; actions: { - add: { label: string; icon: string } - edit: { label: string; icon: string } - delete: { label: string; icon: string } - batchDelete: { label: string; icon: string } - } + add: { label: string; icon: string }; + edit: { label: string; icon: string }; + delete: { label: string; icon: string }; + batchDelete: { label: string; icon: string }; + }; pagination: { - pageSizes: number[] - defaultPageSize: number - } + pageSizes: number[]; + defaultPageSize: number; + }; } type UserTableProps = | { config: UserConfig; data: User[] } - | { config: UserConfig; data: Problem[] } + | { config: UserConfig; data: Problem[] }; type UserForm = { - id?: string - name: string - email: string - password: string - createdAt: string - role: Role - image: string | null - emailVerified: Date | null -} + id?: string; + name: string; + email: string; + password: string; + createdAt: string; + role: Role; + image: string | null; + emailVerified: Date | null; +}; // 新增用户表单类型 -type AddUserForm = Omit +type AddUserForm = Omit; const addUserSchema = z.object({ name: z.string(), @@ -131,10 +134,10 @@ const addUserSchema = z.object({ image: z.string().nullable(), emailVerified: z.date().nullable(), role: z.nativeEnum(Role), -}) +}); const editUserSchema = z.object({ - id: z.string().default(''), + id: z.string().default(""), name: z.string(), email: z.string().email(), password: z.string(), @@ -142,38 +145,40 @@ const editUserSchema = z.object({ image: z.string().nullable(), emailVerified: z.date().nullable(), role: z.nativeEnum(Role), -}) +}); // 题目表单 schema 兼容 null/undefined const addProblemSchema = z.object({ displayId: z.number().optional().default(0), difficulty: z.nativeEnum(Difficulty).default(Difficulty.EASY), -}) +}); export function UserTable(props: UserTableProps) { - const isProblem = props.config.userType === 'problem' - const router = useRouter() - const problemData = isProblem ? (props.data as Problem[]) : undefined + const isProblem = props.config.userType === "problem"; + const router = useRouter(); + const problemData = isProblem ? (props.data as Problem[]) : undefined; - const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) - const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) - const [editingUser, setEditingUser] = useState(null) - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) - const [deleteBatch, setDeleteBatch] = useState(false) - const [rowSelection, setRowSelection] = useState({}) - const [columnVisibility, setColumnVisibility] = useState({}) - const [columnFilters, setColumnFilters] = useState([]) - const [sorting, setSorting] = useState([]) + const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [deleteBatch, setDeleteBatch] = useState(false); + const [rowSelection, setRowSelection] = useState({}); + const [columnVisibility, setColumnVisibility] = useState({}); + const [columnFilters, setColumnFilters] = useState([]); + const [sorting, setSorting] = useState([]); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: props.config.pagination.defaultPageSize, - }) - const [pageInput, setPageInput] = useState(pagination.pageIndex + 1) - const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) - const [pendingDeleteItem, setPendingDeleteItem] = useState(null) + }); + const [pageInput, setPageInput] = useState(pagination.pageIndex + 1); + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); + const [pendingDeleteItem, setPendingDeleteItem] = useState< + User | Problem | null + >(null); useEffect(() => { - setPageInput(pagination.pageIndex + 1) - }, [pagination.pageIndex]) + setPageInput(pagination.pageIndex + 1); + }, [pagination.pageIndex]); // 表格列 const tableColumns = React.useMemo[]>(() => { @@ -183,7 +188,9 @@ export function UserTable(props: UserTableProps) { header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} + onCheckedChange={(value) => + table.toggleAllPageRowsSelected(!!value) + } aria-label="选择所有" /> ), @@ -197,41 +204,43 @@ export function UserTable(props: UserTableProps) { enableSorting: false, enableHiding: false, }, - ] + ]; props.config.columns.forEach((col) => { const column: ColumnDef = { accessorKey: col.key, header: col.label, cell: ({ row }) => { // 类型安全分流 - if (col.key === 'displayId' && isProblem) { - return (row.original as Problem).displayId + if (col.key === "displayId" && isProblem) { + return (row.original as Problem).displayId; } - if ((col.key === 'createdAt' || col.key === 'updatedAt')) { - const value = row.getValue(col.key) + if (col.key === "createdAt" || col.key === "updatedAt") { + const value = row.getValue(col.key); if (value instanceof Date) { - return value.toLocaleString() + return value.toLocaleString(); } - if (typeof value === 'string' && !isNaN(Date.parse(value))) { - return new Date(value).toLocaleString() + if (typeof value === "string" && !isNaN(Date.parse(value))) { + return new Date(value).toLocaleString(); } } - return row.getValue(col.key) + return row.getValue(col.key); }, enableSorting: col.sortable !== false, - filterFn: col.searchable ? (row, columnId, value) => { - const searchValue = String(value).toLowerCase() - const cellValue = String(row.getValue(columnId)).toLowerCase() - return cellValue.includes(searchValue) - } : undefined, - } - columns.push(column) - }) + filterFn: col.searchable + ? (row, columnId, value) => { + const searchValue = String(value).toLowerCase(); + const cellValue = String(row.getValue(columnId)).toLowerCase(); + return cellValue.includes(searchValue); + } + : undefined, + }; + columns.push(column); + }); columns.push({ id: "actions", header: () =>
操作
, cell: ({ row }) => { - const item = row.original + const item = row.original; return (
- ) + ); }, - }) - return columns - }, [props.config, router, isProblem]) + }); + return columns; + }, [props.config, router, isProblem]); const table = useReactTable({ data: props.data, @@ -293,50 +304,77 @@ export function UserTable(props: UserTableProps) { getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), - }) + }); // 添加用户对话框组件(仅用户) - function AddUserDialogUser({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { - const [isLoading, setIsLoading] = useState(false) + function AddUserDialogUser({ + open, + onOpenChange, + }: { + open: boolean; + onOpenChange: (open: boolean) => void; + }) { + const [isLoading, setIsLoading] = useState(false); const form = useForm({ resolver: zodResolver(addUserSchema), - defaultValues: { name: '', email: '', password: '', createdAt: '', image: null, emailVerified: null, role: Role.GUEST }, - }) + defaultValues: { + name: "", + email: "", + password: "", + createdAt: "", + image: null, + emailVerified: null, + role: Role.GUEST, + }, + }); React.useEffect(() => { if (open) { - form.reset({ name: '', email: '', password: '', createdAt: '', image: null, emailVerified: null, role: Role.GUEST }) + form.reset({ + name: "", + email: "", + password: "", + createdAt: "", + image: null, + emailVerified: null, + role: Role.GUEST, + }); } - }, [open, form]) + }, [open, form]); async function onSubmit(data: AddUserForm) { try { - setIsLoading(true) - + setIsLoading(true); + // 验证必填字段 - if (!data.password || data.password.trim() === '') { - toast.error('密码不能为空', { duration: 1500 }) - return + if (!data.password || data.password.trim() === "") { + toast.error("密码不能为空", { duration: 1500 }); + return; } - + const submitData = { ...data, image: data.image ?? null, emailVerified: data.emailVerified ?? null, role: data.role ?? Role.GUEST, - } - if (!submitData.name) submitData.name = '' - if (!submitData.createdAt) submitData.createdAt = new Date().toISOString() - else submitData.createdAt = new Date(submitData.createdAt).toISOString() - if (props.config.userType === 'admin') await createUser('admin', submitData) - else if (props.config.userType === 'teacher') await createUser('teacher', submitData) - else if (props.config.userType === 'guest') await createUser('guest', submitData) - onOpenChange(false) - toast.success('添加成功', { duration: 1500 }) - router.refresh() + }; + if (!submitData.name) submitData.name = ""; + if (!submitData.createdAt) + submitData.createdAt = new Date().toISOString(); + else + submitData.createdAt = new Date(submitData.createdAt).toISOString(); + if (props.config.userType === "admin") + await createUser("admin", submitData); + else if (props.config.userType === "teacher") + await createUser("teacher", submitData); + else if (props.config.userType === "guest") + await createUser("guest", submitData); + onOpenChange(false); + toast.success("添加成功", { duration: 1500 }); + router.refresh(); } catch (error) { - console.error('添加失败:', error) - toast.error('添加失败', { duration: 1500 }) + console.error("添加失败:", error); + toast.error("添加失败", { duration: 1500 }); } finally { - setIsLoading(false) + setIsLoading(false); } } return ( @@ -344,47 +382,84 @@ export function UserTable(props: UserTableProps) { {props.config.actions.add.label} - - 请填写信息,ID自动生成。 - + 请填写信息,ID自动生成。
- {props.config.formFields.filter(field => field.key !== 'id').map((field) => ( -
- - {field.type === 'select' && field.options ? ( - - ) : ( - - )} - {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message && ( -

- {form.formState.errors[field.key as keyof typeof form.formState.errors]?.message as string} -

- )} -
- ))} + {props.config.formFields + .filter((field) => field.key !== "id") + .map((field) => ( +
+ + {field.type === "select" && field.options ? ( + + ) : ( + + )} + {form.formState.errors[ + field.key as keyof typeof form.formState.errors + ]?.message && ( +

+ { + form.formState.errors[ + field.key as keyof typeof form.formState.errors + ]?.message as string + } +

+ )} +
+ ))}
操作题目名称 + 题目名称 + 状态
- + {item.name}
{table.getHeaderGroups().map((headerGroup) => ( @@ -741,7 +888,7 @@ export function UserTable(props: UserTableProps) { header.getContext() )} - ) + ); })} ))} @@ -788,11 +935,13 @@ export function UserTable(props: UserTableProps) {