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:
45
src/app/(dashboard)/parent/children/[studentId]/loading.tsx
Normal file
45
src/app/(dashboard)/parent/children/[studentId]/loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user