Files
NextEdu/src/modules/error-book/components/subject-distribution-chart.tsx
SpecialX eee0145274 feat(error-book): update components and analytics data-access
- Update all error-book components (filters, cards, charts, dialogs)

- Update data-access-analytics.ts

- Update student-error-book-list-client.tsx
2026-07-04 10:22:36 +08:00

136 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts"
import { useTranslations } from "next-intl"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/shared/components/ui/chart"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { cn } from "@/shared/lib/utils"
import type { SubjectErrorDistribution } from "@/modules/error-book/types"
interface SubjectDistributionChartProps {
data: SubjectErrorDistribution[]
className?: string
}
interface SubjectDistChartPayload {
name: string
errorCount: number
masteredCount: number
masteryRate: number
}
function isSubjectDistChartPayload(v: unknown): v is SubjectDistChartPayload {
if (typeof v !== "object" || v === null) return false
// 从 unknown 转换:类型守卫内需要属性访问来校验字段类型
const obj = v as Record<string, unknown>
return (
typeof obj.name === "string" &&
typeof obj.errorCount === "number" &&
typeof obj.masteredCount === "number" &&
typeof obj.masteryRate === "number"
)
}
const SUBJECT_COLORS = [
"var(--color-chart-1)",
"var(--color-chart-2)",
"var(--color-chart-3)",
"var(--color-chart-4)",
"var(--color-chart-5)",
]
/**
* 学科错题分布柱状图(管理员视图)
* 横轴:学科名称,纵轴:错题数
* tooltip 显示已掌握/掌握率
*/
export function SubjectDistributionChart({
data,
className,
}: SubjectDistributionChartProps) {
const t = useTranslations("errorBook")
if (data.length === 0) return null
const chartData = data.map((d) => ({
name: d.subjectName,
errorCount: d.errorCount,
masteredCount: d.masteredCount,
masteryRate: Number((d.masteryRate * 100).toFixed(0)),
}))
const chartConfig: ChartConfig = {
errorCount: {
label: t("subjectDistChart.errorCount"),
color: "var(--color-chart-1)",
},
}
return (
<Card className={cn("overflow-hidden", className)}>
<CardHeader>
<CardTitle className="text-base">{t("subjectDistChart.title")}</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer
config={chartConfig}
className="h-[280px] w-full"
aria-label={t("subjectDistChart.title")}
>
<BarChart data={chartData} margin={{ left: 8, right: 8, top: 8, bottom: 8 }}>
<CartesianGrid vertical={false} strokeDasharray="4 4" strokeOpacity={0.4} />
<XAxis
dataKey="name"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value: string) =>
value.length > 6 ? `${value.slice(0, 6)}...` : value
}
/>
<YAxis tickLine={false} axisLine={false} width={36} />
<ChartTooltip
content={
<ChartTooltipContent
className="w-[200px]"
formatter={(payload: unknown) => {
if (!isSubjectDistChartPayload(payload)) return null
return (
<div className="space-y-1.5">
<div className="font-medium">{payload.name}</div>
<div className="text-muted-foreground">
{t("subjectDistChart.errorCount")}
<span className="font-medium text-foreground">{payload.errorCount}</span>
</div>
<div className="text-muted-foreground">
{t("subjectDistChart.masteredLabel")}
<span className="font-medium text-emerald-600">{payload.masteredCount}</span>
</div>
<div className="text-muted-foreground">
{t("subjectDistChart.masteryRateLabel")}
<span className="font-medium text-foreground">{payload.masteryRate}%</span>
</div>
</div>
)
}}
/>
}
/>
<Bar dataKey="errorCount" radius={[4, 4, 0, 0]}>
{chartData.map((_, idx) => (
<Cell key={idx} fill={SUBJECT_COLORS[idx % SUBJECT_COLORS.length]} />
))}
</Bar>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
)
}