@skyroc/web-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菜单类型,常用 item 或 divider。
menu.badge标准菜单状态,例如红点、数字、new、hot。
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 用于通用菜单状态,例如红点、数字、new、hot。它是结构化数据,适合静态路由和动态后端路由共同使用。

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

字段:

字段说明
typenormal 显示内容 badge,dot 显示红点。
value静态内容,可以是数字或文本。
valueKey动态值 key,从布局包的 badge 值表读取。
variant可选状态色:default、primary、success、warning、error、info。
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 的选择

场景推荐
数字、红点、new、hot、简单状态文本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 返回的菜单同样会经过 badge、extra、children 和排序处理。

Last updated on