• ASP.NET Core 一步步搭建个人网站(3)_菜单管理


    上一章,我们实现了用户的注册和登录,登录之后展示的是我们的主页,页面的左侧是多级的导航菜单,定位并展示用户需要访问的不同页面。目前导航菜单是写死的,考虑以后菜单管理的便捷性,我们这节实现下可视化配置菜单的功能,这样以后我们可以动态的配置导航菜单,不用再编译发布网站程序了。

    增加后台管理模块

    第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 })

    实现后效果:点击菜单后,不再折叠

    小结

    至此,我们第一个后台管理功能--菜单管理已经完成,我们来看下效果:

  • 相关阅读:
    beego框架学习(一)安装
    专题 :JSON处理
    Java中getClassLoader().getResource()和getResource()的区别
    加载WebApplicationContext的方式
    Web.xml配置详解之context-param
    “Could not open ServletContext resource [/WEB-INF/applicationContext.xml]”解决方案
    如何解决 Eclipse中出现-访问限制由于对必需的库XX具有一定限制,因此无法访问类型
    JDK各个JAR包的作用
    eclipse汉化
    模板专题(一)函数模板
  • 原文地址:https://www.cnblogs.com/lizzie-xhu/p/8136442.html
Copyright © 2020-2023  润新知