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,45 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Skeleton } from "@/shared/components/ui/skeleton"
export default function Loading() {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Skeleton className="h-16 w-16 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-7 w-40" />
<Skeleton className="h-4 w-56" />
</div>
</div>
<Skeleton className="h-9 w-full max-w-md" />
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="text-base">
<Skeleton className="h-4 w-32" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">
<Skeleton className="h-4 w-32" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -1,9 +1,17 @@
import { notFound } from "next/navigation"
import { requireAuth } from "@/shared/lib/auth-guard"
import { verifyParentChildRelation, getChildDashboardData } from "@/modules/parent/data-access"
import { getSearchParam } from "@/shared/lib/utils"
import {
verifyParentChildRelation,
getChildDashboardData,
getChildNameList,
} from "@/modules/parent/data-access"
import { ChildDetailHeader } from "@/modules/parent/components/child-detail-header"
import { ChildDetailPanel } from "@/modules/parent/components/child-detail-panel"
import {
ChildDetailPanel,
SiblingSwitcher,
} from "@/modules/parent/components/child-detail-panel"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { ShieldAlert } from "lucide-react"
@@ -11,10 +19,13 @@ export const dynamic = "force-dynamic"
export default async function ChildDetailPage({
params,
searchParams,
}: {
params: Promise<{ studentId: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const { studentId } = await params
const sp = await searchParams
const ctx = await requireAuth()
// 校验当前家长与该子女存在关系(同时按 parentId + studentId 过滤,防止跨家庭信息泄露)
@@ -38,15 +49,29 @@ export default async function ChildDetailPage({
)
}
const child = await getChildDashboardData(studentId, relation)
const [child, siblings] = await Promise.all([
getChildDashboardData(studentId, relation),
getChildNameList(ctx.userId),
])
if (!child) {
notFound()
}
const initialTab = getSearchParam(sp, "tab")
return (
<div className="p-6 md:p-8 space-y-6">
<ChildDetailHeader child={child} />
<ChildDetailPanel child={child} />
<ChildDetailPanel
child={child}
initialTab={initialTab}
siblingSwitcher={
<SiblingSwitcher
current={{ id: child.basicInfo.id, name: child.basicInfo.name }}
siblings={siblings}
/>
}
/>
</div>
)
}