mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2026-05-31 19:02:03 +00:00
refactor(dashboard): track assignment progress by testcases
This commit is contained in:
parent
d87265eb2d
commit
f0ea48ef06
@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `maxPoints` on the `AssignmentProblem` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "AssignmentProblem" DROP COLUMN "maxPoints";
|
||||||
@ -275,7 +275,6 @@ model Assignment {
|
|||||||
model AssignmentProblem {
|
model AssignmentProblem {
|
||||||
assignmentId String
|
assignmentId String
|
||||||
problemId String
|
problemId String
|
||||||
maxPoints Int @default(100)
|
|
||||||
order Int?
|
order Int?
|
||||||
|
|
||||||
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
|
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
|
||||||
|
|||||||
@ -3128,7 +3128,6 @@ export async function main() {
|
|||||||
data: selectedProblems.map((problem, index) => ({
|
data: selectedProblems.map((problem, index) => ({
|
||||||
assignmentId: assignment.id,
|
assignmentId: assignment.id,
|
||||||
problemId: problem.id,
|
problemId: problem.id,
|
||||||
maxPoints: 100,
|
|
||||||
order: index + 1,
|
order: index + 1,
|
||||||
})),
|
})),
|
||||||
skipDuplicates: true,
|
skipDuplicates: true,
|
||||||
|
|||||||
@ -9,37 +9,44 @@ import {
|
|||||||
getAuthenticatedActor,
|
getAuthenticatedActor,
|
||||||
} from "@/app/(protected)/dashboard/actions/course-auth";
|
} from "@/app/(protected)/dashboard/actions/course-auth";
|
||||||
|
|
||||||
interface ProblemScoreRow {
|
interface ProblemProgressRow {
|
||||||
problemId: string;
|
problemId: string;
|
||||||
displayId: number;
|
displayId: number;
|
||||||
title: string;
|
title: string;
|
||||||
maxPoints: number;
|
testcaseCount: number;
|
||||||
earnedPoints: number;
|
passedTestcaseCount: number;
|
||||||
solved: boolean;
|
solved: boolean;
|
||||||
attempts: number;
|
attempts: number;
|
||||||
bestStatus: string;
|
bestStatus: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProblemScoreRows(
|
function buildProblemProgressRows(
|
||||||
assignmentProblems: {
|
assignmentProblems: {
|
||||||
problemId: string;
|
problemId: string;
|
||||||
maxPoints: number;
|
|
||||||
problem: {
|
problem: {
|
||||||
displayId: number;
|
displayId: number;
|
||||||
|
testcases: { id: string }[];
|
||||||
localizations: { content: string }[];
|
localizations: { content: string }[];
|
||||||
};
|
};
|
||||||
}[],
|
}[],
|
||||||
submissions: {
|
submissions: {
|
||||||
problemId: string;
|
problemId: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
judge: {
|
||||||
|
judgeRuns: {
|
||||||
|
testcaseId: string;
|
||||||
|
status: string;
|
||||||
|
}[];
|
||||||
|
} | null;
|
||||||
}[]
|
}[]
|
||||||
): ProblemScoreRow[] {
|
): ProblemProgressRow[] {
|
||||||
const grouped = new Map<
|
const grouped = new Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
attempts: number;
|
attempts: number;
|
||||||
solved: boolean;
|
solved: boolean;
|
||||||
bestStatus: string;
|
bestStatus: string;
|
||||||
|
passedTestcaseCount: number;
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
@ -48,12 +55,27 @@ function buildProblemScoreRows(
|
|||||||
attempts: 0,
|
attempts: 0,
|
||||||
solved: false,
|
solved: false,
|
||||||
bestStatus: "PD",
|
bestStatus: "PD",
|
||||||
|
passedTestcaseCount: 0,
|
||||||
};
|
};
|
||||||
|
const passedTestcaseIds = new Set(
|
||||||
|
submission.judge?.judgeRuns
|
||||||
|
.filter((run) => run.status === "ACCEPTED")
|
||||||
|
.map((run) => run.testcaseId) ?? []
|
||||||
|
);
|
||||||
|
|
||||||
current.attempts += 1;
|
current.attempts += 1;
|
||||||
|
if (passedTestcaseIds.size > current.passedTestcaseCount) {
|
||||||
|
current.passedTestcaseCount = passedTestcaseIds.size;
|
||||||
|
current.bestStatus = submission.status;
|
||||||
|
}
|
||||||
if (submission.status === "AC") {
|
if (submission.status === "AC") {
|
||||||
current.solved = true;
|
current.solved = true;
|
||||||
current.bestStatus = "AC";
|
current.bestStatus = "AC";
|
||||||
} else if (!current.solved) {
|
current.passedTestcaseCount = Math.max(
|
||||||
|
current.passedTestcaseCount,
|
||||||
|
passedTestcaseIds.size
|
||||||
|
);
|
||||||
|
} else if (!current.solved && current.bestStatus === "PD") {
|
||||||
current.bestStatus = submission.status;
|
current.bestStatus = submission.status;
|
||||||
}
|
}
|
||||||
grouped.set(submission.problemId, current);
|
grouped.set(submission.problemId, current);
|
||||||
@ -61,13 +83,16 @@ function buildProblemScoreRows(
|
|||||||
|
|
||||||
return assignmentProblems.map((item) => {
|
return assignmentProblems.map((item) => {
|
||||||
const progress = grouped.get(item.problemId);
|
const progress = grouped.get(item.problemId);
|
||||||
|
const testcaseCount = item.problem.testcases.length;
|
||||||
const solved = Boolean(progress?.solved);
|
const solved = Boolean(progress?.solved);
|
||||||
return {
|
return {
|
||||||
problemId: item.problemId,
|
problemId: item.problemId,
|
||||||
displayId: item.problem.displayId,
|
displayId: item.problem.displayId,
|
||||||
title: item.problem.localizations[0]?.content ?? `题目${item.problem.displayId}`,
|
title: item.problem.localizations[0]?.content ?? `题目${item.problem.displayId}`,
|
||||||
maxPoints: item.maxPoints,
|
testcaseCount,
|
||||||
earnedPoints: solved ? item.maxPoints : 0,
|
passedTestcaseCount: solved
|
||||||
|
? testcaseCount
|
||||||
|
: progress?.passedTestcaseCount ?? 0,
|
||||||
solved,
|
solved,
|
||||||
attempts: progress?.attempts ?? 0,
|
attempts: progress?.attempts ?? 0,
|
||||||
bestStatus: progress?.bestStatus ?? "-",
|
bestStatus: progress?.bestStatus ?? "-",
|
||||||
@ -109,10 +134,12 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
|
|||||||
orderBy: [{ order: "asc" }, { problem: { displayId: "asc" } }],
|
orderBy: [{ order: "asc" }, { problem: { displayId: "asc" } }],
|
||||||
select: {
|
select: {
|
||||||
problemId: true,
|
problemId: true,
|
||||||
maxPoints: true,
|
|
||||||
problem: {
|
problem: {
|
||||||
select: {
|
select: {
|
||||||
displayId: true,
|
displayId: true,
|
||||||
|
testcases: {
|
||||||
|
select: { id: true },
|
||||||
|
},
|
||||||
localizations: {
|
localizations: {
|
||||||
where: { type: "TITLE", locale: "zh" },
|
where: { type: "TITLE", locale: "zh" },
|
||||||
select: { content: true },
|
select: { content: true },
|
||||||
@ -126,6 +153,16 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
|
|||||||
userId: true,
|
userId: true,
|
||||||
problemId: true,
|
problemId: true,
|
||||||
status: true,
|
status: true,
|
||||||
|
judge: {
|
||||||
|
select: {
|
||||||
|
judgeRuns: {
|
||||||
|
select: {
|
||||||
|
testcaseId: true,
|
||||||
|
status: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -138,14 +175,24 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
|
|||||||
await assertCourseManagePermission(assignment.courseId, actor);
|
await assertCourseManagePermission(assignment.courseId, actor);
|
||||||
|
|
||||||
const problemCount = assignment.problems.length;
|
const problemCount = assignment.problems.length;
|
||||||
const maxScore = assignment.problems.reduce(
|
const totalTestcases = assignment.problems.reduce(
|
||||||
(total, problem) => total + problem.maxPoints,
|
(total, problem) => total + problem.problem.testcases.length,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
const submissionGroup = new Map<
|
const submissionGroup = new Map<
|
||||||
string,
|
string,
|
||||||
{ userId: string; problemId: string; status: string }[]
|
{
|
||||||
|
userId: string;
|
||||||
|
problemId: string;
|
||||||
|
status: string;
|
||||||
|
judge: {
|
||||||
|
judgeRuns: {
|
||||||
|
testcaseId: string;
|
||||||
|
status: string;
|
||||||
|
}[];
|
||||||
|
} | null;
|
||||||
|
}[]
|
||||||
>();
|
>();
|
||||||
for (const submission of assignment.submissions) {
|
for (const submission of assignment.submissions) {
|
||||||
const bucket = submissionGroup.get(submission.userId) ?? [];
|
const bucket = submissionGroup.get(submission.userId) ?? [];
|
||||||
@ -154,34 +201,41 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const students = assignment.course.enrollments.map((enrollment) => {
|
const students = assignment.course.enrollments.map((enrollment) => {
|
||||||
const rows = buildProblemScoreRows(
|
const rows = buildProblemProgressRows(
|
||||||
assignment.problems,
|
assignment.problems,
|
||||||
submissionGroup.get(enrollment.userId) ?? []
|
submissionGroup.get(enrollment.userId) ?? []
|
||||||
);
|
);
|
||||||
const score = rows.reduce((sum, row) => sum + row.earnedPoints, 0);
|
const passedTestcaseCount = rows.reduce(
|
||||||
|
(sum, row) => sum + row.passedTestcaseCount,
|
||||||
|
0
|
||||||
|
);
|
||||||
const solvedCount = rows.filter((row) => row.solved).length;
|
const solvedCount = rows.filter((row) => row.solved).length;
|
||||||
const completionPercent =
|
|
||||||
problemCount > 0 ? Math.round((solvedCount / problemCount) * 100) : 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId: enrollment.userId,
|
userId: enrollment.userId,
|
||||||
name: enrollment.user.name,
|
name: enrollment.user.name,
|
||||||
email: enrollment.user.email,
|
email: enrollment.user.email,
|
||||||
totalScore: score,
|
passedTestcaseCount,
|
||||||
maxScore,
|
totalTestcases,
|
||||||
solvedCount,
|
solvedCount,
|
||||||
problemCount,
|
problemCount,
|
||||||
completionPercent,
|
|
||||||
perProblem: rows,
|
perProblem: rows,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const perProblemCoverage = assignment.problems.map((problem) => {
|
const perProblemCoverage = assignment.problems.map((problem) => {
|
||||||
|
const testcaseCount = problem.problem.testcases.length;
|
||||||
const solvedUsers = students.filter((student) =>
|
const solvedUsers = students.filter((student) =>
|
||||||
student.perProblem.some(
|
student.perProblem.some(
|
||||||
(row) => row.problemId === problem.problemId && row.solved
|
(row) => row.problemId === problem.problemId && row.solved
|
||||||
)
|
)
|
||||||
).length;
|
).length;
|
||||||
|
const passedTestcaseCount = students.reduce((total, student) => {
|
||||||
|
const row = student.perProblem.find(
|
||||||
|
(item) => item.problemId === problem.problemId
|
||||||
|
);
|
||||||
|
return total + (row?.passedTestcaseCount ?? 0);
|
||||||
|
}, 0);
|
||||||
const totalUsers = students.length;
|
const totalUsers = students.length;
|
||||||
return {
|
return {
|
||||||
problemId: problem.problemId,
|
problemId: problem.problemId,
|
||||||
@ -189,10 +243,10 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
|
|||||||
title:
|
title:
|
||||||
problem.problem.localizations[0]?.content ??
|
problem.problem.localizations[0]?.content ??
|
||||||
`题目${problem.problem.displayId}`,
|
`题目${problem.problem.displayId}`,
|
||||||
|
passedTestcaseCount,
|
||||||
|
testcaseCount,
|
||||||
solvedUsers,
|
solvedUsers,
|
||||||
totalUsers,
|
totalUsers,
|
||||||
acCoverage:
|
|
||||||
totalUsers > 0 ? Math.round((solvedUsers / totalUsers) * 100) : 0,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -207,8 +261,8 @@ export async function getTeacherAssignmentStats(assignmentId: string) {
|
|||||||
id: assignment.course.id,
|
id: assignment.course.id,
|
||||||
title: assignment.course.title,
|
title: assignment.course.title,
|
||||||
},
|
},
|
||||||
maxScore,
|
|
||||||
problemCount,
|
problemCount,
|
||||||
|
totalTestcases,
|
||||||
students,
|
students,
|
||||||
perProblemCoverage,
|
perProblemCoverage,
|
||||||
};
|
};
|
||||||
@ -238,10 +292,12 @@ export async function getStudentAssignmentSummary(assignmentId: string) {
|
|||||||
orderBy: [{ order: "asc" }, { problem: { displayId: "asc" } }],
|
orderBy: [{ order: "asc" }, { problem: { displayId: "asc" } }],
|
||||||
select: {
|
select: {
|
||||||
problemId: true,
|
problemId: true,
|
||||||
maxPoints: true,
|
|
||||||
problem: {
|
problem: {
|
||||||
select: {
|
select: {
|
||||||
displayId: true,
|
displayId: true,
|
||||||
|
testcases: {
|
||||||
|
select: { id: true },
|
||||||
|
},
|
||||||
localizations: {
|
localizations: {
|
||||||
where: { type: "TITLE", locale: "zh" },
|
where: { type: "TITLE", locale: "zh" },
|
||||||
select: { content: true },
|
select: { content: true },
|
||||||
@ -270,15 +326,26 @@ export async function getStudentAssignmentSummary(assignmentId: string) {
|
|||||||
select: {
|
select: {
|
||||||
problemId: true,
|
problemId: true,
|
||||||
status: true,
|
status: true,
|
||||||
|
judge: {
|
||||||
|
select: {
|
||||||
|
judgeRuns: {
|
||||||
|
select: {
|
||||||
|
testcaseId: true,
|
||||||
|
status: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const rows = buildProblemScoreRows(assignment.problems, submissions);
|
const rows = buildProblemProgressRows(assignment.problems, submissions);
|
||||||
const totalScore = rows.reduce((sum, row) => sum + row.earnedPoints, 0);
|
const passedTestcaseCount = rows.reduce(
|
||||||
const maxScore = rows.reduce((sum, row) => sum + row.maxPoints, 0);
|
(sum, row) => sum + row.passedTestcaseCount,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalTestcases = rows.reduce((sum, row) => sum + row.testcaseCount, 0);
|
||||||
const solvedCount = rows.filter((row) => row.solved).length;
|
const solvedCount = rows.filter((row) => row.solved).length;
|
||||||
const completionPercent =
|
|
||||||
rows.length > 0 ? Math.round((solvedCount / rows.length) * 100) : 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assignment: {
|
assignment: {
|
||||||
@ -290,11 +357,10 @@ export async function getStudentAssignmentSummary(assignmentId: string) {
|
|||||||
published: assignment.published,
|
published: assignment.published,
|
||||||
},
|
},
|
||||||
course: assignment.course,
|
course: assignment.course,
|
||||||
totalScore,
|
passedTestcaseCount,
|
||||||
maxScore,
|
totalTestcases,
|
||||||
solvedCount,
|
solvedCount,
|
||||||
problemCount: rows.length,
|
problemCount: rows.length,
|
||||||
completionPercent,
|
|
||||||
rows,
|
rows,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,7 +103,7 @@ export async function getStudentDashboardData() {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
// 计算题目完成情况
|
// 计算题目通过情况
|
||||||
const completedProblems = new Set<string | number>();
|
const completedProblems = new Set<string | number>();
|
||||||
const attemptedProblems = new Set<string | number>();
|
const attemptedProblems = new Set<string | number>();
|
||||||
const wrongSubmissions = new Map<string | number, number>(); // problemId -> count
|
const wrongSubmissions = new Map<string | number, number>(); // problemId -> count
|
||||||
@ -121,10 +121,10 @@ export async function getStudentDashboardData() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log("尝试过的题目数:", attemptedProblems.size);
|
console.log("尝试过的题目数:", attemptedProblems.size);
|
||||||
console.log("完成的题目数:", completedProblems.size);
|
console.log("通过的题目数:", completedProblems.size);
|
||||||
console.log("错误提交统计:", Object.fromEntries(wrongSubmissions));
|
console.log("错误提交统计:", Object.fromEntries(wrongSubmissions));
|
||||||
|
|
||||||
// 题目完成比例数据
|
// 题目通过情况数据
|
||||||
const completionData = {
|
const completionData = {
|
||||||
total: allProblems.length,
|
total: allProblems.length,
|
||||||
completed: completedProblems.size,
|
completed: completedProblems.size,
|
||||||
@ -134,10 +134,10 @@ export async function getStudentDashboardData() {
|
|||||||
: 0,
|
: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 错题比例数据 - 基于已完成的题目计算
|
// 错题比例数据 - 基于已通过的题目计算
|
||||||
const wrongProblems = new Set<string | number>();
|
const wrongProblems = new Set<string | number>();
|
||||||
|
|
||||||
// 统计在已完成的题目中,哪些题目曾经有过错误提交
|
// 统计在已通过的题目中,哪些题目曾经有过错误提交
|
||||||
userSubmissions.forEach((submission) => {
|
userSubmissions.forEach((submission) => {
|
||||||
if (
|
if (
|
||||||
submission.status !== "AC" &&
|
submission.status !== "AC" &&
|
||||||
@ -148,8 +148,8 @@ export async function getStudentDashboardData() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const errorData = {
|
const errorData = {
|
||||||
total: completedProblems.size, // 已完成的题目总数
|
total: completedProblems.size, // 已通过的题目总数
|
||||||
wrong: wrongProblems.size, // 在已完成的题目中有过错误的题目数
|
wrong: wrongProblems.size, // 在已通过的题目中有过错误的题目数
|
||||||
percentage:
|
percentage:
|
||||||
completedProblems.size > 0
|
completedProblems.size > 0
|
||||||
? Math.round((wrongProblems.size / completedProblems.size) * 100)
|
? Math.round((wrongProblems.size / completedProblems.size) * 100)
|
||||||
@ -181,9 +181,9 @@ export async function getStudentDashboardData() {
|
|||||||
errorData,
|
errorData,
|
||||||
difficultProblems,
|
difficultProblems,
|
||||||
pieChartData: [
|
pieChartData: [
|
||||||
{ name: "已完成", value: completionData.completed },
|
{ name: "已通过", value: completionData.completed },
|
||||||
{
|
{
|
||||||
name: "未完成",
|
name: "未通过",
|
||||||
value: completionData.total - completionData.completed,
|
value: completionData.total - completionData.completed,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -194,7 +194,7 @@ export async function getStudentDashboardData() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
console.log("=== 返回的数据 ===");
|
console.log("=== 返回的数据 ===");
|
||||||
console.log("完成情况:", completionData);
|
console.log("通过情况:", completionData);
|
||||||
console.log("错误情况:", errorData);
|
console.log("错误情况:", errorData);
|
||||||
console.log("易错题数量:", difficultProblems.length);
|
console.log("易错题数量:", difficultProblems.length);
|
||||||
console.log("=== 数据获取完成 ===");
|
console.log("=== 数据获取完成 ===");
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import {
|
|||||||
|
|
||||||
interface AssignmentProblemInput {
|
interface AssignmentProblemInput {
|
||||||
problemId: string;
|
problemId: string;
|
||||||
maxPoints: number;
|
|
||||||
order?: number;
|
order?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,14 +32,6 @@ interface UpdateAssignmentInput {
|
|||||||
problems?: AssignmentProblemInput[];
|
problems?: AssignmentProblemInput[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizePoints(points: number) {
|
|
||||||
const normalized = Number(points);
|
|
||||||
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
||||||
throw new Error("分值必须是正整数");
|
|
||||||
}
|
|
||||||
return Math.round(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateProblems(problems: AssignmentProblemInput[]) {
|
async function validateProblems(problems: AssignmentProblemInput[]) {
|
||||||
if (!Array.isArray(problems) || problems.length === 0) {
|
if (!Array.isArray(problems) || problems.length === 0) {
|
||||||
throw new Error("作业至少需要一道题目");
|
throw new Error("作业至少需要一道题目");
|
||||||
@ -101,7 +92,6 @@ export async function createAssignment(input: CreateAssignmentInput) {
|
|||||||
problems: {
|
problems: {
|
||||||
create: input.problems.map((item, index) => ({
|
create: input.problems.map((item, index) => ({
|
||||||
problemId: item.problemId,
|
problemId: item.problemId,
|
||||||
maxPoints: normalizePoints(item.maxPoints),
|
|
||||||
order: item.order ?? index + 1,
|
order: item.order ?? index + 1,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
@ -185,7 +175,6 @@ export async function updateAssignment(
|
|||||||
data: input.problems.map((item, index) => ({
|
data: input.problems.map((item, index) => ({
|
||||||
assignmentId,
|
assignmentId,
|
||||||
problemId: item.problemId,
|
problemId: item.problemId,
|
||||||
maxPoints: normalizePoints(item.maxPoints),
|
|
||||||
order: item.order ?? index + 1,
|
order: item.order ?? index + 1,
|
||||||
})),
|
})),
|
||||||
skipDuplicates: true,
|
skipDuplicates: true,
|
||||||
@ -239,7 +228,6 @@ export async function getAssignmentDetail(assignmentId: string) {
|
|||||||
orderBy: [{ order: "asc" }, { problem: { displayId: "asc" } }],
|
orderBy: [{ order: "asc" }, { problem: { displayId: "asc" } }],
|
||||||
select: {
|
select: {
|
||||||
problemId: true,
|
problemId: true,
|
||||||
maxPoints: true,
|
|
||||||
order: true,
|
order: true,
|
||||||
problem: {
|
problem: {
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
@ -59,7 +59,7 @@ export async function getProblemCompletionData(): Promise<
|
|||||||
|
|
||||||
const locale = await getLocale();
|
const locale = await getLocale();
|
||||||
|
|
||||||
// 按题目分组统计完成情况(统计独立用户数)
|
// 按题目分组统计通过情况(统计独立用户数)
|
||||||
const problemStats = new Map<
|
const problemStats = new Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
@ -77,7 +77,7 @@ export async function getProblemCompletionData(): Promise<
|
|||||||
const problemTitle = title;
|
const problemTitle = title;
|
||||||
const problemDisplayId = submission.problem.displayId;
|
const problemDisplayId = submission.problem.displayId;
|
||||||
const userId = submission.userId;
|
const userId = submission.userId;
|
||||||
const isCompleted = submission.status === Status.AC; // 只有 Accepted 才算完成
|
const isCompleted = submission.status === Status.AC; // 只有 Accepted 才算通过
|
||||||
|
|
||||||
if (!problemStats.has(problemId)) {
|
if (!problemStats.has(problemId)) {
|
||||||
problemStats.set(problemId, {
|
problemStats.set(problemId, {
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import prisma from "@/lib/prisma";
|
|||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Progress } from "@/components/ui/progress";
|
|
||||||
|
|
||||||
interface Stats {
|
interface Stats {
|
||||||
totalUsers?: number;
|
totalUsers?: number;
|
||||||
@ -239,7 +238,7 @@ export default async function DashboardPage() {
|
|||||||
color: "text-blue-600",
|
color: "text-blue-600",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "已完成",
|
label: "已通过",
|
||||||
value: stats.completedProblems,
|
value: stats.completedProblems,
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
color: "text-green-600",
|
color: "text-green-600",
|
||||||
@ -256,13 +255,6 @@ export default async function DashboardPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const config = getRoleConfig();
|
const config = getRoleConfig();
|
||||||
const completionRate =
|
|
||||||
fullUser.role === "STUDENT"
|
|
||||||
? (stats.totalProblems || 0) > 0
|
|
||||||
? ((stats.completedProblems || 0) / (stats.totalProblems || 1)) * 100
|
|
||||||
: 0
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 p-6">
|
<div className="space-y-6 p-6">
|
||||||
{/* 欢迎区域 */}
|
{/* 欢迎区域 */}
|
||||||
@ -294,7 +286,7 @@ export default async function DashboardPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 学生进度条 */}
|
{/* 学生通过情况 */}
|
||||||
{fullUser.role === "STUDENT" && (
|
{fullUser.role === "STUDENT" && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -303,14 +295,14 @@ export default async function DashboardPage() {
|
|||||||
学习进度
|
学习进度
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
已完成 {stats.completedProblems || 0} / {stats.totalProblems || 0}{" "}
|
已通过 {stats.completedProblems || 0} / {stats.totalProblems || 0}{" "}
|
||||||
道题目
|
道题目
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Progress value={completionRate} className="w-full" />
|
<p className="text-sm text-muted-foreground">
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
已通过 {stats.completedProblems || 0} / {stats.totalProblems || 0}{" "}
|
||||||
完成率: {completionRate.toFixed(1)}%
|
道题目
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import Link from "next/link";
|
|||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Progress } from "@/components/ui/progress";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@ -49,17 +48,15 @@ export default function StudentAssignmentDetailPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
总分:{summary.totalScore}/{summary.maxScore}
|
通过测试点:{summary.passedTestcaseCount}/{summary.totalTestcases}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
完成:{summary.solvedCount}/{summary.problemCount}
|
通过题目:{summary.solvedCount}/{summary.problemCount}
|
||||||
</p>
|
</p>
|
||||||
<Progress value={summary.completionPercent} className="w-full" />
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
完成率 {summary.completionPercent}%
|
|
||||||
{summary.assignment.dueAt
|
{summary.assignment.dueAt
|
||||||
? ` · 截止 ${new Date(summary.assignment.dueAt).toLocaleString()}`
|
? `截止 ${new Date(summary.assignment.dueAt).toLocaleString()}`
|
||||||
: ""}
|
: "暂无截止时间"}
|
||||||
</p>
|
</p>
|
||||||
{error ? <p className="text-sm text-red-500">{error}</p> : null}
|
{error ? <p className="text-sm text-red-500">{error}</p> : null}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -68,7 +65,7 @@ export default function StudentAssignmentDetailPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>题目列表</CardTitle>
|
<CardTitle>题目列表</CardTitle>
|
||||||
<CardDescription>进入题目后会自动按本作业统计提交与得分</CardDescription>
|
<CardDescription>进入题目后会自动按本作业统计提交与测试点</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
{summary.rows.map((row) => (
|
{summary.rows.map((row) => (
|
||||||
@ -81,8 +78,10 @@ export default function StudentAssignmentDetailPage() {
|
|||||||
#{row.displayId} {row.title}
|
#{row.displayId} {row.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
得分 {row.earnedPoints}/{row.maxPoints} · 状态 {row.bestStatus} ·
|
{row.testcaseCount > 0
|
||||||
提交 {row.attempts} 次
|
? `通过测试点 ${row.passedTestcaseCount}/${row.testcaseCount}`
|
||||||
|
: "未配置测试点"}{" "}
|
||||||
|
· 状态 {row.bestStatus} · 提交 {row.attempts} 次
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant={row.solved ? "secondary" : "outline"}>
|
<Button asChild variant={row.solved ? "secondary" : "outline"}>
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export default function StudentCourseDetailPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>作业列表</CardTitle>
|
<CardTitle>作业列表</CardTitle>
|
||||||
<CardDescription>点击进入作业查看得分并开始做题</CardDescription>
|
<CardDescription>点击进入作业查看测试点通过情况并开始做题</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
{error ? <p className="text-sm text-red-500">{error}</p> : null}
|
{error ? <p className="text-sm text-red-500">{error}</p> : null}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export default function StudentCoursesPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>我的课程</CardTitle>
|
<CardTitle>我的课程</CardTitle>
|
||||||
<CardDescription>进入课程查看作业、截止时间和成绩</CardDescription>
|
<CardDescription>进入课程查看作业、截止时间和通过情况</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
{error ? <p className="text-sm text-red-500">{error}</p> : null}
|
{error ? <p className="text-sm text-red-500">{error}</p> : null}
|
||||||
|
|||||||
@ -47,7 +47,7 @@ export default function StudentDashboard() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const dashboardData = await getStudentDashboardData();
|
const dashboardData = await getStudentDashboardData();
|
||||||
console.log("获取到的数据:", dashboardData);
|
console.log("获取到的数据:", dashboardData);
|
||||||
console.log("完成情况:", dashboardData.completionData);
|
console.log("通过情况:", dashboardData.completionData);
|
||||||
console.log("错误情况:", dashboardData.errorData);
|
console.log("错误情况:", dashboardData.errorData);
|
||||||
console.log("易错题:", dashboardData.difficultProblems);
|
console.log("易错题:", dashboardData.difficultProblems);
|
||||||
setData(dashboardData);
|
setData(dashboardData);
|
||||||
@ -106,22 +106,18 @@ export default function StudentDashboard() {
|
|||||||
<h1 className="text-3xl font-bold mb-6">学生仪表板</h1>
|
<h1 className="text-3xl font-bold mb-6">学生仪表板</h1>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{/* 题目完成比例模块 */}
|
{/* 题目通过情况模块 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>题目完成比例</CardTitle>
|
<CardTitle>题目通过情况</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span>
|
<span>
|
||||||
已完成题目:{completionData.completed}/{completionData.total}
|
已通过题目:{completionData.completed}/{completionData.total}
|
||||||
</span>
|
|
||||||
<span className="text-green-500">
|
|
||||||
{completionData.percentage}%
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress value={completionData.percentage} className="h-2" />
|
|
||||||
<div className="h-[200px]">
|
<div className="h-[200px]">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<PieChart>
|
<PieChart>
|
||||||
|
|||||||
@ -118,9 +118,9 @@ export default function TeacherAssignmentDetailPage() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>班级成绩概览</CardTitle>
|
<CardTitle>班级通过情况</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
满分 {stats.maxScore} 分 · 共 {stats.problemCount} 题
|
共 {stats.problemCount} 题 · {stats.totalTestcases} 个测试点
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
@ -133,8 +133,8 @@ export default function TeacherAssignmentDetailPage() {
|
|||||||
{student.name || "未命名"} ({student.email})
|
{student.name || "未命名"} ({student.email})
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
总分 {student.totalScore}/{student.maxScore} · 完成率{" "}
|
通过测试点 {student.passedTestcaseCount}/{student.totalTestcases} ·
|
||||||
{student.completionPercent}%
|
通过题目 {student.solvedCount}/{student.problemCount}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@ -144,7 +144,7 @@ export default function TeacherAssignmentDetailPage() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>每题 AC 覆盖率</CardTitle>
|
<CardTitle>每题通过情况</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
{stats.perProblemCoverage.map((item) => (
|
{stats.perProblemCoverage.map((item) => (
|
||||||
@ -153,7 +153,10 @@ export default function TeacherAssignmentDetailPage() {
|
|||||||
#{item.displayId} {item.title}
|
#{item.displayId} {item.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{item.solvedUsers}/{item.totalUsers} 人通过 · 覆盖率 {item.acCoverage}%
|
{item.testcaseCount > 0
|
||||||
|
? `每人 ${item.testcaseCount} 个测试点`
|
||||||
|
: "未配置测试点"}{" "}
|
||||||
|
· {item.solvedUsers}/{item.totalUsers} 人全通过
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -59,12 +59,10 @@ export default function TeacherCourseDetailPage() {
|
|||||||
const [assignmentTitle, setAssignmentTitle] = useState("");
|
const [assignmentTitle, setAssignmentTitle] = useState("");
|
||||||
const [assignmentDescription, setAssignmentDescription] = useState("");
|
const [assignmentDescription, setAssignmentDescription] = useState("");
|
||||||
const [assignmentDueAt, setAssignmentDueAt] = useState("");
|
const [assignmentDueAt, setAssignmentDueAt] = useState("");
|
||||||
const [selectedProblems, setSelectedProblems] = useState<
|
const [selectedProblems, setSelectedProblems] = useState<string[]>([]);
|
||||||
Record<string, number>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
const selectedProblemIds = useMemo(
|
const selectedProblemIds = useMemo(
|
||||||
() => Object.keys(selectedProblems),
|
() => selectedProblems,
|
||||||
[selectedProblems]
|
[selectedProblems]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -123,14 +121,13 @@ export default function TeacherCourseDetailPage() {
|
|||||||
published: true,
|
published: true,
|
||||||
problems: selectedProblemIds.map((problemId, index) => ({
|
problems: selectedProblemIds.map((problemId, index) => ({
|
||||||
problemId,
|
problemId,
|
||||||
maxPoints: selectedProblems[problemId] ?? 100,
|
|
||||||
order: index + 1,
|
order: index + 1,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
setAssignmentTitle("");
|
setAssignmentTitle("");
|
||||||
setAssignmentDescription("");
|
setAssignmentDescription("");
|
||||||
setAssignmentDueAt("");
|
setAssignmentDueAt("");
|
||||||
setSelectedProblems({});
|
setSelectedProblems([]);
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : "创建作业失败");
|
setError(e instanceof Error ? e.message : "创建作业失败");
|
||||||
@ -147,11 +144,9 @@ export default function TeacherCourseDetailPage() {
|
|||||||
const toggleProblem = (problemId: string, checked: boolean) => {
|
const toggleProblem = (problemId: string, checked: boolean) => {
|
||||||
setSelectedProblems((prev) => {
|
setSelectedProblems((prev) => {
|
||||||
if (!checked) {
|
if (!checked) {
|
||||||
const next = { ...prev };
|
return prev.filter((id) => id !== problemId);
|
||||||
delete next[problemId];
|
|
||||||
return next;
|
|
||||||
}
|
}
|
||||||
return { ...prev, [problemId]: prev[problemId] ?? 100 };
|
return prev.includes(problemId) ? prev : [...prev, problemId];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -220,7 +215,7 @@ export default function TeacherCourseDetailPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>创建作业</CardTitle>
|
<CardTitle>创建作业</CardTitle>
|
||||||
<CardDescription>选择题目并设置每题分值</CardDescription>
|
<CardDescription>选择题目后发布给课程学生练习</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
<Input
|
<Input
|
||||||
@ -242,8 +237,10 @@ export default function TeacherCourseDetailPage() {
|
|||||||
{problems.map((problem) => {
|
{problems.map((problem) => {
|
||||||
const selected = selectedProblemIds.includes(problem.id);
|
const selected = selectedProblemIds.includes(problem.id);
|
||||||
return (
|
return (
|
||||||
<div key={problem.id} className="space-y-2 rounded border p-2">
|
<label
|
||||||
<label className="flex items-center gap-2 text-sm">
|
key={problem.id}
|
||||||
|
className="flex items-center gap-2 rounded border p-2 text-sm"
|
||||||
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selected}
|
checked={selected}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
@ -256,20 +253,6 @@ export default function TeacherCourseDetailPage() {
|
|||||||
{problem.difficulty})
|
{problem.difficulty})
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{selected ? (
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={selectedProblems[problem.id] ?? 100}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSelectedProblems((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[problem.id]: Number(e.target.value || 100),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -43,11 +43,11 @@ const ITEMS_PER_PAGE = 5; // 每页显示的题目数量
|
|||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
completed: {
|
completed: {
|
||||||
label: "已完成",
|
label: "已通过",
|
||||||
color: "#4CAF50", // 使用更鲜明的颜色
|
color: "#4CAF50", // 使用更鲜明的颜色
|
||||||
},
|
},
|
||||||
uncompleted: {
|
uncompleted: {
|
||||||
label: "未完成",
|
label: "未通过",
|
||||||
color: "#FFA726", // 使用更鲜明的颜色
|
color: "#FFA726", // 使用更鲜明的颜色
|
||||||
},
|
},
|
||||||
} satisfies ChartConfig;
|
} satisfies ChartConfig;
|
||||||
@ -114,11 +114,11 @@ export default function TeacherDashboard() {
|
|||||||
<h1 className="text-3xl font-bold mb-6">教师仪表板</h1>
|
<h1 className="text-3xl font-bold mb-6">教师仪表板</h1>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{/* 题目完成情况模块 */}
|
{/* 题目通过情况模块 */}
|
||||||
<Card className="min-h-[450px]">
|
<Card className="min-h-[450px]">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>题目完成情况</CardTitle>
|
<CardTitle>题目通过情况</CardTitle>
|
||||||
<CardDescription>各题目完成及未完成人数图表</CardDescription>
|
<CardDescription>各题目通过及未通过人数图表</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{chartData.length === 0 ? (
|
{chartData.length === 0 ? (
|
||||||
@ -158,7 +158,7 @@ export default function TeacherDashboard() {
|
|||||||
/>
|
/>
|
||||||
<Bar
|
<Bar
|
||||||
dataKey="completedPercent"
|
dataKey="completedPercent"
|
||||||
name="已完成"
|
name="已通过"
|
||||||
fill={chartConfig.completed.color}
|
fill={chartConfig.completed.color}
|
||||||
radius={[4, 4, 0, 0]}
|
radius={[4, 4, 0, 0]}
|
||||||
>
|
>
|
||||||
@ -171,7 +171,7 @@ export default function TeacherDashboard() {
|
|||||||
</Bar>
|
</Bar>
|
||||||
<Bar
|
<Bar
|
||||||
dataKey="uncompletedPercent"
|
dataKey="uncompletedPercent"
|
||||||
name="未完成"
|
name="未通过"
|
||||||
fill={chartConfig.uncompleted.color}
|
fill={chartConfig.uncompleted.color}
|
||||||
radius={[4, 4, 0, 0]}
|
radius={[4, 4, 0, 0]}
|
||||||
>
|
>
|
||||||
@ -217,10 +217,10 @@ export default function TeacherDashboard() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||||
<div className="flex gap-2 leading-none font-medium">
|
<div className="flex gap-2 leading-none font-medium">
|
||||||
完成度趋势 <TrendingUp className="h-4 w-4" />
|
通过趋势 <TrendingUp className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground leading-none">
|
<div className="text-muted-foreground leading-none">
|
||||||
显示各题目完成情况(已完成/未完成)
|
显示各题目通过情况(已通过/未通过)
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ const data = {
|
|||||||
url: "/dashboard/usermanagement/problem",
|
url: "/dashboard/usermanagement/problem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "完成情况",
|
title: "通过情况",
|
||||||
url: "/dashboard/teacher/dashboard",
|
url: "/dashboard/teacher/dashboard",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user