上一章,我们实现了用户的注册和登录,登录之后展示的是我们的主页,页面的左侧是多级的导航菜单,定位并展示用户需要访问的不同页面。目前导航菜单是写死的,考虑以后菜单管理的便捷性,我们这节实现下可视化配置菜单的功能,这样以后我们可以动态的配置导航菜单,不用再编译发布网站程序了。
增加后台管理模块
第1步,左侧导航菜单中,添加后台管理模块,用作管理员登录后,可以进行一些后台管理的操作,当然,目前还没有权限控制(后期加入),所以对所有用户可见。大概菜单结构如下:
有了菜单项,我们还需要控制视图的跳转,所以,接下来需要写对应的控制器和视图。
为了将相关功能组织成一组单独命名空间(路由)和文件夹结构(视图),解决方案中右键添加区域(Area),取名后台管理(Configuration),代表后台管理模块,.Net Core脚手架(scaffold)自动帮我们实现了目录划分:控制器(Controllers)、模型(Models)、视图(Views)
菜单模型定义
菜单的基本属性有:菜单名称、菜单类型、菜单的图标样式、菜单url路径。另外,菜单在逻辑上是树状结构,但是要在物理数据库中存储,需要进行扁平化处理,每个菜单项有个父菜单属性(根节点的父菜单为空),还有同一父节点底下,在组类的排序属性,定义如下:
1 /// <summary> 2 /// 菜单 3 /// </summary> 4 public class Menu 5 { 6 /// <summary> 7 /// 主键ID 8 /// </summary> 9 [DatabaseGenerated(DatabaseGeneratedOption.None)] 10 [Required(ErrorMessage = "请输入菜单编号")] 11 public string Id { get; set; } 12 13 /// <summary> 14 /// 菜单名称 15 /// </summary> 16 [Required(ErrorMessage = "请输入菜单名称")] 17 [StringLength(256)] 18 public string Name { get; set; } 19 20 /// <summary> 21 /// 父级ID 22 /// </summary> 23 [DisplayFormat(NullDisplayText = "无")] 24 public string ParentId { get; set; } 25 26 /// <summary> 27 /// 菜单组内排序 28 /// </summary> 29 [Range(0, 99, ErrorMessage = "请选择1-99范围内的整数")] 30 public int IndexCode { get; set; } 31 32 /// <summary> 33 /// 菜单路径 34 /// </summary> 35 [StringLength(256)] 36 [DisplayFormat(NullDisplayText = "无")] 37 public string Url { get; set; } 38 39 /// <summary> 40 /// 类型:0导航菜单;1操作按钮。 41 /// </summary> 42 [Required(ErrorMessage = "请选择菜单类型")] 43 public MenuTypes? MenuType { get; set; } 44 45 /// <summary> 46 /// 菜单图标名称 47 /// </summary> 48 [Required(ErrorMessage = "请输入菜单图标")] 49 [StringLength(50)] 50 public string Icon { get; set; } 51 52 /// <summary> 53 /// 菜单备注 54 /// </summary> 55 public string Remarks { get; set; } 56 } 57 /// <summary> 58 /// 菜单类型 59 /// </summary> 60 public enum MenuTypes 61 { 62 /// <summary> 63 /// 导航菜单 64 /// </summary> 65 导航菜单, 66 /// <summary> 67 /// 操作菜单 68 /// </summary> 69 操作菜单 70 }
有了我们的菜单模型,在控制器目录中,我们右键建立第1个自己的控制器,取名MenuController,用来菜单管理,上下文选取定义好的Menu模型,还是利用脚手架,自动帮我们生成增删改查对应的后来逻辑和视图。此时,我们把菜单导向该控制器,其实是可以正常访问的,不过还远远达不到我们的要求,所以我们还得完善下自动生成的代码。
菜单控制器改写
为了方便今后的拓展,新增一个AppController控制器,继承Controller,以后所有的控制器,都继承于AppController,方便一些公共的方法调用。
.Net Core有个比较方便的一点,就是实现了构造器的依赖注入,这样我们不用像以前那样手工New一个DBContext对象,直接在控制器将需要的DBContext注入,调用的时候,直接访问注入的对象即可,有关依赖注入的知识,这里就不在多说了,有兴趣大家可以了解一下:.Net Core依赖注入
首先,在ApplicationDbContext添加Menu数据集
1 public class ApplicationDbContext : IdentityDbContext<ApplicationUser> 2 { 3 public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) 4 : base(options) 5 { 6 } 7 8 protected override void OnModelCreating(ModelBuilder builder) 9 { 10 base.OnModelCreating(builder); 11 } 12 13 public DbSet<ApplicationUser> ApplicationUsers { get; set; } 14 15 public DbSet<Menu> Menus { get; set; } 16 }
这里我们修改下MenuController构造器:
1 private readonly ApplicationDbContext _context; 2 3 public MenuController(ApplicationDbContext context, INavMenuService navMenuService) 4 { 5 _context = context; 6 _NavMenuService = navMenuService; 7 }
为了后面方便统一提供下拉框选择,这里实现一个下拉框初始化方法:
1 /// <summary> 2 /// 初始化下拉选择框 3 /// </summary> 4 /// <param name="menu"></param> 5 private void UpdateDropDownList(Menu menu = null) 6 { 7 var menusParent = _context.Menus.AsNoTracking().Where(s => s.MenuType == MenuTypes.导航菜单); 8 List<SelectListItem> listMenusParent = new List<SelectListItem>(); 9 foreach (var menuParent in menusParent) 10 { 11 listMenusParent.Add(new SelectListItem 12 { 13 Value = menuParent.Id, 14 Text = menuParent.Id + $"({menuParent.Name})", 15 Selected = (menu != null && menuParent.Id == menu.ParentId) 16 }); 17 } 18 ViewBag.ParentIds = listMenusParent; 19 20 if (menu == null) 21 { 22 ViewBag.MenuTypes = MenuTypes.导航菜单.GetSelectListByEnum(); 23 } 24 else 25 { 26 ViewBag.MenuTypes = MenuTypes.导航菜单.GetSelectListByEnum(Convert.ToInt32(menu.MenuType)); 27 } 28 }
列表页改写
控制器调整:增加查询传入参数,根据参数筛选查询结果;
1 /// <summary> 2 /// 列表页 3 /// </summary> 4 /// <param name="query"></param> 5 /// <returns></returns> 6 public async Task<IActionResult> Index(MenuIndexQuery query) 7 { 8 var menus = _context.Menus.AsNoTracking(); 9 if (!string.IsNullOrEmpty(query.QName)) 10 { 11 menus = menus.Where(s => s.Name.Contains(query.QName.Trim())); 12 } 13 if (!string.IsNullOrEmpty(query.QId)) 14 { 15 menus = menus.Where(s => s.Id.Contains(query.QId.Trim())); 16 } 17 if (!string.IsNullOrEmpty(query.QParentId)) 18 { 19 menus = menus.Where(s => s.ParentId == query.QParentId.Trim()); 20 } 21 if (query.QMenuType != null) 22 { 23 menus = menus.Where(s => s.MenuType == query.QMenuType); 24 } 25 26 UpdateDropDownList(); 27 return View(new MenuIndexVM { Menus = await menus.ToListAsync(), Query = query }); 28 }
视图调整:用户点击删除时,弹出确认框,调用Ajax方式删除数据,不再通过页面跳转;
1 @using MyWebSite.ViewModels 2 @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 4 @model MyWebSite.Areas.Configuration.ViewModels.MenuIndexVM 5 @{ 6 ViewData["Title"] = "菜单列表"; 7 8 var breadcrumb = new BreadCrumb("菜单列表", "Version 2.0", new List<NavCrumb> 9 { 10 new NavCrumb(name:"菜单管理",url: "/Configuration/Menu"), 11 new NavCrumb(name:"菜单列表"), 12 }); 13 ViewBag.BreadCrumb = breadcrumb; 14 15 Layout = "~/Views/Shared/_Layout.cshtml"; 16 } 17 18 <div class="row"> 19 <div class="col-xs-12"> 20 <div class="box with-border"> 21 <form class="form" asp-action="Index"> 22 <div class="box-header"> 23 <h3 class="box-title"><i class="fa fa-search margin-r-5">查询条件</i></h3> 24 <div class="box-tools pull-right"> 25 <button type="submit" class="btn btn-success margin-r-5"><i class="fa fa-search margin-r-5"></i>查询</button> 26 <a class="btn btn-primary" href="/Configuration/Menu/Create"><i class="fa fa-plus margin-r-5"></i>新建</a> 27 </div> 28 <div asp-validation-summary="All" class="text-danger"></div> 29 <div class="row"> 30 <div class="col-md-3"> 31 <div class="form-group"> 32 <label asp-for="Query.QName">菜单名称:</label> 33 <input asp-for="Query.QName" class="form-control input-sm"> 34 </div> 35 </div> 36 <div class="col-md-3"> 37 <div class="form-group"> 38 <label asp-for="Query.QId">菜单编码:</label> 39 <input asp-for="Query.QId" class="form-control input-sm"> 40 </div> 41 </div> 42 <div class="col-md-3"> 43 <div class="form-group"> 44 <label asp-for="Query.QParentId">父级菜单:</label> 45 <select asp-for="Query.QParentId" class="form-control input-sm select2" asp-items="ViewBag.ParentIds"> 46 <option value="">-- 请选择 --</option> 47 </select> 48 </div> 49 </div> 50 <div class="col-md-3"> 51 <div class="form-group"> 52 <label asp-for="Query.QMenuType">菜单类型:</label> 53 <select asp-for="Query.QMenuType" class="form-control input-sm select2" asp-items="ViewBag.MenuTypes"> 54 <option value="">-- 请选择 --</option> 55 </select> 56 </div> 57 </div> 58 </div> 59 </div> 60 </form> 61 <div class="box-body"> 62 <table class="table table-bordered table-hover" style=" 100%"> 63 <thead> 64 <tr> 65 <th>#</th> 66 <th>菜单名称</th> 67 <th>菜单编号</th> 68 <th>父级编号</th> 69 <th>组内排序</th> 70 <th>菜单类型</th> 71 <th>菜单图标</th> 72 <th>菜单路径</th> 73 <th>操作</th> 74 </tr> 75 </thead> 76 <tbody> 77 @{ 78 var index = 0; 79 } 80 @foreach (var item in Model.Menus) 81 { 82 index++; 83 <tr> 84 <td> 85 @index.ToString("D3") 86 </td> 87 <td> 88 @Html.ActionLink(@item.Name, "Details", new { id = @item.Id }) 89 </td> 90 <td> 91 <span>@item.Id</span> 92 </td> 93 <td> 94 <span>@Html.DisplayFor(modelItem => item.ParentId)</span> 95 </td> 96 <td> 97 <span>@item.IndexCode</span> 98 </td> 99 <td> 100 <span>@item.MenuType</span> 101 </td> 102 <td> 103 <i class="fa @item.Icon" data-toggle="tooltip" data-placement="right" title="@item.Icon"></i> 104 </td> 105 <td> 106 <i class="fa fa-ellipsis-h" data-toggle="tooltip" data-placement="top" title="@Html.DisplayFor(modelItem => item.Url)"></i> 107 </td> 108 <td> 109 @Html.ActionLink("编辑", "Edit", new { id = @item.Id })| 110 @Html.ActionLink("详情", "Details", new { id = @item.Id })| 111 <a href="#" onclick="onDelete('@item.Id', '@item.Name');">删除</a> 112 </td> 113 </tr> 114 } 115 </tbody> 116 </table> 117 </div> 118 </div> 119 </div> 120 </div> 121 122 @section Scripts{ 123 @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} 124 <script> 125 function onDelete(id, name) { 126 BootstrapDialog.show({ 127 message: '确认删除菜单-' + name + '[' + id + ']?', 128 size: BootstrapDialog.SIZE_SMALL, 129 draggable: true, 130 buttons: [ 131 { 132 icon: 'fa fa-check', 133 label: '确定', 134 cssClass: 'btn-primary', 135 action: function (dialogRef) { 136 dialogRef.close(); 137 $.ajax({ 138 type: 'POST', 139 url: '/Configuration/Menu/Delete', 140 data: { id: id }, 141 success: function () { 142 location.reload(); 143 } 144 }); 145 } 146 }, { 147 icon: 'fa fa-close', 148 label: '取消', 149 action: function (dialogRef) { 150 dialogRef.close(); 151 } 152 } 153 ] 154 }); 155 } 156 </script> 157 }
新建页改写
控制器调整:这里控制器有2个Create方法,一个是Http Get类型,用户列表页点新建时,跳转到该方法,另外一个是Http Post类型,用户填完新建的菜单信息后,点击保存,跳转到该方法。在Http Post方法中,为了防止页面over post,需要指定绑定的属性Bind("Id,Name,ParentId,IndexCode,Url,MenuType,Icon,Remarks"),当然,也可以用TryUpdateModel()实现,以后再介绍;
1 /// <summary> 2 /// 新建空白页面 3 /// </summary> 4 /// <returns></returns> 5 public IActionResult Create() 6 { 7 var model = new Menu 8 { 9 Id = "MXX_XX_XX", 10 IndexCode = 1, 11 Icon = "fa-circle-o" 12 }; 13 UpdateDropDownList(); 14 return View(model); 15 } 16 17 /// <summary> 18 /// 新建保存页面 19 /// </summary> 20 /// <param name="menu"></param> 21 /// <returns></returns> 22 [HttpPost] 23 public async Task<IActionResult> Create([Bind("Id,Name,ParentId,IndexCode,Url,MenuType,Icon,Remarks")] Menu menu) 24 { 25 if (ModelState.IsValid) 26 { 27 if (!MenuExists(menu.Id)) 28 { 29 _context.Add(menu); 30 await _context.SaveChangesAsync(); 31 32 _NavMenuService.InitOrUpdate(); 33 return RedirectToAction(nameof(Index)); 34 } 35 else 36 { 37 ModelState.AddModelError("Id", "菜单编号已存在,请修改菜单编号."); 38 } 39 } 40 UpdateDropDownList(menu); 41 return View(menu); 42 }
视图调整: 引入前端数据验证,并增加一些数据控制,比如菜单类型非操作菜单时,菜单路径不可编辑等等;
1 @using MyWebSite.ViewModels 2 @using MyWebSite.Areas.Configuration.Models 3 @model MyWebSite.Areas.Configuration.Models.Menu 4 5 @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 6 7 @{ 8 ViewData["Title"] = "菜单新建"; 9 10 var breadcrumb = new BreadCrumb("菜单新建", "Version 2.0", new List<NavCrumb> 11 { 12 new NavCrumb(name:"菜单管理",url: "/Configuration/Menu"), 13 new NavCrumb(name:"菜单新建"), 14 }); 15 ViewBag.BreadCrumb = breadcrumb; 16 17 Layout = "~/Views/Shared/_Layout.cshtml"; 18 } 19 <section class="content"> 20 <div class="row"> 21 <div class="col-md-8"> 22 <div class="box"> 23 <div class="box-header with-border"> 24 <h3 class="box-title">新建</h3> 25 </div> 26 <form asp-action="Create"> 27 <div asp-validation-summary="All" class="text-danger"></div> 28 <div class="box-body"> 29 <div class="form-group col-md-6"> 30 <label asp-for="Id">菜单编号</label> 31 <input asp-for="Id" class="form-control input-sm"> 32 </div> 33 <div class="form-group col-md-6"> 34 <label asp-for="Name">菜单名称</label> 35 <input asp-for="Name" class="form-control input-sm"> 36 </div> 37 <div class="form-group col-md-6"> 38 <label asp-for="ParentId">父级菜单</label> 39 <select asp-for="ParentId" class="form-control input-sm select2" asp-items="ViewBag.ParentIds"> 40 <option value="">-- 请选择 --</option> 41 </select> 42 </div> 43 <div class="form-group col-md-6"> 44 <label asp-for="IndexCode">组内排序</label> 45 <input asp-for="IndexCode" class="form-control input-sm"> 46 </div> 47 <div class="form-group col-md-6"> 48 <label asp-for="MenuType">菜单类型</label> 49 <select asp-for="MenuType" class="form-control input-sm" asp-items="ViewBag.MenuTypes"> 50 <option value="">-- 请选择 --</option> 51 </select> 52 </div> 53 <div class="form-group col-md-6"> 54 <label asp-for="Icon">菜单图标</label> 55 <div class="input-group"> 56 <span class="input-group-addon"><i id="IconfShow" class="fa @Model.Icon"></i></span> 57 <input asp-for="Icon" class="form-control input-sm"> 58 </div> 59 </div> 60 <div class="form-group col-md-6"> 61 <label asp-for="Url">菜单路径</label> 62 @if (Model.MenuType == MenuTypes.操作菜单) 63 { 64 <input asp-for="Url" class="form-control input-sm"> 65 } 66 else 67 { 68 <input asp-for="Url" class="form-control input-sm" readonly> 69 } 70 </div> 71 <div class="form-group col-md-6"> 72 <label asp-for="Remarks">备注</label> 73 <input asp-for="Remarks" class="form-control input-sm"> 74 </div> 75 </div> 76 <div class="box-footer"> 77 <button type="submit" class="btn btn-primary"><i id="IconfShow" class="fa fa-save"></i> 保存</button> 78 <a asp-action="Index" class="btn btn-default"><i id="IconfShow" class="fa fa-undo"></i> 返回</a> 79 </div> 80 </form> 81 </div> 82 83 </div> 84 </div> 85 </section> 86 @section Scripts { 87 <script src="~/js/Configuration/Menu.js"></script> 88 @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} 89 }
详情页改写
控制器调整:不用大的调整,只是增加了下拉框的初始化工作 ;
1 /// <summary> 2 /// 详情页 3 /// </summary> 4 /// <param name="id"></param> 5 /// <returns></returns> 6 public async Task<IActionResult> Details(string id) 7 { 8 if (id == null) 9 { 10 return NotFound(); 11 } 12 13 var menu = await _context.Menus 14 .SingleOrDefaultAsync(m => m.Id == id); 15 if (menu == null) 16 { 17 return NotFound(); 18 } 19 20 UpdateDropDownList(menu); 21 return View(menu); 22 }
视图调整:跟创建界面大体差不多, 只是控制属性字段不允许编辑,也不用数据验证;
@using MyWebSite.ViewModels @model MyWebSite.Areas.Configuration.Models.Menu @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @{ ViewData["Title"] = "菜单详情"; var breadcrumb = new BreadCrumb("菜单详情", "Version 2.0", new List<NavCrumb> { new NavCrumb(name:"菜单管理",url: "/Configuration/Menu"), new NavCrumb(name:"菜单详情"), new NavCrumb(name:Model.Id), }); ViewBag.BreadCrumb = breadcrumb; Layout = "~/Views/Shared/_Layout.cshtml"; } <section class="content"> <div class="row"> <div class="col-md-8"> <div class="box"> <div class="box-header with-border"> <h3 class="box-title">详情</h3> </div> <form> <div class="box-body"> <div class="form-group col-md-6"> <label asp-for="Id">菜单编号</label> <input asp-for="Id" class="form-control input-sm" readonly> </div> <div class="form-group col-md-6"> <label asp-for="Name">菜单名称</label> <input asp-for="Name" class="form-control input-sm" readonly> </div> <div class="form-group col-md-6"> <label asp-for="ParentId">父级菜单</label> <select asp-for="ParentId" class="form-control input-sm" asp-items="ViewBag.ParentIds" disabled> </select> </div> <div class="form-group col-md-6"> <label asp-for="IndexCode">组内排序</label> <input asp-for="IndexCode" class="form-control input-sm" readonly> </div> <div class="form-group col-md-6"> <label asp-for="MenuType">菜单类型</label> <input asp-for="MenuType" class="form-control input-sm" readonly> </div> <div class="form-group col-md-6"> <label asp-for="Icon">菜单图标</label> <div class="input-group"> <span class="input-group-addon"><i id="IconfShow" class="fa @Model.Icon"></i></span> <input asp-for="Icon" class="form-control input-sm" readonly> </div> </div> <div class="form-group col-md-6"> <label asp-for="Url">菜单路径</label> <input asp-for="Url" class="form-control input-sm" readonly> </div> <div class="form-group col-md-6"> <label asp-for="Remarks">备注</label> <input asp-for="Remarks" class="form-control input-sm" readonly> </div> </div> <div class="box-footer"> <a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-primary"><i id="IconfShow" class="fa fa-edit"></i> 编辑</a> <a asp-action="Index" class="btn btn-default"><i id="IconfShow" class="fa fa-undo"></i> 返回</a> </div> </form> </div> </div> </div> </section>
编辑页面改写
控制器调整:也是有Http Get和Http Post方法,分别是开始编辑和编辑保存跳转的方法,同时加上防止over post字段绑定;
1 /// <summary> 2 /// 开始编辑 3 /// </summary> 4 /// <param name="id"></param> 5 /// <returns></returns> 6 public async Task<IActionResult> Edit(string id) 7 { 8 if (id == null) 9 { 10 return NotFound(); 11 } 12 13 var menu = await _context.Menus.SingleOrDefaultAsync(m => m.Id == id); 14 if (menu == null) 15 { 16 return NotFound(); 17 } 18 19 UpdateDropDownList(menu); 20 return View(menu); 21 } 22 23 /// <summary> 24 /// 编辑保存 25 /// </summary> 26 /// <param name="id"></param> 27 /// <param name="menu"></param> 28 /// <returns></returns> 29 [HttpPost] 30 public async Task<IActionResult> Edit(string id, [Bind("Id,Name,ParentId,IndexCode,Url,MenuType,Icon,Remarks")] Menu menu) 31 { 32 if (id != menu.Id) 33 { 34 return NotFound(); 35 } 36 37 if (ModelState.IsValid) 38 { 39 try 40 { 41 _context.Update(menu); 42 await _context.SaveChangesAsync(); 43 } 44 catch (DbUpdateConcurrencyException) 45 { 46 if (!MenuExists(menu.Id)) 47 { 48 return NotFound(); 49 } 50 else 51 { 52 throw; 53 } 54 } 55 _NavMenuService.InitOrUpdate(); 56 return RedirectToAction(nameof(Index)); 57 } 58 59 UpdateDropDownList(menu); 60 return View(menu); 61 }
视图调整:跟创建界面大体差不多,需要数据验证和数据控制;
1 @using MyWebSite.ViewModels 2 @using MyWebSite.Areas.Configuration.Models 3 @model MyWebSite.Areas.Configuration.Models.Menu 4 5 @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 6 7 @{ 8 ViewData["Title"] = "菜单编辑"; 9 10 var breadcrumb = new BreadCrumb("菜单编辑", "Version 2.0", new List<NavCrumb> 11 { 12 new NavCrumb(name:"菜单管理",url: "/Configuration/Menu"), 13 new NavCrumb(name:"菜单编辑"), 14 new NavCrumb(name:Model.Id), 15 }); 16 ViewBag.BreadCrumb = breadcrumb; 17 18 Layout = "~/Views/Shared/_Layout.cshtml"; 19 } 20 <section class="content"> 21 <div class="row"> 22 <div class="col-md-8"> 23 <div class="box"> 24 <div class="box-header with-border"> 25 <h3 class="box-title">编辑</h3> 26 </div> 27 <form asp-action="Edit"> 28 <div asp-validation-summary="All" class="text-danger"></div> 29 <div class="box-body"> 30 <div class="form-group col-md-6"> 31 <label asp-for="Id">菜单编号</label> 32 <input asp-for="Id" class="form-control input-sm" readonly> 33 </div> 34 <div class="form-group col-md-6"> 35 <label asp-for="Name">菜单名称</label> 36 <input asp-for="Name" class="form-control input-sm"> 37 </div> 38 <div class="form-group col-md-6"> 39 <label asp-for="ParentId">父级菜单</label> 40 <select asp-for="ParentId" class="form-control input-sm select2" asp-items="ViewBag.ParentIds"> 41 <option value="">-- 请选择 --</option> 42 </select> 43 </div> 44 <div class="form-group col-md-6"> 45 <label asp-for="IndexCode">组内排序</label> 46 <input asp-for="IndexCode" class="form-control input-sm"> 47 </div> 48 <div class="form-group col-md-6"> 49 <label asp-for="MenuType">菜单类型</label> 50 <select asp-for="MenuType" class="form-control input-sm" asp-items="ViewBag.MenuTypes"> 51 <option value="">-- 请选择 --</option> 52 </select> 53 </div> 54 <div class="form-group col-md-6"> 55 <label asp-for="Icon">菜单图标</label> 56 <div class="input-group"> 57 <span class="input-group-addon"><i id="IconfShow" class="fa @Model.Icon"></i></span> 58 <input asp-for="Icon" class="form-control input-sm"> 59 </div> 60 </div> 61 <div class="form-group col-md-6"> 62 <label asp-for="Url">菜单路径</label> 63 @if (Model.MenuType == MenuTypes.操作菜单) 64 { 65 <input asp-for="Url" class="form-control input-sm"> 66 } 67 else 68 { 69 <input asp-for="Url" class="form-control input-sm" readonly> 70 } 71 </div> 72 <div class="form-group col-md-6"> 73 <label asp-for="Remarks">备注</label> 74 <input asp-for="Remarks" class="form-control input-sm"> 75 </div> 76 </div> 77 <div class="box-footer"> 78 <button type="submit" class="btn btn-primary"><i id="IconfShow" class="fa fa-save"></i> 保存</button> 79 <a asp-action="Index" class="btn btn-default"><i id="IconfShow" class="fa fa-undo"></i> 返回</a> 80 </div> 81 </form> 82 </div> 83 84 </div> 85 </div> 86 </section> 87 @section Scripts { 88 <script src="~/js/Configuration/Menu.js" ></script> 89 @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} 90 }
以上,我们可以通过增删改查界面操作菜单项了,但是要怎么将数据库中的菜单跟左侧的导航菜单关联呢?下节,我们将实现下这个功能。
动态加载导航菜单
前面章节说过,物理数据库菜单Menu是表格结构,而界面上的导航菜单是树状结构,那应该怎么处理呢?我们考虑先定义树状导航菜单的数据结构,然后表格结构的菜单项通过逻辑处理,转换成树状的导航菜单,就可以满足我们的要求了。
第一步,定义导航菜单:属性跟菜单项差不多,不同的是没有父菜单,而是子菜单列表,这样可以包含子菜单,实现菜单的树状嵌套;
1 /// <summary> 2 /// 导航菜单项 3 /// </summary> 4 public class NavMenu 5 { 6 public string Id { get; set; } 7 public string Name { get; set; } 8 public MenuTypes MenuType { get; set; } 9 public string Url { get; set; } 10 public string Icon { get; set; } 11 public bool IsOpen { get; set; } 12 13 /// <summary> 14 /// 子菜单 15 /// </summary> 16 public IList<NavMenu> SubNavMenus = new List<NavMenu>(); 17 } 18 19 /// <summary> 20 /// 左侧导航菜单视图模型 21 /// </summary> 22 public class NavMenuVM 23 { 24 public IList<NavMenu> NavMenus { get; set; } 25 26 public string[] MenuidsOpen { get; set; } 27 }
第二步,实现获取数据库保存的所有菜单项信息服务NavMenuService:将表格结构的菜单项,转换成树状的导航菜单;
1 /// <summary> 2 /// 菜单服务 3 /// </summary> 4 public class NavMenuService : INavMenuService 5 { 6 private readonly ApplicationDbContext _context; 7 public NavMenuService(ApplicationDbContext context) 8 { 9 _context = context; 10 } 11 12 private static IList<NavMenu> NavMenus { get; set; } 13 14 /// <summary> 15 /// 获取导航菜单 16 /// </summary> 17 /// <returns></returns> 18 public IList<NavMenu> GetNavMenus() 19 { 20 if (NavMenus == null) 21 InitOrUpdate(); 22 23 return NavMenus; 24 } 25 /// <summary> 26 /// 生成导航菜单 27 /// </summary> 28 /// <returns></returns> 29 public void InitOrUpdate() 30 { 31 NavMenus = new List<NavMenu>(); 32 33 var rootMenus = _context.Menus 34 .Where(s => string.IsNullOrEmpty(s.ParentId)) 35 .AsNoTracking() 36 .OrderBy(s => s.IndexCode) 37 .ToList(); 38 39 foreach (var rootMenu in rootMenus) 40 { 41 NavMenus.Add(GetOneNavMenu(rootMenu)); 42 } 43 } 44 /// <summary> 45 /// 根据给定的Menu,生成对应的导航菜单 46 /// </summary> 47 /// <param name="menu"></param> 48 /// <returns></returns> 49 public NavMenu GetOneNavMenu(Menu menu) 50 { 51 //构建菜单项 52 var navMenu = new NavMenu 53 { 54 Id = menu.Id, 55 Name = menu.Name, 56 MenuType = menu.MenuType.Value, 57 Url = menu.Url, 58 Icon = menu.Icon 59 }; 60 61 //构建子菜单 62 var subMenus = _context.Menus 63 .Where(s => s.ParentId == menu.Id) 64 .AsNoTracking() 65 .OrderBy(s => s.IndexCode) 66 .ToList(); 67 68 foreach (var subMenu in subMenus) 69 { 70 navMenu.SubNavMenus.Add(GetOneNavMenu(subMenu)); 71 } 72 73 return navMenu; 74 }
第三步,我们需要定义一个部分视图_NavMenu,具体规定菜单的显示样式,重要的是,如果包含子菜单的时候,子菜单仍然使用_NavMenu递归渲染显示,这样理论上可以支持无穷级别的导航菜单的显示。如果菜单是导航菜单,增加展开样式,并渲染子菜单,如果是操作菜单,定义href为菜单路径;
1 @using MyWebSite.Areas.Configuration.Models 2 @using MyWebSite.Areas.Configuration.ViewModels 3 @model MyWebSite.Areas.Configuration.ViewModels.NavMenuVM 4 5 6 @foreach (var navMenu in Model.NavMenus) 7 { 8 if (navMenu.MenuType == MenuTypes.导航菜单) 9 { 10 <li menuid="@navMenu.Id" class="treeview @(Model.MenuidsOpen.Contains(navMenu.Id) ? "menu-open" : "")"> 11 <a href="#"> 12 <i class="fa @navMenu.Icon"></i> <span>@navMenu.Name</span> 13 <span class="pull-right-container"> 14 <i class="fa fa-angle-left pull-right"></i> 15 </span> 16 </a> 17 <ul class="treeview-menu" @(Model.MenuidsOpen.Contains(navMenu.Id) ? @"style=display:block;" : "")> 18 @await Html.PartialAsync("_NavMenu", new NavMenuVM 19 { 20 NavMenus = navMenu.SubNavMenus, 21 MenuidsOpen = Model.MenuidsOpen 22 }) 23 </ul> 24 </li> 25 } 26 else if ((navMenu.MenuType == MenuTypes.操作菜单)) 27 { 28 <li menuid="@navMenu.Id"> 29 <a href="@navMenu.Url" @(navMenu.Url != null && navMenu.Url.StartsWith("http") ? @"target=_blank" : "")> 30 <i class="fa @navMenu.Icon"></i><span>@navMenu.Name</span> 31 </a> 32 </li> 33 } 34 }
最后,我们渲染下整个导航视图,我们已经有了NavMenuService服务,那怎么在UI界面去访问和使用它呢?其实.Net Core里提供了很方便的机制去访问,直接在Razor视图里将服务注册就行了,如:@inject INavMenuService NavMenuServiceIns
1 @using Microsoft.AspNetCore.Http 2 @using MyWebSite.Areas.Configuration.ViewModels 3 @using MyWebSite.Services.Interfaces 4 @model MyWebSite.Models.ApplicationUser 5 6 @inject IHttpContextAccessor HttpContextAccessorIns 7 @inject INavMenuService NavMenuServiceIns 8 9 <aside class="main-sidebar"> 10 <section class="sidebar"> 11 <div class="user-panel"> 12 <div class="pull-left image"> 13 <img src="~/lib/AdminLTE/dist/img/user2-160x160.jpg" class="img-circle" alt="User Image"> 14 </div> 15 <div class="pull-left info"> 16 <p>@Model.NickName</p> 17 <a href="#"><i class="fa fa-circle text-success"></i> 在线</a> 18 </div> 19 </div> 20 <form action="#" method="get" class="sidebar-form"> 21 <div class="input-group"> 22 <input type="text" name="q" class="form-control" placeholder="Search..."> 23 <span class="input-group-btn"> 24 <button type="submit" name="search" id="search-btn" class="btn btn-flat"> 25 <i class="fa fa-search"></i> 26 </button> 27 </span> 28 </div> 29 </form> 30 <ul class="sidebar-menu" data-widget="tree"> 31 <li class="header">菜单导航</li> 32 @{ 33 var navMenus = NavMenuServiceIns.GetNavMenus(); 34 var cookieMenuidsOpen = HttpContextAccessorIns.HttpContext.Request.Cookies["menuids_open"] ?? ""; 35 } 36 @await Html.PartialAsync("_NavMenu", new NavMenuVM 37 { 38 NavMenus = navMenus, 39 MenuidsOpen = cookieMenuidsOpen == null ? new string[] { } : cookieMenuidsOpen.Split(",") 40 }) 41 42 <li><a href="https://adminlte.io/docs"><i class="fa fa-book"></i> <span>Documentation</span></a></li> 43 <li class="header">LABELS</li> 44 <li><a href="#"><i class="fa fa-circle-o text-red"></i> <span>Important</span></a></li> 45 <li><a href="#"><i class="fa fa-circle-o text-yellow"></i> <span>Warning</span></a></li> 46 <li><a href="#"><i class="fa fa-circle-o text-aqua"></i> <span>Information</span></a></li> 47 </ul> 48 </section> 49 </aside>
导航菜单刷新优化
现在我们的导航菜单的展示功能基本完成了,但是这里有个小小的用户体验的问题,就是每次点击导航菜单项时,由于页面跳转,导致整个Layout页面会刷新,那左侧的导航菜单也会刷新,这样之前展开的菜单就会折叠起来:
要保持原有的菜单不被折叠,有很多方法,比如不使用Layout,点击导航菜单项时,通过Ajax局部刷新右侧内容区域,或者直接做成单页模式的网站,保证左侧的导航菜单不因不同内容而刷新。这里考虑.Net Core使用Layout的便捷性,思路如下:点击导航菜单项时,保存展开的菜单项id到cookie中,跳转下一个界面以后,根据cookie中的菜单项id,重新设置展开状态
1 $('.main-sidebar a').click(function () { 2 //记录菜单展开状态 3 var href = $(this).attr('href') 4 if (href === null || href === "#") return 5 var menuids = []; 6 $('.menu-open').each(function () { 7 menuids.push($(this).attr('menuid')) 8 }) 9 $.cookie('menuids_open', menuids.join(','), { path: "/" }) 10 })
实现后效果:点击菜单后,不再折叠
小结
至此,我们第一个后台管理功能--菜单管理已经完成,我们来看下效果: