- 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)
106 lines
3.2 KiB
TypeScript
106 lines
3.2 KiB
TypeScript
"use client"
|
||
|
||
import { useRouter, useSearchParams } from "next/navigation"
|
||
import { useTranslations } from "next-intl"
|
||
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 t = useTranslations("error-book")
|
||
|
||
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"
|
||
role="tab"
|
||
aria-selected={currentSubjectId === null}
|
||
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">{t("subjectTabs.all")}</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"
|
||
role="tab"
|
||
aria-selected={isActive}
|
||
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"
|
||
)}
|
||
>
|
||
{t("subjectTabs.dueReview", { count: subject.dueReviewCount })}
|
||
</span>
|
||
) : null}
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
)
|
||
}
|