feat(app): add error/loading boundaries across all dashboard routes and new routes

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

- Add admin announcements edit, audit-logs overview, curriculum-map, invitation-codes, permissions, questions, roles routes

- Add admin elective detail and components, files, course-plans, users, scheduling boundaries

- Add messages group-compose route

- Add parent course-plans, elective, grades report-card, practice routes

- Add student course-plans, elective detail, error-book dialogs, grades report-card, learning study-path, leave, schedule boundaries

- Add teacher attendance report, classes boundaries, course-plans boundaries, elective, exams analytics/edit-rich/all/create/new, grades report-card, homework boundaries, leave, lesson-plans calendar

- Add auth loading, onboarding loading, api cron
This commit is contained in:
SpecialX
2026-07-03 10:26:25 +08:00
parent e9a5264fe7
commit 21142f9b99
280 changed files with 7137 additions and 1855 deletions

View File

@@ -1,9 +1,12 @@
import type { JSX } from "react"
import { Suspense } from "react"
import { getTranslations } from "next-intl/server"
import { getAuthContext } from "@/shared/lib/auth-guard"
import { notFound } from "next/navigation"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getLessonPlanById } from "@/modules/lesson-preparation/data-access"
import { getTextbookById, getChaptersByTextbookId } from "@/modules/textbooks/data-access"
import { assertPlanInScope } from "@/modules/lesson-preparation/lib/scope-check"
import { getTextbookById, getChaptersByTextbookId, findChapterById } from "@/modules/textbooks/data-access"
import { LessonPlanReadonlyView } from "@/modules/lesson-preparation/components/lesson-plan-readonly-view"
import { Skeleton } from "@/shared/components/ui/skeleton"
@@ -16,17 +19,17 @@ export default async function StudentLessonPlanViewPage({
}): Promise<JSX.Element> {
const { planId } = await params
const t = await getTranslations("lessonPreparation")
const ctx = await getAuthContext()
// V4 P0-2 修复:页面层补齐 requirePermission 权限校验
const ctx = await requirePermission(Permissions.LESSON_PLAN_READ)
const plan = await getLessonPlanById(planId, ctx.userId)
if (!plan) {
return (
<div className="p-6">
<div className="rounded-md border border-outline-variant bg-surface-container-low p-4 text-on-surface-variant">
{t("readonly.notFound")}
</div>
</div>
)
if (!plan) notFound()
// V4 P0-1 修复:学生仅可查看本年级已发布课案,防止跨年级信息泄露
try {
assertPlanInScope(plan, ctx)
} catch {
notFound()
}
// 学生只能查看已发布的课案
@@ -44,21 +47,14 @@ export default async function StudentLessonPlanViewPage({
let textbookTitle: string | undefined
let chapterTitle: string | undefined
if (plan.textbookId) {
const textbook = await getTextbookById(plan.textbookId)
// V4 P2-1 修复textbook 和 chapters 查询无依赖关系,改为 Promise.all 并行查询
const [textbook, chapters] = await Promise.all([
getTextbookById(plan.textbookId),
getChaptersByTextbookId(plan.textbookId),
])
textbookTitle = textbook?.title
if (plan.chapterId) {
const chapters = await getChaptersByTextbookId(plan.textbookId)
const findChapter = (list: typeof chapters): typeof chapters[number] | undefined => {
for (const ch of list) {
if (ch.id === plan.chapterId) return ch
if (ch.children && ch.children.length > 0) {
const found = findChapter(ch.children as typeof chapters)
if (found) return found
}
}
return undefined
}
const chapter = findChapter(chapters)
const chapter = findChapterById(chapters, plan.chapterId)
chapterTitle = chapter?.title
}
}

View File

@@ -1,17 +1,21 @@
import type { JSX } from "react"
import { Suspense } from "react"
import { getTranslations } from "next-intl/server"
import { getAuthContext } from "@/shared/lib/auth-guard"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getLessonPlans } from "@/modules/lesson-preparation/data-access"
import { getSubjectOptions } from "@/modules/school/data-access"
import { LessonPlanList } from "@/modules/lesson-preparation/components/lesson-plan-list"
import { LessonPlanProviderSetup } from "@/modules/lesson-preparation/providers/lesson-plan-provider-setup"
import { STUDENT_ROLE_CONFIG } from "@/modules/lesson-preparation/providers/lesson-plan-provider"
import { Skeleton } from "@/shared/components/ui/skeleton"
export const dynamic = "force-dynamic"
export default async function StudentLessonPlansPage(): Promise<JSX.Element> {
const t = await getTranslations("lessonPreparation")
const ctx = await getAuthContext()
// V4 P0-2 修复:页面层补齐 requirePermission 权限校验
const ctx = await requirePermission(Permissions.LESSON_PLAN_READ)
const [items, subjects] = await Promise.all([
getLessonPlans({ status: "published" }, ctx.dataScope, ctx.userId),
@@ -24,21 +28,24 @@ export default async function StudentLessonPlansPage(): Promise<JSX.Element> {
<h1 className="text-2xl font-bold tracking-tight">{t("student.title")}</h1>
<p className="text-muted-foreground">{t("student.description")}</p>
</div>
<Suspense
fallback={
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-[180px] w-full" />
))}
</div>
}
>
<LessonPlanList
initialItems={items}
subjects={subjects}
viewMode="student"
/>
</Suspense>
{/* P0-13 修复:包裹 LessonPlanProviderSetup注入 student 角色配置,使筛选功能生效 */}
<LessonPlanProviderSetup roleConfig={STUDENT_ROLE_CONFIG}>
<Suspense
fallback={
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-[180px] w-full" />
))}
</div>
}
>
<LessonPlanList
initialItems={items}
subjects={subjects}
viewMode="student"
/>
</Suspense>
</LessonPlanProviderSetup>
</div>
)
}