mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-07-04 01:10:53 +00:00
refactor: format and relocate code
This commit is contained in:
parent
47feffd62c
commit
0695dd2f61
@ -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();
|
|
||||||
})};
|
|
@ -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());
|
|
@ -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();
|
|
@ -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();
|
|
||||||
});
|
|
@ -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();
|
|
@ -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 <ProblemEditView
|
|
||||||
// problemId={problemId}
|
|
||||||
/>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
@ -1,11 +0,0 @@
|
|||||||
import { AdminProtectedLayout } from "@/features/admin/ui/layouts/admin-protected-layout";
|
|
||||||
|
|
||||||
interface LayoutProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Layout = ({ children }: LayoutProps) => {
|
|
||||||
return <AdminProtectedLayout>{children}</AdminProtectedLayout>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Layout;
|
|
@ -22,7 +22,7 @@ export async function getStudentDashboardData() {
|
|||||||
// 检查用户是否存在
|
// 检查用户是否存在
|
||||||
const currentUser = await prisma.user.findUnique({
|
const currentUser = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
select: { id: true, name: true, email: true, role: true }
|
select: { id: true, name: true, email: true, role: true },
|
||||||
});
|
});
|
||||||
console.log("当前用户信息:", currentUser);
|
console.log("当前用户信息:", currentUser);
|
||||||
|
|
||||||
@ -41,17 +41,20 @@ const allProblems = await prisma.problem.findMany({
|
|||||||
where: {
|
where: {
|
||||||
type: "TITLE",
|
type: "TITLE",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("已发布题目数量:", allProblems.length);
|
console.log("已发布题目数量:", allProblems.length);
|
||||||
console.log("题目列表:", allProblems.map(p => ({
|
console.log(
|
||||||
|
"题目列表:",
|
||||||
|
allProblems.map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
displayId: p.displayId,
|
displayId: p.displayId,
|
||||||
title: p.localizations[0]?.content || "无标题",
|
title: p.localizations[0]?.content || "无标题",
|
||||||
difficulty: p.difficulty
|
difficulty: p.difficulty,
|
||||||
})));
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
// 获取当前学生的所有提交记录(包含题目英文标题)
|
// 获取当前学生的所有提交记录(包含题目英文标题)
|
||||||
const userSubmissions = await prisma.submission.findMany({
|
const userSubmissions = await prisma.submission.findMany({
|
||||||
@ -65,25 +68,28 @@ console.log("题目列表:", allProblems.map(p => ({
|
|||||||
localizations: {
|
localizations: {
|
||||||
where: {
|
where: {
|
||||||
type: "TITLE",
|
type: "TITLE",
|
||||||
locale: "en" // 或者根据需求使用其他语言
|
locale: "en", // 或者根据需求使用其他语言
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
content: true
|
content: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("当前用户提交记录数量:", userSubmissions.length);
|
console.log("当前用户提交记录数量:", userSubmissions.length);
|
||||||
console.log("提交记录详情:", userSubmissions.map(s => ({
|
console.log(
|
||||||
|
"提交记录详情:",
|
||||||
|
userSubmissions.map((s) => ({
|
||||||
problemId: s.problemId,
|
problemId: s.problemId,
|
||||||
problemDisplayId: s.problem.displayId,
|
problemDisplayId: s.problem.displayId,
|
||||||
title: s.problem.localizations[0]?.content || "无标题",
|
title: s.problem.localizations[0]?.content || "无标题",
|
||||||
difficulty: s.problem.difficulty,
|
difficulty: s.problem.difficulty,
|
||||||
status: s.status
|
status: s.status,
|
||||||
})));
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
// 计算题目完成情况
|
// 计算题目完成情况
|
||||||
const completedProblems = new Set<string | number>();
|
const completedProblems = new Set<string | number>();
|
||||||
@ -110,7 +116,10 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
|
|||||||
const completionData = {
|
const completionData = {
|
||||||
total: allProblems.length,
|
total: allProblems.length,
|
||||||
completed: completedProblems.size,
|
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 错题比例数据 - 基于已完成的题目计算
|
// 错题比例数据 - 基于已完成的题目计算
|
||||||
@ -118,7 +127,10 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
|
|||||||
|
|
||||||
// 统计在已完成的题目中,哪些题目曾经有过错误提交
|
// 统计在已完成的题目中,哪些题目曾经有过错误提交
|
||||||
userSubmissions.forEach((submission) => {
|
userSubmissions.forEach((submission) => {
|
||||||
if (submission.status !== "AC" && completedProblems.has(submission.problemId)) {
|
if (
|
||||||
|
submission.status !== "AC" &&
|
||||||
|
completedProblems.has(submission.problemId)
|
||||||
|
) {
|
||||||
wrongProblems.add(submission.problemId);
|
wrongProblems.add(submission.problemId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -126,7 +138,10 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
|
|||||||
const errorData = {
|
const errorData = {
|
||||||
total: completedProblems.size, // 已完成的题目总数
|
total: completedProblems.size, // 已完成的题目总数
|
||||||
wrong: wrongProblems.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,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 易错题列表(按错误次数排序)
|
// 易错题列表(按错误次数排序)
|
||||||
@ -135,7 +150,9 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
|
|||||||
const problem = allProblems.find((p) => p.id === problemId);
|
const problem = allProblems.find((p) => p.id === problemId);
|
||||||
|
|
||||||
// 从 problem.localizations 中获取标题
|
// 从 problem.localizations 中获取标题
|
||||||
const title = problem?.localizations?.find((loc) => loc.type === "TITLE")?.content || "未知题目";
|
const title =
|
||||||
|
problem?.localizations?.find((loc) => loc.type === "TITLE")
|
||||||
|
?.content || "未知题目";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: problem?.displayId || problemId,
|
id: problem?.displayId || problemId,
|
||||||
@ -153,7 +170,10 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
|
|||||||
difficultProblems,
|
difficultProblems,
|
||||||
pieChartData: [
|
pieChartData: [
|
||||||
{ name: "已完成", value: completionData.completed },
|
{ name: "已完成", value: completionData.completed },
|
||||||
{ name: "未完成", value: completionData.total - completionData.completed },
|
{
|
||||||
|
name: "未完成",
|
||||||
|
value: completionData.total - completionData.completed,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
errorPieChartData: [
|
errorPieChartData: [
|
||||||
{ name: "正确", value: errorData.total - errorData.wrong },
|
{ name: "正确", value: errorData.total - errorData.wrong },
|
||||||
@ -170,6 +190,8 @@ console.log("提交记录详情:", userSubmissions.map(s => ({
|
|||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取学生仪表板数据失败:", error);
|
console.error("获取学生仪表板数据失败:", error);
|
||||||
throw new Error(`获取数据失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
throw new Error(
|
||||||
|
`获取数据失败: ${error instanceof Error ? error.message : "未知错误"}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,8 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Locale, Status, ProblemLocalization } from "@/generated/client";
|
|
||||||
import { getLocale } from "next-intl/server";
|
import { getLocale } from "next-intl/server";
|
||||||
|
import { Locale, Status, ProblemLocalization } from "@/generated/client";
|
||||||
|
|
||||||
const getLocalizedTitle = (
|
const getLocalizedTitle = (
|
||||||
localizations: ProblemLocalization[],
|
localizations: ProblemLocalization[],
|
||||||
@ -38,28 +38,33 @@ export interface DifficultProblemData {
|
|||||||
problemDisplayId: number;
|
problemDisplayId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProblemCompletionData(): Promise<ProblemCompletionData[]> {
|
export async function getProblemCompletionData(): Promise<
|
||||||
|
ProblemCompletionData[]
|
||||||
|
> {
|
||||||
// 获取所有提交记录,按题目分组统计
|
// 获取所有提交记录,按题目分组统计
|
||||||
const submissions = await prisma.submission.findMany({
|
const submissions = await prisma.submission.findMany({
|
||||||
include: {
|
include: {
|
||||||
user: true,
|
user: true,
|
||||||
problem: {
|
problem: {
|
||||||
include: {
|
include: {
|
||||||
localizations:true
|
localizations: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const locale = await getLocale();
|
const locale = await getLocale();
|
||||||
|
|
||||||
// 按题目分组统计完成情况(统计独立用户数)
|
// 按题目分组统计完成情况(统计独立用户数)
|
||||||
const problemStats = new Map<string, {
|
const problemStats = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
completedUsers: Set<string>;
|
completedUsers: Set<string>;
|
||||||
totalUsers: Set<string>;
|
totalUsers: Set<string>;
|
||||||
title: string;
|
title: string;
|
||||||
displayId: number;
|
displayId: number;
|
||||||
}>();
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
submissions.forEach((submission) => {
|
submissions.forEach((submission) => {
|
||||||
const localizations = submission.problem.localizations;
|
const localizations = submission.problem.localizations;
|
||||||
@ -92,7 +97,8 @@ export async function getProblemCompletionData(): Promise<ProblemCompletionData[
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 转换为图表数据格式,按题目displayId排序
|
// 转换为图表数据格式,按题目displayId排序
|
||||||
const problemDataArray = Array.from(problemStats.entries()).map(([problemId, stats]) => {
|
const problemDataArray = Array.from(problemStats.entries()).map(
|
||||||
|
([problemId, stats]) => {
|
||||||
const completed = stats.completedUsers.size;
|
const completed = stats.completedUsers.size;
|
||||||
const total = stats.totalUsers.size;
|
const total = stats.totalUsers.size;
|
||||||
|
|
||||||
@ -106,13 +112,18 @@ export async function getProblemCompletionData(): Promise<ProblemCompletionData[
|
|||||||
completedPercent: total > 0 ? (completed / total) * 100 : 0,
|
completedPercent: total > 0 ? (completed / total) * 100 : 0,
|
||||||
uncompletedPercent: total > 0 ? ((total - 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<DifficultProblemData[]> {
|
export async function getDifficultProblemsData(): Promise<
|
||||||
|
DifficultProblemData[]
|
||||||
|
> {
|
||||||
// 获取所有测试用例结果
|
// 获取所有测试用例结果
|
||||||
const testcaseResults = await prisma.testcaseResult.findMany({
|
const testcaseResults = await prisma.testcaseResult.findMany({
|
||||||
include: {
|
include: {
|
||||||
@ -120,8 +131,8 @@ export async function getDifficultProblemsData(): Promise<DifficultProblemData[]
|
|||||||
include: {
|
include: {
|
||||||
problem: {
|
problem: {
|
||||||
include: {
|
include: {
|
||||||
localizations: true
|
localizations: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -134,19 +145,22 @@ export async function getDifficultProblemsData(): Promise<DifficultProblemData[]
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 按问题分组统计错误率
|
// 按问题分组统计错误率
|
||||||
const problemStats = new Map<string, {
|
const problemStats = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
totalAttempts: number;
|
totalAttempts: number;
|
||||||
wrongAttempts: number;
|
wrongAttempts: number;
|
||||||
title: string;
|
title: string;
|
||||||
displayId: number;
|
displayId: number;
|
||||||
users: Set<string>;
|
users: Set<string>;
|
||||||
}>();
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
testcaseResults.forEach((result) => {
|
testcaseResults.forEach((result) => {
|
||||||
const problemId = result.testcase.problemId;
|
const problemId = result.testcase.problemId;
|
||||||
const problemTitle = result.testcase.problem.localizations?.find(
|
const problemTitle =
|
||||||
(loc) => loc.type === "TITLE"
|
result.testcase.problem.localizations?.find((loc) => loc.type === "TITLE")
|
||||||
)?.content || "无标题";
|
?.content || "无标题";
|
||||||
const problemDisplayId = result.testcase.problem.displayId;
|
const problemDisplayId = result.testcase.problem.displayId;
|
||||||
const userId = result.submission.userId;
|
const userId = result.submission.userId;
|
||||||
const isWrong = !result.isCorrect;
|
const isWrong = !result.isCorrect;
|
||||||
@ -181,7 +195,8 @@ export async function getDifficultProblemsData(): Promise<DifficultProblemData[]
|
|||||||
uniqueUsers: stats.users.size,
|
uniqueUsers: stats.users.size,
|
||||||
totalAttempts: stats.totalAttempts,
|
totalAttempts: stats.totalAttempts,
|
||||||
}))
|
}))
|
||||||
.filter((problem) =>
|
.filter(
|
||||||
|
(problem) =>
|
||||||
problem.errorRate > 30 && // 错误率超过30%
|
problem.errorRate > 30 && // 错误率超过30%
|
||||||
problem.totalAttempts >= 3 // 至少有3次尝试
|
problem.totalAttempts >= 3 // 至少有3次尝试
|
||||||
)
|
)
|
@ -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 <ProblemEditView problemId={problemId} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
@ -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 {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { auth } from "@/lib/auth";
|
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
import { redirect } from "next/navigation";
|
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 {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -33,7 +33,7 @@ export default async function Layout({ children }: LayoutProps) {
|
|||||||
// 获取用户的完整信息(包括角色)
|
// 获取用户的完整信息(包括角色)
|
||||||
const fullUser = await prisma.user.findUnique({
|
const fullUser = await prisma.user.findUnique({
|
||||||
where: { id: user.id },
|
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) {
|
if (!fullUser) {
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
// changePassword.ts
|
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import bcrypt from "bcryptjs";
|
|
||||||
|
|
||||||
export async function changePassword(formData: FormData) {
|
export async function changePassword(formData: FormData) {
|
||||||
const oldPassword = formData.get("oldPassword") as string;
|
const oldPassword = formData.get("oldPassword") as string;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// getUserInfo.ts
|
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// index.ts
|
|
||||||
export { getUserInfo } from "./getUserInfo";
|
export { getUserInfo } from "./getUserInfo";
|
||||||
export { updateUserInfo } from "./updateUserInfo";
|
export { updateUserInfo } from "./updateUserInfo";
|
||||||
export { changePassword } from "./changePassword";
|
export { changePassword } from "./changePassword";
|
@ -1,4 +1,3 @@
|
|||||||
// updateUserInfo.ts
|
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
// src/app/(app)/management/change-password/page.tsx
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
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";
|
import { changePassword } from "@/app/(protected)/dashboard/management/actions/changePassword";
|
||||||
|
|
||||||
export default function ChangePasswordPage() {
|
export default function ChangePasswordPage() {
|
||||||
@ -51,40 +52,46 @@ export default function ChangePasswordPage() {
|
|||||||
setShowSuccess(true);
|
setShowSuccess(true);
|
||||||
setTimeout(() => setShowSuccess(false), 3000);
|
setTimeout(() => setShowSuccess(false), 3000);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : '修改密码失败';
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : "修改密码失败";
|
||||||
alert(errorMessage);
|
alert(errorMessage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-6">
|
<div className="h-full w-full p-6">
|
||||||
<div className="h-full w-full bg-white shadow-lg rounded-xl p-8 flex flex-col">
|
<div className="h-full w-full bg-card shadow-lg rounded-xl p-8 flex flex-col">
|
||||||
<h1 className="text-2xl font-bold mb-6">修改密码</h1>
|
<h1 className="text-2xl font-bold mb-6">修改密码</h1>
|
||||||
<form onSubmit={handleSubmit} className="space-y-5 flex-1 flex flex-col">
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="space-y-5 flex-1 flex flex-col"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">旧密码</label>
|
<label className="block text-sm font-medium mb-1">旧密码</label>
|
||||||
<input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
value={oldPassword}
|
value={oldPassword}
|
||||||
onChange={(e) => setOldPassword(e.target.value)}
|
onChange={(e) => 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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">新密码</label>
|
<label className="block text-sm font-medium mb-1">新密码</label>
|
||||||
<input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => 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
|
required
|
||||||
/>
|
/>
|
||||||
{newPassword && (
|
{newPassword && (
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
<p className="mt-1 text-xs">
|
||||||
密码强度:
|
密码强度:
|
||||||
<span className={`inline-block w-12 h-2 rounded ${strengthColor}`}></span>
|
<span
|
||||||
|
className={`inline-block w-12 h-2 rounded ${strengthColor}`}
|
||||||
|
></span>
|
||||||
|
|
||||||
<span className="text-sm">{strengthLabel}</span>
|
<span className="text-sm">{strengthLabel}</span>
|
||||||
</p>
|
</p>
|
||||||
@ -93,25 +100,27 @@ export default function ChangePasswordPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">确认新密码</label>
|
<label className="block text-sm font-medium mb-1">确认新密码</label>
|
||||||
<input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => 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
|
required
|
||||||
/>
|
/>
|
||||||
{newPassword && confirmPassword && newPassword !== confirmPassword && (
|
{newPassword &&
|
||||||
|
confirmPassword &&
|
||||||
|
newPassword !== confirmPassword && (
|
||||||
<p className="mt-1 text-xs text-red-500">密码不一致</p>
|
<p className="mt-1 text-xs text-red-500">密码不一致</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-auto">
|
<div className="mt-auto">
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full bg-black hover:bg-gray-800 text-white font-semibold py-2 px-4 rounded-lg transition-colors"
|
className="w-full font-semibold py-2 px-4 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
提交
|
提交
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,58 +1,50 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import React, { useState } from "react"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { cn } from "@/lib/utils";
|
||||||
import ProfilePage from "./profile/page"
|
import React, { useState } from "react";
|
||||||
import ChangePasswordPage from "./change-password/page"
|
import ProfilePage from "./profile/page";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import ChangePasswordPage from "./change-password/page";
|
||||||
|
|
||||||
export default function ManagementDefaultPage() {
|
export default function ManagementDefaultPage() {
|
||||||
const [activePage, setActivePage] = useState("profile")
|
const [activePage, setActivePage] = useState("profile");
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
switch (activePage) {
|
switch (activePage) {
|
||||||
case "profile":
|
case "profile":
|
||||||
return <ProfilePage />
|
return <ProfilePage />;
|
||||||
case "change-password":
|
case "change-password":
|
||||||
return <ChangePasswordPage />
|
return <ChangePasswordPage />;
|
||||||
default:
|
default:
|
||||||
return <ProfilePage />
|
return <ProfilePage />;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<div className="flex h-full w-full flex-col">
|
||||||
{/* 顶部导航栏 */}
|
{/* 顶部导航栏 */}
|
||||||
<header className="bg-background sticky top-0 z-10 flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
<header className="bg-background sticky top-0 z-10 flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
||||||
|
|
||||||
{/* 页面切换按钮 */}
|
{/* 页面切换按钮 */}
|
||||||
<div className="ml-auto flex space-x-2">
|
<div className="ml-auto flex space-x-2">
|
||||||
<button
|
<Button
|
||||||
|
className={cn("px-3 py-1 rounded-md text-sm transition-colors")}
|
||||||
|
variant={activePage === "profile" ? "default" : "secondary"}
|
||||||
onClick={() => setActivePage("profile")}
|
onClick={() => setActivePage("profile")}
|
||||||
className={`px-3 py-1 rounded-md text-sm transition-colors ${
|
|
||||||
activePage === "profile"
|
|
||||||
? "bg-black text-white"
|
|
||||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
登录信息
|
登录信息
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
className={cn("px-3 py-1 rounded-md text-sm transition-colors")}
|
||||||
|
variant={activePage === "change-password" ? "default" : "secondary"}
|
||||||
onClick={() => setActivePage("change-password")}
|
onClick={() => setActivePage("change-password")}
|
||||||
className={`px-3 py-1 rounded-md text-sm transition-colors ${
|
|
||||||
activePage === "change-password"
|
|
||||||
? "bg-black text-white"
|
|
||||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
修改密码
|
修改密码
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 主体内容 */}
|
{/* 主体内容 */}
|
||||||
<main className="flex-1 overflow-auto">
|
<main className="flex-1 overflow-auto">{renderContent()}</main>
|
||||||
{renderContent()}
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
@ -1,7 +1,8 @@
|
|||||||
// src/app/(app)/management/profile/page.tsx
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
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 { getUserInfo } from "@/app/(protected)/dashboard/management/actions/getUserInfo";
|
||||||
import { updateUserInfo } from "@/app/(protected)/dashboard/management/actions/updateUserInfo";
|
import { updateUserInfo } from "@/app/(protected)/dashboard/management/actions/updateUserInfo";
|
||||||
|
|
||||||
@ -34,8 +35,12 @@ export default function ProfilePage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const nameInput = document.getElementById("name") as HTMLInputElement | null;
|
const nameInput = document.getElementById(
|
||||||
const emailInput = document.getElementById("email") as HTMLInputElement | null;
|
"name"
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
const emailInput = document.getElementById(
|
||||||
|
"email"
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
|
||||||
if (!nameInput || !emailInput) {
|
if (!nameInput || !emailInput) {
|
||||||
alert("表单元素缺失");
|
alert("表单元素缺失");
|
||||||
@ -51,7 +56,8 @@ export default function ProfilePage() {
|
|||||||
setUser(updatedUser);
|
setUser(updatedUser);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = error instanceof Error ? error.message : '更新用户信息失败';
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : "更新用户信息失败";
|
||||||
alert(errorMessage);
|
alert(errorMessage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -60,7 +66,7 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-6">
|
<div className="h-full w-full p-6">
|
||||||
<div className="h-full w-full bg-white shadow-lg rounded-xl p-8 flex flex-col">
|
<div className="h-full w-full bg-card shadow-lg rounded-xl p-8 flex flex-col">
|
||||||
<h1 className="text-2xl font-bold mb-6">用户信息</h1>
|
<h1 className="text-2xl font-bold mb-6">用户信息</h1>
|
||||||
|
|
||||||
<div className="flex items-center space-x-6 mb-6">
|
<div className="flex items-center space-x-6 mb-6">
|
||||||
@ -71,79 +77,91 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
defaultValue={user?.name || ""}
|
defaultValue={user?.name || ""}
|
||||||
className="mt-1 block w-full border border-gray-300 rounded-md p-2"
|
className="mt-1 block w-full border rounded-md p-2"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<h2 className="text-xl font-semibold">{user?.name || "未提供"}</h2>
|
<h2 className="text-xl font-semibold">
|
||||||
|
{user?.name || "未提供"}
|
||||||
|
</h2>
|
||||||
)}
|
)}
|
||||||
<p className="text-gray-500">角色:{user?.role}</p>
|
<p>角色:{user?.role}</p>
|
||||||
<p className="text-gray-500">邮箱验证时间:{user.emailVerified ? new Date(user.emailVerified).toLocaleString() : "未验证"}</p>
|
<p>
|
||||||
|
邮箱验证时间:
|
||||||
|
{user.emailVerified
|
||||||
|
? new Date(user.emailVerified).toLocaleString()
|
||||||
|
: "未验证"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="border-gray-200 mb-6" />
|
<hr className="border-border mb-6" />
|
||||||
|
|
||||||
<div className="space-y-4 flex-1">
|
<div className="space-y-4 flex-1">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">用户ID</label>
|
<label className="block text-sm font-medium">用户ID</label>
|
||||||
<p className="mt-1 text-lg font-medium text-gray-900">{user.id}</p>
|
<p className="mt-1 text-lg font-medium">{user.id}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">邮箱地址</label>
|
<label className="block text-sm font-medium">邮箱地址</label>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
defaultValue={user.email}
|
defaultValue={user.email}
|
||||||
className="mt-1 block w-full border border-gray-300 rounded-md p-2"
|
className="mt-1 block w-full border rounded-md p-2"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-1 text-lg font-medium text-gray-900">{user.email}</p>
|
<p className="mt-1 text-lg font-medium">{user.email}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">注册时间</label>
|
<label className="block text-sm font-medium">注册时间</label>
|
||||||
<p className="mt-1 text-lg font-medium text-gray-900">{new Date(user.createdAt).toLocaleString()}</p>
|
<p className="mt-1 text-lg font-medium">
|
||||||
|
{new Date(user.createdAt).toLocaleString()}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">最后更新时间</label>
|
<label className="block text-sm font-medium">最后更新时间</label>
|
||||||
<p className="mt-1 text-lg font-medium text-gray-900">{new Date(user.updatedAt).toLocaleString()}</p>
|
<p className="mt-1 text-lg font-medium">
|
||||||
|
{new Date(user.updatedAt).toLocaleString()}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 flex justify-end space-x-2">
|
<div className="pt-4 flex justify-end space-x-2">
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setIsEditing(false)}
|
onClick={() => setIsEditing(false)}
|
||||||
type="button"
|
type="button"
|
||||||
className="px-4 py-2 bg-gray-300 rounded-md hover:bg-gray-400 transition-colors"
|
className="px-4 py-2 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
type="button"
|
type="button"
|
||||||
className="px-4 py-2 bg-black text-white rounded-md hover:bg-gray-800 transition-colors"
|
variant="secondary"
|
||||||
|
className="px-4 py-2 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<Button
|
||||||
onClick={() => setIsEditing(true)}
|
onClick={() => setIsEditing(true)}
|
||||||
type="button"
|
type="button"
|
||||||
className="px-4 py-2 bg-black text-white rounded-md hover:bg-gray-800 transition-colors"
|
className="px-4 py-2 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
编辑信息
|
编辑信息
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,3 @@
|
|||||||
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 {
|
import {
|
||||||
Users,
|
Users,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
@ -14,9 +7,22 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Target,
|
Target,
|
||||||
Activity
|
Activity,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
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 {
|
interface Stats {
|
||||||
totalUsers?: number;
|
totalUsers?: number;
|
||||||
@ -45,7 +51,7 @@ export default async function DashboardPage() {
|
|||||||
// 获取用户的完整信息
|
// 获取用户的完整信息
|
||||||
const fullUser = await prisma.user.findUnique({
|
const fullUser = await prisma.user.findUnique({
|
||||||
where: { id: user.id },
|
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) {
|
if (!fullUser) {
|
||||||
@ -58,27 +64,35 @@ export default async function DashboardPage() {
|
|||||||
|
|
||||||
if (fullUser.role === "ADMIN") {
|
if (fullUser.role === "ADMIN") {
|
||||||
// 管理员统计
|
// 管理员统计
|
||||||
const [totalUsers, totalProblems, totalSubmissions, recentUsers] = await Promise.all([
|
const [totalUsers, totalProblems, totalSubmissions, recentUsers] =
|
||||||
|
await Promise.all([
|
||||||
prisma.user.count(),
|
prisma.user.count(),
|
||||||
prisma.problem.count(),
|
prisma.problem.count(),
|
||||||
prisma.submission.count(),
|
prisma.submission.count(),
|
||||||
prisma.user.findMany({
|
prisma.user.findMany({
|
||||||
take: 5,
|
take: 5,
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
select: { id: true, name: true, email: true, role: true, createdAt: true }
|
select: {
|
||||||
})
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
role: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
stats = { totalUsers, totalProblems, totalSubmissions };
|
stats = { totalUsers, totalProblems, totalSubmissions };
|
||||||
recentActivity = recentUsers.map(user => ({
|
recentActivity = recentUsers.map((user) => ({
|
||||||
type: "新用户注册",
|
type: "新用户注册",
|
||||||
title: user.name || user.email,
|
title: user.name || user.email,
|
||||||
description: `角色: ${user.role}`,
|
description: `角色: ${user.role}`,
|
||||||
time: user.createdAt
|
time: user.createdAt,
|
||||||
}));
|
}));
|
||||||
} else if (fullUser.role === "TEACHER") {
|
} else if (fullUser.role === "TEACHER") {
|
||||||
// 教师统计
|
// 教师统计
|
||||||
const [totalStudents, totalProblems, totalSubmissions, recentSubmissions] = await Promise.all([
|
const [totalStudents, totalProblems, totalSubmissions, recentSubmissions] =
|
||||||
|
await Promise.all([
|
||||||
prisma.user.count({ where: { role: "GUEST" } }),
|
prisma.user.count({ where: { role: "GUEST" } }),
|
||||||
prisma.problem.count({ where: { isPublished: true } }),
|
prisma.problem.count({ where: { isPublished: true } }),
|
||||||
prisma.submission.count(),
|
prisma.submission.count(),
|
||||||
@ -90,30 +104,41 @@ export default async function DashboardPage() {
|
|||||||
problem: {
|
problem: {
|
||||||
select: {
|
select: {
|
||||||
displayId: true,
|
displayId: true,
|
||||||
localizations: { where: { type: "TITLE", locale: "zh" }, select: { content: true } }
|
localizations: {
|
||||||
}
|
where: { type: "TITLE", locale: "zh" },
|
||||||
}
|
select: { content: true },
|
||||||
}
|
},
|
||||||
})
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
stats = { totalStudents, totalProblems, totalSubmissions };
|
stats = { totalStudents, totalProblems, totalSubmissions };
|
||||||
recentActivity = recentSubmissions.map(sub => ({
|
recentActivity = recentSubmissions.map((sub) => ({
|
||||||
type: "学生提交",
|
type: "学生提交",
|
||||||
title: `${sub.user.name || sub.user.email} 提交了题目 ${sub.problem.displayId}`,
|
title: `${sub.user.name || sub.user.email} 提交了题目 ${
|
||||||
description: sub.problem.localizations[0]?.content || `题目${sub.problem.displayId}`,
|
sub.problem.displayId
|
||||||
|
}`,
|
||||||
|
description:
|
||||||
|
sub.problem.localizations[0]?.content || `题目${sub.problem.displayId}`,
|
||||||
time: sub.createdAt,
|
time: sub.createdAt,
|
||||||
status: sub.status
|
status: sub.status,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
// 学生统计
|
// 学生统计
|
||||||
const [totalProblems, completedProblems, totalSubmissions, recentSubmissions] = await Promise.all([
|
const [
|
||||||
|
totalProblems,
|
||||||
|
completedProblems,
|
||||||
|
totalSubmissions,
|
||||||
|
recentSubmissions,
|
||||||
|
] = await Promise.all([
|
||||||
prisma.problem.count({ where: { isPublished: true } }),
|
prisma.problem.count({ where: { isPublished: true } }),
|
||||||
prisma.submission.count({
|
prisma.submission.count({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
status: "AC"
|
status: "AC",
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
prisma.submission.count({ where: { userId: user.id } }),
|
prisma.submission.count({ where: { userId: user.id } }),
|
||||||
prisma.submission.findMany({
|
prisma.submission.findMany({
|
||||||
@ -124,20 +149,24 @@ export default async function DashboardPage() {
|
|||||||
problem: {
|
problem: {
|
||||||
select: {
|
select: {
|
||||||
displayId: true,
|
displayId: true,
|
||||||
localizations: { where: { type: "TITLE", locale: "zh" }, select: { content: true } }
|
localizations: {
|
||||||
}
|
where: { type: "TITLE", locale: "zh" },
|
||||||
}
|
select: { content: true },
|
||||||
}
|
},
|
||||||
})
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
stats = { totalProblems, completedProblems, totalSubmissions };
|
stats = { totalProblems, completedProblems, totalSubmissions };
|
||||||
recentActivity = recentSubmissions.map(sub => ({
|
recentActivity = recentSubmissions.map((sub) => ({
|
||||||
type: "我的提交",
|
type: "我的提交",
|
||||||
title: `题目 ${sub.problem.displayId}`,
|
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,
|
time: sub.createdAt,
|
||||||
status: sub.status
|
status: sub.status,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,52 +177,129 @@ export default async function DashboardPage() {
|
|||||||
title: "系统管理后台",
|
title: "系统管理后台",
|
||||||
description: "管理整个系统的用户、题目和统计数据",
|
description: "管理整个系统的用户、题目和统计数据",
|
||||||
stats: [
|
stats: [
|
||||||
{ label: "总用户数", value: stats.totalUsers, icon: Users, color: "text-blue-600" },
|
{
|
||||||
{ label: "总题目数", value: stats.totalProblems, icon: BookOpen, color: "text-green-600" },
|
label: "总用户数",
|
||||||
{ label: "总提交数", value: stats.totalSubmissions, icon: Activity, color: "text-purple-600" }
|
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: [
|
actions: [
|
||||||
{ label: "用户管理", href: "/dashboard/usermanagement/guest", icon: Users },
|
{
|
||||||
{ label: "题目管理", href: "/dashboard/usermanagement/problem", icon: BookOpen },
|
label: "用户管理",
|
||||||
{ label: "管理员设置", href: "/dashboard/management", icon: Target }
|
href: "/dashboard/usermanagement/guest",
|
||||||
]
|
icon: Users,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "题目管理",
|
||||||
|
href: "/dashboard/usermanagement/problem",
|
||||||
|
icon: BookOpen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "管理员设置",
|
||||||
|
href: "/dashboard/management",
|
||||||
|
icon: Target,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
case "TEACHER":
|
case "TEACHER":
|
||||||
return {
|
return {
|
||||||
title: "教师教学平台",
|
title: "教师教学平台",
|
||||||
description: "查看学生学习情况,管理教学资源",
|
description: "查看学生学习情况,管理教学资源",
|
||||||
stats: [
|
stats: [
|
||||||
{ label: "学生数量", value: stats.totalStudents, icon: Users, color: "text-blue-600" },
|
{
|
||||||
{ label: "题目数量", value: stats.totalProblems, icon: BookOpen, color: "text-green-600" },
|
label: "学生数量",
|
||||||
{ label: "提交数量", value: stats.totalSubmissions, icon: Activity, color: "text-purple-600" }
|
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: [
|
actions: [
|
||||||
{ label: "学生管理", href: "/dashboard/usermanagement/guest", icon: Users },
|
{
|
||||||
{ label: "题目管理", href: "/dashboard/usermanagement/problem", icon: BookOpen },
|
label: "学生管理",
|
||||||
{ label: "统计分析", href: "/dashboard/teacher/dashboard", icon: BarChart3 }
|
href: "/dashboard/usermanagement/guest",
|
||||||
]
|
icon: Users,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "题目管理",
|
||||||
|
href: "/dashboard/usermanagement/problem",
|
||||||
|
icon: BookOpen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "统计分析",
|
||||||
|
href: "/dashboard/teacher/dashboard",
|
||||||
|
icon: BarChart3,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
title: "我的学习中心",
|
title: "我的学习中心",
|
||||||
description: "继续您的编程学习之旅",
|
description: "继续您的编程学习之旅",
|
||||||
stats: [
|
stats: [
|
||||||
{ label: "总题目数", value: stats.totalProblems, icon: BookOpen, color: "text-blue-600" },
|
{
|
||||||
{ label: "已完成", value: stats.completedProblems, icon: CheckCircle, color: "text-green-600" },
|
label: "总题目数",
|
||||||
{ label: "提交次数", value: stats.totalSubmissions, icon: Activity, color: "text-purple-600" }
|
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: [
|
actions: [
|
||||||
{ label: "开始做题", href: "/problemset", icon: BookOpen },
|
{ 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 config = getRoleConfig();
|
||||||
const completionRate = fullUser.role === "GUEST" ?
|
const completionRate =
|
||||||
((stats.totalProblems || 0) > 0 ? ((stats.completedProblems || 0) / (stats.totalProblems || 1)) * 100 : 0) : 0;
|
fullUser.role === "GUEST"
|
||||||
|
? (stats.totalProblems || 0) > 0
|
||||||
|
? ((stats.completedProblems || 0) / (stats.totalProblems || 1)) * 100
|
||||||
|
: 0
|
||||||
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 p-6">
|
<div className="space-y-6 p-6">
|
||||||
@ -214,7 +320,9 @@ export default async function DashboardPage() {
|
|||||||
{config.stats.map((stat, index) => (
|
{config.stats.map((stat, index) => (
|
||||||
<Card key={index}>
|
<Card key={index}>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">{stat.label}</CardTitle>
|
<CardTitle className="text-sm font-medium">
|
||||||
|
{stat.label}
|
||||||
|
</CardTitle>
|
||||||
<stat.icon className={`h-4 w-4 ${stat.color}`} />
|
<stat.icon className={`h-4 w-4 ${stat.color}`} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -233,7 +341,8 @@ export default async function DashboardPage() {
|
|||||||
学习进度
|
学习进度
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
已完成 {stats.completedProblems || 0} / {stats.totalProblems || 0} 道题目
|
已完成 {stats.completedProblems || 0} / {stats.totalProblems || 0}{" "}
|
||||||
|
道题目
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -287,7 +396,9 @@ export default async function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium">{activity.title}</p>
|
<p className="text-sm font-medium">{activity.title}</p>
|
||||||
<p className="text-sm text-muted-foreground">{activity.description}</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{activity.description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{new Date(activity.time).toLocaleDateString()}
|
{new Date(activity.time).toLocaleDateString()}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Progress } from "@/components/ui/progress";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -10,9 +8,11 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
|
|
||||||
import { useEffect, useState } from "react";
|
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 {
|
interface DashboardData {
|
||||||
completionData: {
|
completionData: {
|
||||||
@ -86,7 +86,13 @@ export default function StudentDashboard() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { completionData, errorData, difficultProblems, pieChartData, errorPieChartData } = data;
|
const {
|
||||||
|
completionData,
|
||||||
|
errorData,
|
||||||
|
difficultProblems,
|
||||||
|
pieChartData,
|
||||||
|
errorPieChartData,
|
||||||
|
} = data;
|
||||||
const COLORS = ["#4CAF50", "#FFC107"];
|
const COLORS = ["#4CAF50", "#FFC107"];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -102,8 +108,12 @@ export default function StudentDashboard() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span>已完成题目:{completionData.completed}/{completionData.total}</span>
|
<span>
|
||||||
<span className="text-green-500">{completionData.percentage}%</span>
|
已完成题目:{completionData.completed}/{completionData.total}
|
||||||
|
</span>
|
||||||
|
<span className="text-green-500">
|
||||||
|
{completionData.percentage}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={completionData.percentage} className="h-2" />
|
<Progress value={completionData.percentage} className="h-2" />
|
||||||
<div className="h-[200px]">
|
<div className="h-[200px]">
|
||||||
@ -119,9 +129,17 @@ export default function StudentDashboard() {
|
|||||||
paddingAngle={5}
|
paddingAngle={5}
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
>
|
>
|
||||||
{pieChartData.map((entry: { name: string; value: number }, index: number) => (
|
{pieChartData.map(
|
||||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
(
|
||||||
))}
|
entry: { name: string; value: number },
|
||||||
|
index: number
|
||||||
|
) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={COLORS[index % COLORS.length]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</Pie>
|
</Pie>
|
||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
@ -138,7 +156,9 @@ export default function StudentDashboard() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span>错题数量:{errorData.wrong}/{errorData.total}</span>
|
<span>
|
||||||
|
错题数量:{errorData.wrong}/{errorData.total}
|
||||||
|
</span>
|
||||||
<span className="text-yellow-500">{errorData.percentage}%</span>
|
<span className="text-yellow-500">{errorData.percentage}%</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={errorData.percentage} className="h-2" />
|
<Progress value={errorData.percentage} className="h-2" />
|
||||||
@ -155,9 +175,17 @@ export default function StudentDashboard() {
|
|||||||
paddingAngle={5}
|
paddingAngle={5}
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
>
|
>
|
||||||
{errorPieChartData.map((entry: { name: string; value: number }, index: number) => (
|
{errorPieChartData.map(
|
||||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
(
|
||||||
))}
|
entry: { name: string; value: number },
|
||||||
|
index: number
|
||||||
|
) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={COLORS[index % COLORS.length]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</Pie>
|
</Pie>
|
||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
@ -188,14 +216,21 @@ export default function StudentDashboard() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{difficultProblems.map((problem: { id: string | number; title: string; difficulty: string; errorCount: number }) => (
|
{difficultProblems.map(
|
||||||
|
(problem: {
|
||||||
|
id: string | number;
|
||||||
|
title: string;
|
||||||
|
difficulty: string;
|
||||||
|
errorCount: number;
|
||||||
|
}) => (
|
||||||
<TableRow key={problem.id}>
|
<TableRow key={problem.id}>
|
||||||
<TableCell>{problem.id}</TableCell>
|
<TableCell>{problem.id}</TableCell>
|
||||||
<TableCell>{problem.title}</TableCell>
|
<TableCell>{problem.title}</TableCell>
|
||||||
<TableCell>{problem.difficulty}</TableCell>
|
<TableCell>{problem.difficulty}</TableCell>
|
||||||
<TableCell>{problem.errorCount}</TableCell>
|
<TableCell>{problem.errorCount}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
) : (
|
) : (
|
@ -1,10 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { TrendingUp } from "lucide-react";
|
import {
|
||||||
import { Bar, BarChart, XAxis, YAxis, LabelList, CartesianGrid } from "recharts";
|
Bar,
|
||||||
import { Button } from "@/components/ui/button";
|
BarChart,
|
||||||
import { useState, useEffect } from "react";
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
LabelList,
|
||||||
|
CartesianGrid,
|
||||||
|
} from "recharts";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -27,7 +30,14 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} 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; // 每页显示的题目数量
|
const ITEMS_PER_PAGE = 5; // 每页显示的题目数量
|
||||||
|
|
||||||
@ -45,7 +55,9 @@ const chartConfig = {
|
|||||||
export default function TeacherDashboard() {
|
export default function TeacherDashboard() {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [chartData, setChartData] = useState<ProblemCompletionData[]>([]);
|
const [chartData, setChartData] = useState<ProblemCompletionData[]>([]);
|
||||||
const [difficultProblems, setDifficultProblems] = useState<DifficultProblemData[]>([]);
|
const [difficultProblems, setDifficultProblems] = useState<
|
||||||
|
DifficultProblemData[]
|
||||||
|
>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -57,8 +69,8 @@ export default function TeacherDashboard() {
|
|||||||
setChartData(data.problemData);
|
setChartData(data.problemData);
|
||||||
setDifficultProblems(data.difficultProblems);
|
setDifficultProblems(data.difficultProblems);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : '获取数据失败');
|
setError(err instanceof Error ? err.message : "获取数据失败");
|
||||||
console.error('Failed to fetch dashboard data:', err);
|
console.error("Failed to fetch dashboard data:", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -178,7 +190,9 @@ export default function TeacherDashboard() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
onClick={() =>
|
||||||
|
setCurrentPage((prev) => Math.max(prev - 1, 1))
|
||||||
|
}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
>
|
>
|
||||||
上一页
|
上一页
|
||||||
@ -189,7 +203,9 @@ export default function TeacherDashboard() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
|
onClick={() =>
|
||||||
|
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||||
|
}
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
>
|
>
|
||||||
下一页
|
下一页
|
||||||
@ -222,7 +238,9 @@ export default function TeacherDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
{difficultProblems.length === 0 ? (
|
{difficultProblems.length === 0 ? (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="text-lg text-muted-foreground">暂无易错题数据</div>
|
<div className="text-lg text-muted-foreground">
|
||||||
|
暂无易错题数据
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
@ -236,7 +254,10 @@ export default function TeacherDashboard() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{difficultProblems.map((problem) => (
|
{difficultProblems.map((problem) => (
|
||||||
<TableRow key={problem.id}>
|
<TableRow key={problem.id}>
|
||||||
<TableCell>{problem.problemDisplayId || problem.id.substring(0, 8)}</TableCell>
|
<TableCell>
|
||||||
|
{problem.problemDisplayId ||
|
||||||
|
problem.id.substring(0, 8)}
|
||||||
|
</TableCell>
|
||||||
<TableCell>{problem.problemTitle}</TableCell>
|
<TableCell>{problem.problemTitle}</TableCell>
|
||||||
<TableCell>{problem.problemCount}</TableCell>
|
<TableCell>{problem.problemCount}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
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 {
|
interface DashboardData {
|
||||||
problemData: Array<{
|
problemData: Array<{
|
||||||
@ -37,8 +37,8 @@ export default function TestDataPage() {
|
|||||||
const result = await getDashboardStats();
|
const result = await getDashboardStats();
|
||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : '获取数据失败');
|
setError(err instanceof Error ? err.message : "获取数据失败");
|
||||||
console.error('Failed to fetch data:', err);
|
console.error("Failed to fetch data:", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -77,10 +77,14 @@ export default function TestDataPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold mb-2">统计信息</h2>
|
<h2 className="text-xl font-semibold mb-2">统计信息</h2>
|
||||||
<pre className="bg-gray-100 p-4 rounded overflow-auto">
|
<pre className="bg-gray-100 p-4 rounded overflow-auto">
|
||||||
{JSON.stringify({
|
{JSON.stringify(
|
||||||
|
{
|
||||||
totalProblems: data?.totalProblems,
|
totalProblems: data?.totalProblems,
|
||||||
totalDifficultProblems: data?.totalDifficultProblems,
|
totalDifficultProblems: data?.totalDifficultProblems,
|
||||||
}, null, 2)}
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -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<Problem, 'id'|'createdAt'|'updatedAt'>) {
|
|
||||||
await prisma.problem.create({ data })
|
|
||||||
revalidatePath('/usermanagement/problem')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteProblem(id: string) {
|
|
||||||
await prisma.problem.delete({ where: { id } })
|
|
||||||
revalidatePath('/usermanagement/problem')
|
|
||||||
}
|
|
@ -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<User, 'id'|'createdAt'|'updatedAt'> & { 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<Omit<User, 'id'|'createdAt'|'updatedAt'>>
|
|
||||||
) {
|
|
||||||
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}`)
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import ProtectedLayout from "./ProtectedLayout";
|
|
||||||
|
|
||||||
interface GenericLayoutProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
allowedRoles: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GenericLayout({ children, allowedRoles }: GenericLayoutProps) {
|
|
||||||
return <ProtectedLayout allowedRoles={allowedRoles}>{children}</ProtectedLayout>;
|
|
||||||
}
|
|
@ -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<Problem, "id" | "createdAt" | "updatedAt">
|
||||||
|
) {
|
||||||
|
await prisma.problem.create({ data });
|
||||||
|
revalidatePath("/usermanagement/problem");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteProblem(id: string) {
|
||||||
|
await prisma.problem.delete({ where: { id } });
|
||||||
|
revalidatePath("/usermanagement/problem");
|
||||||
|
}
|
@ -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<User, "id" | "createdAt" | "updatedAt"> & { 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<Omit<User, "id" | "createdAt" | "updatedAt">>
|
||||||
|
) {
|
||||||
|
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}`);
|
||||||
|
}
|
@ -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 <GenericLayout allowedRoles={["ADMIN"]}>{children}</GenericLayout>;
|
return <GenericLayout allowedRoles={["ADMIN"]}>{children}</GenericLayout>;
|
||||||
}
|
}
|
@ -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() {
|
export default function AdminPage() {
|
||||||
return <GenericPage userType="admin" config={adminConfig} />
|
return <GenericPage userType="admin" config={adminConfig} />;
|
||||||
}
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import ProtectedLayout from "./ProtectedLayout";
|
||||||
|
|
||||||
|
interface GenericLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
allowedRoles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GenericLayout({
|
||||||
|
children,
|
||||||
|
allowedRoles,
|
||||||
|
}: GenericLayoutProps) {
|
||||||
|
return (
|
||||||
|
<ProtectedLayout allowedRoles={allowedRoles}>{children}</ProtectedLayout>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { auth } from "@/lib/auth";
|
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
interface ProtectedLayoutProps {
|
interface ProtectedLayoutProps {
|
||||||
@ -7,7 +7,10 @@ interface ProtectedLayoutProps {
|
|||||||
allowedRoles: string[];
|
allowedRoles: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ProtectedLayout({ children, allowedRoles }: ProtectedLayoutProps) {
|
export default async function ProtectedLayout({
|
||||||
|
children,
|
||||||
|
allowedRoles,
|
||||||
|
}: ProtectedLayoutProps) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
const userId = session?.user?.id;
|
const userId = session?.user?.id;
|
||||||
|
|
||||||
@ -17,7 +20,7 @@ export default async function ProtectedLayout({ children, allowedRoles }: Protec
|
|||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
select: { role: true }
|
select: { role: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user || !allowedRoles.includes(user.role)) {
|
if (!user || !allowedRoles.includes(user.role)) {
|
@ -1,5 +1,13 @@
|
|||||||
import GenericLayout from "../_components/GenericLayout";
|
import GenericLayout from "../components/GenericLayout";
|
||||||
|
|
||||||
export default function GuestLayout({ children }: { children: React.ReactNode }) {
|
export default function GuestLayout({
|
||||||
return <GenericLayout allowedRoles={["ADMIN", "TEACHER"]}>{children}</GenericLayout>;
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<GenericLayout allowedRoles={["ADMIN", "TEACHER"]}>
|
||||||
|
{children}
|
||||||
|
</GenericLayout>
|
||||||
|
);
|
||||||
}
|
}
|
@ -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() {
|
export default function GuestPage() {
|
||||||
return <GenericPage userType="guest" config={guestConfig} />
|
return <GenericPage userType="guest" config={guestConfig} />;
|
||||||
}
|
}
|
@ -1,5 +1,13 @@
|
|||||||
import GenericLayout from "../_components/GenericLayout";
|
import GenericLayout from "../components/GenericLayout";
|
||||||
|
|
||||||
export default function ProblemLayout({ children }: { children: React.ReactNode }) {
|
export default function ProblemLayout({
|
||||||
return <GenericLayout allowedRoles={["ADMIN", "TEACHER"]}>{children}</GenericLayout>;
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<GenericLayout allowedRoles={["ADMIN", "TEACHER"]}>
|
||||||
|
{children}
|
||||||
|
</GenericLayout>
|
||||||
|
);
|
||||||
}
|
}
|
@ -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() {
|
export default function ProblemPage() {
|
||||||
return <GenericPage userType="problem" config={problemConfig} />
|
return <GenericPage userType="problem" config={problemConfig} />;
|
||||||
}
|
}
|
@ -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 <GenericLayout allowedRoles={["ADMIN"]}>{children}</GenericLayout>;
|
return <GenericLayout allowedRoles={["ADMIN"]}>{children}</GenericLayout>;
|
||||||
}
|
}
|
@ -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() {
|
export default function TeacherPage() {
|
||||||
return <GenericPage userType="teacher" config={teacherConfig} />
|
return <GenericPage userType="teacher" config={teacherConfig} />;
|
||||||
}
|
}
|
@ -33,13 +33,13 @@
|
|||||||
--chart-5: 213 16% 16%;
|
--chart-5: 213 16% 16%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--sidebar-background: 0 0% 98%;
|
--sidebar-background: 0 0% 98%;
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--sidebar-foreground: 213 13% 6%;
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
--sidebar-primary: 213 13% 16%;
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
--sidebar-primary-foreground: 213 13% 76%;
|
||||||
--sidebar-accent: 240 4.8% 95.9%;
|
--sidebar-accent: 0 0% 85%;
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
--sidebar-accent-foreground: 0 0% 25%;
|
||||||
--sidebar-border: 220 13% 91%;
|
--sidebar-border: 0 0% 95%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 213 13% 16%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@ -67,14 +67,14 @@
|
|||||||
--chart-3: 216 28% 22%;
|
--chart-3: 216 28% 22%;
|
||||||
--chart-4: 210 7% 28%;
|
--chart-4: 210 7% 28%;
|
||||||
--chart-5: 210 20% 82%;
|
--chart-5: 210 20% 82%;
|
||||||
--sidebar-background: 240 5.9% 10%;
|
--sidebar-background: 216 28% 5%;
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
--sidebar-foreground: 210 17% 92%;
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
--sidebar-primary: 210 17% 82%;
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
--sidebar-primary-foreground: 210 17% 22%;
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
--sidebar-accent: 216 28% 22%;
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
--sidebar-accent-foreground: 216 28% 82%;
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
--sidebar-border: 216 18% 12%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 210 17% 82%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,14 +119,3 @@ code[data-theme*=" "] span {
|
|||||||
color: var(--shiki-dark);
|
color: var(--shiki-dark);
|
||||||
background-color: var(--shiki-dark-bg);
|
background-color: var(--shiki-dark-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
@apply border-border;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
@apply bg-background text-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -6,7 +6,6 @@ import { NextIntlClientProvider } from "next-intl";
|
|||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { SettingsDialog } from "@/components/settings-dialog";
|
import { SettingsDialog } from "@/components/settings-dialog";
|
||||||
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Judge4c",
|
title: "Judge4c",
|
||||||
description:
|
description:
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import {
|
import {
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
@ -6,9 +5,10 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
export function ShareDialogContent({ link }: { link: string }) {
|
export function ShareDialogContent({ link }: { link: string }) {
|
||||||
return (
|
return (
|
||||||
@ -35,5 +35,5 @@ export function ShareDialogContent({ link }: { link: string }) {
|
|||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog";
|
||||||
import { Check, X, Info, AlertTriangle, Copy, Check as CheckIcon } from "lucide-react"
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Button } from "@/components/ui/button";
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import Link from "next/link"
|
|
||||||
|
|
||||||
export function WrongbookDialog({ problems, children }: { problems: { id: string; name: string; status: string; url?: string }[]; children?: React.ReactNode }) {
|
export function WrongbookDialog({
|
||||||
const [copiedId, setCopiedId] = React.useState<string | null>(null)
|
problems,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
problems: { id: string; name: string; status: string; url?: string }[];
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [copiedId, setCopiedId] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const handleCopyLink = async (item: { id: string; url?: string }) => {
|
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 {
|
try {
|
||||||
await navigator.clipboard.writeText(link)
|
await navigator.clipboard.writeText(link);
|
||||||
setCopiedId(item.id)
|
setCopiedId(item.id);
|
||||||
setTimeout(() => setCopiedId(null), 2000) // 2秒后重置状态
|
setTimeout(() => setCopiedId(null), 2000); // 2秒后重置状态
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy link:', err)
|
console.error("Failed to copy link:", err);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
{children ? children : (
|
{children ? (
|
||||||
<button className="px-4 py-2 rounded bg-primary text-primary-foreground hover:bg-primary/90">全部错题</button>
|
children
|
||||||
|
) : (
|
||||||
|
<button className="px-4 py-2 rounded bg-primary text-primary-foreground hover:bg-primary/90">
|
||||||
|
全部错题
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl p-0">
|
<DialogContent className="max-w-2xl p-0">
|
||||||
@ -44,13 +61,18 @@ export function WrongbookDialog({ problems, children }: { problems: { id: string
|
|||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b bg-muted/50">
|
<tr className="border-b bg-muted/50">
|
||||||
<th className="px-3 py-2 text-left font-semibold">操作</th>
|
<th className="px-3 py-2 text-left font-semibold">操作</th>
|
||||||
<th className="px-3 py-2 text-left font-semibold">题目名称</th>
|
<th className="px-3 py-2 text-left font-semibold">
|
||||||
|
题目名称
|
||||||
|
</th>
|
||||||
<th className="px-3 py-2 text-left font-semibold">状态</th>
|
<th className="px-3 py-2 text-left font-semibold">状态</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{problems.map((item) => (
|
{problems.map((item) => (
|
||||||
<tr key={item.id} className="border-b last:border-0 hover:bg-muted/30 transition">
|
<tr
|
||||||
|
key={item.id}
|
||||||
|
className="border-b last:border-0 hover:bg-muted/30 transition"
|
||||||
|
>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -66,7 +88,10 @@ export function WrongbookDialog({ problems, children }: { problems: { id: string
|
|||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
<Link href={item.url || `/problems/${item.id}`} className="text-primary underline underline-offset-2 hover:text-primary/80">
|
<Link
|
||||||
|
href={item.url || `/problems/${item.id}`}
|
||||||
|
className="text-primary underline underline-offset-2 hover:text-primary/80"
|
||||||
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
@ -74,28 +99,46 @@ export function WrongbookDialog({ problems, children }: { problems: { id: string
|
|||||||
{(() => {
|
{(() => {
|
||||||
if (item.status === "AC") {
|
if (item.status === "AC") {
|
||||||
return (
|
return (
|
||||||
<Badge className="bg-green-500 text-white" variant="default">
|
<Badge
|
||||||
<Check className="w-3 h-3 mr-1" />{item.status}
|
className="bg-green-500 text-white"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
<Check className="w-3 h-3 mr-1" />
|
||||||
|
{item.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
);
|
||||||
} else if (item.status === "WA") {
|
} else if (item.status === "WA") {
|
||||||
return (
|
return (
|
||||||
<Badge className="bg-red-500 text-white" variant="destructive">
|
<Badge
|
||||||
<X className="w-3 h-3 mr-1" />{item.status}
|
className="bg-red-500 text-white"
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3 mr-1" />
|
||||||
|
{item.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
);
|
||||||
} else if (["RE", "CE", "MLE", "TLE"].includes(item.status)) {
|
} else if (
|
||||||
|
["RE", "CE", "MLE", "TLE"].includes(item.status)
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<Badge className="bg-orange-500 text-white" variant="secondary">
|
<Badge
|
||||||
<AlertTriangle className="w-3 h-3 mr-1" />{item.status}
|
className="bg-orange-500 text-white"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
<AlertTriangle className="w-3 h-3 mr-1" />
|
||||||
|
{item.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Badge className="bg-gray-200 text-gray-700" variant="secondary">
|
<Badge
|
||||||
<Info className="w-3 h-3 mr-1" />{item.status}
|
className="bg-gray-200 text-gray-700"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
<Info className="w-3 h-3 mr-1" />
|
||||||
|
{item.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</td>
|
</td>
|
||||||
@ -107,5 +150,5 @@ export function WrongbookDialog({ problems, children }: { problems: { id: string
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
16
src/components/dashboard-button.tsx
Normal file
16
src/components/dashboard-button.tsx
Normal file
@ -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 (
|
||||||
|
<DropdownMenuItem onClick={() => router.push("/dashboard")}>
|
||||||
|
<LayoutDashboardIcon />
|
||||||
|
Dashboard
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,5 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { usePathname } from "next/navigation"
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
@ -8,76 +7,77 @@ import {
|
|||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from "@/components/ui/breadcrumb"
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
interface BreadcrumbItem {
|
interface BreadcrumbItem {
|
||||||
label: string
|
label: string;
|
||||||
href?: string
|
href?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DynamicBreadcrumb() {
|
export function DynamicBreadcrumb() {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname();
|
||||||
|
|
||||||
const generateBreadcrumbs = (): BreadcrumbItem[] => {
|
const generateBreadcrumbs = (): BreadcrumbItem[] => {
|
||||||
const segments = pathname.split('/').filter(Boolean)
|
const segments = pathname.split("/").filter(Boolean);
|
||||||
const breadcrumbs: BreadcrumbItem[] = []
|
const breadcrumbs: BreadcrumbItem[] = [];
|
||||||
|
|
||||||
// 添加首页
|
// 添加首页
|
||||||
breadcrumbs.push({ label: "首页", href: "/" })
|
breadcrumbs.push({ label: "首页", href: "/" });
|
||||||
|
|
||||||
let currentPath = ""
|
let currentPath = "";
|
||||||
|
|
||||||
segments.forEach((segment, index) => {
|
segments.forEach((segment, index) => {
|
||||||
currentPath += `/${segment}`
|
currentPath += `/${segment}`;
|
||||||
|
|
||||||
// 根据路径段生成标签
|
// 根据路径段生成标签
|
||||||
let label = segment
|
let label = segment;
|
||||||
|
|
||||||
// 路径映射
|
// 路径映射
|
||||||
const pathMap: Record<string, string> = {
|
const pathMap: Record<string, string> = {
|
||||||
'dashboard': '仪表板',
|
dashboard: "仪表板",
|
||||||
'management': '管理面板',
|
management: "管理面板",
|
||||||
'profile': '用户信息',
|
profile: "用户信息",
|
||||||
'change-password': '修改密码',
|
"change-password": "修改密码",
|
||||||
'problems': '题目',
|
problems: "题目",
|
||||||
'problemset': '题目集',
|
problemset: "题目集",
|
||||||
'admin': '管理后台',
|
admin: "管理后台",
|
||||||
'teacher': '教师平台',
|
teacher: "教师平台",
|
||||||
'student': '学生平台',
|
student: "学生平台",
|
||||||
'usermanagement': '用户管理',
|
usermanagement: "用户管理",
|
||||||
'userdashboard': '用户仪表板',
|
userdashboard: "用户仪表板",
|
||||||
'protected': '受保护',
|
protected: "受保护",
|
||||||
'app': '应用',
|
app: "应用",
|
||||||
'auth': '认证',
|
auth: "认证",
|
||||||
'sign-in': '登录',
|
"sign-in": "登录",
|
||||||
'sign-up': '注册',
|
"sign-up": "注册",
|
||||||
}
|
};
|
||||||
|
|
||||||
// 如果是数字,可能是题目ID,显示为"题目详情"
|
// 如果是数字,可能是题目ID,显示为"题目详情"
|
||||||
if (/^\d+$/.test(segment)) {
|
if (/^\d+$/.test(segment)) {
|
||||||
label = "详情"
|
label = "详情";
|
||||||
} else if (pathMap[segment]) {
|
} else if (pathMap[segment]) {
|
||||||
label = pathMap[segment]
|
label = pathMap[segment];
|
||||||
} else {
|
} else {
|
||||||
// 将 kebab-case 转换为中文
|
// 将 kebab-case 转换为中文
|
||||||
label = segment
|
label = segment
|
||||||
.split('-')
|
.split("-")
|
||||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
.join(' ')
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最后一个项目不添加链接
|
// 最后一个项目不添加链接
|
||||||
if (index === segments.length - 1) {
|
if (index === segments.length - 1) {
|
||||||
breadcrumbs.push({ label })
|
breadcrumbs.push({ label });
|
||||||
} else {
|
} else {
|
||||||
breadcrumbs.push({ label, href: currentPath })
|
breadcrumbs.push({ label, href: currentPath });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return breadcrumbs
|
return breadcrumbs;
|
||||||
}
|
};
|
||||||
|
|
||||||
const breadcrumbs = generateBreadcrumbs()
|
const breadcrumbs = generateBreadcrumbs();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
@ -86,13 +86,9 @@ export function DynamicBreadcrumb() {
|
|||||||
<div key={index} className="flex items-center">
|
<div key={index} className="flex items-center">
|
||||||
<BreadcrumbItem className="hidden md:block">
|
<BreadcrumbItem className="hidden md:block">
|
||||||
{item.href ? (
|
{item.href ? (
|
||||||
<BreadcrumbLink href={item.href}>
|
<BreadcrumbLink href={item.href}>{item.label}</BreadcrumbLink>
|
||||||
{item.label}
|
|
||||||
</BreadcrumbLink>
|
|
||||||
) : (
|
) : (
|
||||||
<BreadcrumbPage>
|
<BreadcrumbPage>{item.label}</BreadcrumbPage>
|
||||||
{item.label}
|
|
||||||
</BreadcrumbPage>
|
|
||||||
)}
|
)}
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
{index < breadcrumbs.length - 1 && (
|
{index < breadcrumbs.length - 1 && (
|
||||||
@ -102,5 +98,5 @@ export function DynamicBreadcrumb() {
|
|||||||
))}
|
))}
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
)
|
);
|
||||||
}
|
}
|
@ -1,12 +1,5 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { ChevronRight, type LucideIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible"
|
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
@ -17,21 +10,27 @@ import {
|
|||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
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({
|
export function NavMain({
|
||||||
items,
|
items,
|
||||||
}: {
|
}: {
|
||||||
items: {
|
items: {
|
||||||
title: string
|
title: string;
|
||||||
url: string
|
url: string;
|
||||||
icon: LucideIcon
|
icon: LucideIcon;
|
||||||
isActive?: boolean
|
isActive?: boolean;
|
||||||
items?: {
|
items?: {
|
||||||
title: string
|
title: string;
|
||||||
url: string
|
url: string;
|
||||||
}[]
|
}[];
|
||||||
}[]
|
}[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
@ -74,5 +73,5 @@ export function NavMain({
|
|||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BookX,
|
BookX,
|
||||||
@ -9,19 +9,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
Info,
|
Info,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
} from "lucide-react"
|
} 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"
|
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupLabel,
|
SidebarGroupLabel,
|
||||||
@ -30,24 +18,33 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar";
|
||||||
import { WrongbookDialog } from "@/components/UncompletedProject/wrongbook-dialog"
|
import {
|
||||||
import { ShareDialogContent } from "@/components/UncompletedProject/sharedialog"
|
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({
|
export function NavProjects({
|
||||||
projects,
|
projects,
|
||||||
}: {
|
}: {
|
||||||
projects: {
|
projects: {
|
||||||
id: string
|
id: string;
|
||||||
name: string
|
name: string;
|
||||||
status: string
|
status: string;
|
||||||
url?: string
|
url?: string;
|
||||||
}[]
|
}[];
|
||||||
}) {
|
}) {
|
||||||
const { isMobile } = useSidebar()
|
const { isMobile } = useSidebar();
|
||||||
const [shareOpen, setShareOpen] = useState(false)
|
const [shareOpen, setShareOpen] = useState(false);
|
||||||
const [shareLink, setShareLink] = useState("")
|
const [shareLink, setShareLink] = useState("");
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -73,28 +70,30 @@ export function NavProjects({
|
|||||||
<Check className="w-3 h-3" />
|
<Check className="w-3 h-3" />
|
||||||
{item.status}
|
{item.status}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
} else if (item.status === "WA") {
|
} else if (item.status === "WA") {
|
||||||
return (
|
return (
|
||||||
<span className="ml-2 min-w-[60px] text-xs text-right px-2 py-0.5 rounded-full border flex items-center gap-1 border-red-500 bg-red-500 text-white">
|
<span className="ml-2 min-w-[60px] text-xs text-right px-2 py-0.5 rounded-full border flex items-center gap-1 border-red-500 bg-red-500 text-white">
|
||||||
<X className="w-3 h-3" />
|
<X className="w-3 h-3" />
|
||||||
{item.status}
|
{item.status}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
} else if (["RE", "CE", "MLE", "TLE"].includes(item.status)) {
|
} else if (
|
||||||
|
["RE", "CE", "MLE", "TLE"].includes(item.status)
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<span className="ml-2 min-w-[60px] text-xs text-right px-2 py-0.5 rounded-full border flex items-center gap-1 border-orange-500 bg-orange-500 text-white">
|
<span className="ml-2 min-w-[60px] text-xs text-right px-2 py-0.5 rounded-full border flex items-center gap-1 border-orange-500 bg-orange-500 text-white">
|
||||||
<AlertTriangle className="w-3 h-3" />
|
<AlertTriangle className="w-3 h-3" />
|
||||||
{item.status}
|
{item.status}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="ml-2 min-w-[60px] text-xs text-right px-2 py-0.5 rounded-full border flex items-center gap-1 border-gray-400 bg-gray-100 text-gray-700">
|
<span className="ml-2 min-w-[60px] text-xs text-right px-2 py-0.5 rounded-full border flex items-center gap-1 border-gray-400 bg-gray-100 text-gray-700">
|
||||||
<Info className="w-3 h-3" />
|
<Info className="w-3 h-3" />
|
||||||
{item.status}
|
{item.status}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</span>
|
</span>
|
||||||
@ -114,11 +113,11 @@ export function NavProjects({
|
|||||||
>
|
>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
if (item.url) {
|
if (item.url) {
|
||||||
router.push(item.url)
|
router.push(item.url);
|
||||||
} else {
|
} else {
|
||||||
router.push(`/problems/${item.id}`)
|
router.push(`/problems/${item.id}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -127,9 +126,11 @@ export function NavProjects({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation();
|
||||||
setShareLink(`${window.location.origin}/problems/${item.id}`)
|
setShareLink(
|
||||||
setShareOpen(true)
|
`${window.location.origin}/problems/${item.id}`
|
||||||
|
);
|
||||||
|
setShareOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Share className="text-muted-foreground mr-2" />
|
<Share className="text-muted-foreground mr-2" />
|
||||||
@ -153,5 +154,5 @@ export function NavProjects({
|
|||||||
<ShareDialogContent link={shareLink} />
|
<ShareDialogContent link={shareLink} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
@ -1,23 +1,22 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { type LucideIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar";
|
||||||
|
import { type LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
export function NavSecondary({
|
export function NavSecondary({
|
||||||
items,
|
items,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
items: {
|
items: {
|
||||||
title: string
|
title: string;
|
||||||
url: string
|
url: string;
|
||||||
icon: LucideIcon
|
icon: LucideIcon;
|
||||||
}[]
|
}[];
|
||||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
||||||
return (
|
return (
|
||||||
<SidebarGroup {...props}>
|
<SidebarGroup {...props}>
|
||||||
@ -36,5 +35,5 @@ export function NavSecondary({
|
|||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,11 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BadgeCheck,
|
SidebarMenu,
|
||||||
// Bell,
|
SidebarMenuButton,
|
||||||
ChevronsUpDown,
|
SidebarMenuItem,
|
||||||
UserPen,
|
useSidebar,
|
||||||
LogOut,
|
} from "@/components/ui/sidebar";
|
||||||
// Sparkles,
|
|
||||||
} from "lucide-react"
|
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
import { signOut } from "next-auth/react"
|
|
||||||
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
} from "@/components/ui/avatar"
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -24,30 +14,28 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import { signOut } from "next-auth/react";
|
||||||
SidebarMenu,
|
import { useRouter } from "next/navigation";
|
||||||
SidebarMenuButton,
|
import { BadgeCheck, ChevronsUpDown, UserPen, LogOut } from "lucide-react";
|
||||||
SidebarMenuItem,
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
useSidebar,
|
|
||||||
} from "@/components/ui/sidebar"
|
|
||||||
|
|
||||||
export function NavUser({
|
export function NavUser({
|
||||||
user,
|
user,
|
||||||
}: {
|
}: {
|
||||||
user: {
|
user: {
|
||||||
name: string
|
name: string;
|
||||||
email: string
|
email: string;
|
||||||
avatar: string
|
avatar: string;
|
||||||
}
|
};
|
||||||
}) {
|
}) {
|
||||||
const { isMobile } = useSidebar()
|
const { isMobile } = useSidebar();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await signOut({
|
await signOut({
|
||||||
callbackUrl: "/sign-in",
|
callbackUrl: "/sign-in",
|
||||||
redirect: true
|
redirect: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,13 +86,6 @@ export function NavUser({
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{/* <DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<Sparkles />
|
|
||||||
Update
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup> */}
|
|
||||||
{/* <DropdownMenuSeparator /> */}
|
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem onClick={handleAccount}>
|
<DropdownMenuItem onClick={handleAccount}>
|
||||||
<BadgeCheck />
|
<BadgeCheck />
|
||||||
@ -114,10 +95,6 @@ export function NavUser({
|
|||||||
<UserPen />
|
<UserPen />
|
||||||
Switch User
|
Switch User
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{/* <DropdownMenuItem >
|
|
||||||
<Bell />
|
|
||||||
Notifications
|
|
||||||
</DropdownMenuItem> */}
|
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={handleLogout}>
|
<DropdownMenuItem onClick={handleLogout}>
|
||||||
@ -128,5 +105,5 @@ export function NavUser({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,6 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import { siteConfig } from "@/config/site"
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
|
||||||
LifeBuoy,
|
|
||||||
Send,
|
|
||||||
Shield,
|
|
||||||
} from "lucide-react"
|
|
||||||
|
|
||||||
import { NavMain } from "@/components/nav-main"
|
import * as React from "react";
|
||||||
import { NavSecondary } from "@/components/nav-secondary"
|
|
||||||
import { NavUser } from "@/components/nav-user"
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@ -18,14 +9,19 @@ import {
|
|||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar";
|
||||||
import { User } from "next-auth"
|
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 = {
|
const adminData = {
|
||||||
navMain: [
|
navMain: [
|
||||||
{
|
{
|
||||||
title: "管理面板",
|
title: "管理面板",
|
||||||
url: "#",
|
url: "/dashboard",
|
||||||
icon: Shield,
|
icon: Shield,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
items: [
|
items: [
|
||||||
@ -35,19 +31,21 @@ const adminData = {
|
|||||||
{ title: "题目管理", url: "/dashboard/usermanagement/problem" },
|
{ title: "题目管理", url: "/dashboard/usermanagement/problem" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
],
|
],
|
||||||
navSecondary: [
|
navSecondary: [
|
||||||
{ title: "帮助", url: "/", icon: LifeBuoy },
|
{ title: "帮助", url: "/", icon: LifeBuoy },
|
||||||
{ title: "反馈", url: siteConfig.url.repo.github, icon: Send },
|
{ title: "反馈", url: siteConfig.url.repo.github, icon: Send },
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
|
|
||||||
interface AdminSidebarProps {
|
interface AdminSidebarProps {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdminSidebar({ user, ...props }: AdminSidebarProps & React.ComponentProps<typeof Sidebar>) {
|
export function AdminSidebar({
|
||||||
|
user,
|
||||||
|
...props
|
||||||
|
}: AdminSidebarProps & React.ComponentProps<typeof Sidebar>) {
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
name: user.name ?? "管理员",
|
name: user.name ?? "管理员",
|
||||||
email: user.email ?? "",
|
email: user.email ?? "",
|
||||||
@ -81,5 +79,5 @@ export function AdminSidebar({ user, ...props }: AdminSidebarProps & React.Compo
|
|||||||
<NavUser user={userInfo} />
|
<NavUser user={userInfo} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { siteConfig } from "@/config/site";
|
|
||||||
import * as React from "react";
|
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 {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@ -25,6 +11,12 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { User } from "next-auth";
|
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 = {
|
const data = {
|
||||||
navMain: [
|
navMain: [
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import { siteConfig } from "@/config/site"
|
|
||||||
import * as React from "react"
|
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
LifeBuoy,
|
LifeBuoy,
|
||||||
PieChart,
|
PieChart,
|
||||||
Send,
|
Send,
|
||||||
// Settings2,
|
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
} from "lucide-react"
|
} from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
import { NavMain } from "@/components/nav-main"
|
|
||||||
import { NavSecondary } from "@/components/nav-secondary"
|
|
||||||
import { NavUser } from "@/components/nav-user"
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@ -21,8 +16,12 @@ import {
|
|||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar";
|
||||||
import { User } from "next-auth"
|
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 = {
|
const data = {
|
||||||
navMain: [
|
navMain: [
|
||||||
@ -81,13 +80,16 @@ const data = {
|
|||||||
icon: Send,
|
icon: Send,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
|
|
||||||
interface TeacherSidebarProps {
|
interface TeacherSidebarProps {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TeacherSidebar({ user, ...props }: TeacherSidebarProps & React.ComponentProps<typeof Sidebar>) {
|
export function TeacherSidebar({
|
||||||
|
user,
|
||||||
|
...props
|
||||||
|
}: TeacherSidebarProps & React.ComponentProps<typeof Sidebar>) {
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
name: user.name ?? "",
|
name: user.name ?? "",
|
||||||
email: user.email ?? "",
|
email: user.email ?? "",
|
||||||
@ -121,5 +123,5 @@ export function TeacherSidebar({ user, ...props }: TeacherSidebarProps & React.C
|
|||||||
<NavUser user={userInfo} />
|
<NavUser user={userInfo} />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { auth, signIn, signOut } from "@/lib/auth";
|
|||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { SettingsButton } from "@/components/settings-button";
|
import { SettingsButton } from "@/components/settings-button";
|
||||||
|
import { DashboardButton } from "@/components/dashboard-button";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
|
||||||
const handleLogIn = async () => {
|
const handleLogIn = async () => {
|
||||||
@ -88,6 +89,7 @@ const UserAvatar = async () => {
|
|||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
|
<DashboardButton />
|
||||||
<SettingsButton />
|
<SettingsButton />
|
||||||
<DropdownMenuItem onClick={handleLogOut}>
|
<DropdownMenuItem onClick={handleLogOut}>
|
||||||
<LogOutIcon />
|
<LogOutIcon />
|
||||||
|
32
src/features/admin/ui/layouts/protected-layout.tsx
Normal file
32
src/features/admin/ui/layouts/protected-layout.tsx
Normal file
@ -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}</>;
|
||||||
|
};
|
@ -1,28 +1,26 @@
|
|||||||
// import EditCodePanel from "@/components/creater/edit-code-panel";
|
import EditCodePanel from "@/components/creater/edit-code-panel";
|
||||||
// import EditDetailPanel from "@/components/creater/edit-detail-panel";
|
import EditDetailPanel from "@/components/creater/edit-detail-panel";
|
||||||
// import EditSolutionPanel from "@/components/creater/edit-solution-panel";
|
import EditSolutionPanel from "@/components/creater/edit-solution-panel";
|
||||||
// import EditTestcasePanel from "@/components/creater/edit-testcase-panel";
|
import EditTestcasePanel from "@/components/creater/edit-testcase-panel";
|
||||||
// import EditDescriptionPanel from "@/components/creater/edit-description-panel";
|
import EditDescriptionPanel from "@/components/creater/edit-description-panel";
|
||||||
// import { ProblemEditFlexLayout } from "@/features/admin/ui/components/problem-edit-flexlayout";
|
import { ProblemEditFlexLayout } from "@/features/admin/ui/components/problem-edit-flexlayout";
|
||||||
|
|
||||||
// interface ProblemEditViewProps {
|
interface ProblemEditViewProps {
|
||||||
// problemId: string;
|
problemId: string;
|
||||||
// }
|
}
|
||||||
|
|
||||||
export const ProblemEditView = (
|
export const ProblemEditView = ({ problemId }: ProblemEditViewProps) => {
|
||||||
// { problemId }: ProblemEditViewProps
|
const components: Record<string, React.ReactNode> = {
|
||||||
) => {
|
detail: <EditDetailPanel problemId={problemId} />,
|
||||||
// const components: Record<string, React.ReactNode> = {
|
description: <EditDescriptionPanel problemId={problemId} />,
|
||||||
// description: <EditDescriptionPanel problemId={problemId} />,
|
solution: <EditSolutionPanel problemId={problemId} />,
|
||||||
// solution: <EditSolutionPanel problemId={problemId} />,
|
code: <EditCodePanel problemId={problemId} />,
|
||||||
// detail: <EditDetailPanel problemId={problemId} />,
|
testcase: <EditTestcasePanel problemId={problemId} />,
|
||||||
// code: <EditCodePanel problemId={problemId} />,
|
};
|
||||||
// testcase: <EditTestcasePanel problemId={problemId} />,
|
|
||||||
// };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full">
|
<div className="relative flex h-full w-full">
|
||||||
{/* <ProblemEditFlexLayout components={components} /> */}
|
<ProblemEditFlexLayout components={components} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
import { UserTable } from './user-table'
|
import prisma from "@/lib/prisma";
|
||||||
import { UserConfig } from './user-table'
|
import { UserTable } from "./user-table";
|
||||||
import prisma from '@/lib/prisma'
|
import { Role } from "@/generated/client";
|
||||||
import type { User, Problem } from '@/generated/client'
|
import { UserConfig } from "./user-table";
|
||||||
import { Role } from '@/generated/client'
|
import type { User, Problem } from "@/generated/client";
|
||||||
|
|
||||||
interface GenericPageProps {
|
interface GenericPageProps {
|
||||||
userType: 'admin' | 'teacher' | 'guest' | 'problem'
|
userType: "admin" | "teacher" | "guest" | "problem";
|
||||||
config: UserConfig
|
config: UserConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function GenericPage({ userType, config }: GenericPageProps) {
|
export default async function GenericPage({
|
||||||
if (userType === 'problem') {
|
userType,
|
||||||
const data: Problem[] = await prisma.problem.findMany({})
|
config,
|
||||||
return <UserTable config={config} data={data} />
|
}: GenericPageProps) {
|
||||||
|
if (userType === "problem") {
|
||||||
|
const data: Problem[] = await prisma.problem.findMany({});
|
||||||
|
return <UserTable config={config} data={data} />;
|
||||||
} else {
|
} else {
|
||||||
const role = userType.toUpperCase() as Role
|
const role = userType.toUpperCase() as Role;
|
||||||
const data: User[] = await prisma.user.findMany({ where: { role } })
|
const data: User[] = await prisma.user.findMany({ where: { role } });
|
||||||
return <UserTable config={config} data={data} />
|
return <UserTable config={config} data={data} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load Diff
@ -1,17 +1,22 @@
|
|||||||
import { z } from "zod"
|
import {
|
||||||
import { createUserConfig, baseUserSchema, baseAddUserSchema, baseEditUserSchema } from './base-config'
|
createUserConfig,
|
||||||
|
baseUserSchema,
|
||||||
|
baseAddUserSchema,
|
||||||
|
baseEditUserSchema,
|
||||||
|
} from "./base-config";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
// 管理员数据校验 schema
|
// 管理员数据校验 schema
|
||||||
export const adminSchema = baseUserSchema
|
export const adminSchema = baseUserSchema;
|
||||||
export type Admin = z.infer<typeof adminSchema>
|
export type Admin = z.infer<typeof adminSchema>;
|
||||||
|
|
||||||
// 添加管理员表单校验 schema
|
// 添加管理员表单校验 schema
|
||||||
export const addAdminSchema = baseAddUserSchema
|
export const addAdminSchema = baseAddUserSchema;
|
||||||
export type AddAdminFormData = z.infer<typeof addAdminSchema>
|
export type AddAdminFormData = z.infer<typeof addAdminSchema>;
|
||||||
|
|
||||||
// 编辑管理员表单校验 schema
|
// 编辑管理员表单校验 schema
|
||||||
export const editAdminSchema = baseEditUserSchema
|
export const editAdminSchema = baseEditUserSchema;
|
||||||
export type EditAdminFormData = z.infer<typeof editAdminSchema>
|
export type EditAdminFormData = z.infer<typeof editAdminSchema>;
|
||||||
|
|
||||||
// 管理员配置
|
// 管理员配置
|
||||||
export const adminConfig = createUserConfig(
|
export const adminConfig = createUserConfig(
|
||||||
@ -20,4 +25,4 @@ export const adminConfig = createUserConfig(
|
|||||||
"添加管理员",
|
"添加管理员",
|
||||||
"请输入管理员姓名",
|
"请输入管理员姓名",
|
||||||
"请输入管理员邮箱"
|
"请输入管理员邮箱"
|
||||||
)
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod";
|
||||||
|
|
||||||
// 基础用户 schema
|
// 基础用户 schema
|
||||||
export const baseUserSchema = z.object({
|
export const baseUserSchema = z.object({
|
||||||
@ -9,7 +9,7 @@ export const baseUserSchema = z.object({
|
|||||||
role: z.string().optional(),
|
role: z.string().optional(),
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
updatedAt: z.string().optional(),
|
updatedAt: z.string().optional(),
|
||||||
})
|
});
|
||||||
|
|
||||||
// 基础添加用户 schema
|
// 基础添加用户 schema
|
||||||
export const baseAddUserSchema = z.object({
|
export const baseAddUserSchema = z.object({
|
||||||
@ -17,7 +17,7 @@ export const baseAddUserSchema = z.object({
|
|||||||
email: z.string().email("请输入有效的邮箱地址"),
|
email: z.string().email("请输入有效的邮箱地址"),
|
||||||
password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
|
password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
})
|
});
|
||||||
|
|
||||||
// 基础编辑用户 schema
|
// 基础编辑用户 schema
|
||||||
export const baseEditUserSchema = z.object({
|
export const baseEditUserSchema = z.object({
|
||||||
@ -26,23 +26,58 @@ export const baseEditUserSchema = z.object({
|
|||||||
email: z.string().email("请输入有效的邮箱地址"),
|
email: z.string().email("请输入有效的邮箱地址"),
|
||||||
password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
|
password: z.string().min(8, "密码长度8-32位").max(32, "密码长度8-32位"),
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
})
|
});
|
||||||
|
|
||||||
// 基础表格列配置
|
// 基础表格列配置
|
||||||
export const baseColumns = [
|
export const baseColumns = [
|
||||||
{ key: "id", label: "ID", sortable: true },
|
{ key: "id", label: "ID", sortable: true },
|
||||||
{ key: "name", label: "姓名", sortable: true, searchable: true, placeholder: "搜索姓名" },
|
{
|
||||||
{ key: "email", label: "邮箱", sortable: true, searchable: true, placeholder: "搜索邮箱" },
|
key: "name",
|
||||||
|
label: "姓名",
|
||||||
|
sortable: true,
|
||||||
|
searchable: true,
|
||||||
|
placeholder: "搜索姓名",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "email",
|
||||||
|
label: "邮箱",
|
||||||
|
sortable: true,
|
||||||
|
searchable: true,
|
||||||
|
placeholder: "搜索邮箱",
|
||||||
|
},
|
||||||
{ key: "createdAt", label: "创建时间", sortable: true },
|
{ key: "createdAt", label: "创建时间", sortable: true },
|
||||||
]
|
];
|
||||||
|
|
||||||
// 基础表单字段配置
|
// 基础表单字段配置
|
||||||
export const baseFormFields = [
|
export const baseFormFields = [
|
||||||
{ key: "name", label: "姓名", type: "text", placeholder: "请输入姓名", required: true },
|
{
|
||||||
{ key: "email", label: "邮箱", type: "email", placeholder: "请输入邮箱", required: true },
|
key: "name",
|
||||||
{ key: "password", label: "密码", type: "password", placeholder: "请输入8-32位密码", required: true },
|
label: "姓名",
|
||||||
{ key: "createdAt", label: "创建时间", type: "datetime-local", required: false },
|
type: "text",
|
||||||
]
|
placeholder: "请输入姓名",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "email",
|
||||||
|
label: "邮箱",
|
||||||
|
type: "email",
|
||||||
|
placeholder: "请输入邮箱",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "password",
|
||||||
|
label: "密码",
|
||||||
|
type: "password",
|
||||||
|
placeholder: "请输入8-32位密码",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "createdAt",
|
||||||
|
label: "创建时间",
|
||||||
|
type: "datetime-local",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// 基础操作配置
|
// 基础操作配置
|
||||||
export const baseActions = {
|
export const baseActions = {
|
||||||
@ -50,13 +85,13 @@ export const baseActions = {
|
|||||||
edit: { label: "编辑", icon: "PencilIcon" },
|
edit: { label: "编辑", icon: "PencilIcon" },
|
||||||
delete: { label: "删除", icon: "TrashIcon" },
|
delete: { label: "删除", icon: "TrashIcon" },
|
||||||
batchDelete: { label: "批量删除", icon: "TrashIcon" },
|
batchDelete: { label: "批量删除", icon: "TrashIcon" },
|
||||||
}
|
};
|
||||||
|
|
||||||
// 基础分页配置
|
// 基础分页配置
|
||||||
export const basePagination = {
|
export const basePagination = {
|
||||||
pageSizes: [10, 50, 100, 500],
|
pageSizes: [10, 50, 100, 500],
|
||||||
defaultPageSize: 10,
|
defaultPageSize: 10,
|
||||||
}
|
};
|
||||||
|
|
||||||
// 创建用户配置的工厂函数
|
// 创建用户配置的工厂函数
|
||||||
export function createUserConfig(
|
export function createUserConfig(
|
||||||
@ -71,16 +106,19 @@ export function createUserConfig(
|
|||||||
title,
|
title,
|
||||||
apiPath: "/api/user",
|
apiPath: "/api/user",
|
||||||
columns: baseColumns,
|
columns: baseColumns,
|
||||||
formFields: baseFormFields.map(field => ({
|
formFields: baseFormFields.map((field) => ({
|
||||||
...field,
|
...field,
|
||||||
placeholder: field.key === 'name' ? namePlaceholder :
|
placeholder:
|
||||||
field.key === 'email' ? emailPlaceholder :
|
field.key === "name"
|
||||||
field.placeholder
|
? namePlaceholder
|
||||||
|
: field.key === "email"
|
||||||
|
? emailPlaceholder
|
||||||
|
: field.placeholder,
|
||||||
})),
|
})),
|
||||||
actions: {
|
actions: {
|
||||||
...baseActions,
|
...baseActions,
|
||||||
add: { ...baseActions.add, label: addLabel }
|
add: { ...baseActions.add, label: addLabel },
|
||||||
},
|
},
|
||||||
pagination: basePagination,
|
pagination: basePagination,
|
||||||
}
|
};
|
||||||
}
|
}
|
@ -1,5 +1,10 @@
|
|||||||
|
import {
|
||||||
|
createUserConfig,
|
||||||
|
baseUserSchema,
|
||||||
|
baseAddUserSchema,
|
||||||
|
baseEditUserSchema,
|
||||||
|
} from "./base-config";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createUserConfig, baseUserSchema, baseAddUserSchema, baseEditUserSchema } from './base-config'
|
|
||||||
|
|
||||||
export const guestSchema = baseUserSchema;
|
export const guestSchema = baseUserSchema;
|
||||||
export type Guest = z.infer<typeof guestSchema>;
|
export type Guest = z.infer<typeof guestSchema>;
|
||||||
|
@ -24,8 +24,20 @@ export const problemConfig = {
|
|||||||
apiPath: "/api/problem",
|
apiPath: "/api/problem",
|
||||||
columns: [
|
columns: [
|
||||||
{ key: "id", label: "ID", sortable: true },
|
{ key: "id", label: "ID", sortable: true },
|
||||||
{ key: "displayId", label: "题目编号", sortable: true, searchable: true, placeholder: "搜索编号" },
|
{
|
||||||
{ key: "difficulty", label: "难度", sortable: true, searchable: true, placeholder: "搜索难度" },
|
key: "displayId",
|
||||||
|
label: "题目编号",
|
||||||
|
sortable: true,
|
||||||
|
searchable: true,
|
||||||
|
placeholder: "搜索编号",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "difficulty",
|
||||||
|
label: "难度",
|
||||||
|
sortable: true,
|
||||||
|
searchable: true,
|
||||||
|
placeholder: "搜索难度",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
formFields: [
|
formFields: [
|
||||||
{ key: "displayId", label: "题目编号", type: "number", required: true },
|
{ key: "displayId", label: "题目编号", type: "number", required: true },
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
|
import {
|
||||||
|
createUserConfig,
|
||||||
|
baseUserSchema,
|
||||||
|
baseAddUserSchema,
|
||||||
|
baseEditUserSchema,
|
||||||
|
} from "./base-config";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createUserConfig, baseUserSchema, baseAddUserSchema, baseEditUserSchema } from './base-config'
|
|
||||||
|
|
||||||
export const teacherSchema = baseUserSchema;
|
export const teacherSchema = baseUserSchema;
|
||||||
export type Teacher = z.infer<typeof teacherSchema>;
|
export type Teacher = z.infer<typeof teacherSchema>;
|
||||||
|
@ -247,8 +247,3 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getCurrentUser=async ()=>{
|
|
||||||
const session=await auth();
|
|
||||||
return session?.user
|
|
||||||
}
|
|
||||||
|
@ -14,85 +14,85 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
background: 'hsl(var(--background))',
|
background: "hsl(var(--background))",
|
||||||
foreground: 'hsl(var(--foreground))',
|
foreground: "hsl(var(--foreground))",
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: 'hsl(var(--card))',
|
DEFAULT: "hsl(var(--card))",
|
||||||
foreground: 'hsl(var(--card-foreground))'
|
foreground: "hsl(var(--card-foreground))",
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: 'hsl(var(--popover))',
|
DEFAULT: "hsl(var(--popover))",
|
||||||
foreground: 'hsl(var(--popover-foreground))'
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: 'hsl(var(--primary))',
|
DEFAULT: "hsl(var(--primary))",
|
||||||
foreground: 'hsl(var(--primary-foreground))'
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: 'hsl(var(--secondary))',
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
foreground: 'hsl(var(--secondary-foreground))'
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: 'hsl(var(--muted))',
|
DEFAULT: "hsl(var(--muted))",
|
||||||
foreground: 'hsl(var(--muted-foreground))'
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: 'hsl(var(--accent))',
|
DEFAULT: "hsl(var(--accent))",
|
||||||
foreground: 'hsl(var(--accent-foreground))'
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: 'hsl(var(--destructive))',
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
foreground: 'hsl(var(--destructive-foreground))'
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
},
|
},
|
||||||
border: 'hsl(var(--border))',
|
border: "hsl(var(--border))",
|
||||||
input: 'hsl(var(--input))',
|
input: "hsl(var(--input))",
|
||||||
ring: 'hsl(var(--ring))',
|
ring: "hsl(var(--ring))",
|
||||||
chart: {
|
chart: {
|
||||||
'1': 'hsl(var(--chart-1))',
|
"1": "hsl(var(--chart-1))",
|
||||||
'2': 'hsl(var(--chart-2))',
|
"2": "hsl(var(--chart-2))",
|
||||||
'3': 'hsl(var(--chart-3))',
|
"3": "hsl(var(--chart-3))",
|
||||||
'4': 'hsl(var(--chart-4))',
|
"4": "hsl(var(--chart-4))",
|
||||||
'5': 'hsl(var(--chart-5))'
|
"5": "hsl(var(--chart-5))",
|
||||||
},
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
DEFAULT: "hsl(var(--sidebar-background))",
|
||||||
foreground: 'hsl(var(--sidebar-foreground))',
|
foreground: "hsl(var(--sidebar-foreground))",
|
||||||
primary: 'hsl(var(--sidebar-primary))',
|
primary: "hsl(var(--sidebar-primary))",
|
||||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
||||||
accent: 'hsl(var(--sidebar-accent))',
|
accent: "hsl(var(--sidebar-accent))",
|
||||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||||
border: 'hsl(var(--sidebar-border))',
|
border: "hsl(var(--sidebar-border))",
|
||||||
ring: 'hsl(var(--sidebar-ring))'
|
ring: "hsl(var(--sidebar-ring))",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: 'var(--radius)',
|
lg: "var(--radius)",
|
||||||
md: 'calc(var(--radius) - 2px)',
|
md: "calc(var(--radius) - 2px)",
|
||||||
sm: 'calc(var(--radius) - 4px)'
|
sm: "calc(var(--radius) - 4px)",
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
'accordion-down': {
|
"accordion-down": {
|
||||||
from: {
|
from: {
|
||||||
height: '0'
|
height: "0",
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
height: 'var(--radix-accordion-content-height)'
|
height: "var(--radix-accordion-content-height)",
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'accordion-up': {
|
},
|
||||||
|
"accordion-up": {
|
||||||
from: {
|
from: {
|
||||||
height: 'var(--radix-accordion-content-height)'
|
height: "var(--radix-accordion-content-height)",
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
height: '0'
|
height: "0",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
plugins: [animate],
|
plugins: [animate],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
Loading…
Reference in New Issue
Block a user