import type { JSX } from "react" import Link from "next/link" import { PlusCircle, BarChart3, ClipboardList } from "lucide-react" import { getTranslations } from "next-intl/server" import { Button } from "@/shared/components/ui/button" import { EmptyState } from "@/shared/components/ui/empty-state" import { ListPagination, computePagination } from "@/shared/components/ui/list-pagination" import { WidgetBoundary } from "@/shared/components/widget-boundary" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { getParam, type SearchParams } from "@/shared/lib/search-params" import { getTeacherClasses } from "@/modules/classes/data-access" import { getGradeRecords } from "@/modules/grades/data-access" import { getSubjectOptions } from "@/modules/school/data-access" import { GradeQueryFilters } from "@/modules/grades/components/grade-query-filters" import { GradeRecordList } from "@/modules/grades/components/grade-record-list" import { ExportButton } from "@/modules/grades/components/export-button" import { ExcelImportDialog } from "@/modules/grades/components/excel-import-dialog" import { isGradeType, isSemester } from "@/modules/grades/lib/type-guards" export const dynamic = "force-dynamic" const PAGE_SIZE = 20 export default async function TeacherGradesPage({ searchParams, }: { searchParams: Promise }): Promise { const sp = await searchParams const ctx = await requirePermission(Permissions.GRADE_RECORD_READ) const t = await getTranslations("grades") const classId = getParam(sp, "classId") const subjectId = getParam(sp, "subjectId") const type = getParam(sp, "type") const semester = getParam(sp, "semester") // P3 修复:使用 DB 层分页,移除重复计算 const { page } = computePagination(sp, PAGE_SIZE) const offset = (page - 1) * PAGE_SIZE const [classes, allSubjects, result] = await Promise.all([ getTeacherClasses(), getSubjectOptions(), getGradeRecords({ scope: ctx.dataScope, currentUserId: ctx.userId, classId: classId && classId !== "all" ? classId : undefined, subjectId: subjectId && subjectId !== "all" ? subjectId : undefined, type: type && type !== "all" ? isGradeType(type) ? type : undefined : undefined, semester: semester && semester !== "all" ? isSemester(semester) ? semester : undefined : undefined, limit: PAGE_SIZE, offset, }), ]) const classOptions = classes.map((c) => ({ id: c.id, name: c.name })) const subjectOptions = allSubjects.map((s) => ({ id: s.id, name: s.name })) // 使用 DB 返回的 total 和 totalPages,移除重复计算 const total = result.total const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) const currentPage = Math.min(page, totalPages) const pagedRecords = result.records const hasFilters = Boolean(classId || subjectId || type || semester) return (

{t("title.list")}

{t("page.list.description")}

({ id: c.id, name: c.name }))} subjects={allSubjects.map((s) => ({ id: s.id, name: s.name ?? "Unknown" }))} />
{total === 0 && !hasFilters ? ( ) : (
{/* P1-9: 用 WidgetBoundary 包裹独立数据区块,隔离故障域 */} {total > 0 ? ( ) : null}
)}
) }