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