Files
NextEdu/src/modules/error-book/components/subject-tabs.tsx
SpecialX 2dd8c2197c 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)
2026-07-03 10:30:41 +08:00

106 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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[]
/** 当前选中的学科 IDnull 表示全部) */
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>
)
}