P0 修复(严重):
- admin ContentRow 标签与值错配(stats.users→textbooks 等 6 处)
- admin/error.tsx 硬编码中文替换为 useTranslations
- UserGrowthChart 空数据时渲染 EmptyState(userGrowth/homeworkTrend 永远为空数组)
P1 修复(高):
- 新增 admin/dashboard 和 student/dashboard 的 loading.tsx + error.tsx
- 抽取 DashboardLoadingSkeleton 和 DashboardErrorFallback 共享组件,消除 5 套重复文件
- formatDate/formatLongDate 传入用户 locale(admin/teacher/student 共 6 个组件)
- 移除死代码:getCachedAdminDashboard、AvatarImage src={undefined}、TeacherStats isLoading prop
- filterTodaySchedule 改为泛型函数,消除 as 类型断言
- 辅助函数 getStatus/getDueUrgency 新增显式返回类型
- UserGrowthChart 新增 labelKey prop 区分用户增长/作业提交趋势标签
P2 修复(中):
- 4 个组件从客户端转为服务端组件(DashboardGreetingHeader、TeacherQuickActions、TeacherDashboardHeader、StudentDashboardHeader)
- Student dashboard 空状态新增 CTA(viewSchedule、viewAll)
- TeacherHomeworkCard 图标按钮新增 aria-label
- TeacherTodoCard 排序逻辑重写为可读的 if/return 模式
同步更新:
- docs/architecture/005_architecture_data.json 新增 DashboardLoadingSkeleton、DashboardErrorFallback 条目
- 新增 docs/architecture/audit/dashboard-audit-report-v3.md 审计报告
- dashboard.json 新增 6 个 i18n 键(textbooks/chapters/questions/exams/totalAssignments/totalSubmissions)
144 lines
5.7 KiB
TypeScript
144 lines
5.7 KiB
TypeScript
import Link from "next/link"
|
||
import { PenTool, TriangleAlert } from "lucide-react"
|
||
|
||
import { Badge } from "@/shared/components/ui/badge"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||
import { StatusBadge } from "@/shared/components/ui/status-badge"
|
||
import { cn, formatDate } from "@/shared/lib/utils"
|
||
import {
|
||
STUDENT_HOMEWORK_PROGRESS_VARIANT,
|
||
STUDENT_HOMEWORK_PROGRESS_LABEL,
|
||
} from "@/modules/homework/types"
|
||
import type { ChildHomeworkSummaryData } from "@/modules/parent/types"
|
||
|
||
type DueUrgency = "overdue" | "urgent" | "normal"
|
||
|
||
const getDueUrgency = (dueAt: string | null, now: Date): DueUrgency | null => {
|
||
if (!dueAt) return null
|
||
const due = new Date(dueAt)
|
||
const diffHours = (due.getTime() - now.getTime()) / (1000 * 60 * 60)
|
||
if (diffHours < 0) return "overdue"
|
||
if (diffHours < 48) return "urgent"
|
||
return "normal"
|
||
}
|
||
|
||
export function ChildHomeworkSummary({
|
||
summary,
|
||
childId,
|
||
childName,
|
||
}: {
|
||
summary: ChildHomeworkSummaryData
|
||
childId: string
|
||
childName: string
|
||
}) {
|
||
const hasAssignments = summary.recentAssignments.length > 0
|
||
// hoist:在组件作用域计算一次 now,避免每次 map 迭代都 new Date()
|
||
const now = new Date()
|
||
|
||
return (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2 text-base">
|
||
<PenTool className="h-4 w-4 text-muted-foreground" aria-hidden />
|
||
{childName}'s Homework
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-center">
|
||
<div className="rounded-md bg-muted/50 p-2">
|
||
<div className="text-xs text-muted-foreground">Pending</div>
|
||
<div className="text-lg font-semibold tabular-nums">{summary.pendingCount}</div>
|
||
</div>
|
||
<div className="rounded-md bg-muted/50 p-2">
|
||
<div className="text-xs text-muted-foreground">Submitted</div>
|
||
<div className="text-lg font-semibold tabular-nums">{summary.submittedCount}</div>
|
||
</div>
|
||
<div className="rounded-md bg-muted/50 p-2">
|
||
<div className="text-xs text-muted-foreground">Graded</div>
|
||
<div className="text-lg font-semibold tabular-nums">{summary.gradedCount}</div>
|
||
</div>
|
||
<div className="rounded-md bg-muted/50 p-2">
|
||
<div className="text-xs text-muted-foreground flex items-center justify-center gap-1">
|
||
<TriangleAlert className="h-3 w-3" aria-hidden />
|
||
Overdue
|
||
</div>
|
||
<div
|
||
className={cn(
|
||
"text-lg font-semibold tabular-nums",
|
||
summary.overdueCount > 0 && "text-destructive",
|
||
)}
|
||
>
|
||
{summary.overdueCount}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{!hasAssignments ? (
|
||
<EmptyState
|
||
icon={PenTool}
|
||
title="No assignments"
|
||
description="No homework assigned right now."
|
||
className="border-none h-48"
|
||
/>
|
||
) : (
|
||
<div className="space-y-2">
|
||
<div className="text-xs font-medium uppercase text-muted-foreground">
|
||
Recent Assignments
|
||
</div>
|
||
{summary.recentAssignments.map((a) => {
|
||
const urgency = getDueUrgency(a.dueAt, now)
|
||
const isGraded = a.progressStatus === "graded"
|
||
const scoreText = a.latestScore !== null ? `${a.latestScore} pts` : isGraded ? "Graded" : "-"
|
||
return (
|
||
<Link
|
||
key={a.id}
|
||
href={`/parent/children/${childId}?tab=homework`}
|
||
className="flex min-h-[44px] items-center justify-between rounded-md border bg-card p-3 hover:bg-muted/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||
>
|
||
<div className="min-w-0 flex-1 space-y-1">
|
||
<div className="flex items-center gap-2">
|
||
{a.subjectName ? (
|
||
<Badge variant="outline" className="text-[10px] shrink-0">
|
||
{a.subjectName}
|
||
</Badge>
|
||
) : null}
|
||
<div className="font-medium text-sm truncate">{a.title}</div>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||
<StatusBadge
|
||
status={a.progressStatus}
|
||
variantMap={STUDENT_HOMEWORK_PROGRESS_VARIANT}
|
||
labelMap={STUDENT_HOMEWORK_PROGRESS_LABEL}
|
||
className="text-[10px]"
|
||
/>
|
||
{a.dueAt ? (
|
||
<span
|
||
className={cn(
|
||
!isGraded && urgency === "overdue" && "text-destructive font-medium",
|
||
)}
|
||
>
|
||
Due {formatDate(a.dueAt)}
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
<div className="text-sm font-medium tabular-nums shrink-0 ml-2">
|
||
{scoreText}
|
||
</div>
|
||
</Link>
|
||
)
|
||
})}
|
||
<Link
|
||
href={`/parent/children/${childId}?tab=homework`}
|
||
className="block text-center text-xs text-muted-foreground hover:text-foreground transition-colors pt-1 min-h-[36px] flex items-center justify-center"
|
||
>
|
||
View all
|
||
</Link>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
}
|