"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 { Badge } from "@/shared/components/ui/badge" import { cn } from "@/shared/lib/utils" import type { KnowledgePointWeakness } from "@/modules/error-book/types" /** recharts 内联对象常量:避免每次渲染创建新对象触发 re-render */ const CHART_MARGIN = { left: 8, right: 16, top: 8, bottom: 8 } const GRID_PROPS = { horizontal: false, strokeDasharray: "4 4", strokeOpacity: 0.4 } const X_AXIS_PROPS = { type: "number" as const, tickLine: false, axisLine: false } const Y_AXIS_BASE_PROPS = { type: "number" as const, dataKey: "name", tickLine: false, axisLine: false, width: 100, } const BAR_RADIUS: [number, number, number, number] = [0, 4, 4, 0] function truncateTick(value: string): string { return value.length > 8 ? `${value.slice(0, 8)}...` : value } interface KnowledgePointWeaknessChartProps { data: KnowledgePointWeakness[] className?: string } interface KpChartPayload { name: string errorCount: number masteredCount: number masteryRate: number chapterTitle: string } function isKpChartPayload(v: unknown): v is KpChartPayload { if (typeof v !== "object" || v === null) return false // 从 unknown 转换:类型守卫内需要属性访问来校验字段类型 const obj = v as Record return ( typeof obj.name === "string" && typeof obj.errorCount === "number" && typeof obj.masteredCount === "number" && typeof obj.masteryRate === "number" && typeof obj.chapterTitle === "string" ) } /** * 知识点薄弱度图表 * 横向柱状图,按错题数降序 * 颜色按掌握率区分(红/黄/绿) * 显示所属章节 */ export function KnowledgePointWeaknessChart({ data, className, }: KnowledgePointWeaknessChartProps) { const t = useTranslations("errorBook") const renderTooltip = useCallback( (payload: unknown) => { if (!isKpChartPayload(payload)) return null return (
{payload.name}
{t("weaknessChart.chapterLabel", { title: payload.chapterTitle })}
{t("weaknessChart.errorCount")}: {payload.errorCount} {t("weaknessChart.masteredLabel")}: {payload.masteredCount}
{t("weaknessChart.masteryRateLabel")}: {payload.masteryRate}%
) }, [t] ) if (data.length === 0) return null const chartData = data.map((d) => ({ name: d.knowledgePointName, errorCount: d.errorCount, masteredCount: d.masteredCount, masteryRate: Number((d.masteryRate * 100).toFixed(0)), chapterTitle: d.chapterTitle ?? t("weaknessChart.unclassified"), })) const chartConfig: ChartConfig = { errorCount: { label: t("weaknessChart.errorCount"), color: "var(--color-chart-1)", }, } return ( {t("weaknessChart.title", { count: data.length })} } /> {chartData.map((entry, idx) => ( ))} {/* 知识点详情列表(带章节归属) */}
{data.slice(0, 8).map((kp) => (
{kp.knowledgePointName}
{kp.chapterTitle ? (
{kp.chapterTitle}
) : null}
{Math.round(kp.masteryRate * 100)}% {kp.errorCount}
))}
) }