☰
ant-design-blazor 菜单实战:用 OpenKeys 与 Accordion 实现“只展开当前父级菜单“的内嵌侧边导航
2026/10/11 11:32:10 网站建设 项目流程
  • 前端
  • UI组件
  • 设计系统

【免费下载链接】ant-design-blazor

基于 Ant Design 与 Blazor 的前端组件库。让开发者解放生产力,实现更大价值。

项目地址:https://gitcode.com/ant-design-blazor/ant-design-blazor
点击查看免费下载

本篇技术指南聚焦 ant-design-blazor 的Menu组件在**侧边内嵌导航(inline 模式)**下的一个高频交互需求:点击某个父级菜单时,自动收起其他已展开的父级菜单,始终保持界面上只有一个 SubMenu 处于展开状态。文章将以官方示例sider-current(只展开当前父级菜单)为骨架,完整讲解基于OpenKeys+OnOpenChange的受控实现方案、基于Accordion的内置手风琴方案,并结合组件源码剖析两种方案的底层行为差异与适用场景。读完你可以直接在自己的 Blazor 项目中落地这一交互,并理解其内部实现原理。

需求场景:多级侧边导航的"聚焦"交互

侧边导航通常承载网站的层级架构(例如"仪表盘 / 业务模块 / 系统设置"等多个一级菜单,每个一级菜单下又有多层子菜单)。当一级菜单较多时,如果用户可以同时展开多个 SubMenu,导航区域会变得冗长、遮挡主要内容,失去聚焦。

官方示例sider-current要解决的问题正是如此:点击菜单,收起其他展开的所有菜单,保持菜单聚焦简洁。这一交互与 Layout 组件的Sider配合使用,常用于后台管理系统的侧边导航栏,可参考 Menu 组件文档 与 通用布局 layout 的组合方式。

前置基础:内嵌菜单与展开状态管理

要实现"只展开当前父级菜单",第一步是让菜单运行在内嵌模式下。在 ant-design-blazor 中,Menu的Mode参数类型为 MenuMode 枚举,包含三个取值:

值行为
Vertical垂直模式,子菜单以弹出浮层(Overlay)方式展示
Horizontal水平模式,子菜单在下方弹出,适合顶部导航
Inline内嵌模式,子菜单直接展开在菜单区域内部,适合侧边导航

示例中菜单声明为Mode="MenuMode.Inline",并设置了Style="width:256px ;"来模拟侧边栏的宽度。

内嵌模式下,SubMenu 的展开/收起由OpenKeys这一"受控"参数驱动。从 Menu.razor.cs 源码 可以看到:

  • OpenKeys(string[]):当前展开的 SubMenu 菜单项 key 数组,默认值为空数组;
  • OnOpenChange(EventCallback<string[]>):当 SubMenu 展开/关闭时触发,回调参数为最新的 openKeys 数组;
  • DefaultOpenKeys:仅用于初始化时展开的 key 数组,属于非受控入口(源码在OnInitialized中将其转换为OpenKeys,见 Menu.razor.cs);
  • SelectedKeys/DefaultSelectedKeys:选中项 key 数组,与展开状态相互独立。

由于OpenKeys是受控属性,展开状态完全由外部代码持有,这为我们手动实现"同一时刻只展开一个父级菜单"提供了操作入口。

方案一:OpenKeys + OnOpenChange 手动裁剪(官方 sider-current 示例)

官方示例sider-current的核心做法是:监听展开变化,判断最新打开的 SubMenu 是否属于一级菜单,若是则只保留它一个,否则原样保留全部展开项。完整实现见 SiderCurrent.razor。

第一步:声明受控状态与监听回调

<Menu Mode="MenuMode.Inline" OpenKeys=this.openKeys OnOpenChange=this.onOpenChange Style="width:256px ;">

在@code块中:

// submenu keys of first level string[] rootSubmenuKeys = {"sub1", "sub2", "sub4"}; string[] openKeys = {"sub1"}; void onOpenChange(string[] openKeys) { var latestOpenKey = openKeys.FirstOrDefault(key => !this.openKeys.Contains(key)); if (!rootSubmenuKeys.Contains(latestOpenKey)) { this.openKeys = openKeys; } else { this.openKeys = !string.IsNullOrEmpty(latestOpenKey) ? new[] {latestOpenKey} : Array.Empty<string>(); } }

第二步:构造多级菜单结构

<SubMenu Key="sub1" TitleTemplate=@sub1Title> <MenuItem Key="1">Option 1</MenuItem> <MenuItem Key="2">Option 2</MenuItem> <MenuItem Key="3">Option 3</MenuItem> <MenuItem Key="4">Option 4</MenuItem> </SubMenu> <SubMenu Key="sub2" TitleTemplate=@sub2Title> <MenuItem Key="5">Option 5</MenuItem> <MenuItem Key="6">Option 6</MenuItem> <SubMenu Key="sub3" Title="Submenu"> <MenuItem Key="7">Option 7</MenuItem> <MenuItem Key="8">Option 8</MenuItem> </SubMenu> </SubMenu> <SubMenu Key="sub4" TitleTemplate=@sub4Title> <MenuItem Key="9">Option 9</MenuItem> <MenuItem Key="10">Option 10</MenuItem> <MenuItem Key="11">Option 11</MenuItem> <MenuItem Key="12">Option 12</MenuItem> </SubMenu>

注意菜单结构特意设计了一个二级 SubMenu(sub2下的sub3),这正是该方案的用武之地:rootSubmenuKeys只登记sub1、sub2、sub4三个一级菜单。

第三步:理解裁剪逻辑的关键分支

回调中的latestOpenKey表示"相对上一次状态而言,本次新增展开的那个 key"。判断逻辑分为两种情况:

  1. 最新展开的不是一级菜单(例如用户点开了sub2内部的sub3):说明用户是在已展开的sub2下做层级导航,此时不应该收起sub2及其兄弟菜单,因此this.openKeys = openKeys,直接把最新展开数组原样写回,sub1、sub2、sub4可以同时处于展开状态(为了满足"只展开当前父级"的严格语义,实践中也可在进入sub2时先收起其他一级菜单,这取决于产品交互设计)。

  2. 最新展开的是一级菜单(例如从sub1切换到sub4):执行"替换"逻辑——只保留最新的一级菜单 key,其余全部收起。new[] {latestOpenKey}即把展开数组裁剪为只剩一个元素;如果latestOpenKey为空(表示本次是收起操作而非展开),则置为空数组,允许用户手动收起所有菜单。

这里需要特别留意示例中参数与字段同名的写法:void onOpenChange(string[] openKeys)中的openKeys是方法参数,而this.openKeys才是菜单字段。理解这一区分是读懂裁剪逻辑的前提。

方案二:直接启用 Accordion 手风琴模式

如果你需要的语义就是"任何时刻最多只有一个一级 SubMenu 展开",且不要求二级及以下层级额外展开多个,ant-design-blazor 还提供了更省事的内置方案:Menu的Accordion参数。

官方示例sider-current2(配套说明见 sider-current2.md)展示了极简写法,完整代码见 SiderCurrent2.razor:

<Menu Mode="MenuMode.Inline" Accordion SelectedKeys="@(new[] { "1"})" OpenKeys="@(new[] { "sub1" })" Style="width:256px ;"> @* 菜单结构与方案一相同:sub1 / sub2(含 sub3) / sub4 *@ </Menu>

与方案一的区别一目了然:

  • 无需自己维护openKeys字段,也无需编写onOpenChange回调——Accordion是组件内置行为;
  • OpenKeys在这里只承担初始展开作用(配合SelectedKeys指定默认选中项);
  • 手风琴语义由组件在内部强制:每次展开一个 SubMenu 时,其余 SubMenu 全部关闭。

从 Menu.razor.cs 源码 的SelectSubmenu方法可以印证其实现原理:

if (Accordion) { foreach (SubMenu item in _submenus.Where(x => x != menu && x != menu.Parent)) { item.Close(); } }

即组件在_submenus列表(由AddSubmenu注册的所有 SubMenu 集合)中,除当前点击的menu与其父级menu.Parent之外,其余一律调用Close()。这意味着手风琴行为同时作用于全部层级——这也是它与方案一在行为上的本质差异。

两种方案的行为差异与选型建议

对比维度方案一(OpenKeys + OnOpenChange)方案二(Accordion)
代码量需维护字段与回调,代码较多一个属性即可,代码最少
控制力完全受控,可自定义任何展开/收起策略固定为"只保留当前及其父级"策略
二级菜单行为可在父级展开下同时展开多个二级 SubMenu(示例中sub3被允许与sub2并存)打开sub3时,除sub2(其父级)外其他 SubMenu 也会被收起
关闭能力裁剪为空数组时允许全部收起组件侧始终保留当前点击项展开
适用场景需要精细定制(如结合路由、缓存上次展开状态、多级同时展开)标准后台侧边栏、交互最简单直接

选择建议:如果业务要求严格"同一时刻仅一个一级菜单展开",且没有多级同开需求,Accordion是最佳选择;如果需要在sub2内部同时展开多个二级子菜单、或在展开/收起时做额外业务处理(例如记录用户习惯、路由联动),则应采用方案一的受控写法。

源码层面的补充细节

展开状态如何流向 DOM

OpenKeys变化时,Menu.razor.cs 的 HandleOpenKeySet 方法 会遍历_submenus,对 key 出现在OpenKeys中的 SubMenu 调用Open(),其余调用Close()。而SubMenu.Open()在 SubMenu.razor.cs 中会级联调用Parent?.Open(),保证展开子菜单时其所有祖先也保持展开。

收起/展开的动画实现

当Menu的Animation参数开启时,SubMenu通过HandleExpand/HandleCollapse(见 SubMenu.razor.cs)驱动ant-motion-collapse-*系列 CSS 类与高度过渡,实现平滑展开收起动画;内嵌模式下收起还会先Task.Delay(300)等待动画完成。

缩进与层级

内嵌模式下,SubMenu 的每级缩进宽度由Menu.InlineIndent控制(默认 24px)。SubMenu.Padding为Level * InlineIndent(SubMenu.razor.cs),MenuItem的缩进为(ParentMenu.Level + 1) * InlineIndent(MenuItem.razor.cs),这就形成了从一级到多级的递进视觉层级。

测试佐证

仓库测试 Menu.Inline.Tests.razor 通过 bUnit 对 inline 菜单的渲染与交互做了多组断言,例如:

  • Basic_inline_menu_onClick_opens_submenu:点击 SubMenu 标题后,其ul子层应带上ant-menu-submenu-open类;
  • Basic_inline_menu_click_to_select_submenu_option:选中子项后,SubMenu 同时获得ant-menu-submenu-selected与ant-menu-submenu-open;
  • Basic_inline_menu_expanded_with_selected_nested_option_renders_correctly:验证DefaultOpenKeys与DefaultSelectedKeys组合下的初始渲染。

这些用例可帮助你确认OpenKeys、选中态、展开态在 inline 模式下的预期 DOM 输出。

实战注意事项

  1. 初始展开:方案一中openKeys字段初始值{"sub1"}即"首次渲染即展开 sub1";也可以改用DefaultOpenKeys声明初始展开,让展开状态在初始化后才受控。
  2. 不要混用受控与非受控:OpenKeys一旦由外部绑定,展开状态的唯一来源就是该字段;此时再依赖DefaultOpenKeys不会响应后续变化(源码在OnInitialized中只做一次赋值,见 Menu.razor.cs)。
  3. 与 Sider 折叠联动:侧边栏常见的"收起为图标"由InlineCollapsed参数实现(其取值会受Sider的Collapsed级联影响,见 Menu.razor.cs)。折叠时组件内部会将展开状态全部关闭并切换为浮层展示,注意折叠/还原后重新设置OpenKeys以恢复用户预期状态。
  4. key 的唯一性:SubMenu与MenuItem的Key在 源码实现 中未指定时会回退到组件自动生成的Id;做受控展开时务必为每个 SubMenu 显式设置稳定、唯一的Key,否则裁剪逻辑无法匹配。
  5. RTL 支持:内嵌缩进在 RTL 模式下会自动切换为padding-right(SubMenu.razor.cs),无需额外处理。

相关资源索引

  • 官方示例与说明:sider-current.md、SiderCurrent.razor、sider-current2.md、SiderCurrent2.razor
  • 组件文档(完整 API 参数表):index.zh-CN.md
  • 核心源码:Menu.razor.cs、SubMenu.razor.cs、MenuItem.razor.cs、MenuMode.cs
  • 单元测试:Menu.Inline.Tests.razor

通过本文的两种方案与源码剖析,你可以按需实现"只展开当前父级菜单"的侧边导航交互:需要极简与标准行为时使用Accordion,需要精细控制与多级并存时采用OpenKeys+OnOpenChange的受控写法。

  • 前端
  • UI组件
  • 设计系统

【免费下载链接】ant-design-blazor

基于 Ant Design 与 Blazor 的前端组件库。让开发者解放生产力,实现更大价值。

项目地址:https://gitcode.com/ant-design-blazor/ant-design-blazor
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询