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:
@@ -5,8 +5,7 @@ import { getTranslations } from "next-intl/server"
|
||||
|
||||
import { requirePermission } from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import { getAnnouncementById } from "@/modules/announcements/data-access"
|
||||
import { getGrades } from "@/modules/school/data-access"
|
||||
import { getEditAnnouncementPageData } from "@/modules/announcements/data-access"
|
||||
import { AnnouncementForm } from "@/modules/announcements/components/announcement-form"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
@@ -28,10 +27,7 @@ export default async function EditAnnouncementPage({
|
||||
const { id } = await params
|
||||
const t = await getTranslations("announcements")
|
||||
|
||||
const [announcement, grades] = await Promise.all([
|
||||
getAnnouncementById(id),
|
||||
getGrades(),
|
||||
])
|
||||
const { announcement, grades } = await getEditAnnouncementPageData(id)
|
||||
|
||||
if (!announcement) notFound()
|
||||
|
||||
@@ -44,7 +40,7 @@ export default async function EditAnnouncementPage({
|
||||
<AnnouncementForm
|
||||
mode="edit"
|
||||
announcement={announcement}
|
||||
grades={grades.map((g) => ({ id: g.id, name: g.name }))}
|
||||
grades={grades}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,9 +4,7 @@ import { getTranslations } from "next-intl/server"
|
||||
|
||||
import { requirePermission } from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import { getAnnouncements } from "@/modules/announcements/data-access"
|
||||
import { getGrades } from "@/modules/school/data-access"
|
||||
import { getAdminClasses } from "@/modules/classes/data-access"
|
||||
import { getAdminAnnouncementsPageData } from "@/modules/announcements/data-access"
|
||||
import { AdminAnnouncementsView } from "@/modules/announcements/components/admin-announcements-view"
|
||||
import { getSearchParam, type SearchParams } from "@/shared/lib/utils"
|
||||
import type { AnnouncementStatus } from "@/modules/announcements/types"
|
||||
@@ -34,17 +32,13 @@ export default async function AdminAnnouncementsPage({
|
||||
const statusParam = getSearchParam(sp, "status")
|
||||
const status = isValidStatus(statusParam) ? statusParam : undefined
|
||||
|
||||
const [announcements, grades, classes] = await Promise.all([
|
||||
getAnnouncements({ status }),
|
||||
getGrades(),
|
||||
getAdminClasses(),
|
||||
])
|
||||
const { announcements, grades, classes } = await getAdminAnnouncementsPageData(status)
|
||||
|
||||
return (
|
||||
<AdminAnnouncementsView
|
||||
announcements={announcements}
|
||||
grades={grades.map((g) => ({ id: g.id, name: g.name }))}
|
||||
classes={classes.map((c) => ({ id: c.id, name: c.name }))}
|
||||
grades={grades}
|
||||
classes={classes}
|
||||
initialStatus={status}
|
||||
/>
|
||||
)
|
||||
|
||||
7
src/app/(dashboard)/admin/dashboard/error.tsx
Normal file
7
src/app/(dashboard)/admin/dashboard/error.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { DashboardErrorFallback } from "@/modules/dashboard/components/dashboard-error-fallback"
|
||||
|
||||
export default function AdminDashboardError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
||||
return <DashboardErrorFallback error={error} reset={reset} />
|
||||
}
|
||||
5
src/app/(dashboard)/admin/dashboard/loading.tsx
Normal file
5
src/app/(dashboard)/admin/dashboard/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { DashboardLoadingSkeleton } from "@/modules/dashboard/components/dashboard-loading-skeleton"
|
||||
|
||||
export default function AdminDashboardLoading() {
|
||||
return <DashboardLoadingSkeleton />
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import { AlertCircle } from "lucide-react"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
|
||||
export default function AdminError({ reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
||||
const t = useTranslations("dashboard")
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-4 p-8">
|
||||
<EmptyState
|
||||
icon={AlertCircle}
|
||||
title="页面加载失败"
|
||||
description="抱歉,页面加载时发生了意外错误。请稍后重试。"
|
||||
title={t("error.loadFailed")}
|
||||
description={t("error.loadFailedDesc")}
|
||||
action={{
|
||||
label: "重试",
|
||||
label: t("error.retry"),
|
||||
onClick: () => reset(),
|
||||
}}
|
||||
className="border-none shadow-none h-auto"
|
||||
|
||||
27
src/app/(dashboard)/admin/school/grades/insights/error.tsx
Normal file
27
src/app/(dashboard)/admin/school/grades/insights/error.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { AlertCircle } from "lucide-react"
|
||||
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
|
||||
export default function AdminGradesInsightsError({
|
||||
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>
|
||||
)
|
||||
}
|
||||
22
src/app/(dashboard)/admin/school/grades/insights/loading.tsx
Normal file
22
src/app/(dashboard)/admin/school/grades/insights/loading.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export default function AdminGradesInsightsLoading() {
|
||||
return (
|
||||
<div className="h-full flex-1 flex-col space-y-8 p-8 md:flex">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<Skeleton className="h-4 w-96" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-32" />
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-80" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Link from "next/link"
|
||||
import Link from "next/link"
|
||||
import type { Metadata } from "next"
|
||||
import type { JSX } from "react"
|
||||
import { BarChart3 } from "lucide-react"
|
||||
@@ -7,13 +7,16 @@ import { requirePermission } from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import { getGrades } from "@/modules/school/data-access"
|
||||
import { getGradeHomeworkInsights } from "@/modules/classes/data-access"
|
||||
import { getSchoolWideGradeSummary } from "@/modules/grades/data-access-analytics"
|
||||
import { SchoolWideSummaryCard } from "@/modules/grades/components/school-wide-summary-card"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import { StatCard } from "@/shared/components/ui/stat-card"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table"
|
||||
import { formatDate, formatNumber, getSearchParam, type SearchParams } from "@/shared/lib/utils"
|
||||
import { formatDate, formatNumber } from "@/shared/lib/utils"
|
||||
import { getParam, type SearchParams } from "@/shared/lib/search-params"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "年级作业洞察 - Next_Edu",
|
||||
@@ -27,15 +30,17 @@ export default async function AdminGradeInsightsPage({
|
||||
}: {
|
||||
searchParams: Promise<SearchParams>
|
||||
}): Promise<JSX.Element> {
|
||||
await requirePermission(Permissions.SCHOOL_MANAGE)
|
||||
const ctx = await requirePermission(Permissions.SCHOOL_MANAGE)
|
||||
const params = await searchParams
|
||||
const gradeId = getSearchParam(params, "gradeId")
|
||||
const gradeId = getParam(params, "gradeId")
|
||||
const selected = gradeId && gradeId !== "all" ? gradeId : ""
|
||||
|
||||
// grades 与 insights 无数据依赖,并行查询
|
||||
const [grades, insights] = await Promise.all([
|
||||
// grades、insights、全校汇总无数据依赖,并行查询
|
||||
const [grades, insights, schoolWideSummary] = await Promise.all([
|
||||
getGrades(),
|
||||
selected ? getGradeHomeworkInsights({ gradeId: selected, limit: 50 }) : Promise.resolve(null),
|
||||
// v3-P2-9:管理员全校成绩汇总视图
|
||||
getSchoolWideGradeSummary(ctx.dataScope),
|
||||
])
|
||||
|
||||
return (
|
||||
@@ -50,6 +55,11 @@ export default async function AdminGradeInsightsPage({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* v3-P2-9: 全校成绩汇总视图 */}
|
||||
{schoolWideSummary.grades.length > 0 && (
|
||||
<SchoolWideSummaryCard summary={schoolWideSummary} />
|
||||
)}
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="text-base">筛选</CardTitle>
|
||||
|
||||
@@ -3,5 +3,5 @@ import { redirect } from "next/navigation"
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default function AdminSchoolPage(): never {
|
||||
redirect("/admin/school/classes")
|
||||
redirect("/admin/school/schools")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user