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 条目
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
import { use } from "react"
|
||
import { getTranslations } from "next-intl/server"
|
||
import { UserX } from "lucide-react"
|
||
|
||
import type { ActionState } from "@/shared/types/action-state"
|
||
import type { StudentDashboardProps } from "@/modules/dashboard/types"
|
||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||
|
||
import { DashboardSection } from "../dashboard-section"
|
||
import { StudentDashboardHeader } from "./student-dashboard-header"
|
||
import { StudentGradesCard } from "./student-grades-card"
|
||
import { StudentStatsGrid } from "./student-stats-grid"
|
||
import { StudentTodayScheduleCard } from "./student-today-schedule-card"
|
||
import { StudentUpcomingAssignmentsCard } from "./student-upcoming-assignments-card"
|
||
|
||
type StudentDashboardResult = ActionState<{
|
||
student: { id: string; name: string } | null
|
||
dashboardProps: Omit<StudentDashboardProps, "studentName"> | null
|
||
}>
|
||
|
||
/**
|
||
* 学生仪表盘视图(P2-1 流式架构)
|
||
*
|
||
* 接收未解析的 Promise,用 React `use()` 消费。
|
||
* 页面外壳立即渲染,数据到达后在 DashboardSection 的 Suspense 边界内填充。
|
||
*/
|
||
export function StudentDashboard({ dataPromise }: { dataPromise: Promise<StudentDashboardResult> }) {
|
||
const result = use(dataPromise)
|
||
if (!result.success || !result.data) {
|
||
throw new Error(result.message ?? "Failed to load student dashboard")
|
||
}
|
||
|
||
return <StudentDashboardBody result={result.data} />
|
||
}
|
||
|
||
async function StudentDashboardBody({
|
||
result,
|
||
}: {
|
||
result: NonNullable<StudentDashboardResult["data"]>
|
||
}) {
|
||
const t = await getTranslations("dashboard")
|
||
|
||
if (!result.student || !result.dashboardProps) {
|
||
return (
|
||
<EmptyState
|
||
title={t("empty.noStudent")}
|
||
description={t("empty.noStudentDesc")}
|
||
icon={UserX}
|
||
className="border-none shadow-none h-auto"
|
||
/>
|
||
)
|
||
}
|
||
|
||
const { student, dashboardProps } = result
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<header>
|
||
<StudentDashboardHeader studentName={student.name} />
|
||
</header>
|
||
|
||
<DashboardSection variant="stats">
|
||
<StudentStatsGrid
|
||
enrolledClassCount={dashboardProps.enrolledClassCount}
|
||
dueSoonCount={dashboardProps.dueSoonCount}
|
||
overdueCount={dashboardProps.overdueCount}
|
||
gradedCount={dashboardProps.gradedCount}
|
||
ranking={dashboardProps.grades.ranking}
|
||
/>
|
||
</DashboardSection>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
<section
|
||
aria-label={t("sections.upcomingAssignments")}
|
||
className="lg:col-span-2 space-y-6"
|
||
>
|
||
<DashboardSection variant="list">
|
||
<StudentUpcomingAssignmentsCard upcomingAssignments={dashboardProps.upcomingAssignments} />
|
||
</DashboardSection>
|
||
<DashboardSection variant="card">
|
||
<StudentGradesCard grades={dashboardProps.grades} />
|
||
</DashboardSection>
|
||
</section>
|
||
<aside aria-label={t("sections.todaySchedule")} className="space-y-6">
|
||
<DashboardSection variant="card">
|
||
<StudentTodayScheduleCard items={dashboardProps.todayScheduleItems} />
|
||
</DashboardSection>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|