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:
SpecialX
2026-07-03 10:30:41 +08:00
parent 56c3f32e2d
commit 2dd8c2197c
20 changed files with 1090 additions and 1228 deletions

View File

@@ -1,6 +1,7 @@
"use client"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Cell } from "recharts"
import { useTranslations } from "next-intl"
import {
ChartContainer,
ChartTooltip,
@@ -17,6 +18,25 @@ interface SubjectDistributionChartProps {
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)",
@@ -34,6 +54,8 @@ export function SubjectDistributionChart({
data,
className,
}: SubjectDistributionChartProps) {
const t = useTranslations("error-book")
if (data.length === 0) return null
const chartData = data.map((d) => ({
@@ -45,7 +67,7 @@ export function SubjectDistributionChart({
const chartConfig: ChartConfig = {
errorCount: {
label: "错题数",
label: t("subjectDistChart.errorCount"),
color: "var(--color-chart-1)",
},
}
@@ -53,10 +75,14 @@ export function SubjectDistributionChart({
return (
<Card className={cn("overflow-hidden", className)}>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
<CardTitle className="text-base">{t("subjectDistChart.title")}</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-[280px] w-full">
<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
@@ -74,23 +100,21 @@ export function SubjectDistributionChart({
<ChartTooltipContent
className="w-[200px]"
formatter={(payload: unknown) => {
const p = payload as unknown as {
name: string
errorCount: number
masteredCount: number
masteryRate: number
}
if (!isSubjectDistChartPayload(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>
{t("subjectDistChart.errorCount")}
<span className="font-medium text-foreground">{payload.errorCount}</span>
</div>
<div className="text-muted-foreground">
<span className="font-medium text-emerald-600">{p.masteredCount}</span>
{t("subjectDistChart.masteredLabel")}
<span className="font-medium text-emerald-600">{payload.masteredCount}</span>
</div>
<div className="text-muted-foreground">
<span className="font-medium text-foreground">{p.masteryRate}%</span>
{t("subjectDistChart.masteryRateLabel")}
<span className="font-medium text-foreground">{payload.masteryRate}%</span>
</div>
</div>
)