refactor(dashboard): track assignment progress by testcases

This commit is contained in:
cfngc4594 2026-05-29 14:04:30 +08:00
parent d87265eb2d
commit f0ea48ef06
16 changed files with 180 additions and 147 deletions

View File

@ -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";

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)

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,

View File

@ -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,
};
}

View File

@ -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("=== 数据获取完成 ===");

View File

@ -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: {

View File

@ -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, {

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>

View File

@ -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"}>

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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>
))}

View File

@ -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>

View File

@ -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>

View File

@ -35,7 +35,7 @@ const data = {
url: "/dashboard/usermanagement/problem",
},
{
title: "完成情况",
title: "通过情况",
url: "/dashboard/teacher/dashboard",
},
{