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,97 @@
import { BookOpen, Brain, CheckCircle2, Clock, TrendingUp } from "lucide-react"
import { Card, CardContent } from "@/shared/components/ui/card"
import { cn } from "@/shared/lib/utils"
interface AnalyticsStatsCardsProps {
totalStudents: number
studentsWithErrorBook: number
totalErrorItems: number
averageMasteryRate: number
dueReviewCount: number
/** 涉及的知识点数 */
knowledgePointCount?: number
className?: string
}
/**
* 错题分析统计卡片(教师/管理员视图)
* 5 个卡片:覆盖学生/错题总数/平均掌握率/待复习/知识点数
*/
export function AnalyticsStatsCards({
totalStudents,
studentsWithErrorBook,
totalErrorItems,
averageMasteryRate,
dueReviewCount,
knowledgePointCount,
className,
}: AnalyticsStatsCardsProps) {
const cards = [
{
label: "覆盖学生",
value: studentsWithErrorBook,
sub: `/ ${totalStudents}`,
icon: BookOpen,
color: "text-blue-600 dark:text-blue-400",
bg: "bg-blue-50 dark:bg-blue-950/30",
},
{
label: "错题总数",
value: totalErrorItems,
sub: `人均 ${(totalStudents > 0 ? totalErrorItems / totalStudents : 0).toFixed(1)}`,
icon: TrendingUp,
color: "text-rose-600 dark:text-rose-400",
bg: "bg-rose-50 dark:bg-rose-950/30",
},
{
label: "平均掌握率",
value: `${Math.round(averageMasteryRate * 100)}%`,
sub: averageMasteryRate >= 0.6 ? "整体良好" : "需加强",
icon: CheckCircle2,
color: "text-emerald-600 dark:text-emerald-400",
bg: "bg-emerald-50 dark:bg-emerald-950/30",
},
{
label: "待复习",
value: dueReviewCount,
sub: dueReviewCount > 0 ? "需要关注" : "无到期",
icon: Clock,
color: "text-amber-600 dark:text-amber-400",
bg: "bg-amber-50 dark:bg-amber-950/30",
},
{
label: "涉及知识点",
value: knowledgePointCount ?? 0,
sub: knowledgePointCount && knowledgePointCount > 5 ? "范围较广" : "集中",
icon: Brain,
color: "text-purple-600 dark:text-purple-400",
bg: "bg-purple-50 dark:bg-purple-950/30",
},
]
return (
<div className={cn("grid gap-3 sm:grid-cols-2 lg:grid-cols-5", className)}>
{cards.map((card) => {
const Icon = card.icon
return (
<Card key={card.label} className="overflow-hidden">
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<div className="text-xs text-muted-foreground">{card.label}</div>
<div className={cn("mt-1 text-2xl font-bold", card.color)}>
{card.value}
</div>
<div className="mt-0.5 text-xs text-muted-foreground">{card.sub}</div>
</div>
<div className={cn("rounded-lg p-2", card.bg)}>
<Icon className={cn("h-4 w-4", card.color)} />
</div>
</div>
</CardContent>
</Card>
)
})}
</div>
)
}