- 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)
110 lines
3.5 KiB
TypeScript
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>
|
|
)
|
|
}
|