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,11 +1,13 @@
import type { JSX } from "react"
import { Suspense } from "react"
import { BarChart3 } from "lucide-react"
import { getTranslations } from "next-intl/server"
import { requirePermission } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { Skeleton } from "@/shared/components/ui/skeleton"
import { WidgetBoundary } from "@/shared/components/widget-boundary"
import { getParam, type SearchParams } from "@/shared/lib/search-params"
import { getStudentIdsByClassIds, getClassIdsByGradeIds } from "@/modules/classes/data-access"
@@ -17,7 +19,7 @@ import {
getSubjectErrorOverviews,
getClassErrorOverviews,
getChapterWeakness,
} from "@/modules/error-book/data-access"
} from "@/modules/error-book/data-access-analytics"
import { TopWrongQuestions } from "@/modules/error-book/components/top-wrong-questions"
import { SubjectTabs } from "@/modules/error-book/components/subject-tabs"
import { ClassFilter } from "@/modules/error-book/components/class-filter"
@@ -34,6 +36,7 @@ async function TeacherErrorBookContent({
}: {
searchParams: Promise<SearchParams>
}): Promise<JSX.Element> {
const t = await getTranslations("errorBook")
const ctx = await requirePermission(Permissions.ERROR_BOOK_ANALYTICS_READ)
const params = await searchParams
@@ -45,13 +48,13 @@ async function TeacherErrorBookContent({
return (
<div className="flex h-full flex-col space-y-8 p-8">
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
<h1 className="text-2xl font-bold tracking-tight">{t("teacher.title")}</h1>
<p className="text-muted-foreground">{t("teacher.descriptionShort")}</p>
</div>
<EmptyState
icon={BarChart3}
title="暂无可查看的班级"
description="您还未被分配到任何班级,无法查看错题分析数据。"
title={t("teacher.noClass")}
description={t("teacher.noClassDesc")}
className="h-[360px] bg-card"
/>
</div>
@@ -72,13 +75,13 @@ async function TeacherErrorBookContent({
return (
<div className="flex h-full flex-col space-y-8 p-8">
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
<h1 className="text-2xl font-bold tracking-tight">{t("teacher.title")}</h1>
<p className="text-muted-foreground">{t("teacher.descriptionShort")}</p>
</div>
<EmptyState
icon={BarChart3}
title="班级暂无学生"
description="班级中没有学生,无法查看错题分析数据。"
title={t("teacher.noStudent")}
description={t("teacher.noStudentDesc")}
className="h-[360px] bg-card"
/>
</div>
@@ -128,9 +131,9 @@ async function TeacherErrorBookContent({
return (
<div className="flex h-full flex-col space-y-6 p-8">
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<h1 className="text-2xl font-bold tracking-tight">{t("teacher.title")}</h1>
<p className="text-muted-foreground">
{t("teacher.description")}
</p>
</div>
@@ -155,71 +158,86 @@ async function TeacherErrorBookContent({
) : null}
{/* 统计卡片 */}
<AnalyticsStatsCards
totalStudents={queryStudentIds.length}
studentsWithErrorBook={studentsWithErrorBook.length}
totalErrorItems={totalErrorItems}
averageMasteryRate={averageMasteryRate}
dueReviewCount={totalDueReview}
knowledgePointCount={knowledgePointCount}
/>
<WidgetBoundary title={t("teacher.title")} skeletonHeight={120}>
<AnalyticsStatsCards
totalStudents={queryStudentIds.length}
studentsWithErrorBook={studentsWithErrorBook.length}
totalErrorItems={totalErrorItems}
averageMasteryRate={averageMasteryRate}
dueReviewCount={totalDueReview}
knowledgePointCount={knowledgePointCount}
/>
</WidgetBoundary>
{/* 班级错题对比图(仅在"全部班级"视图下显示) */}
{effectiveClassId === "all" && classOverviews.length > 1 ? (
<ClassErrorBarChart data={classOverviews} />
<WidgetBoundary title={t("teacher.subjectDist")} skeletonHeight={300}>
<ClassErrorBarChart data={classOverviews} />
</WidgetBoundary>
) : null}
{/* 章节错题分布 + 知识点薄弱度(并排) */}
<div className="grid gap-4 lg:grid-cols-2">
{chapterWeakness.length > 0 ? (
<ChapterWeaknessChart data={chapterWeakness} />
) : (
<EmptyState
icon={BarChart3}
title="暂无章节错题数据"
description="尚未关联知识点到章节,无法显示章节维度统计。"
className="h-[300px] bg-card"
/>
)}
{weakKps.length > 0 ? (
<KnowledgePointWeaknessChart data={weakKps} />
) : (
<EmptyState
icon={BarChart3}
title="暂无知识点数据"
description="错题尚未关联知识点,无法显示薄弱知识点统计。"
className="h-[300px] bg-card"
/>
)}
<WidgetBoundary title={t("teacher.weakPoints")} skeletonHeight={300}>
{chapterWeakness.length > 0 ? (
<ChapterWeaknessChart data={chapterWeakness} />
) : (
<EmptyState
icon={BarChart3}
title={t("teacher.noChapterDataTitle")}
description={t("teacher.noChapterDataDesc")}
className="h-[300px] bg-card"
/>
)}
</WidgetBoundary>
<WidgetBoundary title={t("teacher.weakPoints")} skeletonHeight={300}>
{weakKps.length > 0 ? (
<KnowledgePointWeaknessChart data={weakKps} />
) : (
<EmptyState
icon={BarChart3}
title={t("teacher.noKpDataTitle")}
description={t("teacher.noKpDataDesc")}
className="h-[300px] bg-card"
/>
)}
</WidgetBoundary>
</div>
{/* 学生错题详情(按班级分组) */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold"></h2>
<h2 className="text-lg font-semibold">{t("teacher.studentDetail")}</h2>
<span className="text-sm text-muted-foreground">
{queryStudentIds.length} {studentsWithErrorBook.length}
{t("teacher.studentsCount", {
total: queryStudentIds.length,
withErrors: studentsWithErrorBook.length,
})}
</span>
</div>
{sortedSummaries.length > 0 ? (
<GroupedStudentErrorTable
students={sortedSummaries}
studentNames={nameMap}
basePath="/teacher/error-book"
/>
) : (
<EmptyState
icon={BarChart3}
title="暂无学生错题"
description="所选范围内没有学生错题数据。"
className="h-[200px] bg-card"
/>
)}
<WidgetBoundary title={t("teacher.studentDetail")} skeletonHeight={300}>
{sortedSummaries.length > 0 ? (
<GroupedStudentErrorTable
students={sortedSummaries}
studentNames={nameMap}
basePath="/teacher/error-book"
/>
) : (
<EmptyState
icon={BarChart3}
title={t("teacher.noStudentErrorsTitle")}
description={t("teacher.noStudentErrorsDesc")}
className="h-[200px] bg-card"
/>
)}
</WidgetBoundary>
</div>
{/* 高频错题 Top 10 */}
{topWrongQuestions.length > 0 ? (
<TopWrongQuestions questions={topWrongQuestions} />
<WidgetBoundary title={t("teacher.topWrong")} skeletonHeight={300}>
<TopWrongQuestions questions={topWrongQuestions} />
</WidgetBoundary>
) : null}
</div>
)