feat(dashboard): 仪表盘模块审计重构 — 权限校验 + i18n + 逻辑抽离
基于 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 / 文件清单)
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import type { TeacherDashboardData, TeacherTodayScheduleItem } from "@/modules/dashboard/types"
|
||||
import type { TeacherDashboardData } from "@/modules/dashboard/types"
|
||||
import type { TeacherDashboardMetrics } from "@/modules/dashboard/lib/dashboard-utils"
|
||||
import { getTranslations } from "next-intl/server"
|
||||
import type { TeacherTodoItem } from "./teacher-todo-card"
|
||||
|
||||
import { TeacherClassesCard } from "./teacher-classes-card"
|
||||
import { TeacherDashboardHeader } from "./teacher-dashboard-header"
|
||||
@@ -7,52 +10,36 @@ import { RecentSubmissions } from "./recent-submissions"
|
||||
import { TeacherSchedule } from "./teacher-schedule"
|
||||
import { TeacherStats } from "./teacher-stats"
|
||||
import { TeacherGradeTrends } from "./teacher-grade-trends"
|
||||
import { TeacherTodoCard, type TeacherTodoItem } from "./teacher-todo-card"
|
||||
import { TeacherTodoCard } from "./teacher-todo-card"
|
||||
|
||||
const toWeekday = (d: Date): 1 | 2 | 3 | 4 | 5 | 6 | 7 => {
|
||||
const day = d.getDay()
|
||||
return (day === 0 ? 7 : day) as 1 | 2 | 3 | 4 | 5 | 6 | 7
|
||||
interface TeacherDashboardViewProps {
|
||||
data: TeacherDashboardData & { metrics: TeacherDashboardMetrics }
|
||||
}
|
||||
|
||||
export function TeacherDashboardView({ data }: { data: TeacherDashboardData }) {
|
||||
const todayWeekday = toWeekday(new Date())
|
||||
export async function TeacherDashboardView({ data }: TeacherDashboardViewProps) {
|
||||
const t = await getTranslations("dashboard")
|
||||
const { metrics } = data
|
||||
|
||||
const classNameById = new Map(data.classes.map((c) => [c.id, c.name] as const))
|
||||
const todayScheduleItems: TeacherTodayScheduleItem[] = data.schedule
|
||||
.filter((s) => s.weekday === todayWeekday)
|
||||
.sort((a, b) => a.startTime.localeCompare(b.startTime))
|
||||
.map((s): TeacherTodayScheduleItem => ({
|
||||
id: s.id,
|
||||
classId: s.classId,
|
||||
className: classNameById.get(s.classId) ?? "Class",
|
||||
course: s.course,
|
||||
startTime: s.startTime,
|
||||
endTime: s.endTime,
|
||||
location: s.location ?? null,
|
||||
}))
|
||||
|
||||
const submittedSubmissions = data.submissions.filter((s) => Boolean(s.submittedAt))
|
||||
const toGradeCount = submittedSubmissions.filter((s) => s.status === "submitted").length
|
||||
|
||||
const submissionsToGrade = submittedSubmissions
|
||||
.filter(s => s.status === "submitted")
|
||||
.sort((a, b) => (a.submittedAt ? new Date(a.submittedAt).getTime() : 0) - (b.submittedAt ? new Date(b.submittedAt).getTime() : 0))
|
||||
.slice(0, 6);
|
||||
|
||||
const activeAssignmentsCount = data.assignments.filter(a => a.status === "published").length
|
||||
|
||||
const totalTrendScore = data.gradeTrends.reduce((acc, curr) => acc + curr.averageScore, 0)
|
||||
const averageScore = data.gradeTrends.length > 0 ? totalTrendScore / data.gradeTrends.length : 0
|
||||
|
||||
const totalSubmissions = data.gradeTrends.reduce((acc, curr) => acc + curr.submissionCount, 0)
|
||||
const totalPotentialSubmissions = data.gradeTrends.reduce((acc, curr) => acc + curr.totalStudents, 0)
|
||||
const submissionRate = totalPotentialSubmissions > 0 ? (totalSubmissions / totalPotentialSubmissions) * 100 : 0
|
||||
|
||||
// 待办聚合
|
||||
// 待办聚合(使用预计算指标)
|
||||
const todoItems: TeacherTodoItem[] = [
|
||||
{ label: "待批改作业", count: toGradeCount, href: "/teacher/homework/submissions", variant: toGradeCount > 0 ? "urgent" : "normal" },
|
||||
{ label: "今日待考勤", count: todayScheduleItems.length, href: "/teacher/attendance/sheet", variant: "info" },
|
||||
{ label: "进行中作业", count: activeAssignmentsCount, href: "/teacher/homework/assignments", variant: "normal" },
|
||||
{
|
||||
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 (
|
||||
@@ -60,34 +47,34 @@ export function TeacherDashboardView({ data }: { data: TeacherDashboardData }) {
|
||||
<TeacherDashboardHeader teacherName={data.teacherName} />
|
||||
|
||||
<TeacherStats
|
||||
toGradeCount={toGradeCount}
|
||||
activeAssignmentsCount={activeAssignmentsCount}
|
||||
averageScore={averageScore}
|
||||
submissionRate={submissionRate}
|
||||
toGradeCount={metrics.toGradeCount}
|
||||
activeAssignmentsCount={metrics.activeAssignmentsCount}
|
||||
averageScore={metrics.averageScore}
|
||||
submissionRate={metrics.submissionRate}
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-12">
|
||||
{/* 移动端优先展示:今日课表 → 待办 → 待批改 */}
|
||||
<div className="flex flex-col gap-6 lg:col-span-8">
|
||||
<div className="lg:hidden">
|
||||
<TeacherSchedule items={todayScheduleItems} />
|
||||
</div>
|
||||
<TeacherTodoCard items={todoItems} />
|
||||
<TeacherGradeTrends trends={data.gradeTrends} />
|
||||
<RecentSubmissions
|
||||
submissions={submissionsToGrade}
|
||||
title="待批改"
|
||||
emptyTitle="全部批改完成!"
|
||||
emptyDescription="暂无待批改的提交。"
|
||||
/>
|
||||
<div className="lg:hidden">
|
||||
<TeacherSchedule items={metrics.todayScheduleItems} />
|
||||
</div>
|
||||
<TeacherTodoCard items={todoItems} />
|
||||
<TeacherGradeTrends trends={data.gradeTrends} />
|
||||
<RecentSubmissions
|
||||
submissions={metrics.submissionsToGrade}
|
||||
title={t("sections.pendingGrading")}
|
||||
emptyTitle={t("empty.allGraded")}
|
||||
emptyDescription={t("empty.allGradedDesc")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col gap-6 lg:col-span-4">
|
||||
<div className="hidden lg:block">
|
||||
<TeacherSchedule items={todayScheduleItems} />
|
||||
</div>
|
||||
<TeacherHomeworkCard assignments={data.assignments} />
|
||||
<TeacherClassesCard classes={data.classes} />
|
||||
<div className="hidden lg:block">
|
||||
<TeacherSchedule items={metrics.todayScheduleItems} />
|
||||
</div>
|
||||
<TeacherHomeworkCard assignments={data.assignments} />
|
||||
<TeacherClassesCard classes={data.classes} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user