為何說要多用組合少用繼承?如何決定該用組合還是繼承?

在面向對象編程中,有一條非常經典的設計原則,那就是:組合優於繼承,多用組合少用繼承。為什麼不推薦使用繼承?組合相比繼承有哪些優勢?如何判斷該用組合還是繼承?今天,我們就圍繞着這三個問題,來詳細講解一下這條設計原則。

為什麼不推薦使用繼承?

繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關係,可以解決代碼復用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。所以,對於是否應該在項目中使用繼承,網上有很多爭議。很多人覺得繼承是一種反模式,應該盡量少用,甚至不用。為什麼會有這樣的爭議?我們通過一個例子來解釋一下。

假設我們要設計一個關於鳥的類。我們將“鳥類”這樣一個抽象的事物概念,定義為一個抽象類 AbstractBird。所有更細分的鳥,比如麻雀、鴿子、烏鴉等,都繼承這個抽象類。

我們知道,大部分鳥都會飛,那我們可不可以在 AbstractBird 抽象類中,定義一個 fly() 方法呢?答案是否定的。儘管大部分鳥都會飛,但也有特例,比如鴕鳥就不會飛。鴕鳥繼承具有 fly() 方法的父類,那鴕鳥就具有“飛”這樣的行為,這顯然不符合我們對現實世界中事物的認識。當然,你可能會說,我在鴕鳥這個子類中重寫(override)fly() 方法,讓它拋出 UnSupportedMethodException 異常不就可以了嗎?具體的代碼實現如下所示:

public class AbstractBird {
  //...省略其他屬性和方法...
  public void fly() { //... }
}

public class Ostrich extends AbstractBird { //鴕鳥
  //...省略其他屬性和方法...
  public void fly() {
    throw new UnSupportedMethodException("I can't fly.'");
  }
}

這種設計思路雖然可以解決問題,但不夠優美。因為除了鴕鳥之外,不會飛的鳥還有很多,比如企鵝。對於這些不會飛的鳥來說,我們都需要重寫 fly() 方法,拋出異常。這樣的設計,一方面,徒增了編碼的工作量;另一方面,也違背了我們之後要講的最小知識原則(Least Knowledge Principle,也叫最少知識原則或者迪米特法則),暴露不該暴露的接口給外部,增加了類使用過程中被誤用的概率。

可能又會說,那我們再通過 AbstractBird 類派生出兩個更加細分的抽象類:會飛的鳥類 AbstractFlyableBird 和不會飛的鳥類 AbstractUnFlyableBird,讓麻雀、烏鴉這些會飛的鳥都繼承 AbstractFlyableBird,讓鴕鳥、企鵝這些不會飛的鳥,都繼承 AbstractUnFlyableBird 類,不就可以了嗎?具體的繼承關係如下圖所示:

從圖中我們可以看出,繼承關係變成了三層。不過,整體上來講,目前的繼承關係還比較簡單,層次比較淺,也算是一種可以接受的設計思路。我們再繼續加點難度。在剛剛這個場景中,我們只關注“鳥會不會飛”,但如果我們還關注“鳥會不會叫”,那這個時候,我們又該如何設計類之間的繼承關係呢?

是否會飛?是否會叫?兩個行為搭配起來會產生四種情況:會飛會叫、不會飛會叫、會飛不會叫、不會飛不會叫。如果我們繼續沿用剛才的設計思路,那就需要再定義四個抽象類(AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird、AbstractUnFlyableUnTweetableBird)。

如果我們還需要考慮“是否會下蛋”這樣一個行為,那估計就要組合爆炸了。類的繼承層次會越來越深、繼承關係會越來越複雜。而這種層次很深、很複雜的繼承關係,一方面,會導致代碼的可讀性變差。因為我們要搞清楚某個類具有哪些方法、屬性,必須閱讀父類的代碼、父類的父類的代碼……一直追溯到最頂層父類的代碼。另一方面,這也破壞了類的封裝特性,將父類的實現細節暴露給了子類。子類的實現依賴父類的實現,兩者高度耦合,一旦父類代碼修改,就會影響所有子類的邏輯。

總之,繼承最大的問題就在於:繼承層次過深、繼承關係過於複雜會影響到代碼的可讀性和可維護性。這也是為什麼我們不推薦使用繼承。那剛剛例子中繼承存在的問題,我們又該如何來解決呢?你可以先自己思考一下,再聽我下面的講解。

組合相比繼承有哪些優勢?

實際上,我們可以利用組合(composition)、接口、委託(delegation)三個技術手段,一塊兒來解決剛剛繼承存在的問題。

我們前面講到接口的時候說過,接口表示具有某種行為特性。針對“會飛”這樣一個行為特性,我們可以定義一個 Flyable 接口,只讓會飛的鳥去實現這個接口。對於會叫、會下蛋這些行為特性,我們可以類似地定義 Tweetable 接口、EggLayable 接口。

public interface Flyable {
  void fly();
}
public interface Tweetable {
  void tweet();
}
public interface EggLayable {
  void layEgg();
}
public class Ostrich implements Tweetable, EggLayable {//鴕鳥
  //... 省略其他屬性和方法...
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}
public class Sparrow impelents Flayable, Tweetable, EggLayable {//麻雀
  //... 省略其他屬性和方法...
  @Override
  public void fly() { //... }
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}

不過,我們知道,接口只聲明方法,不定義實現。也就是說,每個會下蛋的鳥都要實現一遍 layEgg() 方法,並且實現邏輯是一樣的,這就會導致代碼重複的問題。那這個問題又該如何解決呢?

我們可以針對三個接口再定義三個實現類,它們分別是:實現了 fly() 方法的 FlyAbility 類、實現了 tweet() 方法的 TweetAbility 類、實現了 layEgg() 方法的 EggLayAbility 類。然後,通過組合和委託技術來消除代碼重複。具體的代碼實現如下所示:

public interface Flyable {
  void fly();
}
public class FlyAbility implements Flyable {
  @Override
  public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {//鴕鳥
  private TweetAbility tweetAbility = new TweetAbility(); //組合
  private EggLayAbility eggLayAbility = new EggLayAbility(); //組合
  //... 省略其他屬性和方法...
  @Override
  public void tweet() {
    tweetAbility.tweet(); // 委託
  }
  @Override
  public void layEgg() {
    eggLayAbility.layEgg(); // 委託
  }
}

我們知道繼承主要有三個作用:表示 is-a 關係,支持多態特性,代碼復用。而這三個作用都可以通過其他技術手段來達成。比如 is-a 關係,我們可以通過組合和接口的 has-a 關係來替代;多態特性我們可以利用接口來實現;代碼復用我們可以通過組合和委託來實現。所以,從理論上講,通過組合、接口、委託三個技術手段,我們完全可以替換掉繼承,在項目中不用或者少用繼承關係,特別是一些複雜的繼承關係。

如何判斷該用組合還是繼承?

儘管我們鼓勵多用組合少用繼承,但組合也並不是完美的,繼承也並非一無是處。從上面的例子來看,繼承改寫成組合意味着要做更細粒度的類的拆分。這也就意味着,我們要定義更多的類和接口。類和接口的增多也就或多或少地增加代碼的複雜程度和維護成本。所以,在實際的項目開發中,我們還是要根據具體的情況,來具體選擇該用繼承還是組合。

如果類之間的繼承結構穩定(不會輕易改變),繼承層次比較淺(比如,最多有兩層繼承關係),繼承關係不複雜,我們就可以大膽地使用繼承。反之,系統越不穩定,繼承層次很深,繼承關係複雜,我們就盡量使用組合來替代繼承。

除此之外,還有一些設計模式會固定使用繼承或者組合。比如,裝飾者模式(decorator pattern)、策略模式(strategy pattern)、組合模式(composite pattern)等都使用了組合關係,而模板模式(template pattern)使用了繼承關係。

前面我們講到繼承可以實現代碼復用。利用繼承特性,我們把相同的屬性和方法,抽取出來,定義到父類中。子類復用父類中的屬性和方法,達到代碼復用的目的。但是,有的時候,從業務含義上,A 類和 B 類並不一定具有繼承關係。比如,Crawler 類和 PageAnalyzer 類,它們都用到了 URL 拼接和分割的功能,但並不具有繼承關係(既不是父子關係,也不是兄弟關係)。僅僅為了代碼復用,生硬地抽象出一個父類出來,會影響到代碼的可讀性。如果不熟悉背後設計思路的同事,發現 Crawler 類和 PageAnalyzer 類繼承同一個父類,而父類中定義的卻只是 URL 相關的操作,會覺得這個代碼寫得莫名其妙,理解不了。這個時候,使用組合就更加合理、更加靈活。具體的代碼實現如下所示:

public class Url {
  //...省略屬性和方法
}

public class Crawler {
  private Url url; // 組合
  public Crawler() {
    this.url = new Url();
  }
  //...
}

public class PageAnalyzer {
  private Url url; // 組合
  public PageAnalyzer() {
    this.url = new Url();
  }
  //..
}

還有一些特殊的場景要求我們必須使用繼承。如果你不能改變一個函數的入參類型,而入參又非接口,為了支持多態,只能採用繼承來實現。比如下面這樣一段代碼,其中 FeignClient 是一個外部類,我們沒有權限去修改這部分代碼,但是我們希望能重寫這個類在運行時執行的 encode() 函數。這個時候,我們只能採用繼承來實現了。

public class FeignClient { // feighn client框架代碼
  //...省略其他代碼...
  public void encode(String url) { //... }
}

public void demofunction(FeignClient feignClient) {
  //...
  feignClient.encode(url);
  //...
}

public class CustomizedFeignClient extends FeignClient {
  @Override
  public void encode(String url) { //...重寫encode的實現...}
}

// 調用
FeignClient client = new CustomizedFeignClient();
demofunction(client);

儘管有些人說,要杜絕繼承,100% 用組合代替繼承,但是我的觀點沒那麼極端!之所以“多用組合少用繼承”這個口號喊得這麼響,只是因為,長期以來,我們過度使用繼承。還是那句話,組合併不完美,繼承也不是一無是處。只要我們控制好它們的副作用、發揮它們各自的優勢,在不同的場合下,恰當地選擇使用繼承還是組合,這才是我們所追求的境界。

重點回顧

為什麼不推薦使用繼承?

繼承是面向對象的四大特性之一,用來表示類之間的 is-a 關係,可以解決代碼復用的問題。雖然繼承有諸多作用,但繼承層次過深、過複雜,也會影響到代碼的可維護性。在這種情況下,我們應該盡量少用,甚至不用繼承。

組合相比繼承有哪些優勢?

繼承主要有三個作用:表示 is-a 關係,支持多態特性,代碼復用。而這三個作用都可以通過組合、接口、委託三個技術手段來達成。除此之外,利用組合還能解決層次過深、過複雜的繼承關係影響代碼可維護性的問題。

如何判斷該用組合還是繼承?

儘管我們鼓勵多用組合少用繼承,但組合也並不是完美的,繼承也並非一無是處。在實際的項目開發中,我們還是要根據具體的情況,來選擇該用繼承還是組合。如果類之間的繼承結構穩定,層次比較淺,關係不複雜,我們就可以大膽地使用繼承。反之,我們就盡量使用組合來替代繼承。除此之外,還有一些設計模式、特殊的應用場景,會固定使用繼承或者組合。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

.net core3.1 abp動態菜單和動態權限(動態菜單實現和動態權限添加) (三)

我們來創建動態菜單吧 

首先,先對動態菜單的概念、操作、流程進行約束:
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>

View Code

 

修改_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>

View Code

修改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 AbpLearn.Sessions; 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; private readonly ISessionAppService _sessionAppService; public IDictionary<string, MenuDefinition> Menus { get; private set; } public MenuDefinition MainMenu { get { return Menus["MainMenu"]; } } public UserNavigationManager( INavigationManager navigationManager, ILocalizationContext localizationContext, IMenusAppService menuAppService, ISessionAppService sessionAppService, IIocResolver iocResolver) { _navigationManager = navigationManager; _localizationContext = localizationContext; _iocResolver = iocResolver; AbpSession = NullAbpSession.Instance; _menuAppService = menuAppService; _sessionAppService = sessionAppService; } public async Task<UserMenu> GetMenuAsync(string menuName, UserIdentifier user) { var loginInfo = await _sessionAppService.GetCurrentLoginInformations(); 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,我們的動態菜單已經完成了

 

添加jstree

 當然,我的菜單使用的是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地址

本文github:https://github.com/wangpengzong/AbpLearn

下一篇開始動態權限

 吐槽區域(寫的不好、不對,歡迎吐槽)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

虎鯨生氣了!多艘船隻罕見遭圍攻損壞 科學家憂環境破壞

摘錄自2020年9月14日自由時報報導

過去兩個月來虎鯨開始攻擊船隻,造成嚴重損害,科學家認為,這可能與虎鯨的生存壓力有關。

《衛報》報導,據傳虎鯨騷擾航行西班牙與葡萄牙之間的船隻,讓科學家感到困惑,過去兩個月,多艘船隻發出求救訊息,至少有一艘船因為嚴重損壞回港。船員莫里斯(Victoria Morris)表示,他所在的船隻被9頭虎鯨包圍,這些重達6噸的虎鯨不斷撞擊船隻達一小時,導致船隻轉了180度,發動機也關閉,同時虎鯨們還發出巨大的「口哨聲」。

報導表示,科學家指出虎鯨是高度群聚且好奇的動物,跟隨小船並嬉戲不是太罕見的事,但這件事奇怪的地方在於,虎鯨表現出侵略性,一般而言牠們不太會蓄意攻擊。科學家認為,這可能代表直布羅陀海峽的虎鯨有巨大的生存壓力,牠們要與漁船爭搶食物,且繁忙的航線對環境造成破壞,當地的虎鯨已經瀕臨滅絕,只剩下大約30頭成體。

海洋
國際新聞
虎鯨

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

研究:1993年《生物多樣性公約》 至今成功保育48個物種免於滅絕

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

少砍樹啤酒就降價 巴西酒商護亞馬遜雨林

摘錄自2020年9月14日中央社報導

全球最大酒商安海斯-布希英博集團(AB InBev)旗下的巴西酒商美洲飲料公司(Ambev)推出了不尋常的新啤酒,將拯救亞馬遜雨林以及熱愛啤酒兩大訴求結合。

這款名為科羅拉多亞馬遜(Colorado Amazonica)的新啤酒,根據亞馬遜雨林的林木砍伐速度來定價。林木砍伐數量減少時,這種啤酒就會比較便宜,但林木越少,價格就越貴。所有收入將捐贈給亞馬遜地區的一個原住民、當地居民和小農耕種者社區網。

科羅拉多亞馬遜啤酒已在9月3日上市,售價約一美元,到了10日,價格已上漲46%。

氣候變遷
國際新聞
巴西
亞馬遜雨林

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

飼料費太貴養不起! 無尾熊遭大阪天王寺動物園「裁員」

摘錄自2020年9月15日自由時報報導

根據日本《讀賣新聞》報導,為了讓動物園經營合理化,大阪天王寺動物園園長牧慎一郎持續對園內動物進行「裁員」,其中包括超人氣的無尾熊。天王寺動物園2014年在園內曾有3隻無尾熊,但飼料費一年高達6400萬日圓(約新台幣1800萬元),占園區整體動物飼料費4至5成,沉重的負擔讓園方經營日漸困難。

為了減省開支,園內最後一隻無尾熊「Ark」於2019年10月免費借給國外的動物園。

牧慎一郎認為,在有限的預算與土地下,動物也必須進行「集中與選擇」。而園方原本計畫,園內飼養的212種動物中,要「裁員」22種,但因高齡動物死亡、動物打架致死等意外,至2019年底時,園內只剩下184種動物。

生物多樣性
國際新聞
日本
無尾熊
動物園
動物福利

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

麥肯錫「未來亞洲」報告:氣候變遷將影響東南亞最劇

環境資訊中心綜合外電;黃鈺婷 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

氣候暖化衝擊 冰島數百冰川快速消融

摘錄自2020年9月16日公視報導

去年才被聯合國納入世界遺產,冰島最大的瓦特納冰川,目前估計流失冰層每秒達15萬公升、每年高達130億公升,5年後地貌將明顯走樣。

冰島有11%的面積為冰川所覆蓋,壯闊的冰川帶來豐沛的水資源,提供水力發電,並且創造了可觀的觀光收益;然而冰島人也正面臨冰川消失的危機。

科學家擔心,冰島上400多座冰川,可能在200年內全部走入歷史。冰河學家甘納森表示,「150年或200年左右,所有冰河都會消失,冰島將會變成無冰河狀態。」

氣候變遷
國際新聞
冰島
冰川消融
全球暖化

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

美西野火肆虐 波特蘭全球空污最嚴重

摘錄自2020年9月16日公視報導

美國西岸野火重大危機,波特蘭在15日早上成為全球空氣污染最嚴重城市。

奧勒岡州野火肆虐,厚重煙霧壟罩波特蘭市區。根據瑞士空氣清淨科技公司IQAir的報告,15日上午,波特蘭、溫哥華,以及西雅圖,是全球空氣污染最嚴重的三大城市。根據空氣品質指標,波特蘭為455;指標範圍從0到500,300以上為最嚴重等級「危害」,健康威脅達到緊急,所有人都可能受到影響。西雅圖則為161,溫哥華為158,兩城市的等級都是「不健康」。

奧勒岡州州長已要求總統川普宣布這次野火為「重大災難」,才能啟動聯邦協助,避免事態擴大,並釋出25萬個N95口罩,提供給農工、部落族群,以及其他呼吸道易危族群。目前野火已經燒掉超過4000平方公里的面積以及數百棟房屋。

污染治理
國際新聞
美國
野火
空污

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

這些低價高配的合資SUV,最低僅11萬不到!

67萬元在我們的印象中,法國人總喜歡在造型設計上大下筆墨,怎麼不實用怎麼來,所以大家都覺得很少會有一款法系車能與“性價比”沾邊。但是。從天逸C5 AIRCROSS開始情況似乎就有點不一樣了。這款緊湊型SUV不僅完美繼承了法系車飄逸的設計風格,並且在配置和價格上都算得上實在,這對於預算吃緊又想得到更多的消費者來說可是個好事。

不知大家有沒有覺得,15萬左右的購車預算是最尷尬的,說多不多,說少不少,尤其是在挑選SUV車型的時候,如今國產+合資一大堆選擇,看得眼花繚亂。

而隨着國產SUV的崛起,不少人也在鼓吹買國產車就是“低價高配”,這也使得很多對汽車不怎麼了解的消費者錯過了一些優秀的合資車型。那麼今天就要給大家介紹幾款極具性價比的合資品牌SUV,讓大家知道所謂的“低價高配”並不是國產車的專屬。

1、北京現代 全新ix35

官方指導價:11.58-18.88萬元

作為一款合資品牌車型,ix35的價格的確是非常給力了,最入門的車型在有些4S店僅11萬不到,實在是令人意想不到。而參考現代車一直以來的耐用性,ix35可以說就是目前市面上你能買到的價格最低,且總體表現最穩定的合資緊湊型SUV了。

從外形上看,新款車型在歷經換代后整體風格更加陽剛硬朗了,甚至還有點小硬漢的感覺。內飾也同樣如此,採用全新的設計和布局,而方正的造型也明顯與外觀呼應。

配置方面,全系標配ESp、上坡輔助、后駐車雷達等,中配及以上車型則相應增加了胎壓監測、陡坡緩降、無鑰匙進入/啟動、9.6英寸中控液晶显示屏(帶倒車雷達/影像)、自動大燈與電加熱/摺疊后視鏡等等。

考慮到其中配車型13.99萬元的售價,以合資品牌的標準看,這配置已經足夠厚道了。

動力方面,ix35全系均搭載了傳統的2.0L自然吸氣發動機,傳動上匹配6速手動或6速手自一體變速箱。雖然數據並不亮眼,但勝在性能成熟穩定,並且後期的使用故障率和維護成本都較低,所以從家用車SUV的角度來看,ix35無疑是一款非常省錢又省心的車型。

2、東風雪鐵龍 天逸C5 AIRCROSS

官方指導價:15.27-23.67萬元

在我們的印象中,法國人總喜歡在造型設計上大下筆墨,怎麼不實用怎麼來,所以大家都覺得很少會有一款法系車能與“性價比”沾邊。

但是!從天逸C5 AIRCROSS開始情況似乎就有點不一樣了。這款緊湊型SUV不僅完美繼承了法系車飄逸的設計風格,並且在配置和價格上都算得上實在,這對於預算吃緊又想得到更多的消費者來說可是個好事。

首先進入到車內,第一感覺就是讓你懷疑這根本就不是一輛售價僅15萬元起的SUV,因為無論是獨具一格的內飾設計,還是雙色皮革的搭配,都讓你感受到實實在在的質感提升。

配置上就更不用說了,除入門版車型外,其餘車型均配備了有多個安全氣囊、胎壓監測、全景天窗、無鑰匙進入/啟動、皮質多功能方向盤、全液晶儀錶盤、全車車窗一鍵升降、電加熱/摺疊后視鏡,自動大燈與自動空調等等,應有盡有!

所以對於一款法系車來說,天逸C5 AIRCROSS的確在“性價比”方面打了一場漂亮的翻身仗,也證明了美貌和實力是可以兼得的。

3、上汽斯柯達 柯珞克

預計售價:13.88萬起

斯柯達也是一個容易被人國內消費者忽略的品牌,所以最後推薦的就是近期即將上市的一款入門緊湊型SUV-柯珞克,而13.88萬的預售價格也十分值得期待。

在外形上,柯珞克採用了與柯迪亞克相同的家族式設計語言,中庸的造型適合絕大部分消費者。而在車身尺寸上,新車長寬高為4432/1841/1614mm,軸距為2688mm,也就是比繽智、昂科拉都要稍大一些。

動力上,新車搭載的是1.2T和1.4T發動機,匹配7速DSG雙離合變速箱,想想不到15萬就能買到這樣一款德系品質(大眾平台)SUV,是不是覺得自己賺了?所以野帝停產後,柯珞克或將成為大眾集團在15萬元左右SUV市場的主力車型。

好了!以上就是今天給大家推薦的三款15萬元左右、極具性價比的合資品牌SUV。大家如果還有其他想了解的購車話題,歡迎留言評論,盡量滿足大家需求。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?