feat(app): add lesson-plans, practice, and grade dashboard routes

- Add admin/lesson-plans, parent/lesson-plans, student/lesson-plans routes

- Add student/practice and teacher/practice routes for adaptive practice

- Add management/grade/dashboard and management/grade/practice routes

- Add teacher/lesson-plans error and loading boundaries

- Update existing admin, parent, student, teacher pages with new features

- Update globals.css and proxy middleware
This commit is contained in:
SpecialX
2026-06-24 12:03:47 +08:00
parent 8c2fe14c20
commit 37d2688a28
84 changed files with 2665 additions and 661 deletions

View File

@@ -1,8 +1,8 @@
import type { JSX } from "react"
import { getTeacherClasses } from "@/modules/classes/data-access"
import { getTeacherClasses, getClassGradeIdsByClassIds } 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 { getExamsForGradeEntry, getExamForGradeEntry } from "@/modules/exams/data-access"
import { BatchGradeEntryByExam } 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"
@@ -19,63 +19,73 @@ export default async function BatchEntryPage({
const ctx = await requirePermission(Permissions.GRADE_RECORD_MANAGE)
const sp = await searchParams
const defaultClassId = getParam(sp, "classId")
const defaultSubjectId = getParam(sp, "subjectId")
const examId = getParam(sp, "examId")
const classId = getParam(sp, "classId")
// P3 修复:添加 scope 校验,对 class_taught scope 限制可录入的班级
const [classes, allSubjects, students] = await Promise.all([
// 获取试卷列表 + 班级列表
const [exams, teacherClasses] = await Promise.all([
getExamsForGradeEntry(ctx.dataScope),
getTeacherClasses(),
getSubjectOptions(),
defaultClassId
? getClassStudentsForEntry(defaultClassId, ctx.dataScope)
: Promise.resolve([] as Awaited<ReturnType<typeof getClassStudentsForEntry>>),
])
// 对 class_taught scope过滤掉不在 scope 中的班级
// scope 过滤班级
const allowedClassIds =
ctx.dataScope.type === "class_taught" ? ctx.dataScope.classIds : null
const scopedClasses = allowedClassIds
? classes.filter((c) => allowedClassIds.includes(c.id))
: classes
? teacherClasses.filter((c) => allowedClassIds.includes(c.id))
: teacherClasses
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>
)
// 获取 classId → gradeId 映射(用于客户端按试卷年级过滤班级)
const classGradeMap: Record<string, string> = {}
if (scopedClasses.length > 0) {
const gradeMap = await getClassGradeIdsByClassIds(
scopedClasses.map((c) => c.id)
)
for (const [cid, gid] of gradeMap.entries()) {
classGradeMap[cid] = gid
}
}
// 如果有 examId获取试卷详情含题目列表
const exam = examId
? await getExamForGradeEntry(examId, ctx.dataScope)
: null
// 如果有 examId + classId获取学生列表
const students =
examId && classId
? await getClassStudentsForEntry(classId, ctx.dataScope)
: []
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>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">
Excel
</p>
</div>
<BatchGradeEntry
classes={classOptions}
subjects={subjectOptions}
students={students}
defaultClassId={defaultClassId}
defaultSubjectId={defaultSubjectId}
/>
{exams.length === 0 ? (
<EmptyState
title="没有可用的试卷"
description="请先在试卷管理中创建试卷并添加题目,才能录入成绩。"
icon={ClipboardList}
className="border-none shadow-none"
/>
) : (
<BatchGradeEntryByExam
exams={exams}
classes={classOptions}
classGradeMap={classGradeMap}
exam={exam}
students={students}
defaultExamId={examId}
defaultClassId={classId}
/>
)}
</div>
)
}