// 路由配置 — 全站唯一路由声明处。 // 页面组件一律 React.lazy 懒加载;菜单(menuItems)与页面标题(findRouteMeta) // 均由本文件派生,新增页面只需在这里加一条配置。 import { lazy } from 'react'; import { LayoutDashboard, Server, Monitor, HardDrive, Rocket, RadioTower, Settings, UsersRound, Wand2, ScrollText, BellRing, BarChart3, History, Boxes, ShieldCheck, Container, } from 'lucide-react'; import type { AppRoute, MenuItem, RouteMeta } from '@/types/router'; const Login = lazy(() => import('@/pages/login')); const Dashboard = lazy(() => import('@/pages/dashboard')); const Clusters = lazy(() => import('@/pages/clusters')); const Hosts = lazy(() => import('@/pages/hosts')); const Storage = lazy(() => import('@/pages/storage')); const Deployments = lazy(() => import('@/pages/deployments')); const Collectors = lazy(() => import('@/pages/collectors')); const SettingsPage = lazy(() => import('@/pages/settings')); const LdapPage = lazy(() => import('@/pages/settings/ldap')); const RegistryPage = lazy(() => import('@/pages/settings/registry')); const NotFound = lazy(() => import('@/pages/not-found')); const WizardPage = lazy(() => import('@/pages/wizard')); const LogsPage = lazy(() => import('@/pages/logs')); const AlertsPage = lazy(() => import('@/pages/alerts')); const UsersPage = lazy(() => import('@/pages/users')); const AnalysisPage = lazy(() => import('@/pages/analysis')); const AuditPage = lazy(() => import('@/pages/audit')); export const routes: AppRoute[] = [ { path: '/login', component: Login, auth: false, meta: { title: 'menu.login', hideInMenu: true } }, { path: '/', component: Dashboard, auth: true, meta: { title: 'menu.dashboard', icon: LayoutDashboard, order: 1, adminOnly: true } }, { path: '/wizard', component: WizardPage, auth: true, meta: { title: 'menu.wizard', icon: Wand2, order: 2, adminOnly: true } }, // ── 资源接入(集群 / 主机 / 存储):装完基本不动的配置,合并为一个入口 ── { path: '/clusters', component: Clusters, auth: true, meta: { title: 'menu.clusters', icon: Server, order: 3, adminOnly: true, group: { key: 'resources', label: 'menu.groups.resources', icon: Boxes, order: 3 }, }, }, { path: '/hosts', component: Hosts, auth: true, meta: { title: 'menu.hosts', icon: Monitor, order: 4, adminOnly: true, groupKey: 'resources' } }, { path: '/storage', component: Storage, auth: true, meta: { title: 'menu.storage', icon: HardDrive, order: 5, adminOnly: true, groupKey: 'resources' } }, // ── 日志服务(Loki 实例 / 采集器):部署与采集是同一件事的两半 ── { path: '/deployments', component: Deployments, auth: true, meta: { // 组内 tab 名用「组件部署」:部署对象是基础设施组件(Loki 实例), // 与业务侧大量 cex-* 业务服务明确区分,避免误以为这里是发布业务服务。 title: 'menu.deployments', icon: Rocket, order: 4, adminOnly: true, group: { key: 'services', label: 'menu.groups.services', icon: Rocket, order: 4 }, }, }, { path: '/collectors', component: Collectors, auth: true, meta: { title: 'menu.collectors', icon: RadioTower, order: 7, adminOnly: true, groupKey: 'services' } }, // ── 日志查询 / 日志分析:高频入口,保持独立菜单项(不折叠)── { path: '/logs', component: LogsPage, auth: true, meta: { title: 'menu.logs', icon: ScrollText, order: 5 } }, { path: '/analysis', component: AnalysisPage, auth: true, meta: { title: 'menu.analysis', icon: BarChart3, order: 6, adminOnly: true } }, // ── 告警:独立入口(要盯的,不能被折叠) ── // 告警仅管理员:普通用户按「查询授权」只看指定的日志(菜单只剩日志查询)。 // 告警规则涉及阈值/通知通道等平台级配置,不属于日志查询者的职责。 { path: '/alerts', component: AlertsPage, auth: true, meta: { title: 'menu.alerts', icon: BellRing, order: 7, adminOnly: true } }, // ── 系统管理(用户 / 审计 / 设置):低频管理动作 ── { path: '/users', component: UsersPage, auth: true, meta: { // 系统设置(tab 起点)/ 用户管理 / 审计日志 同属「系统设置」分组, // 侧栏只占一个入口,页面内用 tab 切换。 title: 'menu.users', icon: UsersRound, order: 9, adminOnly: true, groupKey: 'system', }, }, { path: '/audit', component: AuditPage, auth: true, meta: { title: 'menu.audit', icon: History, order: 10, adminOnly: true, groupKey: 'system' } }, { path: '/settings/ldap', component: LdapPage, auth: true, meta: { title: 'menu.ldap', icon: ShieldCheck, order: 11, adminOnly: true, groupKey: 'system' } }, { path: '/settings/registry', component: RegistryPage, auth: true, meta: { title: 'menu.registry', icon: Container, order: 12, adminOnly: true, groupKey: 'system' } }, // 系统设置放回侧栏,并作为「系统设置」分组的 tab 起点(order 最小 = 排在第一个) { path: '/settings', component: SettingsPage, auth: true, meta: { title: 'menu.settings', icon: Settings, order: 8, adminOnly: true, group: { key: 'system', label: 'menu.groups.system', icon: Settings, order: 8 }, }, }, { path: '*', component: NotFound, auth: false, meta: { title: 'menu.notFound', hideInMenu: true } }, ]; /** * 侧栏菜单:登录后可见的路由,按 order 排序。 * * 分组规则(解决"菜单太多"的观感问题): * - 带 meta.group 的路由是一个分组的**代表入口**,菜单显示分组的 label/icon * - 带 meta.groupKey 的路由说明它属于某个已有分组 → 不进菜单,只在页内 tab 出现 * 这样 13 个路由收敛成 7 个菜单项,且 URL 全部保持不变(书签不会失效)。 */ export const menuItems: MenuItem[] = (() => { const grouped = new Set(); const items: MenuItem[] = []; const sorted = routes .filter((route) => route.auth && !route.meta.hideInMenu) .sort((a, b) => { const orderA = a.meta.group?.order ?? a.meta.order ?? 0; const orderB = b.meta.group?.order ?? b.meta.order ?? 0; return orderA - orderB; }); for (const route of sorted) { const { group, groupKey } = route.meta; // 属于某个分组的非代表路由:不占菜单位 if (groupKey && !group) continue; if (group) { if (grouped.has(group.key)) continue; grouped.add(group.key); items.push({ path: route.path, icon: group.icon ?? route.meta.icon, label: group.label, adminOnly: route.meta.adminOnly, groupKey: group.key, }); continue; } items.push({ path: route.path, icon: route.meta.icon, label: route.meta.title, adminOnly: route.meta.adminOnly, }); } return items; })(); /** 取某个分组下的所有页面(用于页内 tab 切换),按 order 排序 */ export function findGroupRoutes(groupKey: string): AppRoute[] { return routes .filter((route) => route.meta.groupKey === groupKey || route.meta.group?.key === groupKey) .sort((a, b) => (a.meta.order ?? 0) - (b.meta.order ?? 0)); } /** 当前路径所属的分组标识(不在分组内返回 undefined) */ export function findGroupKey(pathname: string): string | undefined { const meta = findRouteMeta(pathname); return meta?.group?.key ?? meta?.groupKey; } /** 按路径查 meta(通配/未匹配返回 undefined,调用方自行兜底) */ export function findRouteMeta(pathname: string): RouteMeta | undefined { return routes.find((route) => route.path === pathname)?.meta; }