Files
NextEdu/src/modules/error-book/components/subject-distribution-chart.tsx
SpecialX 2dd8c2197c feat(error-book): update components, actions, data-access, and add analytics
- Add data-access-analytics for error book analytics queries

- Update actions, data-access for improved error book operations

- Update components: add-error-book-dialog, analytics-stats-cards, chapter-weakness-chart, class-error-bar-chart, class-filter, error-book-detail-dialog, error-book-filters, error-book-item-card, error-book-list, error-book-stats-cards, grouped-student-error-table, knowledge-point-weakness-chart, review-buttons, subject-distribution-chart, subject-tabs, top-wrong-questions

- Remove class-error-overview (replaced by new components)
2026-07-03 10:30:41 +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("error-book")
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>
)
}