You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor Server App:移除导航父项show CSS类的实现问题

问题与解决方案

问题说明

  • 目标:实现多级可折叠子菜单导航,点击子菜单项时关闭对应最上层父菜单,并移除顶级导航锚点的"show" CSS类
  • 当前异常:默认所有父导航项显示为展开状态,仅点击菜单链接后才恢复非展开的默认状态;且所有顶级菜单共享同一展开状态,无法独立控制

代码问题分析

  1. 使用**全局变量navMenuExpanded**控制所有顶级菜单状态,导致所有菜单同步展开/折叠,不符合多级菜单独立控制需求
  2. CSS类逻辑错误:NavMenuCssClassCollapse默认返回"collapse",与Bootstrap原生dropdown的show类控制逻辑冲突,导致默认状态异常
  3. 子菜单项点击事件仅切换全局状态,未针对当前顶级菜单执行精准关闭操作

修正后的代码

@inject ISiteMapData SiteMapData
@inject IJSRuntime JS

<div>
    <nav class="navbar navbar-expand-lg bg-body-tertiary">
        <div class="container-fluid">
            <a class="navbar-brand" href="https://my.boeing.com">
                <img class="img-fluid" src="/img/boeing_logo_white.png" alt="Boeing Logo">
            </a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" 
                aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    @if (siteMap is not null)
                    {
                        IEnumerable<SiteMapModel> parentNavigationItems = siteMap.Where(x => x.ParentNodeID == null).OrderBy(o => o.SortOrder);

                        @foreach (SiteMapModel parentNavigationItem in parentNavigationItems)
                        {
                            IEnumerable<SiteMapModel>? childNavigationItems = ChildRows(parentNavigationItem.NodeID);

                            @if (childNavigationItems.Count() != 0)
                            {
                                bool isExpanded = _expandedMenus.TryGetValue(parentNavigationItem.NodeID, out bool expanded) ? expanded : false;
                                <li class="nav-item dropdown">
                                    <a title="@parentNavigationItem.Description" 
                                       class="dropdown-item dropdown-toggle nav-link @(isExpanded ? "show" : "")" 
                                       tabindex="-1" 
                                       data-node-id="@parentNavigationItem.NodeID"
                                       data-bs-toggle="dropdown" 
                                       data-bs-auto-close="outside" 
                                       aria-expanded="@isExpanded.ToString().ToLower()">
                                        @parentNavigationItem.Title
                                    </a>
                                    <ul class="dropdown-menu dropdown-submenu @(isExpanded ? "show" : "")">
                                        @foreach (SiteMapModel childNavigationItem in childNavigationItems)
                                        {
                                            IEnumerable<SiteMapModel>? secondChildNavigationItems = ChildRows(childNavigationItem.NodeID);

                                            if (secondChildNavigationItems.Count() != 0)
                                            {
                                                <li class="dropend">
                                                    <a class="dropdown-item dropdown-toggle" tabindex="-1" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
                                                        @childNavigationItem.Title
                                                    </a>
                                                    <ul class="dropdown-menu dropdown-submenu">
                                                        @foreach (SiteMapModel secondChildNavigationItem in secondChildNavigationItems)
                                                        {
                                                            IEnumerable<SiteMapModel>? thirdChildNavigationItems = ChildRows(secondChildNavigationItem.NodeID);

                                                            if (thirdChildNavigationItems.Count() != 0)
                                                            {
                                                                <li class="dropstart">
                                                                    <a class="dropdown-item dropdown-toggle" tabindex="-1" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
                                                                        @secondChildNavigationItem.Title
                                                                    </a>
                                                                    <ul class="dropdown-menu dropdown-submenu">
                                                                        @foreach (SiteMapModel thirdChildNavigationItem in thirdChildNavigationItems)
                                                                        {
                                                                            <li title="@thirdChildNavigationItem.Description">
                                                                                <a class="dropdown-item" href="@thirdChildNavigationItem.URL" @onclick="() => CloseParentMenu(parentNavigationItem.NodeID)">@thirdChildNavigationItem.Title</a>
                                                                            </li>
                                                                        }
                                                                    </ul>
                                                                </li>
                                                            }
                                                            else
                                                            {
                                                                <li title="@secondChildNavigationItem.Description">
                                                                    <a class="dropdown-item" href="@secondChildNavigationItem.URL" @onclick="() => CloseParentMenu(parentNavigationItem.NodeID)">@secondChildNavigationItem.Title</a>
                                                                </li>
                                                            }
                                                        }
                                                    </ul>
                                                </li>
                                            }
                                            else
                                            {
                                                <li title="@childNavigationItem.Description">
                                                    <a class="dropdown-item" href="@childNavigationItem.URL" @onclick="() => CloseParentMenu(parentNavigationItem.NodeID)">@childNavigationItem.Title</a>
                                                </li>
                                            }
                                        }
                                    </ul>
                                </li>
                            }
                            else
                            {
                                <li class="nav-item">
                                    <a id="@parentNavigationItem.Title.ToLower()" class="nav-link" aria-current="page" href="" title="@parentNavigationItem.Description">
                                        @parentNavigationItem.Title
                                    </a>
                                </li>
                            }
                        }
                    }
                </ul>
                <form class="d-flex search-area" role="search">
                    <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
                    <button class="btn btn-outline-success search-button" type="submit">Search</button>
                </form>
            </div>
        </div>
    </nav>
</div>

@code {
    private IEnumerable<SiteMapModel>? siteMap;
    // 用字典记录每个顶级菜单的独立展开状态
    private Dictionary<int, bool> _expandedMenus = new();

    protected override async Task OnInitializedAsync()
    {
        siteMap = await SiteMapData.GetSiteMap(1234567890);
        // 初始化所有顶级菜单为折叠状态
        if (siteMap != null)
        {
            var parentIds = siteMap.Where(x => x.ParentNodeID == null).Select(x => x.NodeID);
            foreach (var id in parentIds)
            {
                _expandedMenus[id] = false;
            }
        }
    }

    private IEnumerable<SiteMapModel>? ChildRows(int parentNavigationItemID)
    {
        if (siteMap is not null)
        {
            return siteMap.Where(x => x.ParentNodeID == parentNavigationItemID).OrderBy(o => o.SortOrder);
        }

        return null;
    }

    // 精准关闭指定顶级父菜单
    private void CloseParentMenu(int parentNodeId)
    {
        if (_expandedMenus.ContainsKey(parentNodeId))
        {
            _expandedMenus[parentNodeId] = false;
            // 调用JS关闭Bootstrap dropdown,保证状态完全同步
            _ = JS.InvokeVoidAsync("document.querySelector('[data-node-id=\"" + parentNodeId + "\"]').dispatchEvent(new Event('hide.bs.dropdown'))");
        }
    }
}

关键修改点

  1. 替换全局状态为字典:用Dictionary<int, bool>记录每个顶级菜单的独立展开状态,默认全部设为折叠
  2. 修正Bootstrap样式绑定:根据菜单状态添加/移除show类,同步aria-expanded属性,符合原生组件规范
  3. 子菜单点击事件优化:每个子菜单项点击时调用CloseParentMenu,传入对应顶级菜单ID,实现精准关闭
  4. JS同步状态:调用JS触发Bootstrap dropdown的隐藏事件,保证UI与本地状态完全一致

内容的提问来源于stack exchange,提问作者Daniel Carriveau

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 21:24:58