Files
NextEdu/src/modules/dashboard/components/teacher-dashboard/teacher-dashboard-view.tsx
SpecialX 2c0f81391b feat(dashboard): 实现所有长期问题修复(P2-1/P2-5/P2-7/P2-9)
P2-9: TeacherSchedule 重复渲染优化
- 将移动端(lg:hidden)和桌面端(hidden lg:block)的双实例渲染改为单实例
- 使用 CSS flex order + grid col-start/row-start 实现响应式布局重排序
- 消除服务端 HTML 负载翻倍问题

P2-5: StudentTodayScheduleCard 时间过时修复
- 新增 useCurrentTime hook(src/shared/hooks/use-current-time.ts)
- 每分钟自动更新当前时间,useMemo 依赖 [items, now] 确保徽章不过时
- SSR 安全:初始渲染用 new Date(),挂载后 setInterval 更新

P2-1: 流式/Suspense 架构改造
- 新增 getAdminDashboardStreams(streams.ts):返回各独立数据源的未解析 Promise
- Admin dashboard:7 个分区组件用 React use() 独立消费 Promise,各 Suspense 边界独立流式渲染
- Teacher/Student/Parent dashboard:传入未解析 Promise,视图用 use() 消费,启用 Suspense 流式
- 页面外壳(标题 + 快捷操作)立即渲染,数据到达后各分区按各自速度填充

P2-7: 组件测试 + 路由测试修复
- 修复 dashboard-routing.test.ts:移除误导性的 permissions 字段(实际用 resolvePermissions(roles))
- 新增 fallback 路由测试(未知角色 → teacher dashboard)
- 新增 DashboardSection 组件测试(6 个测试:骨架屏变体 + 错误边界 + 正常渲染)
- 新增 useCurrentTime hook 测试(3 个测试:初始值 + 间隔更新 + 清理)

同步更新:
- docs/architecture/005_architecture_data.json 新增 7 个流式组件 + useCurrentTime hook + getAdminDashboardStreams 条目
2026-06-23 09:04:40 +08:00

113 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { use } from "react"
import type { TeacherDashboardData } from "@/modules/dashboard/types"
import type { TeacherDashboardMetrics } from "@/modules/dashboard/lib/dashboard-utils"
import type { ActionState } from "@/shared/types/action-state"
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"
import { RecentSubmissions } from "./recent-submissions"
import { TeacherSchedule } from "./teacher-schedule"
import { TeacherStats } from "./teacher-stats"
import { TeacherGradeTrends } from "./teacher-grade-trends"
import { TeacherTodoCard } from "./teacher-todo-card"
type TeacherDashboardResult = ActionState<TeacherDashboardData & { metrics: TeacherDashboardMetrics }>
/**
* 教师仪表盘视图P2-1 流式架构)
*
* 接收未解析的 Promise用 React `use()` 消费。
* 页面外壳立即渲染,数据到达后在 DashboardSection 的 Suspense 边界内填充。
*/
export function TeacherDashboardView({ dataPromise }: { dataPromise: Promise<TeacherDashboardResult> }) {
const result = use(dataPromise)
if (!result.success || !result.data) {
throw new Error(result.message ?? "Failed to load teacher dashboard")
}
return <TeacherDashboardContent data={result.data} />
}
async function TeacherDashboardContent({ data }: { data: TeacherDashboardData & { metrics: TeacherDashboardMetrics } }) {
const t = await getTranslations("dashboard")
const { metrics } = data
// 待办聚合(使用预计算指标)
const todoItems: TeacherTodoItem[] = [
{
label: t("todo.toGrade"),
count: metrics.toGradeCount,
href: "/teacher/homework/submissions",
variant: metrics.toGradeCount > 0 ? "urgent" : "normal",
},
{
label: t("todo.todayAttendance"),
count: metrics.todayScheduleItems.length,
href: "/teacher/attendance/sheet",
variant: "info",
},
{
label: t("todo.activeAssignments"),
count: metrics.activeAssignmentsCount,
href: "/teacher/homework/assignments",
variant: "normal",
},
]
return (
<div className="flex h-full flex-col space-y-6 p-8">
<header>
<TeacherDashboardHeader teacherName={data.teacherName} />
</header>
<DashboardSection variant="stats">
<TeacherStats
toGradeCount={metrics.toGradeCount}
activeAssignmentsCount={metrics.activeAssignmentsCount}
averageScore={metrics.averageScore}
submissionRate={metrics.submissionRate}
/>
</DashboardSection>
<div className="flex flex-col gap-6 lg:grid lg:grid-cols-12">
{/* 课表:移动端首位,桌面端右上 — 仅渲染一次P2-9 修复,原为双实例) */}
<div className="order-1 lg:col-start-9 lg:col-span-4 lg:row-start-1">
<DashboardSection variant="card">
<TeacherSchedule items={metrics.todayScheduleItems} />
</DashboardSection>
</div>
<section aria-label={t("sections.pendingGrading")} className="flex flex-col gap-6 order-2 lg:col-start-1 lg:col-span-8 lg:row-start-1 lg:row-span-2">
<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>
</section>
<aside aria-label={t("sections.myClasses")} className="flex flex-col gap-6 order-3 lg:col-start-9 lg:col-span-4 lg:row-start-2">
<DashboardSection variant="list">
<TeacherHomeworkCard assignments={data.assignments} />
</DashboardSection>
<DashboardSection variant="list">
<TeacherClassesCard classes={data.classes} />
</DashboardSection>
</aside>
</div>
</div>
)
}