- 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)
136 lines
4.4 KiB
TypeScript
136 lines
4.4 KiB
TypeScript
"use client"
|
||
|
||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts"
|
||
import { useTranslations } from "next-intl"
|
||
import {
|
||
ChartContainer,
|
||
ChartTooltip,
|
||
ChartTooltipContent,
|
||
type ChartConfig,
|
||
} from "@/shared/components/ui/chart"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||
import { cn } from "@/shared/lib/utils"
|
||
|
||
import type { SubjectErrorDistribution } from "@/modules/error-book/types"
|
||
|
||
interface SubjectDistributionChartProps {
|
||
data: SubjectErrorDistribution[]
|
||
className?: string
|
||
}
|
||
|
||
interface SubjectDistChartPayload {
|
||
name: string
|
||
errorCount: number
|
||
masteredCount: number
|
||
masteryRate: number
|
||
}
|
||
|
||
function isSubjectDistChartPayload(v: unknown): v is SubjectDistChartPayload {
|
||
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"
|
||
)
|
||
}
|
||
|
||
const SUBJECT_COLORS = [
|
||
"var(--color-chart-1)",
|
||
"var(--color-chart-2)",
|
||
"var(--color-chart-3)",
|
||
"var(--color-chart-4)",
|
||
"var(--color-chart-5)",
|
||
]
|
||
|
||
/**
|
||
* 学科错题分布柱状图(管理员视图)
|
||
* 横轴:学科名称,纵轴:错题数
|
||
* tooltip 显示已掌握/掌握率
|
||
*/
|
||
export function SubjectDistributionChart({
|
||
data,
|
||
className,
|
||
}: SubjectDistributionChartProps) {
|
||
const t = useTranslations("error-book")
|
||
|
||
if (data.length === 0) return null
|
||
|
||
const chartData = data.map((d) => ({
|
||
name: d.subjectName,
|
||
errorCount: d.errorCount,
|
||
masteredCount: d.masteredCount,
|
||
masteryRate: Number((d.masteryRate * 100).toFixed(0)),
|
||
}))
|
||
|
||
const chartConfig: ChartConfig = {
|
||
errorCount: {
|
||
label: t("subjectDistChart.errorCount"),
|
||
color: "var(--color-chart-1)",
|
||
},
|
||
}
|
||
|
||
return (
|
||
<Card className={cn("overflow-hidden", className)}>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">{t("subjectDistChart.title")}</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<ChartContainer
|
||
config={chartConfig}
|
||
className="h-[280px] w-full"
|
||
aria-label={t("subjectDistChart.title")}
|
||
>
|
||
<BarChart data={chartData} margin={{ left: 8, right: 8, top: 8, bottom: 8 }}>
|
||
<CartesianGrid vertical={false} strokeDasharray="4 4" strokeOpacity={0.4} />
|
||
<XAxis
|
||
dataKey="name"
|
||
tickLine={false}
|
||
axisLine={false}
|
||
tickMargin={8}
|
||
tickFormatter={(value: string) =>
|
||
value.length > 6 ? `${value.slice(0, 6)}...` : value
|
||
}
|
||
/>
|
||
<YAxis tickLine={false} axisLine={false} width={36} />
|
||
<ChartTooltip
|
||
content={
|
||
<ChartTooltipContent
|
||
className="w-[200px]"
|
||
formatter={(payload: unknown) => {
|
||
if (!isSubjectDistChartPayload(payload)) return null
|
||
return (
|
||
<div className="space-y-1.5">
|
||
<div className="font-medium">{payload.name}</div>
|
||
<div className="text-muted-foreground">
|
||
{t("subjectDistChart.errorCount")}:
|
||
<span className="font-medium text-foreground">{payload.errorCount}</span>
|
||
</div>
|
||
<div className="text-muted-foreground">
|
||
{t("subjectDistChart.masteredLabel")}:
|
||
<span className="font-medium text-emerald-600">{payload.masteredCount}</span>
|
||
</div>
|
||
<div className="text-muted-foreground">
|
||
{t("subjectDistChart.masteryRateLabel")}:
|
||
<span className="font-medium text-foreground">{payload.masteryRate}%</span>
|
||
</div>
|
||
</div>
|
||
)
|
||
}}
|
||
/>
|
||
}
|
||
/>
|
||
<Bar dataKey="errorCount" radius={[4, 4, 0, 0]}>
|
||
{chartData.map((_, idx) => (
|
||
<Cell key={idx} fill={SUBJECT_COLORS[idx % SUBJECT_COLORS.length]} />
|
||
))}
|
||
</Bar>
|
||
</BarChart>
|
||
</ChartContainer>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
}
|