Skyroc Web Kit
Admin Layouts

菜单系统

Admin Layouts 菜单系统的公共概念:生成入口、统一结构、权限、badge、extra 与 menuNodeCallback

菜单系统把 TanStack Router 的 staticData 或后端返回的 BackendRoute 转成统一的 GeneratedMenu,再渲染成 Ant Design Menu 项。

这一页只讲静态模式和动态模式都会复用的公共概念。两条生成链路的细节拆在独立页面里:

菜单链路

静态路由 staticData / 动态 BackendRoute
  └─ Router.Meta
       ├─ title / i18nKey / permissions / tab
       └─ menu
            ├─ icon / localIcon / order / type / hide / activeMenu
            ├─ badge
            └─ extra

menuGenerator.generate(...)
  ├─ allMenus: Map<categoryKey, GeneratedMenu[]>
  ├─ quickReferenceMenus: Map<categoryKey, Map<path, routeInfo>>
  └─ home

renderCommonMenus(...)
  └─ Menu.CommonMenu[]
       ├─ label
       ├─ icon
       ├─ extra
       └─ children

生成模式

菜单生成由 setupAdminLayouts({ routeMode }) 决定:

模式数据来源适用场景深入文档
staticTanStack Router route 的 staticData路由和菜单由前端仓库管理,权限也由前端 meta 参与过滤。静态路由菜单生成
dynamicloadDynamicRoutes 返回的后端路由树菜单、首页和路由树由后端或权限中心下发。动态路由菜单生成

无论哪种模式,最终都会得到同一组运行时数据:

数据说明
allMenusMap<categoryKey, GeneratedMenu[]>,用于渲染菜单树。
quickReferenceMenusMap<categoryKey, Map<path, routeInfo>>,用于根据路由快速定位菜单、tab、父级路径和激活项。
home当前用户首页。静态模式来自 defaultHome,动态模式可以由后端返回值覆盖。

菜单分类

menuCategories 不是业务菜单分组,也不是菜单数据本身。它描述的是“哪一组菜单归哪个后台 layout 管”。

setupAdminLayouts({
  menuCategories: {
    admin: {
      key: 'admin',
      layout: '/(admin)'
    }
  }
});

这组配置有三个用途:

场景分类的作用
静态模式通过 layout 找到 TanStack Router 的 layout route,只扫描该 layout 下的子路由生成菜单。
动态模式后端顶层 BackendRoute.layout 写分类 key,例如 admin,布局包据此把路由放进对应菜单组。
渲染布局AdminLayout 通过 categoryKey 选择要读取的菜单组,未传时默认使用第一个分类。

单后台应用通常只需要一个 admin 分类。只有多后台入口、多工作台,或同一个应用里需要多套独立菜单时,才需要增加第二个分类。

GeneratedMenu

interface GeneratedMenu {
  badge?: Router.MenuBadge | null;
  children?: GeneratedMenu[];
  extra?: Router.Extra | null;
  i18nKey?: I18n.I18nKey | null;
  icon?: string;
  key: string;
  localIcon?: string;
  order?: number;
  path?: Router.RoutePath;
  title?: string;
  type?: string;
}

GeneratedMenu 是菜单渲染层唯一关心的结构。静态路由、动态路由和 menuNodeCallback 追加的节点都会先被归一成这个结构。

通用菜单字段

字段说明
title菜单和 tab 的兜底标题。
i18nKey多语言 key,存在时优先由 I18nLabel 渲染。
permissions当前路由需要的角色列表。
menu.iconIconify 图标名。
menu.localIcon本地图标名。
menu.order同级菜单排序值。
menu.hide是否从菜单树隐藏,但仍可进入路由。
menu.activeMenu隐藏详情页进入时激活哪个菜单。
menu.type菜单类型,常用 itemdivider
menu.badge标准菜单状态,例如红点、数字、newhot
menu.extra自定义菜单额外组件 key,用于标准 badge 不能表达的业务 UI。

权限过滤

路由权限来自 Router.Meta.permissions。静态模式的菜单生成会调用 hasRoutePermission(staticData, userInfo),没有权限的 route 不会进入菜单和快速索引。

staticData: {
  permissions: ['R_ADMIN'],
  menu: {
    icon: 'ic:round-manage-accounts',
    order: 1
  }
}

permissionSuperRole 可以配置超级角色,命中后跳过普通权限限制。

setupAdminLayouts({
  permissionSuperRole: import.meta.env.VITE_STATIC_SUPER_ROLE
});

后台 layout route 可以使用 hasMatchedRoutePermission(matches, userInfo) 对当前匹配到的 route chain 做整体校验。

动态模式通常由后端先完成权限裁剪,布局包不会在 transformBackendRouteToMenu 中再次调用 hasRoutePermission。如果动态路由仍带有前端 route meta,页面守卫仍可以继续使用 hasMatchedRoutePermission

标准 Badge

menu.badge 用于通用菜单状态,例如红点、数字、newhot。它是结构化数据,适合静态路由和动态后端路由共同使用。

menu: {
  badge: {
    type: 'normal',
    valueKey: 'todo.count',
    showZero: false
  }
}

字段:

字段说明
typenormal 显示内容 badge,dot 显示红点。
value静态内容,可以是数字或文本。
valueKey动态值 key,从布局包的 badge 值表读取。
variant可选状态色:defaultprimarysuccesswarningerrorinfo
showZero值为 0 时是否仍显示。

动态更新:

import { useAdminMenuBadges } from '@skyroc/web-admin-layouts';

const Home = () => {
  const { setMenuBadgeValue } = useAdminMenuBadges();

  useEffect(() => {
    setMenuBadgeValue('todo.count', 25);
  }, [setMenuBadgeValue]);

  return <Dashboard />;
};

也可以在非组件代码里直接使用 action:

import { setMenuBadgeValues } from '@skyroc/web-admin-layouts';

setMenuBadgeValues({
  'todo.count': 25,
  'message.unread': 4
});

自定义 Extra

menu.extra 是业务组件扩展口,用于标准 badge 不能表达的 UI,例如发布通道、活动标签、会员标识或复杂组合状态。

先注册组件:

// features/menus/extras.ts
import ReleaseChannelMenuExtra from './components/ReleaseChannelMenuExtra';

export const menuExtras = {
  ReleaseChannel: ReleaseChannelMenuExtra
};

再在启动配置中注入:

setupAdminLayouts({
  extras: menuExtras
});

最后在路由上使用 key:

staticData: {
  title: 'about',
  menu: {
    extra: 'ReleaseChannel',
    icon: 'fluent:book-information-24-regular',
    order: 22
  }
}

Badge 和 Extra 的选择

场景推荐
数字、红点、newhot、简单状态文本menu.badge
后端动态菜单需要表达通用状态menu.badge
需要订阅统一动态数量valueKey + useAdminMenuBadges
完全不同的业务 UI 或组合样式menu.extra
需要组件内部自行读取业务数据menu.extra

menuNodeCallback 是菜单生成阶段的扩展点,用于给某个 route 节点追加“不是 route 文件本身声明出来的菜单节点”。

它适合这些场景:

场景说明
分割菜单区块在某个 layout 或菜单节点下追加 type: 'divider'
追加项目级入口追加由项目配置决定的菜单,例如固定功能入口、聚合入口或手写子菜单。
复用同一套扩展逻辑静态路由和动态路由都需要同样的额外节点时,放在一个 callback 里。

不要把普通页面菜单写进 menuNodeCallback。普通页面应该优先通过静态 route 的 staticData.menu 或动态接口返回的 menu 声明。menuNodeCallback 只处理“路由树之外的补充节点”。

routeId 表示当前正在生成菜单的 route 节点:

模式routeId 来源
静态模式TanStack Router 的 route id,例如 /(admin)/(admin)/manage
动态模式后端 BackendRoute.id,例如 system

没有要追加的节点时返回空数组。

import type { MenuNodeCallback } from '@skyroc/web-admin-layouts';

export const menuNodeCallback: MenuNodeCallback = routeId => {
  if (routeId !== '/(admin)') return [];

  return [
    {
      id: 'admin-feature-divider',
      menu: {
        order: 6,
        type: 'divider'
      }
    },
    {
      id: 'admin-about-divider',
      menu: {
        order: 20,
        type: 'divider'
      }
    }
  ];
};

普通扩展菜单需要提供可导航的 path,否则不会进入菜单树:

export const menuNodeCallback: MenuNodeCallback = routeId => {
  if (routeId !== '/(admin)') return [];

  return [
    {
      id: 'reports-overview',
      menu: {
        icon: 'mdi:file-chart-outline',
        order: 30
      },
      path: '/reports',
      title: 'reports'
    }
  ];
};

menuNodeCallback 返回的菜单同样会经过 badgeextrachildren 和排序处理。

On this page