95

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

 6 years ago
source link: https://www.cnblogs.com/lizzie-xhu/p/8136442.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
欢迎大家前往我的个人博客,获取更好的阅读体验和更多分享文章~

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

增加后台管理模块

第1步,左侧导航菜单中,添加后台管理模块,用作管理员登录后,可以进行一些后台管理的操作,当然,目前还没有权限控制(后期加入),所以对所有用户可见。大概菜单结构如下:

371995-20171228105813566-843439505.png

有了菜单项,我们还需要控制视图的跳转,所以,接下来需要写对应的控制器和视图。

为了将相关功能组织成一组单独命名空间(路由)和文件夹结构(视图),解决方案中右键添加区域(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="width: 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 }

371995-20171228141832616-1124577518.png

新建页改写

 控制器调整:这里控制器有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 }

371995-20171228143555022-1598498622.png

详情页改写

 控制器调整:不用大的调整,只是增加了下拉框的初始化工作 ;

 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>

371995-20171228150423834-586017278.png

编辑页面改写

控制器调整:也是有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 }

 

371995-20171228150504788-1173707728.png

以上,我们可以通过增删改查界面操作菜单项了,但是要怎么将数据库中的菜单跟左侧的导航菜单关联呢?下节,我们将实现下这个功能。

动态加载导航菜单

 前面章节说过,物理数据库菜单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页面会刷新,那左侧的导航菜单也会刷新,这样之前展开的菜单就会折叠起来:

 371995-20171228161317350-290079642.gif

 要保持原有的菜单不被折叠,有很多方法,比如不使用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 })

371995-20171228161505491-733929899.gif实现后效果:点击菜单后,不再折叠

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

371995-20171228163753538-1491482338.gif

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK