feat(portal-shell): wire AppFrame + navigation + permission filter (P1-1)

实现 ARCHITECTURE.md §10 P1-1:
- 新增 src/shared/lib/navigation.ts:27 项静态导航注册表,
  按 teacher/student/parent/admin 四角色分区,group 字段用于角色过滤
- 新增 src/app/shell/layout.tsx:RSC AppFrame,从 headers() 读取
  middleware 注入的身份头,batchCheckRoutePermission 按位图二次过滤
- 新增 src/shared/components/layout/user-menu.tsx:顶部用户菜单,
  显示 userId + role,登出 POST /api/auth/logout
- 新增 src/shared/lib/__tests__/navigation.test.ts:P1-1 验收单测
  (7 用例:href 登记一致性 + 4 角色隔离 + 权限检查)
- 修改 src/shared/lib/route-permissions.ts:补全 7 个列表页根路由
  的 EXACT 登记(/shell/admin/announcements、/shell/admin/classes、
  /shell/teacher/exams 等),与 PREFIX 表互补避免 catch-all 拒绝

验收:
- vitest run navigation → 7/7 passed
- tsc --noEmit 通过;eslint(5 文件)通过

回填 ARCHITECTURE.md §10 P1-1 状态为 ,附验收证据。

关联:ARCHITECTURE.md §7.2 AppFrame / §10 P1-1
This commit is contained in:
SpecialX
2026-07-22 12:17:16 +08:00
parent dc13a2afb4
commit f92fdf8efe
6 changed files with 602 additions and 12 deletions

View File

@@ -0,0 +1,300 @@
/**
* 静态导航注册表ARCHITECTURE.md §7.2 AppFrame / §10 P1-1
*
* 导航项按角色分组group 字段),渲染前由 layout.tsx 调
* batchCheckRoutePermission 按用户位图二次过滤。
*
* 导航项 href 与 route-permissions.ts 四张表对齐:
* - 仪表盘路由 → DASHBOARD_ROUTE_PERMISSIONS
* - 精确路由 → EXACT_ROUTE_PERMISSIONS
* - 前缀路由(如 /shell/teacher/exams→ PREFIX_ROUTE_PERMISSIONS
* (前缀以 "/" 结尾layout.tsx 检查时补 "/" 匹配)
*
* 关联portal-shell ARCHITECTURE.md §7.2、§10 P1-1
*/
import type { LucideIcon } from "lucide-react";
import {
Activity,
AlertCircle,
Bot,
BookMarked,
BookOpen,
Building2,
CalendarCheck,
ClipboardList,
FileCheck,
FileText,
GraduationCap,
HelpCircle,
LayoutDashboard,
ListChecks,
Lock,
Megaphone,
Puzzle,
Route,
School,
ScrollText,
Shield,
Ticket,
Users,
} from "lucide-react";
import type { Role } from "@edu/shared-ts/contracts";
/** 导航项定义 */
export interface NavigationItem {
/** 路由路径(与 route-permissions 表对齐) */
href: string;
/** 显示名称P1-4 i18n 后替换为 i18n key */
label: string;
/** 图标lucide-react */
icon: LucideIcon;
/** 所属角色区(用于分组渲染与角色过滤) */
group: Role;
/** 所需角色(元数据,与 route-permissions 对齐) */
requiredRoles?: Role[];
/** 所需权限点(元数据,与 route-permissions 对齐) */
requiredPermissions?: string[];
}
/**
* 全量导航项(按角色分区,扁平数组)
*
* group 字段区分角色区teacher / student / parent / admin
*/
export const NAVIGATION_ITEMS: NavigationItem[] = [
// ── teacher 组 ──────────────────────────────────────────
{
href: "/shell/teacher",
label: "仪表盘",
icon: LayoutDashboard,
group: "teacher",
requiredRoles: ["teacher"],
requiredPermissions: ["DASHBOARD_TEACHER_READ"],
},
{
href: "/shell/teacher/exams",
label: "考试管理",
icon: FileText,
group: "teacher",
requiredRoles: ["teacher", "admin"],
requiredPermissions: ["EXAM_READ"],
},
{
href: "/shell/teacher/homework",
label: "作业管理",
icon: BookOpen,
group: "teacher",
requiredRoles: ["teacher", "admin"],
requiredPermissions: ["HOMEWORK_READ"],
},
{
href: "/shell/teacher/grades",
label: "成绩录入",
icon: GraduationCap,
group: "teacher",
requiredRoles: ["teacher", "admin"],
requiredPermissions: ["GRADE_RECORD_READ"],
},
{
href: "/shell/teacher/attendance",
label: "考勤",
icon: CalendarCheck,
group: "teacher",
requiredRoles: ["teacher", "admin"],
requiredPermissions: ["ATTENDANCE_READ"],
},
{
href: "/shell/teacher/diagnostics",
label: "学情诊断",
icon: Activity,
group: "teacher",
requiredRoles: ["teacher", "admin"],
requiredPermissions: ["DIAGNOSTIC_READ"],
},
{
href: "/shell/teacher/lesson-plans",
label: "备课",
icon: ClipboardList,
group: "teacher",
requiredRoles: ["teacher"],
requiredPermissions: ["LESSON_PLAN_READ"],
},
{
href: "/shell/teacher/question-bank",
label: "题库",
icon: HelpCircle,
group: "teacher",
requiredRoles: ["teacher"],
requiredPermissions: ["QUESTION_READ"],
},
{
href: "/shell/teacher/textbooks",
label: "教材",
icon: BookMarked,
group: "teacher",
requiredRoles: ["teacher", "admin"],
requiredPermissions: ["TEXTBOOK_READ"],
},
// ── student 组 ──────────────────────────────────────────
{
href: "/shell/student",
label: "仪表盘",
icon: LayoutDashboard,
group: "student",
requiredRoles: ["student"],
requiredPermissions: ["DASHBOARD_STUDENT_READ"],
},
{
href: "/shell/student/error-book",
label: "错题本",
icon: AlertCircle,
group: "student",
requiredRoles: ["student"],
requiredPermissions: ["ERROR_BOOK_READ"],
},
{
href: "/shell/student/learning-path",
label: "学习路径",
icon: Route,
group: "student",
requiredRoles: ["student"],
requiredPermissions: ["LEARNING_PATH_READ"],
},
{
href: "/shell/student/electives",
label: "选课",
icon: ListChecks,
group: "student",
requiredRoles: ["student"],
requiredPermissions: ["ELECTIVE_READ"],
},
{
href: "/shell/student/ai-tutor",
label: "AI 辅导",
icon: Bot,
group: "student",
requiredRoles: ["student"],
requiredPermissions: ["AI_TUTOR_USE"],
},
// ── parent 组 ───────────────────────────────────────────
{
href: "/shell/parent",
label: "仪表盘",
icon: LayoutDashboard,
group: "parent",
requiredRoles: ["parent"],
requiredPermissions: ["DASHBOARD_PARENT_READ"],
},
{
href: "/shell/parent/children",
label: "孩子信息",
icon: Users,
group: "parent",
requiredRoles: ["parent"],
requiredPermissions: ["GRADE_READ_CHILD"],
},
{
href: "/shell/parent/leave-approval",
label: "请假审批",
icon: FileCheck,
group: "parent",
requiredRoles: ["parent"],
requiredPermissions: ["LEAVE_APPROVAL_MANAGE"],
},
// ── admin 组 ────────────────────────────────────────────
{
href: "/shell/admin",
label: "仪表盘",
icon: LayoutDashboard,
group: "admin",
requiredRoles: ["admin"],
requiredPermissions: ["DASHBOARD_ADMIN_READ"],
},
{
href: "/shell/admin/users",
label: "用户管理",
icon: Users,
group: "admin",
requiredRoles: ["admin"],
requiredPermissions: ["USER_MANAGE"],
},
{
href: "/shell/admin/roles",
label: "角色管理",
icon: Shield,
group: "admin",
requiredRoles: ["admin"],
requiredPermissions: ["ROLE_MANAGE"],
},
{
href: "/shell/admin/permissions",
label: "权限管理",
icon: Lock,
group: "admin",
requiredRoles: ["admin"],
requiredPermissions: ["PERMISSION_MANAGE"],
},
{
href: "/shell/admin/audit-logs",
label: "审计日志",
icon: ScrollText,
group: "admin",
requiredRoles: ["admin"],
requiredPermissions: ["AUDIT_LOG_READ"],
},
{
href: "/shell/admin/school",
label: "学校管理",
icon: Building2,
group: "admin",
requiredRoles: ["admin"],
requiredPermissions: ["SCHOOL_MANAGE"],
},
{
href: "/shell/admin/plugins",
label: "插件管理",
icon: Puzzle,
group: "admin",
requiredRoles: ["admin"],
requiredPermissions: ["PLUGIN_REGISTRY_MANAGE"],
},
{
href: "/shell/admin/invitation-codes",
label: "邀请码",
icon: Ticket,
group: "admin",
requiredRoles: ["admin"],
requiredPermissions: ["INVITATION_CODE_MANAGE"],
},
{
href: "/shell/admin/announcements",
label: "公告管理",
icon: Megaphone,
group: "admin",
requiredRoles: ["admin"],
requiredPermissions: ["ANNOUNCEMENT_MANAGE"],
},
{
href: "/shell/admin/classes",
label: "班级管理",
icon: School,
group: "admin",
requiredRoles: ["admin"],
requiredPermissions: ["CLASS_READ"],
},
];
/**
* 返回指定角色可见的导航项
*
* 按 group 字段过滤,实现"4 角色各见各菜单"。
* 调用方layout.tsx再用 batchCheckRoutePermission 按权限位图二次过滤。
*/
export function getNavigationItemsForRole(role: Role): NavigationItem[] {
return NAVIGATION_ITEMS.filter((item) => item.group === role);
}