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