export interface AppMenuItem { key: string; label: string; icon?: string; children?: AppMenuItem[]; } interface MenuEntry extends AppMenuItem { permission: string; } interface MenuSection extends AppMenuItem { roles: string[]; children: MenuEntry[]; } const ROLE_ALIASES: Record = { 老师: 'teacher', 任课老师: 'teacher', teacher: 'teacher', 教务: 'academic', 教务管理员: 'academic', academic: 'academic', 宿管老师: 'accommodation', 宿管: 'accommodation', 财务: 'accommodation', 住宿运营管理员: 'accommodation', accommodation_operations: 'accommodation', 机构负责人: 'classroom', 教室运营管理员: 'classroom', classroom_operations: 'classroom', 系统管理员: 'system', system_admin: 'system', 超管: 'super', 超级管理员: 'super', super_admin: 'super', }; const SECTIONS: MenuSection[] = [ { key: 'teaching-group', label: '教学工作', icon: 'calendar', roles: ['teacher'], children: [ { key: '/teacher-workspace', label: '今日教学', icon: 'workspace', permission: 'teacher-workspace:view', }, { key: '/schedules', label: '我的排课', icon: 'calendar', permission: 'schedule:view' }, { key: '/attendance', label: '课程考勤', icon: 'attendance', permission: 'attendance:view' }, ], }, { key: 'academic-group', label: '教务管理', icon: 'academic', roles: ['academic', 'super'], children: [ { key: '/students', label: '学生管理', icon: 'students', permission: 'student:view' }, { key: '/classes', label: '班级管理', icon: 'classes', permission: 'class:view' }, { key: '/exams', label: '考试管理', icon: 'exam', permission: 'exam:view' }, { key: '/teachers', label: '教师管理', icon: 'teachers', permission: 'teacher:view' }, { key: '/schedules', label: '排课管理', icon: 'calendar', permission: 'schedule:view' }, { key: '/attendance', label: '历史考勤', icon: 'attendance', permission: 'attendance:view' }, { key: '/classrooms', label: '教室查看', icon: 'classroom', permission: 'classroom:view' }, ], }, { key: 'accommodation-group', label: '住宿运营', icon: 'home', roles: ['accommodation', 'super'], children: [ { key: '/room-visual', label: '住宿总览', icon: 'overview', permission: 'room:view' }, { key: '/rooms', label: '房间管理', icon: 'home', permission: 'room:view' }, { key: '/occupancies', label: '入住管理', icon: 'occupancy', permission: 'occupancy:view' }, { key: '/expenses', label: '费用管理', icon: 'expense', permission: 'expense:view' }, { key: '/bills', label: '账单管理', icon: 'bill', permission: 'bill:view' }, { key: '/wallets', label: '学生余额', icon: 'wallet', permission: 'wallet:view' }, { key: '/deposits', label: '押金管理', icon: 'deposit', permission: 'deposit:view' }, ], }, { key: 'classroom-group', label: '教室运营', icon: 'classroom', roles: ['classroom', 'super'], children: [ { key: '/classroom-schedule', label: '教室排期', icon: 'calendar', permission: 'rental:view', }, { key: '/classrooms', label: '教室管理', icon: 'classroom', permission: 'classroom:view' }, { key: '/attendance-devices', label: '考勤机绑定', icon: 'attendance', permission: 'classroom:view', }, { key: '/classroom-rentals', label: '租赁订单', icon: 'rental', permission: 'rental:view' }, { key: '/organizations', label: '机构管理', icon: 'organization', permission: 'organization:view', }, ], }, { key: 'system-group', label: '系统管理', icon: 'settings', roles: ['system', 'super'], children: [ { key: '/users', label: '账号管理', icon: 'users', permission: 'user:view' }, { key: '/roles', label: '角色管理', icon: 'role', permission: 'role:view' }, { key: '/permissions', label: '权限一览', icon: 'permission', permission: 'role:view' }, { key: '/operation-logs', label: '操作日志', icon: 'log', permission: 'log:view' }, { key: '/integration-config', label: '钉钉集成', icon: 'integration', permission: 'integration:read', }, { key: '/ai-config', label: 'AI 配置', icon: 'ai', permission: 'ai:config:read' }, ], }, ]; export function getRoleDomains( roles: readonly string[], permissions: readonly string[], ): Set { const normalized = new Set(roles.map((role) => ROLE_ALIASES[role]).filter(Boolean)); // 权限可以来自多个叠加角色,因此业务域按能力累加,而不是只选择一个。 if (permissions.includes('student:view') || permissions.includes('class:view')) { normalized.add('academic'); } if ( (permissions.includes('room:view') && (permissions.includes('occupancy:view') || permissions.includes('expense:view'))) || permissions.includes('wallet:view') ) { normalized.add('accommodation'); } if (permissions.includes('rental:view') || permissions.includes('organization:view')) { normalized.add('classroom'); } if (permissions.includes('user:view') || permissions.includes('role:view')) { normalized.add('system'); } const hasAdministrativeDomain = [...normalized].some((role) => role !== 'teacher'); if (!hasAdministrativeDomain && permissions.includes('teacher-workspace:view')) { normalized.add('teacher'); } return normalized; } function dedupeMenu(items: AppMenuItem[]): AppMenuItem[] { const usedPaths = new Set(); const result: AppMenuItem[] = []; for (const item of items) { if (item.children) { const children = item.children.filter((child) => { if (usedPaths.has(child.key)) return false; usedPaths.add(child.key); return true; }); if (children.length > 0) result.push({ ...item, children }); continue; } if (!usedPaths.has(item.key)) { usedPaths.add(item.key); result.push(item); } } return result; } export function buildMenu(roles: readonly string[], permissions: readonly string[]): AppMenuItem[] { const roleSet = getRoleDomains(roles, permissions); const permissionSet = new Set(permissions); const sections: AppMenuItem[] = []; if (permissionSet.has('dashboard:view') && !roleSet.has('teacher')) { sections.push({ key: '/dashboard', label: '数据面板', icon: 'dashboard' }); } for (const section of SECTIONS) { if (!section.roles.some((role) => roleSet.has(role))) continue; const children = section.children .filter((child) => permissionSet.has(child.permission)) .map(({ permission: _, ...child }) => child); if (children.length > 0) sections.push({ ...section, children, roles: undefined } as AppMenuItem); } if (permissionSet.has('notification:view')) { sections.push({ key: '/notifications', label: '通知中心', icon: 'notification' }); } return dedupeMenu(sections); } export function collectMenuPaths(items: readonly AppMenuItem[]): string[] { return items.flatMap((item) => (item.children ? collectMenuPaths(item.children) : [item.key])); } export function findRoleAwareLandingPath( roles: readonly string[], permissions: readonly string[], ): string | null { return collectMenuPaths(buildMenu(roles, permissions))[0] ?? null; }