refactor(dashboard): track assignment progress by testcases

This commit is contained in:
2026-05-29 14:04:30 +08:00
parent d87265eb2d
commit f0ea48ef06
16 changed files with 180 additions and 147 deletions
@@ -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";
-1
View File
@@ -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)
-1
View File
@@ -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, {
+6 -14
View File
@@ -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>
+1 -1
View File
@@ -35,7 +35,7 @@ const data = {
url: "/dashboard/usermanagement/problem",
},
{
title: "完成情况",
title: "通过情况",
url: "/dashboard/teacher/dashboard",
},
{