feat(error-book): add analytics stats, charts, and error collection
- Add analytics-stats-cards, chapter-weakness-chart, class-error-bar-chart - Add knowledge-point-weakness-chart, subject-distribution-chart, subject-tabs - Add class-filter and grouped-student-error-table components - Add data-access-collection for error aggregation from multiple sources - Update error-book-detail-dialog, data-access, and types
This commit is contained in:
99
src/modules/error-book/components/subject-tabs.tsx
Normal file
99
src/modules/error-book/components/subject-tabs.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
|
||||
import type { SubjectErrorOverview } from "@/modules/error-book/types"
|
||||
|
||||
interface SubjectTabsProps {
|
||||
subjects: SubjectErrorOverview[]
|
||||
/** 当前选中的学科 ID(null 表示全部) */
|
||||
currentSubjectId: string | null
|
||||
/** URL 参数名(默认 "subject") */
|
||||
paramName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 学科切换 Tab(教师/管理员视图)
|
||||
* 显示每个学科的错题数概览,点击切换
|
||||
*/
|
||||
export function SubjectTabs({
|
||||
subjects,
|
||||
currentSubjectId,
|
||||
paramName = "subject",
|
||||
}: SubjectTabsProps) {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const handleSelect = (subjectId: string | null) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
if (subjectId === null) {
|
||||
params.delete(paramName)
|
||||
} else {
|
||||
params.set(paramName, subjectId)
|
||||
}
|
||||
router.push(`?${params.toString()}`, { scroll: false })
|
||||
}
|
||||
|
||||
if (subjects.length === 0) return null
|
||||
|
||||
const totalErrors = subjects.reduce((sum, s) => sum + s.totalErrorItems, 0)
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect(null)}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm transition-colors",
|
||||
currentSubjectId === null
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "bg-card hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<span className="font-medium">全部学科</span>
|
||||
<Badge
|
||||
variant={currentSubjectId === null ? "secondary" : "outline"}
|
||||
className="text-xs"
|
||||
>
|
||||
{totalErrors}
|
||||
</Badge>
|
||||
</button>
|
||||
{subjects.map((subject) => {
|
||||
const isActive = currentSubjectId === subject.subjectId
|
||||
return (
|
||||
<button
|
||||
key={subject.subjectId}
|
||||
type="button"
|
||||
onClick={() => handleSelect(subject.subjectId)}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm transition-colors",
|
||||
isActive
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "bg-card hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<span className="font-medium">{subject.subjectName}</span>
|
||||
<Badge
|
||||
variant={isActive ? "secondary" : "outline"}
|
||||
className="text-xs"
|
||||
>
|
||||
{subject.totalErrorItems}
|
||||
</Badge>
|
||||
{subject.dueReviewCount > 0 ? (
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs",
|
||||
isActive ? "text-primary-foreground/80" : "text-rose-600"
|
||||
)}
|
||||
>
|
||||
待复习 {subject.dueReviewCount}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user