refactor(grades,diagnostic): 完成成绩和学情诊断模块审计 P1+P2 改进项
P1-1: 抽取 stats-service.ts,将 8 个统计计算纯函数从 data-access 层分离 P1-5: 创建 WidgetBoundary 组件 + 补齐 teacher 路由 loading.tsx/error.tsx (14 文件) P1-6: 同步架构图文档 004/005,新增 stats-service 与 widget-boundary 节点 P2-1: 补充 a11y ARIA 属性(5 图表 role=img + aria-label,2 表格 caption,3 列表 role=list,3 按钮 aria-label) P2-3: 修复班级报告 studentId 字段语义错误(schema 改为可空 + 迁移 + 代码适配) P2-4: 修复 grade_managed scope 返回空数据(改为子查询 classes 表按 gradeId 过滤) P2-5: 新增 /parent/diagnostic/ 页面(多子女学情诊断聚合 + loading + error) P2-6: 统一 SearchParams 工具(student/grades 和 management/grade/insights 改用 @/shared/lib/search-params)
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import type { Metadata } from "next"
|
||||
import type { JSX } from "react"
|
||||
|
||||
import { getTranslations } from "next-intl/server"
|
||||
import { requirePermission } from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import { getTeacherIdForMutations } from "@/modules/classes/data-access"
|
||||
@@ -11,22 +15,23 @@ import { Button } from "@/shared/components/ui/button"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table"
|
||||
import { BarChart3 } from "lucide-react"
|
||||
import { formatDate } from "@/shared/lib/utils"
|
||||
import { getParam, type SearchParams } from "@/shared/lib/search-params"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
type SearchParams = { [key: string]: string | string[] | undefined }
|
||||
|
||||
const getParam = (params: SearchParams, key: string) => {
|
||||
const v = params[key]
|
||||
if (typeof v === "string") return v
|
||||
if (Array.isArray(v)) return v[0]
|
||||
return undefined
|
||||
}
|
||||
|
||||
const formatScore = (v: number | null, digits = 1) => (typeof v === "number" && Number.isFinite(v) ? v.toFixed(digits) : "-")
|
||||
|
||||
export default async function TeacherGradeInsightsPage({ searchParams }: { searchParams: Promise<SearchParams> }) {
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("school")
|
||||
return {
|
||||
title: `${t("classManagement.grade.insights.title")} - Next_Edu`,
|
||||
description: t("classManagement.grade.insights.description"),
|
||||
}
|
||||
}
|
||||
|
||||
export default async function TeacherGradeInsightsPage({ searchParams }: { searchParams: Promise<SearchParams> }): Promise<JSX.Element> {
|
||||
await requirePermission(Permissions.GRADE_RECORD_READ)
|
||||
const t = await getTranslations("school")
|
||||
const params = await searchParams
|
||||
const gradeId = getParam(params, "gradeId")
|
||||
|
||||
@@ -41,13 +46,13 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-2xl font-bold tracking-tight">Grade Insights</h2>
|
||||
<p className="text-muted-foreground">View grade-level homework statistics for grades you lead.</p>
|
||||
<h2 className="text-2xl font-bold tracking-tight">{t("classManagement.grade.insights.title")}</h2>
|
||||
<p className="text-muted-foreground">{t("classManagement.grade.insights.description")}</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title="No grades assigned"
|
||||
description="You are not assigned as a grade head or teaching head for any grade."
|
||||
title={t("classManagement.grade.insights.noGrades")}
|
||||
description={t("classManagement.grade.insights.noGradesDescription")}
|
||||
className="h-[360px] bg-card"
|
||||
/>
|
||||
</div>
|
||||
@@ -57,27 +62,27 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc
|
||||
return (
|
||||
<div className="flex h-full flex-col space-y-8 p-8">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-2xl font-bold tracking-tight">Grade Insights</h2>
|
||||
<p className="text-muted-foreground">Homework statistics aggregated across all classes in a grade.</p>
|
||||
<h2 className="text-2xl font-bold tracking-tight">{t("classManagement.grade.insights.title")}</h2>
|
||||
<p className="text-muted-foreground">{t("classManagement.grade.insights.description")}</p>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="text-base">Filters</CardTitle>
|
||||
<CardTitle className="text-base">{t("classManagement.grade.insights.filters")}</CardTitle>
|
||||
<Badge variant="secondary" className="tabular-nums">
|
||||
{grades.length}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form action="/management/grade/insights" method="get" className="flex flex-col gap-3 md:flex-row md:items-center">
|
||||
<label htmlFor="gradeId" className="text-sm font-medium">Grade</label>
|
||||
<label htmlFor="gradeId" className="text-sm font-medium">{t("classManagement.grade.insights.grade")}</label>
|
||||
<select
|
||||
id="gradeId"
|
||||
name="gradeId"
|
||||
defaultValue={selected || "all"}
|
||||
className="h-10 w-full rounded-md border bg-background px-3 text-sm md:w-[360px]"
|
||||
>
|
||||
<option value="all">Select a grade</option>
|
||||
<option value="all">{t("classManagement.grade.insights.selectGrade")}</option>
|
||||
{grades.map((g) => (
|
||||
<option key={g.id} value={g.id}>
|
||||
{g.school.name} / {g.name}
|
||||
@@ -85,7 +90,7 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc
|
||||
))}
|
||||
</select>
|
||||
<Button type="submit" className="md:ml-2">
|
||||
Apply
|
||||
{t("classManagement.grade.insights.apply")}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
@@ -94,47 +99,47 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc
|
||||
{!selected ? (
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title="Select a grade to view insights"
|
||||
description="Pick a grade to see latest homework and historical score statistics."
|
||||
title={t("classManagement.grade.insights.selectToView")}
|
||||
description={t("classManagement.grade.insights.selectToViewDescription")}
|
||||
className="h-[360px] bg-card"
|
||||
/>
|
||||
) : !insights ? (
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title="Grade not found"
|
||||
description="This grade may not exist or has no accessible data."
|
||||
title={t("classManagement.grade.insights.notFound")}
|
||||
description={t("classManagement.grade.insights.notFoundDescription")}
|
||||
className="h-[360px] bg-card"
|
||||
/>
|
||||
) : insights.assignments.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={BarChart3}
|
||||
title="No homework data for this grade"
|
||||
description="No homework assignments were targeted to students in this grade yet."
|
||||
title={t("classManagement.grade.insights.noData")}
|
||||
description={t("classManagement.grade.insights.noDataDescription")}
|
||||
className="h-[360px] bg-card"
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<StatCard
|
||||
title="Classes"
|
||||
title={t("classManagement.grade.insights.classes")}
|
||||
value={insights.classCount}
|
||||
description={`${insights.grade.school.name} / ${insights.grade.name}`}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
<StatCard
|
||||
title="Students"
|
||||
title={t("classManagement.grade.insights.students")}
|
||||
value={insights.studentCounts.total}
|
||||
description={`Active ${insights.studentCounts.active} • Inactive ${insights.studentCounts.inactive}`}
|
||||
description={`${t("classManagement.grade.insights.active")} ${insights.studentCounts.active} • ${t("classManagement.grade.insights.inactive")} ${insights.studentCounts.inactive}`}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
<StatCard
|
||||
title="Overall Avg"
|
||||
title={t("classManagement.grade.insights.overallAvg")}
|
||||
value={formatScore(insights.overallScores.avg)}
|
||||
description="Across graded homework"
|
||||
description="-"
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
<StatCard
|
||||
title="Latest Avg"
|
||||
title={t("classManagement.grade.insights.latestAvg")}
|
||||
value={formatScore(insights.latest?.scoreStats.avg ?? null)}
|
||||
description={insights.latest ? insights.latest.title : "-"}
|
||||
valueClassName="tabular-nums"
|
||||
@@ -143,7 +148,7 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="text-base">Homework timeline</CardTitle>
|
||||
<CardTitle className="text-base">{t("classManagement.grade.insights.homeworkTimeline")}</CardTitle>
|
||||
<Badge variant="secondary" className="tabular-nums">
|
||||
{insights.assignments.length}
|
||||
</Badge>
|
||||
@@ -153,14 +158,14 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead>Assignment</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead className="text-right">Targeted</TableHead>
|
||||
<TableHead className="text-right">Submitted</TableHead>
|
||||
<TableHead className="text-right">Graded</TableHead>
|
||||
<TableHead className="text-right">Avg</TableHead>
|
||||
<TableHead className="text-right">Median</TableHead>
|
||||
<TableHead>{t("classManagement.grade.insights.assignment")}</TableHead>
|
||||
<TableHead>{t("classManagement.grade.insights.status")}</TableHead>
|
||||
<TableHead>{t("classManagement.grade.insights.created")}</TableHead>
|
||||
<TableHead className="text-right">{t("classManagement.grade.insights.targeted")}</TableHead>
|
||||
<TableHead className="text-right">{t("classManagement.grade.insights.submitted")}</TableHead>
|
||||
<TableHead className="text-right">{t("classManagement.grade.insights.graded")}</TableHead>
|
||||
<TableHead className="text-right">{t("classManagement.grade.insights.avg")}</TableHead>
|
||||
<TableHead className="text-right">{t("classManagement.grade.insights.median")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -188,7 +193,7 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="text-base">Class ranking</CardTitle>
|
||||
<CardTitle className="text-base">{t("classManagement.grade.insights.classRanking")}</CardTitle>
|
||||
<Badge variant="secondary" className="tabular-nums">
|
||||
{insights.classes.length}
|
||||
</Badge>
|
||||
@@ -198,12 +203,12 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead>Class</TableHead>
|
||||
<TableHead className="text-right">Students</TableHead>
|
||||
<TableHead className="text-right">Latest Avg</TableHead>
|
||||
<TableHead className="text-right">Prev Avg</TableHead>
|
||||
<TableHead className="text-right">Δ</TableHead>
|
||||
<TableHead className="text-right">Overall Avg</TableHead>
|
||||
<TableHead>{t("classManagement.grade.insights.class")}</TableHead>
|
||||
<TableHead className="text-right">{t("classManagement.grade.insights.students")}</TableHead>
|
||||
<TableHead className="text-right">{t("classManagement.grade.insights.latestAvgCol")}</TableHead>
|
||||
<TableHead className="text-right">{t("classManagement.grade.insights.prevAvg")}</TableHead>
|
||||
<TableHead className="text-right">{t("classManagement.grade.insights.delta")}</TableHead>
|
||||
<TableHead className="text-right">{t("classManagement.grade.insights.overallAvgCol")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -230,4 +235,3 @@ export default async function TeacherGradeInsightsPage({ searchParams }: { searc
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
27
src/app/(dashboard)/parent/diagnostic/error.tsx
Normal file
27
src/app/(dashboard)/parent/diagnostic/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 ParentDiagnosticError({
|
||||
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>
|
||||
)
|
||||
}
|
||||
32
src/app/(dashboard)/parent/diagnostic/loading.tsx
Normal file
32
src/app/(dashboard)/parent/diagnostic/loading.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
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-8 p-6 md:p-8">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Skeleton className="h-72 w-full" />
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{Array.from({ length: 2 }).map((_, j) => (
|
||||
<Skeleton key={j} className="h-40 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
70
src/app/(dashboard)/parent/diagnostic/page.tsx
Normal file
70
src/app/(dashboard)/parent/diagnostic/page.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Stethoscope } from "lucide-react"
|
||||
|
||||
import { requirePermission } from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import { getStudentMasterySummary } from "@/modules/diagnostic/data-access"
|
||||
import { getDiagnosticReports } from "@/modules/diagnostic/data-access-reports"
|
||||
import { StudentDiagnosticView } from "@/modules/diagnostic/components/student-diagnostic-view"
|
||||
import {
|
||||
ParentChildrenDataPage,
|
||||
ParentNoChildrenPage,
|
||||
} from "@/modules/parent/components/parent-children-data-page"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
export default async function ParentDiagnosticPage() {
|
||||
const ctx = await requirePermission(Permissions.DIAGNOSTIC_READ)
|
||||
|
||||
if (ctx.dataScope.type !== "children" || ctx.dataScope.childrenIds.length === 0) {
|
||||
return (
|
||||
<ParentNoChildrenPage
|
||||
title="Children Diagnostic"
|
||||
description="View your children's knowledge point mastery and diagnostic reports."
|
||||
icon={Stethoscope}
|
||||
emptyTitle="No children linked"
|
||||
emptyDescription="Your account is not linked to any student accounts yet. Please contact the school administrator."
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// 使用 allSettled 容错:单个子女查询失败不影响其他子女展示
|
||||
const results = await Promise.allSettled(
|
||||
ctx.dataScope.childrenIds.map(async (id) => {
|
||||
const [summary, reports] = await Promise.all([
|
||||
getStudentMasterySummary(id),
|
||||
getDiagnosticReports({ studentId: id }),
|
||||
])
|
||||
return { summary, reports, studentId: id }
|
||||
}),
|
||||
)
|
||||
const validItems = results
|
||||
.filter(
|
||||
(r): r is PromiseFulfilledResult<{
|
||||
summary: Awaited<ReturnType<typeof getStudentMasterySummary>>
|
||||
reports: Awaited<ReturnType<typeof getDiagnosticReports>>
|
||||
studentId: string
|
||||
}> => r.status === "fulfilled",
|
||||
)
|
||||
.map((r) => r.value)
|
||||
|
||||
return (
|
||||
<ParentChildrenDataPage
|
||||
title="Children Diagnostic"
|
||||
description="View knowledge point mastery and diagnostic reports for all your children."
|
||||
icon={Stethoscope}
|
||||
noRecordsTitle="No diagnostic data"
|
||||
noRecordsDescription="Your children don't have any diagnostic data yet."
|
||||
items={validItems}
|
||||
renderItem={({ summary, reports }) => (
|
||||
<>
|
||||
<div className="border-b pb-2">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{summary?.studentName ?? "Unknown student"}
|
||||
</h3>
|
||||
</div>
|
||||
<StudentDiagnosticView summary={summary} reports={reports} />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +1,21 @@
|
||||
import { getAuthContext } from "@/shared/lib/auth-guard"
|
||||
import { requirePermission } from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import { getStudentGradeSummary } from "@/modules/grades/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 { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import { UserX } from "lucide-react"
|
||||
import { getParam, type SearchParams } from "@/shared/lib/search-params"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
type SearchParams = { [key: string]: string | string[] | undefined }
|
||||
|
||||
const getParam = (params: SearchParams, key: string) => {
|
||||
const v = params[key]
|
||||
return Array.isArray(v) ? v[0] : v
|
||||
}
|
||||
|
||||
export default async function StudentGradesPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<SearchParams>
|
||||
}) {
|
||||
const ctx = await getAuthContext()
|
||||
const ctx = await requirePermission(Permissions.GRADE_RECORD_READ)
|
||||
const [sp, summary] = await Promise.all([
|
||||
searchParams,
|
||||
getStudentGradeSummary(ctx.userId),
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { AlertCircle } from "lucide-react"
|
||||
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
|
||||
export default function DiagnosticClassError({
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Card, CardContent, CardHeader } from "@/shared/components/ui/card"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-40" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-72 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-40" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-8 w-full" />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
src/app/(dashboard)/teacher/diagnostic/error.tsx
Normal file
27
src/app/(dashboard)/teacher/diagnostic/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 TeacherDiagnosticError({
|
||||
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>
|
||||
)
|
||||
}
|
||||
23
src/app/(dashboard)/teacher/diagnostic/loading.tsx
Normal file
23
src/app/(dashboard)/teacher/diagnostic/loading.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Card, CardContent, CardHeader } from "@/shared/components/ui/card"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-40" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { AlertCircle } from "lucide-react"
|
||||
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
|
||||
export default function DiagnosticStudentError({
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Card, CardContent, CardHeader } from "@/shared/components/ui/card"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-40" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-72 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-40" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-8 w-full" />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
src/app/(dashboard)/teacher/grades/analytics/error.tsx
Normal file
27
src/app/(dashboard)/teacher/grades/analytics/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 TeacherGradesAnalyticsError({
|
||||
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>
|
||||
)
|
||||
}
|
||||
31
src/app/(dashboard)/teacher/grades/analytics/loading.tsx
Normal file
31
src/app/(dashboard)/teacher/grades/analytics/loading.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Card, CardContent, CardHeader } from "@/shared/components/ui/card"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-40" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-40" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
src/app/(dashboard)/teacher/grades/entry/error.tsx
Normal file
27
src/app/(dashboard)/teacher/grades/entry/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 TeacherGradesEntryError({
|
||||
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>
|
||||
)
|
||||
}
|
||||
23
src/app/(dashboard)/teacher/grades/entry/loading.tsx
Normal file
23
src/app/(dashboard)/teacher/grades/entry/loading.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Card, CardContent, CardHeader } from "@/shared/components/ui/card"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-40" />
|
||||
<Skeleton className="h-4 w-56" />
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-48" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
src/app/(dashboard)/teacher/grades/error.tsx
Normal file
27
src/app/(dashboard)/teacher/grades/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 TeacherGradesError({
|
||||
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>
|
||||
)
|
||||
}
|
||||
28
src/app/(dashboard)/teacher/grades/loading.tsx
Normal file
28
src/app/(dashboard)/teacher/grades/loading.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Card, CardContent, CardHeader } from "@/shared/components/ui/card"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-40" />
|
||||
<Skeleton className="h-4 w-56" />
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Skeleton className="h-9 w-32" />
|
||||
<Skeleton className="h-9 w-32" />
|
||||
<Skeleton className="h-9 w-32" />
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-48" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-10 w-full" />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
src/app/(dashboard)/teacher/grades/stats/error.tsx
Normal file
27
src/app/(dashboard)/teacher/grades/stats/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 TeacherGradesStatsError({
|
||||
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>
|
||||
)
|
||||
}
|
||||
33
src/app/(dashboard)/teacher/grades/stats/loading.tsx
Normal file
33
src/app/(dashboard)/teacher/grades/stats/loading.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Card, CardContent, CardHeader } from "@/shared/components/ui/card"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-40" />
|
||||
<Skeleton className="h-4 w-56" />
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-8 w-20" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-48" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user