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

@@ -0,0 +1,27 @@
"use client"
import { AlertCircle } from "lucide-react"
import { EmptyState } from "@/shared/components/ui/empty-state"
export default function StudentGradesError({
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="flex h-full flex-col items-center justify-center space-y-4 p-8">
<EmptyState
icon={AlertCircle}
title="成绩查询页面加载失败"
description="抱歉,页面加载时发生了意外错误。请稍后重试。"
action={{
label: "重试",
onClick: () => reset(),
}}
className="border-none shadow-none h-auto"
/>
</div>
)
}

View File

@@ -1,9 +1,13 @@
import { getTranslations } from "next-intl/server"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { getStudentGradeSummary } from "@/modules/grades/data-access"
import { getRankingTrend, getClassAverageTrend } from "@/modules/grades/data-access-ranking"
import { getSubjectOptions } from "@/modules/school/data-access"
import { StudentGradeSummary } from "@/modules/grades/components/student-grade-summary"
import { GradeFilters } from "@/modules/grades/components/grade-filters"
import { GradeTrendCard } from "@/modules/grades/components/grade-trend-card"
import { RankingTrendCard } from "@/modules/grades/components/ranking-trend-card"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { UserX } from "lucide-react"
import { getParam, type SearchParams } from "@/shared/lib/search-params"
@@ -16,21 +20,28 @@ export default async function StudentGradesPage({
searchParams: Promise<SearchParams>
}) {
const ctx = await requirePermission(Permissions.GRADE_RECORD_READ)
const [sp, summary] = await Promise.all([
const [sp, summary, rankingTrend, classAverageTrend, subjectOptions] = await Promise.all([
searchParams,
getStudentGradeSummary(ctx.userId),
getStudentGradeSummary(ctx.userId, ctx.dataScope),
// v3-P1-3接入排名趋势图
getRankingTrend(ctx.userId, undefined, undefined, ctx.dataScope),
// v3-P2-2接入班级平均趋势对比线
getClassAverageTrend(ctx.userId, undefined, undefined, ctx.dataScope),
// v3-P2-1获取科目列表用于过滤器
getSubjectOptions(),
])
if (!summary) {
const t = await getTranslations("grades")
return (
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold tracking-tight">My Grades</h2>
<p className="text-muted-foreground">View your grade records.</p>
<h2 className="text-2xl font-bold tracking-tight">{t("title.myGrades")}</h2>
<p className="text-muted-foreground">{t("summary.noDataDescription")}</p>
</div>
<EmptyState
title="No user found"
description="Unable to load your student profile."
title={t("summary.noDataTitle")}
description={t("summary.noDataDescription")}
icon={UserX}
className="border-none shadow-none"
/>
@@ -46,7 +57,8 @@ export default async function StudentGradesPage({
const filteredRecords = summary.records.filter((r) => {
if (q && !r.title.toLowerCase().includes(q)) return false
if (subjectFilter !== "all" && r.subjectName !== subjectFilter) return false
// v3-P2-1 修复:按 subjectId 而非 subjectName 过滤
if (subjectFilter !== "all" && r.subjectId !== subjectFilter) return false
if (typeFilter !== "all" && r.type !== typeFilter) return false
if (semesterFilter !== "all" && r.semester !== semesterFilter) return false
return true
@@ -60,11 +72,16 @@ export default async function StudentGradesPage({
return (
<div className="space-y-8">
<div>
<h2 className="text-2xl font-bold tracking-tight">My Grades</h2>
<p className="text-muted-foreground">View your grade records.</p>
<h2 className="text-2xl font-bold tracking-tight">{summary.studentName}</h2>
<p className="text-muted-foreground">{summary.records.length} </p>
</div>
<GradeFilters />
{filteredSummary.records.length > 0 && <GradeTrendCard summary={filteredSummary} />}
<GradeFilters subjects={subjectOptions.map((s) => ({ id: s.id, name: s.name }))} />
{filteredSummary.records.length > 0 && (
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<GradeTrendCard summary={filteredSummary} classAverageData={classAverageTrend} />
<RankingTrendCard trend={rankingTrend} />
</div>
)}
<StudentGradeSummary summary={filteredSummary} />
</div>
)