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 条目
101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
"use client"
|
|
|
|
import { useMemo } from "react"
|
|
import Link from "next/link"
|
|
import { CalendarDays, CalendarX } from "lucide-react"
|
|
import { useTranslations } from "next-intl"
|
|
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { Badge } from "@/shared/components/ui/badge"
|
|
import { ScheduleList } from "@/shared/components/schedule/schedule-list"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
|
import { EmptyState } from "@/shared/components/ui/empty-state"
|
|
import { useCurrentTime } from "@/shared/hooks"
|
|
import { cn } from "@/shared/lib/utils"
|
|
import type { StudentTodayScheduleItem } from "@/modules/dashboard/types"
|
|
|
|
const timeToMinutes = (t: string): number => {
|
|
const [h, m] = t.split(":").map(Number)
|
|
return (h ?? 0) * 60 + (m ?? 0)
|
|
}
|
|
|
|
export function StudentTodayScheduleCard({ items }: { items: StudentTodayScheduleItem[] }) {
|
|
const t = useTranslations("dashboard")
|
|
const hasSchedule = items.length > 0
|
|
const now = useCurrentTime()
|
|
|
|
const { currentId, nextId } = useMemo(() => {
|
|
const nowMin = now.getHours() * 60 + now.getMinutes()
|
|
let currentId: string | null = null
|
|
let nextId: string | null = null
|
|
for (const item of items) {
|
|
const start = timeToMinutes(item.startTime)
|
|
const end = timeToMinutes(item.endTime)
|
|
if (nowMin >= start && nowMin < end) {
|
|
currentId = item.id
|
|
break
|
|
}
|
|
if (nowMin < start) {
|
|
nextId = item.id
|
|
break
|
|
}
|
|
}
|
|
return { currentId, nextId }
|
|
}, [items, now])
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<CalendarDays className="h-4 w-4 text-muted-foreground" />
|
|
{t("sections.todaySchedule")}
|
|
</CardTitle>
|
|
<Button asChild variant="outline" size="sm">
|
|
<Link href="/student/schedule">{t("quickActions.viewAll")}</Link>
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{!hasSchedule ? (
|
|
<EmptyState
|
|
icon={CalendarX}
|
|
title={t("empty.noClassesToday")}
|
|
description={t("empty.noClassesTodayDesc")}
|
|
action={{ label: t("quickActions.viewSchedule"), href: "/student/schedule" }}
|
|
className="border-none h-72"
|
|
/>
|
|
) : (
|
|
<ScheduleList
|
|
items={items}
|
|
variant="separator"
|
|
spacingClassName="space-y-4"
|
|
renderTrailing={(item) => {
|
|
const isCurrent = item.id === currentId
|
|
const isNext = item.id === nextId
|
|
if (isCurrent) {
|
|
return (
|
|
<Badge className="shrink-0 bg-emerald-500 text-white hover:bg-emerald-500">
|
|
{t("badge.inProgress")}
|
|
</Badge>
|
|
)
|
|
}
|
|
if (isNext) {
|
|
return (
|
|
<Badge variant="outline" className="shrink-0 border-primary text-primary">
|
|
{t("badge.upNext")}
|
|
</Badge>
|
|
)
|
|
}
|
|
return item.className ? (
|
|
<Badge variant="secondary" className="shrink-0">
|
|
{item.className}
|
|
</Badge>
|
|
) : null
|
|
}}
|
|
className={cn(currentId && "[&_div:first-child]:bg-emerald-50/50")}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|