Files
NextEdu/src/modules/error-book/components/analytics-stats-cards.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

110 lines
3.5 KiB
TypeScript

"use client"
import { BookOpen, Brain, CheckCircle2, Clock, TrendingUp } from "lucide-react"
import { useTranslations } from "next-intl"
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 t = useTranslations("error-book")
const avg = (totalStudents > 0 ? totalErrorItems / totalStudents : 0).toFixed(1)
const cards = [
{
label: t("analyticsStats.coverage"),
value: studentsWithErrorBook,
sub: t("analyticsStats.coverageSub", { total: totalStudents }),
icon: BookOpen,
color: "text-blue-600 dark:text-blue-400",
bg: "bg-blue-50 dark:bg-blue-950/30",
},
{
label: t("analyticsStats.totalErrors"),
value: totalErrorItems,
sub: t("analyticsStats.totalErrorsSub", { avg }),
icon: TrendingUp,
color: "text-rose-600 dark:text-rose-400",
bg: "bg-rose-50 dark:bg-rose-950/30",
},
{
label: t("analyticsStats.avgMastery"),
value: `${Math.round(averageMasteryRate * 100)}%`,
sub: averageMasteryRate >= 0.6
? t("analyticsStats.avgMasteryGood")
: t("analyticsStats.avgMasteryNeedImprove"),
icon: CheckCircle2,
color: "text-emerald-600 dark:text-emerald-400",
bg: "bg-emerald-50 dark:bg-emerald-950/30",
},
{
label: t("analyticsStats.dueReview"),
value: dueReviewCount,
sub: dueReviewCount > 0
? t("analyticsStats.dueReviewNeedAttention")
: t("analyticsStats.dueReviewNone"),
icon: Clock,
color: "text-amber-600 dark:text-amber-400",
bg: "bg-amber-50 dark:bg-amber-950/30",
},
{
label: t("analyticsStats.knowledgePoints"),
value: knowledgePointCount ?? 0,
sub: knowledgePointCount && knowledgePointCount > 5
? t("analyticsStats.knowledgePointsWide")
: t("analyticsStats.knowledgePointsFocused"),
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>
)
}