基于 dashboard-audit-report.md 审计结论,对仪表盘模块进行 P0/P1 级修复:
- 新增 4 个 dashboard 权限点(DASHBOARD_ADMIN/TEACHER/STUDENT/PARENT_READ),补充到 permissions.ts 和角色-权限映射
- 新建 actions.ts:4 个 Server Action 均调用 requirePermission() 校验权限,消除 admin 页面零鉴权、teacher/student/parent 仅 requireAuth 的安全隐患
- 根重定向页 /dashboard 改用 resolvePermissions() + 权限点判断,不再 role === xxx 硬编码
- 新建 lib/dashboard-utils.ts:抽取 toWeekday / countStudentAssignments / sortUpcomingAssignments / filterTodaySchedule / computeTeacherMetrics / getGreetingKey 纯函数,与 UI 分离,便于单测
- 新建 messages/{zh-CN,en}/dashboard.json 翻译文件,i18n request.ts 加载 dashboard 命名空间;所有视图组件接入 useTranslations / getTranslations,消除中英混杂硬编码
- 重构 4 个角色 page.tsx:通过 actions 获取数据,generateMetadata 使用 i18n
- 同步更新架构图 004 / 005 文档(dashboard exports / permissions / 文件清单)
108 lines
3.9 KiB
TypeScript
108 lines
3.9 KiB
TypeScript
import Link from "next/link"
|
|
import { getTranslations } from "next-intl/server"
|
|
import {
|
|
CalendarCheck,
|
|
CalendarDays,
|
|
GraduationCap,
|
|
Megaphone,
|
|
Users,
|
|
} from "lucide-react"
|
|
|
|
import { Card, CardContent } from "@/shared/components/ui/card"
|
|
import { EmptyState } from "@/shared/components/ui/empty-state"
|
|
import type { ParentDashboardData } from "@/modules/parent/types"
|
|
import { getGreetingKey } from "@/modules/dashboard/lib/dashboard-utils"
|
|
import { ChildCard } from "./child-card"
|
|
import { ParentAttentionBanner } from "./parent-attention-banner"
|
|
|
|
export async function ParentDashboard({ data }: { data: ParentDashboardData }) {
|
|
const t = await getTranslations("dashboard")
|
|
const { parentName, children } = data
|
|
const hasChildren = children.length > 0
|
|
|
|
const greetingKey = getGreetingKey(new Date())
|
|
|
|
const QUICK_ENTRIES = [
|
|
{ href: "/parent/grades", label: t("quickActions.grades"), icon: GraduationCap },
|
|
{ href: "/parent/attendance", label: t("quickActions.attendance"), icon: CalendarCheck },
|
|
{ href: "/announcements", label: t("quickActions.announcements"), icon: Megaphone },
|
|
{ href: "/parent/leave", label: t("quickActions.leaveRequest"), icon: CalendarDays },
|
|
] as const
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="space-y-1">
|
|
<h1 className="text-2xl font-bold tracking-tight">{t("title.parent")}</h1>
|
|
<div className="text-sm text-muted-foreground">
|
|
{t(`greeting.${greetingKey}`)}
|
|
{parentName ? `, ${parentName}` : ""}. {t("description.parent")}
|
|
</div>
|
|
</div>
|
|
|
|
{hasChildren ? (
|
|
<>
|
|
<ParentAttentionBanner data={data} />
|
|
|
|
<nav
|
|
aria-label={t("quickActions.announcements")}
|
|
className="grid grid-cols-2 gap-3 sm:grid-cols-4"
|
|
>
|
|
{QUICK_ENTRIES.map((entry) => (
|
|
<Link
|
|
key={entry.href}
|
|
href={entry.href}
|
|
className="group"
|
|
aria-label={entry.label}
|
|
>
|
|
<Card className="h-full transition-colors hover:bg-muted/50 focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
|
|
<CardContent className="flex flex-col items-center justify-center gap-2 p-4 text-center">
|
|
<entry.icon
|
|
className="h-6 w-6 text-muted-foreground group-hover:text-foreground"
|
|
aria-hidden
|
|
/>
|
|
<span className="text-sm font-medium">{entry.label}</span>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Users className="h-4 w-4" aria-hidden />
|
|
<span>
|
|
{t("badge.childrenLinked", { count: children.length })}
|
|
</span>
|
|
</div>
|
|
{/* 移动端水平滑动卡片,桌面端网格布局 */}
|
|
<div
|
|
className="flex gap-4 overflow-x-auto pb-2 snap-x snap-mandatory sm:hidden"
|
|
aria-label={t("title.parent")}
|
|
>
|
|
{children.map((child) => (
|
|
<div key={child.basicInfo.id} className="snap-start shrink-0 w-[85%]">
|
|
<ChildCard child={child} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="hidden sm:grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{children.map((child) => (
|
|
<ChildCard key={child.basicInfo.id} child={child} />
|
|
))}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<EmptyState
|
|
icon={Users}
|
|
title={t("empty.noChildren")}
|
|
description={t("empty.noChildrenDesc")}
|
|
className="border-none shadow-none"
|
|
action={{
|
|
label: t("empty.contactSupport"),
|
|
href: "/messages",
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|