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)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts"
|
||||
import { useTranslations } from "next-intl"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
@@ -18,12 +19,36 @@ interface ChapterWeaknessChartProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
interface ChapterChartPayload {
|
||||
name: string
|
||||
errorCount: number
|
||||
masteredCount: number
|
||||
masteryRate: number
|
||||
knowledgePointCount: number
|
||||
topKps: Array<{ knowledgePointName: string; errorCount: number }>
|
||||
}
|
||||
|
||||
function isChapterChartPayload(v: unknown): v is ChapterChartPayload {
|
||||
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" &&
|
||||
typeof obj.knowledgePointCount === "number"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 章节薄弱度图表(哪些课在错)
|
||||
* 横向柱状图,按错题数降序
|
||||
* 每个柱子可展开显示该章节下错得最多的知识点
|
||||
*/
|
||||
export function ChapterWeaknessChart({ data, className }: ChapterWeaknessChartProps) {
|
||||
const t = useTranslations("error-book")
|
||||
|
||||
if (data.length === 0) return null
|
||||
|
||||
const chartData = data.map((d) => ({
|
||||
@@ -37,7 +62,7 @@ export function ChapterWeaknessChart({ data, className }: ChapterWeaknessChartPr
|
||||
|
||||
const chartConfig: ChartConfig = {
|
||||
errorCount: {
|
||||
label: "错题数",
|
||||
label: t("chapterChart.errorCount"),
|
||||
color: "var(--color-chart-2)",
|
||||
},
|
||||
}
|
||||
@@ -45,10 +70,14 @@ export function ChapterWeaknessChart({ data, className }: ChapterWeaknessChartPr
|
||||
return (
|
||||
<Card className={cn("overflow-hidden", className)}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">章节错题分布(哪些课在错)</CardTitle>
|
||||
<CardTitle className="text-base">{t("chapterChart.title")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<ChartContainer config={chartConfig} className="h-[300px] w-full">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[300px] w-full"
|
||||
aria-label={t("chapterChart.title")}
|
||||
>
|
||||
<BarChart
|
||||
data={chartData}
|
||||
layout="vertical"
|
||||
@@ -71,29 +100,32 @@ export function ChapterWeaknessChart({ data, className }: ChapterWeaknessChartPr
|
||||
<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 }>
|
||||
}
|
||||
if (!isChapterChartPayload(payload)) return null
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="font-medium">{p.name}</div>
|
||||
<div className="font-medium">{payload.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>
|
||||
{t("chapterChart.errorCount")}:
|
||||
<span className="font-medium text-foreground">{payload.errorCount}</span>
|
||||
<span className="ml-2">
|
||||
{t("chapterChart.masteredLabel")}:
|
||||
<span className="font-medium text-emerald-600">{payload.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>
|
||||
{t("chapterChart.masteryRateLabel")}:
|
||||
<span className="font-medium text-foreground">{payload.masteryRate}%</span>
|
||||
<span className="ml-2">
|
||||
{t("chapterChart.knowledgePointCount")}:
|
||||
<span className="font-medium text-foreground">{payload.knowledgePointCount}</span>
|
||||
</span>
|
||||
</div>
|
||||
{p.topKps && p.topKps.length > 0 ? (
|
||||
{payload.topKps && payload.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 className="text-xs text-muted-foreground mb-1">
|
||||
{t("chapterChart.weakKpsLabel")}
|
||||
</div>
|
||||
{payload.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>
|
||||
@@ -129,7 +161,7 @@ export function ChapterWeaknessChart({ data, className }: ChapterWeaknessChartPr
|
||||
<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} 个知识点
|
||||
{t("chapterChart.knowledgePointBadge", { count: chapter.knowledgePointCount })}
|
||||
</Badge>
|
||||
</div>
|
||||
{chapter.topKnowledgePoints.length > 0 ? (
|
||||
@@ -144,7 +176,9 @@ export function ChapterWeaknessChart({ data, className }: ChapterWeaknessChartPr
|
||||
</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>
|
||||
<span className="text-muted-foreground">
|
||||
{t("chapterChart.masteryRateLabel")} {Math.round(chapter.masteryRate * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user