"use client" import { useCallback } from "react" 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 { ClassErrorOverview } from "@/modules/error-book/types" /** recharts 内联对象常量:避免每次渲染创建新对象触发 re-render */ const CHART_MARGIN = { left: 8, right: 8, top: 8, bottom: 8 } const GRID_PROPS = { vertical: false, strokeDasharray: "4 4", strokeOpacity: 0.4 } const X_AXIS_BASE_PROPS = { dataKey: "name", tickLine: false, axisLine: false, tickMargin: 8, } const Y_AXIS_PROPS = { tickLine: false, axisLine: false, width: 36 } const BAR_RADIUS: [number, number, number, number] = [4, 4, 0, 0] function truncateTick(value: string): string { return value.length > 8 ? `${value.slice(0, 8)}...` : value } interface ClassErrorBarChartProps { data: ClassErrorOverview[] className?: string } interface ClassBarChartPayload { name: string totalErrorItems: number studentCount: number averageErrorPerStudent: number averageMasteryRate: number dueReviewCount: number } function isClassBarChartPayload(v: unknown): v is ClassBarChartPayload { if (typeof v !== "object" || v === null) return false // 从 unknown 转换:类型守卫内需要属性访问来校验字段类型 const obj = v as Record return ( typeof obj.name === "string" && typeof obj.totalErrorItems === "number" && typeof obj.studentCount === "number" && typeof obj.averageErrorPerStudent === "number" && typeof obj.averageMasteryRate === "number" && typeof obj.dueReviewCount === "number" ) } const CHART_COLORS = [ "var(--color-chart-1)", "var(--color-chart-2)", "var(--color-chart-3)", "var(--color-chart-4)", "var(--color-chart-5)", ] /** * 班级错题数对比柱状图(教师视图) * 横轴:班级名称,纵轴:错题总数 * 颜色按班级区分,tooltip 显示学生数/人均/掌握率 */ export function ClassErrorBarChart({ data, className }: ClassErrorBarChartProps) { const t = useTranslations("errorBook") const renderTooltip = useCallback( (payload: unknown) => { if (!isClassBarChartPayload(payload)) return null return (
{payload.name}
{t("classErrorBar.errorCount")}: {payload.totalErrorItems}
{t("classErrorBar.studentCount")}: {payload.studentCount}
{t("classErrorBar.avgPerStudent")}: {payload.averageErrorPerStudent}
{t("classErrorBar.avgMastery")}: {payload.averageMasteryRate}%
{t("classErrorBar.dueReview")}: {payload.dueReviewCount}
) }, [t] ) if (data.length === 0) return null const chartData = data.map((d) => ({ name: d.className, totalErrorItems: d.totalErrorItems, studentCount: d.studentCount, averageErrorPerStudent: Number(d.averageErrorPerStudent.toFixed(1)), averageMasteryRate: Number((d.averageMasteryRate * 100).toFixed(0)), dueReviewCount: d.dueReviewCount, })) const chartConfig: ChartConfig = { totalErrorItems: { label: t("classErrorBar.errorCount"), color: "var(--color-chart-1)", }, } return ( {t("classErrorBar.title")} {/* arbitrary-value: chart canvas fixed size */} } /> {chartData.map((_, idx) => ( ))} ) }