Files
NextEdu/src/modules/dashboard/components/student-dashboard/student-grades-card.tsx
SpecialX e997abaf5e refactor(dashboard): V2 审计重构 — i18n 补齐 + 共享抽象 + 单测 + a11y
V2 审计报告(docs/architecture/audit/dashboard-audit-report-v2.md)发现并修复:

- P0 i18n:10 个子组件硬编码字符串全部接入 next-intl(teacher-quick-actions /
  teacher-classes-card / teacher-homework-card / teacher-schedule /
  recent-submissions / teacher-grade-trends / student-grades-card /
  student-today-schedule-card / student-upcoming-assignments-card /
  admin-dashboard),新增 ~50 个翻译键
- P1 共享抽象:新增 DashboardGreetingHeader 组件,消除 teacher/student
  头部 90% 重复代码,两个 Header 改为薄包装
- P2 单测:为 6 个纯函数添加 31 个单元测试
  (tests/integration/dashboard/dashboard-utils.test.ts)
- P2 a11y:admin 表格 caption、teacher/student 视图语义化标签
  (header / section aria-label / aside aria-label)
- 同步架构图 004/005
2026-06-22 17:01:00 +08:00

116 lines
4.3 KiB
TypeScript

"use client"
import Link from "next/link"
import { BarChart3 } from "lucide-react"
import { useTranslations } from "next-intl"
import { Button } from "@/shared/components/ui/button"
import { ChartCardShell } from "@/shared/components/charts/chart-card-shell"
import { TrendLineChart } from "@/shared/components/charts/trend-line-chart"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table"
import { formatDate } from "@/shared/lib/utils"
import type { StudentDashboardGradeProps } from "@/modules/homework/types"
export function StudentGradesCard({ grades }: { grades: StudentDashboardGradeProps }) {
const t = useTranslations("dashboard")
const hasGradeTrend = grades.trend.length > 0
const hasRecentGrades = grades.recent.length > 0
const chartData = grades.trend.map((item) => ({
title: item.assignmentTitle,
score: Math.round(item.percentage),
fullTitle: item.assignmentTitle,
submittedAt: formatDate(item.submittedAt),
rawScore: item.score,
maxScore: item.maxScore,
}))
const latestGrade = grades.trend[grades.trend.length - 1]
return (
<ChartCardShell
title={t("sections.recentGrades")}
icon={BarChart3}
iconClassName="text-muted-foreground"
isEmpty={!hasGradeTrend}
emptyTitle={t("empty.noGradedWork")}
emptyDescription={t("empty.noGradedWorkDesc")}
emptyClassName="h-72"
action={
hasGradeTrend ? (
<Button asChild variant="outline" size="sm">
<Link href="/student/grades">{t("quickActions.viewAll")}</Link>
</Button>
) : null
}
>
<div className="space-y-4">
<div className="rounded-md border bg-card p-4">
<TrendLineChart
data={chartData}
series={[
{
dataKey: "score",
name: t("chart.scorePercent"),
color: "hsl(var(--primary))",
dotRadius: 4,
activeDotRadius: 6,
},
]}
heightClassName="h-[200px]"
margin={{ left: 12, right: 12, top: 12, bottom: 12 }}
yWidth={30}
tooltipClassName="w-[200px]"
/>
{latestGrade ? (
<div className="mt-3 flex items-center justify-between text-sm text-muted-foreground">
<div>
{t("chart.latest")}:{" "}
<span className="font-medium text-foreground tabular-nums">
{Math.round(latestGrade.percentage)}%
</span>
</div>
<div>
{t("chart.points")}:{" "}
<span className="font-medium text-foreground tabular-nums">
{latestGrade.score}/{latestGrade.maxScore}
</span>
</div>
</div>
) : null}
</div>
{!hasRecentGrades ? null : (
<div className="rounded-md border bg-card">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="text-xs font-medium uppercase text-muted-foreground">{t("table.assignment")}</TableHead>
<TableHead className="text-xs font-medium uppercase text-muted-foreground">{t("table.score")}</TableHead>
<TableHead className="text-xs font-medium uppercase text-muted-foreground">{t("table.when")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{grades.recent.map((r) => (
<TableRow key={r.assignmentId} className="h-12">
<TableCell className="font-medium">
<Link href={`/student/learning/assignments/${r.assignmentId}`} className="hover:underline">
{r.assignmentTitle}
</Link>
</TableCell>
<TableCell className="tabular-nums">
{r.score}/{r.maxScore} <span className="text-muted-foreground">({Math.round(r.percentage)}%)</span>
</TableCell>
<TableCell className="text-muted-foreground">{formatDate(r.submittedAt)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
</ChartCardShell>
)
}