feat(error-book): add analytics stats, charts, and error collection

- Add analytics-stats-cards, chapter-weakness-chart, class-error-bar-chart

- Add knowledge-point-weakness-chart, subject-distribution-chart, subject-tabs

- Add class-filter and grouped-student-error-table components

- Add data-access-collection for error aggregation from multiple sources

- Update error-book-detail-dialog, data-access, and types
This commit is contained in:
SpecialX
2026-06-24 12:02:16 +08:00
parent d7876c5854
commit 61e76f0d67
12 changed files with 1681 additions and 287 deletions

View File

@@ -0,0 +1,155 @@
"use client"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts"
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 { ChapterWeakness } from "@/modules/error-book/types"
interface ChapterWeaknessChartProps {
data: ChapterWeakness[]
className?: string
}
/**
* 章节薄弱度图表(哪些课在错)
* 横向柱状图,按错题数降序
* 每个柱子可展开显示该章节下错得最多的知识点
*/
export function ChapterWeaknessChart({ data, className }: ChapterWeaknessChartProps) {
if (data.length === 0) return null
const chartData = data.map((d) => ({
name: d.chapterTitle,
errorCount: d.errorCount,
masteredCount: d.masteredCount,
masteryRate: Number((d.masteryRate * 100).toFixed(0)),
knowledgePointCount: d.knowledgePointCount,
topKps: d.topKnowledgePoints,
}))
const chartConfig: ChartConfig = {
errorCount: {
label: "错题数",
color: "var(--color-chart-2)",
},
}
return (
<Card className={cn("overflow-hidden", className)}>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<ChartContainer config={chartConfig} className="h-[300px] w-full">
<BarChart
data={chartData}
layout="vertical"
margin={{ left: 8, right: 16, top: 8, bottom: 8 }}
>
<CartesianGrid horizontal={false} strokeDasharray="4 4" strokeOpacity={0.4} />
<XAxis type="number" tickLine={false} axisLine={false} />
<YAxis
type="number"
dataKey="name"
tickLine={false}
axisLine={false}
width={120}
tickFormatter={(value: string) =>
value.length > 10 ? `${value.slice(0, 10)}...` : value
}
/>
<ChartTooltip
content={
<ChartTooltipContent
className="w-[280px]"
formatter={(payload: unknown) => {
const p = payload as unknown as {
name: string
errorCount: number
masteredCount: number
masteryRate: number
knowledgePointCount: number
topKps: Array<{ knowledgePointName: string; errorCount: number }>
}
return (
<div className="space-y-1.5">
<div className="font-medium">{p.name}</div>
<div className="text-muted-foreground">
<span className="font-medium text-foreground">{p.errorCount}</span>
<span className="ml-2"><span className="font-medium text-emerald-600">{p.masteredCount}</span></span>
</div>
<div className="text-muted-foreground">
<span className="font-medium text-foreground">{p.masteryRate}%</span>
<span className="ml-2"><span className="font-medium text-foreground">{p.knowledgePointCount}</span></span>
</div>
{p.topKps && p.topKps.length > 0 ? (
<div className="border-t pt-1.5 mt-1.5">
<div className="text-xs text-muted-foreground mb-1"></div>
{p.topKps.map((kp) => (
<div key={kp.knowledgePointName} className="flex justify-between text-xs">
<span>{kp.knowledgePointName}</span>
<span className="font-medium text-rose-600">{kp.errorCount}</span>
</div>
))}
</div>
) : null}
</div>
)
}}
/>
}
/>
<Bar dataKey="errorCount" radius={[0, 4, 4, 0]}>
{chartData.map((entry, idx) => (
<Cell
key={idx}
fill={entry.masteryRate < 30 ? "var(--color-rose-500)" : entry.masteryRate < 60 ? "var(--color-amber-500)" : "var(--color-emerald-500)"}
/>
))}
</Bar>
</BarChart>
</ChartContainer>
{/* 章节详情列表 */}
<div className="space-y-2">
{data.slice(0, 5).map((chapter) => (
<div
key={chapter.chapterId}
className="flex items-start justify-between gap-3 rounded-lg border p-3"
>
<div className="min-w-0 flex-1 space-y-1">
<div className="flex items-center gap-2">
<span className="truncate font-medium text-sm">{chapter.chapterTitle}</span>
<Badge variant="outline" className="shrink-0 text-xs">
{chapter.knowledgePointCount}
</Badge>
</div>
{chapter.topKnowledgePoints.length > 0 ? (
<div className="flex flex-wrap gap-1">
{chapter.topKnowledgePoints.map((kp) => (
<Badge key={kp.knowledgePointId} variant="secondary" className="text-xs">
{kp.knowledgePointName} · {kp.errorCount}
</Badge>
))}
</div>
) : null}
</div>
<div className="flex shrink-0 flex-col items-end gap-0.5 text-xs">
<span className="font-bold text-rose-600">{chapter.errorCount}</span>
<span className="text-muted-foreground"> {Math.round(chapter.masteryRate * 100)}%</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)
}