我们来创建动态菜单吧
首先,先对动态菜单的概念、操作、流程进行约束:
1.Host和各个Tenant有自己的自定义菜单
2.Host和各个Tenant的权限与自定义菜单相关联
2.Tenant有一套默认的菜单,规定对应的TenantId=-1,在添加租户时自动将标准菜单和标准菜单的权限初始化到添加的租户
一、先实现菜单在数据库中的增删改查
第一步:创建表、实体,添加DbContext
我们需要创建一个菜单表,延续Abp的命名方法,表名叫AbpMenus吧(菜单和权限、验证我们要关联,所以文件尽量放在Authorization文件夹下)
把创建的实体放在AbpLearn.Core/Authorization下面,新建一个Menus文件夹,再创建Menus实体
public class AbpMenus : Entity<int> { public string MenuName { set; get; } public string PageName { set; get; } public string Name { set; get; } public string Url { set; get; } public string Icon { set; get; } public int ParentId { set; get; } public bool IsActive { set; get; } public int Orders { set; get; } public int? TenantId { set; get; } }
如果翻过源码中实体的定义,可以发现很多实体的继承,例如:
1.继承接口 IMayHaveTenant,继承后生成的sql语句将自动增加TenantId的查询条件,表中必须包含TenantId列
2.继承接口 IPassivable,继承后表中必须包含IsActive列
3.继承接口 FullAuditedEntity<TPrimaryKey> TPrimaryKey可以是long、int等值类型,必须包含IsDeleted、DeleterUserId、DeletionTime,其中这个接口
还继承了AuditedEntity<TPrimaryKey>, IFullAudited, IAudited, ICreationAudited, IHasCreationTime, IModificationAudited, IHasModificationTime, IDeletionAudited, IHasDeletionTime, ISoftDelete,这些父类型、接口的定义自己F12就可以看到
AbpLearn.EntityFrameworkCore/EntityFrameworkCore/AbpLearnDbContext.cs增加DbSet
public class AbpLearnDbContext : AbpZeroDbContext<Tenant, Role, User, AbpLearnDbContext> { /* Define a DbSet for each entity of the application */ public AbpLearnDbContext(DbContextOptions<AbpLearnDbContext> options) : base(options) { } public DbSet<AbpMenus> AbpMenus { set; get; } }
再去数据库中添加AbpMenus表 字段长度请自行调整
DROP TABLE IF EXISTS `AbpMenus`;
CREATE TABLE `AbpMenus` (
`Id` int NOT NULL AUTO_INCREMENT,
`MenuName` varchar(50) DEFAULT NULL,
`PageName` varchar(50) DEFAULT NULL,
`LName` varchar(50) DEFAULT NULL,
`Url` varchar(50) DEFAULT NULL,
`Icon` varchar(20) DEFAULT NULL,
`ParentId` int DEFAULT NULL,
`IsActive` bit(1) NOT NULL DEFAULT b‘0‘,
`Orders` int DEFAULT NULL,
`TenantId` int DEFAULT NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
第二步:添加Service和Dto
AbpLearn.Application/Authorization下添加Menus文件夹,然后添加IMenusAppService、MenusAppService,然后添加Dto文件夹
第三步:添加控制器和前台页面、js
Controller文件,MenusController.cs
前台添加Menus及对应的js文件,可以简单省事的把其他文件夹复制粘贴一份,然后关键词修改下
这些文件太多了,我会把这套代码上传到github中,文章最低部会把链接挂出来
添加完之后我们就可以生成预览一下Menus,因为SetNavigation中未将Menus的url加进去,我们自己手打链接进入
此时, 我们的菜单这一块的crud已经做好了,我们可以看到有一个Host管理员这个部分是什么意思哪?
我们为了在当前Host中可以控制所有租户的菜单和权限,将当前Host、标准菜单、租户做一个select,代码如下
public class ChangeModalViewModel { public int? TenantId { get; set; } public string TenancyName { get; set; } public int? TenantMenuType { get; set; } public List<ComboboxItemDto> TeneacyItems { get; set; } }
public async Task<IActionResult> IndexAsync(int? id = 0) { var loginTenant = id <= 0 ? null : _tenantManager.GetById((int)id); var viewModel = new ChangeModalViewModel { TenancyName = loginTenant?.TenancyName, TenantId = id }; viewModel.TeneacyItems = _tenantManager.Tenants .Select(p => new ComboboxItemDto(p.Id.ToString(), p.Name) { IsSelected = viewModel.TenancyName == p.TenancyName }) .ToList(); viewModel.TeneacyItems.Add(new ComboboxItemDto("0","Host管理员") { IsSelected = id == 0 }); viewModel.TeneacyItems.Add(new ComboboxItemDto("-1", "默认菜单") { IsSelected = id == -1 }); ViewBag.LoginInfo = await _sessionAppService.GetCurrentLoginInformations(); return View(viewModel); }
然后在Index.cshtml中添加或修改
@model ChangeModalViewModel // 添加
@await Html.PartialAsync("~/Views/Menus/Index.AdvancedSearch.cshtml", Model) //修改
@await Html.PartialAsync("~/Views/Menus/_CreateModal.cshtml",Model.TenantId) //修改
//添加
$("#ChangeTenancyName").change(function (e) {
location.href = "/Menus/Index/" + this.options[this.selectedIndex].value;
});
修改_CreateModal.cshtml
@using Abp.Authorization.Users @using Abp.MultiTenancy @using AbpLearn.MultiTenancy @using AbpLearn.Web.Models.Common.Modals @model int @{ Layout = null; } <div class="modal fade" id="MenuCreateModal" tabindex="-1" role="dialog" aria-labelledby="MenuCreateModalLabel" data-backdrop="static"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> @await Html.PartialAsync("~/Views/Shared/Modals/_ModalHeader.cshtml", new ModalHeaderViewModel(L("CreateNewMenu"))) <form name="systemMenuCreateForm" role="form" class="form-horizontal"> <div class="modal-body"> <div class="form-group row required"> <label class="col-md-3 col-form-label">@L("MenuName")</label> <div class="col-md-9"> <input type="text" name="MenuName" class="form-control" required minlength="2"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label">@L("LName")</label> <div class="col-md-9"> <input type="text" name="LName" class="form-control" required> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label">@L("Url")</label> <div class="col-md-9"> <input type="text" name="Url" class="form-control"> </div> </div> <div class="form-group row"> <label class="col-md-3 col-form-label">@L("PageName")</label> <div class="col-md-9"> <input type="text" name="PageName" class="form-control"> </div> </div> <div class="form-group row"> <label class="col-md-3 col-form-label">@L("ParentId")</label> <div class="col-md-9"> <input type="text" name="ParentId" class="form-control"> </div> </div> <div class="form-group row"> <label class="col-md-3 col-form-label">@L("Orders")</label> <div class="col-md-9"> <input type="text" name="Orders" class="form-control"> </div> </div> <div class="form-group row"> <label class="col-md-3 col-form-label" for="CreateMenuIsActive">@L("IsActive")</label> <div class="col-md-9"> <input id="CreateMenuIsActive" type="checkbox" name="IsActive" value="true" checked /> </div> </div> </div> <input type="hidden" name="TenantId" value="@(Model)" /> @await Html.PartialAsync("~/Views/Shared/Modals/_ModalFooterWithSaveAndCancel.cshtml") </form> </div> </div> </div>
修改_EditModal.cshtml
@using AbpLearn.Authorization.Menus.Dto @using AbpLearn.Web.Models.Common.Modals @model MenuDto @{ Layout = null; } @await Html.PartialAsync("~/Views/Shared/Modals/_ModalHeader.cshtml", new ModalHeaderViewModel(L("EditMenu"))) <form name="MenuEditForm" role="form" class="form-horizontal"> <input type="hidden" name="Id" value="@Model.Id" /> <div class="modal-body"> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="tenancy-name">@L("MenuName")</label> <div class="col-md-9"> <input id="tenancy-name" type="text" class="form-control" name="MenuName" value="@Model.MenuName" required maxlength="64" minlength="2"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="name">@L("LName")</label> <div class="col-md-9"> <input id="name" type="text" class="form-control" name="LName" value="@Model.LName" required maxlength="128"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="name">@L("Url")</label> <div class="col-md-9"> <input id="name" type="text" class="form-control" name="Url" value="@Model.Url" required maxlength="128"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="name">@L("PageName")</label> <div class="col-md-9"> <input id="name" type="text" class="form-control" name="PageName" value="@Model.PageName" required maxlength="128"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="name">@L("ParentId")</label> <div class="col-md-9"> <input id="name" type="text" class="form-control" name="ParentId" value="@Model.ParentId" required maxlength="128"> </div> </div> <div class="form-group row required"> <label class="col-md-3 col-form-label" for="name">@L("Orders")</label> <div class="col-md-9"> <input id="name" type="text" class="form-control" name="Orders" value="@Model.Orders" required maxlength="128"> </div> </div> <div class="form-group row"> <label class="col-md-3 col-form-label" for="isactive">@L("IsActive")</label> <div class="col-md-9"> <input id="isactive" type="checkbox" name="IsActive" value="true" @(Model.IsActive ? "checked" : "") /> </div> </div> </div> @await Html.PartialAsync("~/Views/Shared/Modals/_ModalFooterWithSaveAndCancel.cshtml") </form> <script src="~/view-resources/Views/Menus/_EditModal.js" asp-append-version="true"></script>
修改Index.AdvancedSearch.cshtml
@using AbpLearn.Web.Views.Shared.Components.TenantChange @using Abp.Application.Services.Dto @model ChangeModalViewModel <div class="abp-advanced-search"> <form id="MenusSearchForm" class="form-horizontal"> <input type="hidden" name="TenantId" value="@Model.TenantId" /> </form> <div class="form-horizontal"> <div class="form-group"> @Html.DropDownList( "ChangeTenancyNames", Model.TeneacyItems.Select(i => i.ToSelectListItem()), new { @class = "form-control edited", id = "ChangeTenancyName" }) </div> </div> </div>
因为在abp里面加载当前列表调用的是abp.services.app.menus.getAll方法,我们还需要对MenusAppService中的GetAllAsync做一下修改
[Serializable] public class MenusPagedResultRequestDto: PagedResultRequestDto, IPagedAndSortedResultRequest { public virtual int? TenantId { get; set; } public virtual string Sorting { get; set; } public virtual bool ShowAll { get; set; } }
#region 查询全部菜单 /// <summary> /// 查询全部菜单 /// </summary> /// <param name="input"></param> /// <returns></returns> public override async Task<PagedResultDto<MenuDto>> GetAllAsync(MenusPagedResultRequestDto input) { IQueryable<AbpMenus> query; query = CreateFilteredQuery(input).Where(o => o.TenantId == (input.TenantId == 0 ? null : input.TenantId)); var totalCount = await AsyncQueryableExecuter.CountAsync(query); query = ApplySorting(query, input); if (!input.ShowAll) query = ApplyPaging(query, input); var entities = await AsyncQueryableExecuter.ToListAsync(query); return new PagedResultDto<MenuDto>( totalCount, entities.Select(MapToEntityDto).ToList() ); } #endregion
这样,我们在选中下面中的任意一个Tenant时,将会跳到对应的菜单里面了
我们先把Host管理员菜单和默认菜单配置一下
二、实现添加租户时,初始化标准菜单和权限
首先我们找到添加租户的地方,去TenantAppService里面去找,可以看到有CreateAsync的重写
public override async Task<TenantDto> CreateAsync(CreateTenantDto input) { CheckCreatePermission(); // Create tenant var tenant = ObjectMapper.Map<Tenant>(input); tenant.ConnectionString = input.ConnectionString.IsNullOrEmpty() ? null : SimpleStringCipher.Instance.Encrypt(input.ConnectionString); var defaultEdition = await _editionManager.FindByNameAsync(EditionManager.DefaultEditionName); if (defaultEdition != null) { tenant.EditionId = defaultEdition.Id; } await _tenantManager.CreateAsync(tenant); await CurrentUnitOfWork.SaveChangesAsync(); // To get new tenant‘s id. // Create tenant database _abpZeroDbMigrator.CreateOrMigrateForTenant(tenant); // We are working entities of new tenant, so changing tenant filter using (CurrentUnitOfWork.SetTenantId(tenant.Id)) { // Create static roles for new tenant CheckErrors(await _roleManager.CreateStaticRoles(tenant.Id)); await CurrentUnitOfWork.SaveChangesAsync(); // To get static role ids // Grant all permissions to admin role var adminRole = _roleManager.Roles.Single(r => r.Name == StaticRoleNames.Tenants.Admin); await _roleManager.GrantAllPermissionsAsync(adminRole); // Create admin user for the tenant var adminUser = User.CreateTenantAdminUser(tenant.Id, input.AdminEmailAddress); await _userManager.InitializeOptionsAsync(tenant.Id); CheckErrors(await _userManager.CreateAsync(adminUser, User.DefaultPassword)); await CurrentUnitOfWork.SaveChangesAsync(); // To get admin user‘s id // Assign admin user to role! CheckErrors(await _userManager.AddToRoleAsync(adminUser, adminRole.Name)); await CurrentUnitOfWork.SaveChangesAsync(); } return MapToEntityDto(tenant); }
我们需要做的是,在 using (CurrentUnitOfWork.SetTenantId(tenant.Id)) 的内部尾部添加赋予菜单和权限的方法即可
赋予菜单和权限的方法我们分开写,都放在MenusAppService中,
public interface IMenusAppService : IAsyncCrudAppService<MenuDto, int, MenusPagedResultRequestDto, CreateMenuDto, MenuDto> { /// <summary> /// 赋予默认菜单 /// </summary> /// <param name="input"></param> /// <returns></returns> Task GiveMenusAsync(EntityDto<int> input); /// <summary> /// 赋予当前租户Admin角色菜单权限 /// </summary> /// <param name="input"></param> /// <returns></returns> Task GivePermissionsAsync(EntityDto<int> input); }
#region 赋予默认菜单 public async Task GiveMenusAsync(EntityDto<int> input) { if (input.Id > 0) { var tenant = await _tenantManager.GetByIdAsync(input.Id); using (_unitOfWorkManager.Current.SetTenantId(tenant.Id)) { var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == tenant.Id); var systemMenus = await AsyncQueryableExecuter.ToListAsync(query); if (!systemMenus.Any()) { query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == -1); var defaultMenus = await AsyncQueryableExecuter.ToListAsync(query); if (defaultMenus.Any()) { List<MenusInsert> GetMenusInserts(List<AbpMenus> abpMenus,int parentId = 0) { List<MenusInsert> menusInserts = new List<MenusInsert>(); foreach (var entity in abpMenus.Where(o => o.ParentId == parentId)) { var insert = new MenusInsert() { LName = entity.LName, MenuName = entity.MenuName, PageName = entity.PageName, Icon = entity.Icon, Url = entity.Url, IsActive = entity.IsActive, Orders = entity.Orders, ParentId = entity.ParentId, TenantId = tenant.Id }; insert.menusInserts = GetMenusInserts(abpMenus, entity.Id); menusInserts.Add(insert); } return menusInserts; } async Task InsertMenusAsync(List<MenusInsert> inserts,int parentId = 0) { foreach (var insert in inserts) { var entity = await CreateAsync(new AbpMenus() { LName = insert.LName, MenuName = insert.MenuName, PageName = insert.PageName, Icon = insert.Icon, Url = insert.Url, IsActive = insert.IsActive, Orders = insert.Orders, ParentId = parentId, TenantId = tenant.Id }); if (insert.menusInserts.Any()) { await InsertMenusAsync(insert.menusInserts, entity.Id); } } } await InsertMenusAsync(GetMenusInserts(defaultMenus)); } } } } } #endregion #region 赋予当前租户Admin角色菜单权限 /// <summary> /// 赋予当前租户Admin角色菜单权限 /// </summary> /// <param name="input"></param> /// <returns></returns> public async Task GivePermissionsAsync(EntityDto<int> input) { if (input.Id > 0) { var tenant = await _tenantManager.GetByIdAsync(input.Id); using (_unitOfWorkManager.Current.SetTenantId(tenant.Id)) { var adminRoles = await _roleRepository.GetAllListAsync(o => o.Name == StaticRoleNames.Tenants.Admin && o.TenantId == tenant.Id); if (adminRoles.FirstOrDefault() != null) { var adminRole = adminRoles.FirstOrDefault(); var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == tenant.Id); var systemMenus = await AsyncQueryableExecuter.ToListAsync(query); var permissions = ConvertTenantPermissions(systemMenus); //await _roleManager.ResetAllPermissionsAsync(adminRole.FirstOrDefault()); //重置授权 var active_BatchCount = 10; var active_permissions = ConvertTenantPermissions(systemMenus.Where(o => o.IsActive).ToList()); for (int i = 0; i < active_permissions.Count(); i += 10)//每次后移5位 { //await _roleManager.SetGrantedPermissionsAsync(adminRole.FirstOrDefault().Id, active_permissions.Take(active_BatchCount).Skip(i)); foreach (var notActive_permission in active_permissions.Take(active_BatchCount).Skip(i)) { await _roleManager.GrantPermissionAsync(adminRole, notActive_permission); } active_BatchCount += 10;//每次从数组中选出N+10位,skip前N位 } var notActive_BatchCount = 10; var notActive_permissions = ConvertTenantPermissions(systemMenus.Where(o => !o.IsActive).ToList()); for (int i = 0; i < notActive_permissions.Count(); i += 10)//每次后移5位 { foreach (var notActive_permission in notActive_permissions.Take(notActive_BatchCount).Skip(i)) { await _roleManager.ProhibitPermissionAsync(adminRole, notActive_permission); } notActive_BatchCount += 10;//每次从数组中选出N+10位,skip前N位 } } else { throw new AbpDbConcurrencyException("未获取到当前租户的Admin角色!"); } } } else { var adminRoles = await _roleRepository.GetAllListAsync(o => o.Name == StaticRoleNames.Tenants.Admin && o.TenantId == null); if (adminRoles.FirstOrDefault() != null) { var adminRole = adminRoles.FirstOrDefault(); var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == null || o.TenantId == 0); var systemMenus = await AsyncQueryableExecuter.ToListAsync(query); //await _roleManager.ResetAllPermissionsAsync(adminRole.FirstOrDefault()); //重置授权 var active_BatchCount = 10; var active_permissions = ConvertHostPermissions(systemMenus.Where(o => o.IsActive).ToList()); for (int i = 0; i < active_permissions.Count(); i += 10)//每次后移5位 { //await _roleManager.SetGrantedPermissionsAsync(adminRole.FirstOrDefault().Id, active_permissions.Take(active_BatchCount).Skip(i)); foreach (var notActive_permission in active_permissions.Take(active_BatchCount).Skip(i)) { await _roleManager.GrantPermissionAsync(adminRole, notActive_permission); } active_BatchCount += 10;//每次从数组中选出N+10位,skip前N位 } var notActive_BatchCount = 10; var notActive_permissions = ConvertHostPermissions(systemMenus.Where(o => !o.IsActive).ToList()); for (int i = 0; i < notActive_permissions.Count(); i += 10)//每次后移5位 { foreach (var notActive_permission in notActive_permissions.Take(notActive_BatchCount).Skip(i)) { await _roleManager.ProhibitPermissionAsync(adminRole, notActive_permission); } notActive_BatchCount += 10;//每次从数组中选出N+10位,skip前N位 } } } } public IEnumerable<Permission> ConvertTenantPermissions(IReadOnlyList<AbpMenus> systemMenus) { return systemMenus.Select(o => new Permission(o.PageName, L(o.MenuName), L(o.LName), MultiTenancySides.Tenant)); } public IEnumerable<Permission> ConvertHostPermissions(IReadOnlyList<AbpMenus> systemMenus) { return systemMenus.Select(o => new Permission(o.PageName, L(o.MenuName), L(o.LName), MultiTenancySides.Host)); } #endregion
TenantAppService.cs中做一下修改
public override async Task<TenantDto> CreateAsync(CreateTenantDto input) { CheckCreatePermission(); // Create tenant var tenant = ObjectMapper.Map<Tenant>(input); tenant.ConnectionString = input.ConnectionString.IsNullOrEmpty() ? null : SimpleStringCipher.Instance.Encrypt(input.ConnectionString); var defaultEdition = await _editionManager.FindByNameAsync(EditionManager.DefaultEditionName); if (defaultEdition != null) { tenant.EditionId = defaultEdition.Id; } await _tenantManager.CreateAsync(tenant); await CurrentUnitOfWork.SaveChangesAsync(); // To get new tenant‘s id. // Create tenant database _abpZeroDbMigrator.CreateOrMigrateForTenant(tenant); // We are working entities of new tenant, so changing tenant filter using (CurrentUnitOfWork.SetTenantId(tenant.Id)) { // Create static roles for new tenant CheckErrors(await _roleManager.CreateStaticRoles(tenant.Id)); await CurrentUnitOfWork.SaveChangesAsync(); // To get static role ids // Grant all permissions to admin role var adminRole = _roleManager.Roles.Single(r => r.Name == StaticRoleNames.Tenants.Admin); await _roleManager.GrantAllPermissionsAsync(adminRole); // Create admin user for the tenant var adminUser = User.CreateTenantAdminUser(tenant.Id, input.AdminEmailAddress); await _userManager.InitializeOptionsAsync(tenant.Id); CheckErrors(await _userManager.CreateAsync(adminUser, User.DefaultPassword)); await CurrentUnitOfWork.SaveChangesAsync(); // To get admin user‘s id // Assign admin user to role! CheckErrors(await _userManager.AddToRoleAsync(adminUser, adminRole.Name)); await CurrentUnitOfWork.SaveChangesAsync(); await _menusAppService.GiveMenusAsync(new EntityDto<int>() { Id = tenant.Id }); await CurrentUnitOfWork.SaveChangesAsync(); await _menusAppService.GivePermissionsAsync(new EntityDto<int>() { Id = tenant.Id }); await CurrentUnitOfWork.SaveChangesAsync(); } return MapToEntityDto(tenant); }
现在我们添加租户企业1、企业2
现在菜单已经同步好了,我们去数据库看下权限的同步
TenantId:
null是Host
1是abp页面第一次加载时初始化的Default租户
2是我之前添加的旧的企业1,那个时候方法没写好,就把2的删掉了
3是企业2
4是企业1
由此可以看出,我们添加的菜单对应的PageName已经作为权限添加到权限表了
三、实现菜单修改后,权限赋予对应租户
这一个其实在二里面已经写好了,前台做一个按钮,赋予权限,调用一下就好了
例如:
Index.cshtml //为什么要加getCurrentLoginInformationsOutput.Tenant == null的判断?是因为租户在进入菜单管理的地方,我们不给他们添加、赋予权限的权限
在/wwwroot/view-resources/Views/Menus/Index.js中添加
$(document).on(‘click‘, ‘#GivePermissions‘, function (e) { var tenantId = $(this).attr(‘data-tenant-id‘); abp.message.confirm( abp.utils.formatString( "是否赋予当前租户管理员账号所有权限?", "系统" ), null, (isConfirmed) => { if (isConfirmed) { _menuService .givePermissions({ id: tenantId }) .done(() => { abp.notify.info("操作成功!"); _$menusTable.ajax.reload(); }); } } ); });
四、实现菜单的动态加载
在https://www.cnblogs.com/wangpengzong/p/13089690.html中我们找到了菜单生成的地方,在最底部,通过NavigationManager来获取到Menus,这里其实有一个初始化方法(Initialize),调用的是AbpLearnNavigationProvider的SetNavigation方法来进行本地化,然后在
NavigationManager的非静态构造函数中去获取已经本地化的Menus,但是本地化Menus因为是在初始化时,程序的初始化我们无法获取到当前的Tenant信息,所以只能将获取Menus的地方推迟,放在倒数第二个类UserNavigationManager里面的GetMenuAsync方法中,我们来看下GetMenuAsync
public async Task<UserMenu> GetMenuAsync(string menuName, UserIdentifier user) { var menuDefinition = _navigationManager.Menus.GetOrDefault(menuName); if (menuDefinition == null) { throw new AbpException("There is no menu with given name: " + menuName); } var userMenu = new UserMenu(menuDefinition, _localizationContext); await FillUserMenuItems(user, menuDefinition.Items, userMenu.Items); return userMenu; }
第一句话获取menuDefinition是关键点,我们将menuDefinition修改为从数据库中获取,在AbpLearn.Application/Authorization/Menus下添加UserNavigationManager.cs
using Abp; using Abp.Application.Features; using Abp.Application.Navigation; using Abp.Authorization; using Abp.Dependency; using Abp.Localization; using Abp.MultiTenancy; using Abp.Runtime.Session; using AbpLearn.Authorization.Menus.Dto; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AbpLearn.Authorization.Menus { public class UserNavigationManager : IUserNavigationManager, ITransientDependency { public IAbpSession AbpSession { get; set; } private readonly INavigationManager _navigationManager; private readonly ILocalizationContext _localizationContext; private readonly IIocResolver _iocResolver; private readonly IMenusAppService _menuAppService; public IDictionary<string, MenuDefinition> Menus { get; private set; } public MenuDefinition MainMenu { get { return Menus["MainMenu"]; } } public UserNavigationManager( INavigationManager navigationManager, ILocalizationContext localizationContext, IMenusAppService menuAppService, IIocResolver iocResolver) { _navigationManager = navigationManager; _localizationContext = localizationContext; _iocResolver = iocResolver; AbpSession = NullAbpSession.Instance; _menuAppService = menuAppService; } public async Task<UserMenu> GetMenuAsync(string menuName, UserIdentifier user) { Menus = new Dictionary<string, MenuDefinition> { {"MainMenu", new MenuDefinition("MainMenu", new LocalizableString("MainMenu", AbpConsts.LocalizationSourceName))} }; var lists = await _menuAppService.GetAllAsync(new MenusPagedResultRequestDto() { ShowAll = true, TenantId = (loginInfo.Tenant == null ? 0 : loginInfo.Tenant.Id) }); var ParentMenu = lists.Items.Where(k => k.IsActive).ToList().Where(x => x.ParentId == 0).ToList(); if (ParentMenu.Any()) { ParentMenu.ForEach(g => { var menu = new MenuItemDefinition( g.LName, MenuL(g.MenuName), g.Icon, g.Url, false, g.Orders ); BuildSubMenu(menu, g.Id, lists.Items.Where(k => k.IsActive).ToList()); MainMenu.AddItem(menu); }); } var menuDefinition = MainMenu; if (menuDefinition == null) { throw new AbpException("There is no menu with given name: " + menuName); } var userMenu = new UserMenu(); userMenu.Name = menuDefinition.Name; userMenu.DisplayName = menuDefinition.DisplayName.Localize(_localizationContext); userMenu.CustomData = menuDefinition.CustomData; userMenu.Items = new List<UserMenuItem>(); await FillUserMenuItems(user, menuDefinition.Items, userMenu.Items); return userMenu; } public async Task<IReadOnlyList<UserMenu>> GetMenusAsync(UserIdentifier user) { var userMenus = new List<UserMenu>(); foreach (var menu in _navigationManager.Menus.Values) { userMenus.Add(await GetMenuAsync(menu.Name, user)); } return userMenus; } public void BuildSubMenu(MenuItemDefinition menu, int parentId, List<MenuDto> list) { var nList = list.Where(x => x.ParentId == parentId).ToList(); if (nList != null && nList.Count > 0) { nList.ForEach(g => { var subMenu = new MenuItemDefinition( g.PageName, MenuL(g.MenuName), g.Icon, g.Url, false, g.Orders ); menu.AddItem(subMenu); BuildSubMenu(subMenu, g.Id, list); }); } } private static ILocalizableString MenuL(string name) { return new LocalizableString(name, AbpLearnConsts.LocalizationSourceName); } private async Task<int> FillUserMenuItems(UserIdentifier user, IList<MenuItemDefinition> menuItemDefinitions, IList<UserMenuItem> userMenuItems) { //TODO: Can be optimized by re-using FeatureDependencyContext. var addedMenuItemCount = 0; using (var scope = _iocResolver.CreateScope()) { var permissionDependencyContext = scope.Resolve<PermissionDependencyContext>(); permissionDependencyContext.User = user; var featureDependencyContext = scope.Resolve<FeatureDependencyContext>(); featureDependencyContext.TenantId = user == null ? null : user.TenantId; foreach (var menuItemDefinition in menuItemDefinitions) { if (menuItemDefinition.RequiresAuthentication && user == null) { continue; } if (menuItemDefinition.PermissionDependency != null && (user == null || !(await menuItemDefinition.PermissionDependency.IsSatisfiedAsync(permissionDependencyContext)))) { continue; } if (menuItemDefinition.FeatureDependency != null && (AbpSession.MultiTenancySide == MultiTenancySides.Tenant || (user != null && user.TenantId != null)) && !(await menuItemDefinition.FeatureDependency.IsSatisfiedAsync(featureDependencyContext))) { continue; } var userMenuItem = new UserMenuItem(menuItemDefinition, _localizationContext); if (menuItemDefinition.IsLeaf || (await FillUserMenuItems(user, menuItemDefinition.Items, userMenuItem.Items)) > 0) { userMenuItems.Add(userMenuItem); ++addedMenuItemCount; } } } return addedMenuItemCount; } } }
然后在Mvc项目的Startup.cs/ConfigureServices下增加
services.AddScoped<IUserNavigationManager, UserNavigationManager>();
因为在abp中菜单被做做成了模块,在程序初始化时模块添加进去,但是我们将菜单修改成了每次读取数据库加载,那么我们就不需要加载这个模块了
在mvc项目的AbpLearnWebMvcModule.cs注释下面这句话
//Configuration.Navigation.Providers.Add<AbpLearnNavigationProvider>();
将AbpLearnNavigationProvider.cs/SetNavigation方法的内容全部注释掉
预览一下mvc,用Host登录一下
用企业1登陆下,登录切换Host和Tenant,是在登录界面 Current tenant: 未选 (Change) 点击Change,在弹框中输入 E1(因为上面设置的企业1标识是E1),点击save,页面刷新后就变为了 Current tenant: E1 (Change) ,输入账号密码登录
OK,我们的动态菜单已经完成了
当然,我的菜单使用的是table来显示,你也可以使用tree来,我找到了一个jstree,下面修改一下
MenusAppService.cs
#region 获取当前账户的菜单树 /// <summary> /// 获取当前账户的菜单树 /// </summary> /// <param name="input"></param> /// <returns></returns> public async Task<string> GetTreeAsync(MenusPagedResultRequestDto input) { var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == input.TenantId); var systemMenus = await AsyncQueryableExecuter.ToListAsync(query); var childJObject = new JObject(); var openJObject = new JObject(); openJObject.Add("opened", true); childJObject.Add("id", 0); childJObject.Add("text", "根目录"); childJObject.Add("icon", ""); childJObject.Add("state", openJObject); childJObject.Add("children", GetJArray(systemMenus, 0)); return childJObject.ToString(); } #region 获取目录Array /// <summary> /// 获取目录Array /// </summary> /// <param name="systemMenus"></param> /// <param name="parentdId"></param> /// <returns></returns> private JArray GetJArray(List<AbpMenus> systemMenus, int parentdId) { JArray jArray = new JArray(); foreach (var menu in systemMenus.Where(o => o.ParentId == parentdId)) { var jObject = new JObject(); jObject.Add("id", menu.Id); jObject.Add("text", menu.MenuName); jObject.Add("icon", menu.Icon); //jObject.Add("state", menu.Icon); if (systemMenus.Any(o => o.ParentId == menu.Id)) { jObject.Add("children", GetJArray(systemMenus, menu.Id)); } jArray.Add(jObject); } return jArray; } #endregion #endregion
前端Index.cshtml jstree去https://github.com/vakata/jstree/zipball/3.3.8下载,下载后在mvc项目的wwwroot文件夹下添加jstree文件夹,下载文件的src里面内容全部赋值到jstree文件夹
注释掉table标签
添加jstree1
例如:
@section styles { <link href="~/jstree/themes/default/style.css" rel="stylesheet" /> }
<div id="jstree1" style="width:100%;"></div> @section scripts { <environment names="Development"> <script src="~/view-resources/Views/Menus/Index.js" asp-append-version="true"></script> </environment> <environment names="Staging,Production"> <script src="~/view-resources/Views/Menus/Index.min.js" asp-append-version="true"></script> </environment> <script type="application/javascript" src="~/jstree/jstree.js"></script> <script type="application/javascript" src="~/jstree/jstree.contextmenu.js"></script> <script type="text/javascript"> $(function () { var _menuService = abp.services.app.menus; l = abp.localization.getSource(‘A_b_p‘); $(‘#jstree1‘).jstree({ "core": { "data": function (node, callback) { var filter = $(‘#MenusSearchForm‘).serializeFormToObject(true); this, _menuService.getTree(filter).done(function (result) { callback.call(this, JSON.parse(result)); }); }, "themes": { "variant": "large",//加大 "ellipsis": true //文字多时省略 }, "check_callback": true, }, "plugins": ["contextmenu", "wholerow", "themes"],//"checkbox" "contextmenu": { select_node: false, show_at_node: true, "items": { "create": { "label": "新增子菜单", "action": function (obj) { var inst = jQuery.jstree.reference(obj.reference); var clickedNode = inst.get_node(obj.reference); if (parseInt(clickedNode.original.id) >= 0) { $("#ParentId").val(clickedNode.original.id); $("#MenuCreateModal").modal(); } else { abp.notify.info("父节点获取出错"); } }, }, "rename": { "label": "修改", "action": function (obj) { var inst = jQuery.jstree.reference(obj.reference); var clickedNode = inst.get_node(obj.reference); if (parseInt(clickedNode.original.id) >= 0) { abp.ajax({ url: abp.appPath + ‘Menus/EditModal?menuId=‘ + clickedNode.original.id, type: ‘POST‘, dataType: ‘html‘, success: function (content) { $("#MenuEditModal").modal(); $(‘#MenuEditModal div.modal-content‘).html(content); }, error: function (e) { } }); } else { abp.notify.info("菜单获取出错"); } } }, "delete": { "label": "更改菜单状态", "action": function (obj) { var inst = jQuery.jstree.reference(obj.reference); var clickedNode = inst.get_node(obj.reference); abp.message.confirm( abp.utils.formatString("是否" + (clickedNode.original.state.disabled?"启用":"禁用") + "当前菜单:" + clickedNode.original.text + "?"), null, (isConfirmed) => { if (isConfirmed) { _menuService .delete({ id: clickedNode.original.id }) .done(() => { abp.notify.info(l(‘SuccessfullyDeleted‘)); location.reload(); }); } } ); }, } } } }).on(‘select_node.jstree‘, function (event, data) { console.log(data.node); }).on(‘changed.jstree‘, function (event, data) { console.log("-----------changed.jstree"); console.log("action:" + data.action); console.log(data.node); }); }); </script> }
预览一下吧
本文github:https://github.com/wangpengzong/AbpLearn
下一篇开始动态权限
#region 赋予默认菜单 public async Task GiveMenusAsync(EntityDto<int> input) { if (input.Id > 0) { var tenant = await _tenantManager.GetByIdAsync(input.Id);
using (_unitOfWorkManager.Current.SetTenantId(tenant.Id)) { var query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == tenant.Id);
var systemMenus = await AsyncQueryableExecuter.ToListAsync(query);
if (!systemMenus.Any()) { query = CreateFilteredQuery(new MenusPagedResultRequestDto()).Where(o => o.TenantId == -1);
var defaultMenus = await AsyncQueryableExecuter.ToListAsync(query); if (defaultMenus.Any()) { List<MenusInsert> GetMenusInserts(List<AbpMenus> abpMenus,int parentId = 0) { List<MenusInsert> menusInserts = new List<MenusInsert>(); foreach (var entity in abpMenus.Where(o => o.ParentId == parentId)) { var insert = new MenusInsert() { LName = entity.LName, MenuName = entity.MenuName, PageName = entity.PageName, Icon = entity.Icon, Url = entity.Url, IsActive = entity.IsActive, Orders = entity.Orders, ParentId = entity.ParentId, TenantId = tenant.Id }; insert.menusInserts = GetMenusInserts(abpMenus, entity.Id); menusInserts.Add(insert); } return menusInserts; }
async Task InsertMenusAsync(List<MenusInsert> inserts,int parentId = 0) { foreach (var insert in inserts) { var entity = await CreateAsync(new AbpMenus() { LName = insert.LName, MenuName = insert.MenuName, PageName = insert.PageName, Icon = insert.Icon, Url = insert.Url, IsActive = insert.IsActive, Orders = insert.Orders, ParentId = parentId, TenantId = tenant.Id }); if (insert.menusInserts.Any()) { await InsertMenusAsync(insert.menusInserts, entity.Id); } } } await InsertMenusAsync(GetMenusInserts(defaultMenus)); } } } }
} #endregion