feat(dashboard): 新增分区 Error Boundary + Suspense 骨架屏(P2)
新增 components/dashboard-section.tsx,包含: - DashboardSectionErrorBoundary:分区级 Error Boundary,单区块崩溃仅替换该区块不波及整页 - DashboardSectionSkeleton:5 种骨架变体(stats/card/chart/table/list),匹配不同数据区块布局 - DashboardSection:组合 Error Boundary + Suspense + 骨架屏的包装器 将 admin/teacher/student 三个仪表盘视图的每个独立数据区块用 DashboardSection 包裹,i18n 补充 sectionLoadFailed/sectionLoadFailedDesc 翻译键,同步更新架构图 004/005 文档
This commit is contained in:
@@ -26,6 +26,7 @@ import { Button } from "@/shared/components/ui/button"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table"
|
||||
import { formatDate } from "@/shared/lib/utils"
|
||||
import { DashboardSection } from "../dashboard-section"
|
||||
import { UserGrowthChart } from "./user-growth-chart"
|
||||
|
||||
export async function AdminDashboardView({ data }: { data: AdminDashboardData }) {
|
||||
@@ -62,12 +63,14 @@ export async function AdminDashboardView({ data }: { data: AdminDashboardData })
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard title={t("stats.users")} value={data.userCount} icon={Users} valueClassName="tabular-nums" />
|
||||
<StatCard title={t("stats.classes")} value={data.classCount} icon={LayoutDashboard} valueClassName="tabular-nums" />
|
||||
<StatCard title={t("stats.homeworkPublished")} value={data.homeworkAssignmentPublishedCount} icon={ClipboardList} valueClassName="tabular-nums" />
|
||||
<StatCard title={t("stats.toGrade")} value={data.homeworkSubmissionToGradeCount} icon={FileText} valueClassName="tabular-nums" />
|
||||
</div>
|
||||
<DashboardSection variant="stats">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard title={t("stats.users")} value={data.userCount} icon={Users} valueClassName="tabular-nums" />
|
||||
<StatCard title={t("stats.classes")} value={data.classCount} icon={LayoutDashboard} valueClassName="tabular-nums" />
|
||||
<StatCard title={t("stats.homeworkPublished")} value={data.homeworkAssignmentPublishedCount} icon={ClipboardList} valueClassName="tabular-nums" />
|
||||
<StatCard title={t("stats.toGrade")} value={data.homeworkSubmissionToGradeCount} icon={FileText} valueClassName="tabular-nums" />
|
||||
</div>
|
||||
</DashboardSection>
|
||||
|
||||
{/* 快捷操作 */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -111,108 +114,120 @@ export async function AdminDashboardView({ data }: { data: AdminDashboardData })
|
||||
|
||||
{/* 趋势图表 */}
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t("sections.userGrowthTrend")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<UserGrowthChart data={data.userGrowth} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t("sections.homeworkSubmissionTrend")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<UserGrowthChart data={data.homeworkTrend} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DashboardSection variant="chart">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t("sections.userGrowthTrend")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<UserGrowthChart data={data.userGrowth} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DashboardSection>
|
||||
<DashboardSection variant="chart">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">{t("sections.homeworkSubmissionTrend")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<UserGrowthChart data={data.homeworkTrend} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DashboardSection>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("sections.userRoles")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{data.userRoleCounts.length === 0 ? (
|
||||
<EmptyState title={t("empty.noUsers")} description={t("empty.noUsersDesc")} />
|
||||
) : (
|
||||
data.userRoleCounts.map((r) => (
|
||||
<div key={r.role} className="flex items-center justify-between">
|
||||
<Badge variant="secondary">{r.role}</Badge>
|
||||
<div className="text-sm font-medium tabular-nums">{r.count}</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DashboardSection variant="card">
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("sections.userRoles")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{data.userRoleCounts.length === 0 ? (
|
||||
<EmptyState title={t("empty.noUsers")} description={t("empty.noUsersDesc")} />
|
||||
) : (
|
||||
data.userRoleCounts.map((r) => (
|
||||
<div key={r.role} className="flex items-center justify-between">
|
||||
<Badge variant="secondary">{r.role}</Badge>
|
||||
<div className="text-sm font-medium tabular-nums">{r.count}</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DashboardSection>
|
||||
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("sections.content")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3">
|
||||
<ContentRow label={t("stats.users")} value={data.textbookCount} icon={<Library className="h-4 w-4 text-muted-foreground" />} />
|
||||
<ContentRow label={t("stats.classes")} value={data.chapterCount} icon={<BookOpen className="h-4 w-4 text-muted-foreground" />} />
|
||||
<ContentRow label={t("stats.toGrade")} value={data.questionCount} icon={<FileText className="h-4 w-4 text-muted-foreground" />} />
|
||||
<ContentRow label={t("stats.homeworkPublished")} value={data.examCount} icon={<ClipboardList className="h-4 w-4 text-muted-foreground" />} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DashboardSection variant="card">
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("sections.content")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3">
|
||||
<ContentRow label={t("stats.users")} value={data.textbookCount} icon={<Library className="h-4 w-4 text-muted-foreground" />} />
|
||||
<ContentRow label={t("stats.classes")} value={data.chapterCount} icon={<BookOpen className="h-4 w-4 text-muted-foreground" />} />
|
||||
<ContentRow label={t("stats.toGrade")} value={data.questionCount} icon={<FileText className="h-4 w-4 text-muted-foreground" />} />
|
||||
<ContentRow label={t("stats.homeworkPublished")} value={data.examCount} icon={<ClipboardList className="h-4 w-4 text-muted-foreground" />} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DashboardSection>
|
||||
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("sections.homeworkActivity")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3">
|
||||
<ContentRow label={t("stats.activeAssignments")} value={data.homeworkAssignmentCount} icon={<ClipboardList className="h-4 w-4 text-muted-foreground" />} />
|
||||
<ContentRow label={t("stats.submissionRate")} value={data.homeworkSubmissionCount} icon={<FileText className="h-4 w-4 text-muted-foreground" />} />
|
||||
<ContentRow label={t("stats.toGrade")} value={data.homeworkSubmissionToGradeCount} icon={<Activity className="h-4 w-4 text-muted-foreground" />} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DashboardSection variant="card">
|
||||
<Card className="lg:col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>{t("sections.homeworkActivity")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3">
|
||||
<ContentRow label={t("stats.activeAssignments")} value={data.homeworkAssignmentCount} icon={<ClipboardList className="h-4 w-4 text-muted-foreground" />} />
|
||||
<ContentRow label={t("stats.submissionRate")} value={data.homeworkSubmissionCount} icon={<FileText className="h-4 w-4 text-muted-foreground" />} />
|
||||
<ContentRow label={t("stats.toGrade")} value={data.homeworkSubmissionToGradeCount} icon={<Activity className="h-4 w-4 text-muted-foreground" />} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DashboardSection>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("sections.recentUsers")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data.recentUsers.length === 0 ? (
|
||||
<EmptyState title={t("empty.noUsersYet")} description={t("empty.seedHint")} />
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("table.name")}</TableHead>
|
||||
<TableHead>{t("table.email")}</TableHead>
|
||||
<TableHead>{t("table.role")}</TableHead>
|
||||
<TableHead>{t("table.created")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.recentUsers.map((u) => (
|
||||
<TableRow key={u.id}>
|
||||
<TableCell className="font-medium">{u.name || "-"}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{u.email}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{u.role ?? "unknown"}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{formatDate(u.createdAt)}</TableCell>
|
||||
<DashboardSection variant="table">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t("sections.recentUsers")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{data.recentUsers.length === 0 ? (
|
||||
<EmptyState title={t("empty.noUsersYet")} description={t("empty.seedHint")} />
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("table.name")}</TableHead>
|
||||
<TableHead>{t("table.email")}</TableHead>
|
||||
<TableHead>{t("table.role")}</TableHead>
|
||||
<TableHead>{t("table.created")}</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link href="/admin/users">
|
||||
{t("sections.viewAllUsers")}
|
||||
<ChevronRight className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.recentUsers.map((u) => (
|
||||
<TableRow key={u.id}>
|
||||
<TableCell className="font-medium">{u.name || "-"}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{u.email}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{u.role ?? "unknown"}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{formatDate(u.createdAt)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<div className="flex justify-end pt-4">
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link href="/admin/users">
|
||||
{t("sections.viewAllUsers")}
|
||||
<ChevronRight className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DashboardSection>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
170
src/modules/dashboard/components/dashboard-section.tsx
Normal file
170
src/modules/dashboard/components/dashboard-section.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
"use client"
|
||||
|
||||
import { Component, type ReactNode, Suspense } from "react"
|
||||
import { AlertCircle } from "lucide-react"
|
||||
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import { Card, CardContent, CardHeader } from "@/shared/components/ui/card"
|
||||
import { Skeleton } from "@/shared/components/ui/skeleton"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
/**
|
||||
* 仪表盘分区 Error Boundary
|
||||
*
|
||||
* 包裹每个独立数据区块,避免单个区块崩溃导致整页不可用。
|
||||
* 与路由级 error.tsx 不同,此组件仅替换出错区块,其余区块继续渲染。
|
||||
*/
|
||||
export class DashboardSectionErrorBoundary extends Component<
|
||||
{ children: ReactNode },
|
||||
{ hasError: boolean }
|
||||
> {
|
||||
state: { hasError: boolean } = { hasError: false }
|
||||
|
||||
static getDerivedStateFromError(): { hasError: boolean } {
|
||||
return { hasError: true }
|
||||
}
|
||||
|
||||
handleRetry = (): void => {
|
||||
this.setState({ hasError: false })
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.hasError) {
|
||||
return <DashboardSectionErrorFallback onRetry={this.handleRetry} />
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
function DashboardSectionErrorFallback({
|
||||
onRetry,
|
||||
}: {
|
||||
onRetry: () => void
|
||||
}): ReactNode {
|
||||
const t = useTranslations("dashboard.error")
|
||||
return (
|
||||
<EmptyState
|
||||
icon={AlertCircle}
|
||||
title={t("sectionLoadFailed")}
|
||||
description={t("sectionLoadFailedDesc")}
|
||||
action={{ label: t("retry"), onClick: onRetry }}
|
||||
className="h-auto border-none shadow-none"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 分区骨架屏变体
|
||||
*
|
||||
* 不同数据区块使用不同骨架布局,提供更贴近真实内容的加载占位。
|
||||
*/
|
||||
type SkeletonVariant = "stats" | "card" | "chart" | "table" | "list"
|
||||
|
||||
export function DashboardSectionSkeleton({
|
||||
variant = "card",
|
||||
}: {
|
||||
variant?: SkeletonVariant
|
||||
}): ReactNode {
|
||||
switch (variant) {
|
||||
case "stats":
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader className="pb-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-8 w-16" />
|
||||
<Skeleton className="mt-2 h-3 w-28" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
case "chart":
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-[200px] w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
case "table":
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-10 w-full" />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
case "list":
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-3">
|
||||
<Skeleton className="h-9 w-9 rounded-full" />
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<Skeleton className="h-3 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
case "card":
|
||||
default:
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-5/6" />
|
||||
<Skeleton className="h-4 w-4/6" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 仪表盘分区包装器
|
||||
*
|
||||
* 组合 Error Boundary + Suspense + 骨架屏,包裹每个独立数据区块。
|
||||
* 单个区块出错或加载中时,仅影响该区块,不波及整页。
|
||||
*
|
||||
* @example
|
||||
* <DashboardSection variant="stats">
|
||||
* <TeacherStats ... />
|
||||
* </DashboardSection>
|
||||
*/
|
||||
export function DashboardSection({
|
||||
children,
|
||||
variant = "card",
|
||||
}: {
|
||||
children: ReactNode
|
||||
variant?: SkeletonVariant
|
||||
}): ReactNode {
|
||||
return (
|
||||
<DashboardSectionErrorBoundary>
|
||||
<Suspense fallback={<DashboardSectionSkeleton variant={variant} />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</DashboardSectionErrorBoundary>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { StudentDashboardProps } from "@/modules/dashboard/types"
|
||||
|
||||
import { DashboardSection } from "../dashboard-section"
|
||||
import { StudentDashboardHeader } from "./student-dashboard-header"
|
||||
import { StudentGradesCard } from "./student-grades-card"
|
||||
import { StudentStatsGrid } from "./student-stats-grid"
|
||||
@@ -20,21 +21,29 @@ export async function StudentDashboard({
|
||||
<div className="space-y-6">
|
||||
<StudentDashboardHeader studentName={studentName} />
|
||||
|
||||
<StudentStatsGrid
|
||||
enrolledClassCount={enrolledClassCount}
|
||||
dueSoonCount={dueSoonCount}
|
||||
overdueCount={overdueCount}
|
||||
gradedCount={gradedCount}
|
||||
ranking={grades.ranking}
|
||||
/>
|
||||
<DashboardSection variant="stats">
|
||||
<StudentStatsGrid
|
||||
enrolledClassCount={enrolledClassCount}
|
||||
dueSoonCount={dueSoonCount}
|
||||
overdueCount={overdueCount}
|
||||
gradedCount={gradedCount}
|
||||
ranking={grades.ranking}
|
||||
/>
|
||||
</DashboardSection>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<StudentUpcomingAssignmentsCard upcomingAssignments={upcomingAssignments} />
|
||||
<StudentGradesCard grades={grades} />
|
||||
<DashboardSection variant="list">
|
||||
<StudentUpcomingAssignmentsCard upcomingAssignments={upcomingAssignments} />
|
||||
</DashboardSection>
|
||||
<DashboardSection variant="card">
|
||||
<StudentGradesCard grades={grades} />
|
||||
</DashboardSection>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<StudentTodayScheduleCard items={todayScheduleItems} />
|
||||
<DashboardSection variant="card">
|
||||
<StudentTodayScheduleCard items={todayScheduleItems} />
|
||||
</DashboardSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { TeacherDashboardMetrics } from "@/modules/dashboard/lib/dashboard-
|
||||
import { getTranslations } from "next-intl/server"
|
||||
import type { TeacherTodoItem } from "./teacher-todo-card"
|
||||
|
||||
import { DashboardSection } from "../dashboard-section"
|
||||
import { TeacherClassesCard } from "./teacher-classes-card"
|
||||
import { TeacherDashboardHeader } from "./teacher-dashboard-header"
|
||||
import { TeacherHomeworkCard } from "./teacher-homework-card"
|
||||
@@ -46,35 +47,51 @@ export async function TeacherDashboardView({ data }: TeacherDashboardViewProps)
|
||||
<div className="flex h-full flex-col space-y-6 p-8">
|
||||
<TeacherDashboardHeader teacherName={data.teacherName} />
|
||||
|
||||
<TeacherStats
|
||||
toGradeCount={metrics.toGradeCount}
|
||||
activeAssignmentsCount={metrics.activeAssignmentsCount}
|
||||
averageScore={metrics.averageScore}
|
||||
submissionRate={metrics.submissionRate}
|
||||
/>
|
||||
<DashboardSection variant="stats">
|
||||
<TeacherStats
|
||||
toGradeCount={metrics.toGradeCount}
|
||||
activeAssignmentsCount={metrics.activeAssignmentsCount}
|
||||
averageScore={metrics.averageScore}
|
||||
submissionRate={metrics.submissionRate}
|
||||
/>
|
||||
</DashboardSection>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-12">
|
||||
{/* 移动端优先展示:今日课表 → 待办 → 待批改 */}
|
||||
<div className="flex flex-col gap-6 lg:col-span-8">
|
||||
<div className="lg:hidden">
|
||||
<TeacherSchedule items={metrics.todayScheduleItems} />
|
||||
<DashboardSection variant="card">
|
||||
<TeacherSchedule items={metrics.todayScheduleItems} />
|
||||
</DashboardSection>
|
||||
</div>
|
||||
<TeacherTodoCard items={todoItems} />
|
||||
<TeacherGradeTrends trends={data.gradeTrends} />
|
||||
<RecentSubmissions
|
||||
submissions={metrics.submissionsToGrade}
|
||||
title={t("sections.pendingGrading")}
|
||||
emptyTitle={t("empty.allGraded")}
|
||||
emptyDescription={t("empty.allGradedDesc")}
|
||||
/>
|
||||
<DashboardSection variant="card">
|
||||
<TeacherTodoCard items={todoItems} />
|
||||
</DashboardSection>
|
||||
<DashboardSection variant="chart">
|
||||
<TeacherGradeTrends trends={data.gradeTrends} />
|
||||
</DashboardSection>
|
||||
<DashboardSection variant="list">
|
||||
<RecentSubmissions
|
||||
submissions={metrics.submissionsToGrade}
|
||||
title={t("sections.pendingGrading")}
|
||||
emptyTitle={t("empty.allGraded")}
|
||||
emptyDescription={t("empty.allGradedDesc")}
|
||||
/>
|
||||
</DashboardSection>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6 lg:col-span-4">
|
||||
<div className="hidden lg:block">
|
||||
<TeacherSchedule items={metrics.todayScheduleItems} />
|
||||
<DashboardSection variant="card">
|
||||
<TeacherSchedule items={metrics.todayScheduleItems} />
|
||||
</DashboardSection>
|
||||
</div>
|
||||
<TeacherHomeworkCard assignments={data.assignments} />
|
||||
<TeacherClassesCard classes={data.classes} />
|
||||
<DashboardSection variant="list">
|
||||
<TeacherHomeworkCard assignments={data.assignments} />
|
||||
</DashboardSection>
|
||||
<DashboardSection variant="list">
|
||||
<TeacherClassesCard classes={data.classes} />
|
||||
</DashboardSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +115,9 @@
|
||||
"error": {
|
||||
"loadFailed": "Page load failed",
|
||||
"loadFailedDesc": "Sorry, an unexpected error occurred while loading the page. Please try again later.",
|
||||
"retry": "Retry"
|
||||
"retry": "Retry",
|
||||
"sectionLoadFailed": "Section load failed",
|
||||
"sectionLoadFailedDesc": "An error occurred while loading this section. Please retry."
|
||||
},
|
||||
"chart": {
|
||||
"newUsers": "New users",
|
||||
|
||||
@@ -115,7 +115,9 @@
|
||||
"error": {
|
||||
"loadFailed": "页面加载失败",
|
||||
"loadFailedDesc": "抱歉,页面加载时发生了意外错误。请稍后重试。",
|
||||
"retry": "重试"
|
||||
"retry": "重试",
|
||||
"sectionLoadFailed": "区块加载失败",
|
||||
"sectionLoadFailedDesc": "该数据区块加载时出错,请重试。"
|
||||
},
|
||||
"chart": {
|
||||
"newUsers": "新增用户",
|
||||
|
||||
Reference in New Issue
Block a user