feat(classes): optimize teacher dashboard ui and implement grade management

This commit is contained in:
SpecialX
2026-01-14 13:59:11 +08:00
parent ade8d4346c
commit 9bfc621d3f
104 changed files with 12793 additions and 2309 deletions

View File

@@ -1,103 +1,8 @@
"use client"
import type { HomeworkAssignmentQuestionAnalytics } from "@/modules/homework/types"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
function ErrorRateChart({
questions,
gradedSampleCount,
}: {
questions: HomeworkAssignmentQuestionAnalytics[]
gradedSampleCount: number
}) {
const w = 100
const h = 60
const padL = 10
const padR = 3
const padT = 4
const padB = 10
const plotW = w - padL - padR
const plotH = h - padT - padB
const n = questions.length
const clamp01 = (v: number) => Math.max(0, Math.min(1, v))
const xFor = (i: number) => padL + (n <= 1 ? 0 : (i / (n - 1)) * plotW)
const yFor = (rate: number) => padT + (1 - clamp01(rate)) * plotH
const points = questions.map((q, i) => `${xFor(i)},${yFor(q.errorRate)}`).join(" ")
const areaD =
n === 0
? ""
: `M ${padL} ${padT + plotH} L ${points.split(" ").join(" L ")} L ${padL + plotW} ${padT + plotH} Z`
const gridYs = [
{ v: 1, label: "100%" },
{ v: 0.5, label: "50%" },
{ v: 0, label: "0%" },
]
return (
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" className="h-full w-full">
{gridYs.map((g) => {
const y = yFor(g.v)
return (
<g key={g.label}>
<line x1={padL} y1={y} x2={padL + plotW} y2={y} className="stroke-border" strokeWidth={0.5} />
<text x={2} y={y + 1.2} className="fill-muted-foreground text-[3px]">
{g.label}
</text>
</g>
)
})}
<line x1={padL} y1={padT} x2={padL} y2={padT + plotH} className="stroke-border" strokeWidth={0.7} />
<line
x1={padL}
y1={padT + plotH}
x2={padL + plotW}
y2={padT + plotH}
className="stroke-border"
strokeWidth={0.7}
/>
{n >= 2 ? <path d={areaD} className="fill-primary/10" /> : null}
<polyline
points={points}
fill="none"
className="stroke-primary"
strokeWidth={1.2}
strokeLinejoin="round"
strokeLinecap="round"
/>
{questions.map((q, i) => {
const cx = xFor(i)
const cy = yFor(q.errorRate)
const label = `Q${i + 1}`
return (
<g key={q.questionId}>
<circle cx={cx} cy={cy} r={1.2} className="fill-primary" />
<title>{`${label}: ${(q.errorRate * 100).toFixed(1)}% (${q.errorCount} / ${gradedSampleCount})`}</title>
</g>
)
})}
{questions.map((q, i) => {
if (n > 12 && i % 2 === 1) return null
const x = xFor(i)
return (
<text
key={`x-${q.questionId}`}
x={x}
y={h - 2}
textAnchor="middle"
className="fill-muted-foreground text-[3px]"
>
{i + 1}
</text>
)
})}
</svg>
)
}
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
export function HomeworkAssignmentQuestionErrorOverviewCard({
questions,
@@ -106,26 +11,78 @@ export function HomeworkAssignmentQuestionErrorOverviewCard({
questions: HomeworkAssignmentQuestionAnalytics[]
gradedSampleCount: number
}) {
const data = questions.map((q, index) => ({
name: `Q${index + 1}`,
errorRate: q.errorRate * 100,
errorCount: q.errorCount,
total: gradedSampleCount,
}))
return (
<Card className="md:col-span-1">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">Question Error Overview</CardTitle>
<CardTitle className="text-sm font-medium text-muted-foreground">Error Rate Overview</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="h-72">
{questions.length === 0 || gradedSampleCount === 0 ? (
<div className="text-sm text-muted-foreground">
No graded submissions yet. Error analytics will appear here after grading.
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
No graded submissions yet.
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Graded students</span>
<span className="font-medium text-foreground">{gradedSampleCount}</span>
</div>
<div className="h-56 rounded-md border bg-muted/40 px-3 py-2">
<ErrorRateChart questions={questions} gradedSampleCount={gradedSampleCount} />
</div>
</div>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis
dataKey="name"
tickLine={false}
axisLine={false}
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
interval={0}
/>
<YAxis
tickLine={false}
axisLine={false}
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
tickFormatter={(value) => `${value}%`}
domain={[0, 100]}
/>
<Tooltip
cursor={{ fill: "hsl(var(--muted)/0.2)" }}
content={({ active, payload }) => {
if (active && payload && payload.length) {
const d = payload[0].payload
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">Question</span>
<span className="font-bold text-muted-foreground">{d.name}</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">Error Rate</span>
<span className="font-bold">{d.errorRate.toFixed(1)}%</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">Errors</span>
<span className="font-bold">
{d.errorCount} / {d.total}
</span>
</div>
</div>
</div>
)
}
return null
}}
/>
<Bar
dataKey="errorRate"
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={40}
/>
</BarChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>