feat(app): add error/loading boundaries and update dashboard routes

- Add error.tsx and loading.tsx boundaries for admin, parent, student, teacher routes

- Add dashboard-error-fallback and dashboard-loading-skeleton components

- Add student/learning page, parent/leave routes, teacher textbook components

- Update existing app routes across auth, dashboard, and API endpoints

- Update proxy middleware and next-auth type declarations
This commit is contained in:
SpecialX
2026-06-23 17:38:28 +08:00
parent c4d3433cc9
commit 1a9377222c
90 changed files with 1690 additions and 741 deletions

View File

@@ -3,7 +3,11 @@ import { getTeacherClasses } from "@/modules/classes/data-access"
import { getClassStudentsForEntry } from "@/modules/grades/data-access"
import { getSubjectOptions } from "@/modules/school/data-access"
import { BatchGradeEntry } from "@/modules/grades/components/batch-grade-entry"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getParam, type SearchParams } from "@/shared/lib/search-params"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { ClipboardList } from "lucide-react"
export const dynamic = "force-dynamic"
@@ -12,22 +16,52 @@ export default async function BatchEntryPage({
}: {
searchParams: Promise<SearchParams>
}): Promise<JSX.Element> {
const ctx = await requirePermission(Permissions.GRADE_RECORD_MANAGE)
const sp = await searchParams
const defaultClassId = getParam(sp, "classId")
const defaultSubjectId = getParam(sp, "subjectId")
// P3 修复:添加 scope 校验,对 class_taught scope 限制可录入的班级
const [classes, allSubjects, students] = await Promise.all([
getTeacherClasses(),
getSubjectOptions(),
defaultClassId
? getClassStudentsForEntry(defaultClassId)
? getClassStudentsForEntry(defaultClassId, ctx.dataScope)
: Promise.resolve([] as Awaited<ReturnType<typeof getClassStudentsForEntry>>),
])
const classOptions = classes.map((c) => ({ id: c.id, name: c.name }))
// 对 class_taught scope过滤掉不在 scope 中的班级
const allowedClassIds =
ctx.dataScope.type === "class_taught" ? ctx.dataScope.classIds : null
const scopedClasses = allowedClassIds
? classes.filter((c) => allowedClassIds.includes(c.id))
: classes
const classOptions = scopedClasses.map((c) => ({ id: c.id, name: c.name }))
const subjectOptions = allSubjects.map((s) => ({ id: s.id, name: s.name }))
// 如果指定了 classId 但 scope 不允许,显示提示
if (defaultClassId && students.length === 0 && scopedClasses.length > 0) {
const classExists = scopedClasses.some((c) => c.id === defaultClassId)
if (!classExists) {
return (
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
<div>
<h1 className="text-2xl font-bold tracking-tight">Batch Grade Entry</h1>
<p className="text-muted-foreground">Enter grades for all students in a class at once.</p>
</div>
<EmptyState
title="无权访问该班级"
description="您没有权限为该班级录入成绩。"
icon={ClipboardList}
className="border-none shadow-none"
/>
</div>
)
}
}
return (
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
<div>