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

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

為什麼不推薦使用繼承?

繼承是面向對象的四大特性之一,用來表示類之間的 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

下一篇開始動態權限

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

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

巴西大沼澤火災 美洲豹主要棲息地逾7成燒毀

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

位於巴西中西部馬托格羅索州大沼澤的水遇州立公園是全世界美洲豹最集中的地方,如今卻因森林火災蔓延,嚴重威脅美洲豹和其他物種的生存。

英國廣播公司巴西新聞網(BBC News Brasil)今(15日)報導,位於馬托格羅索州(Mato Grosso)占地10.8公頃的水遇州立公園(Parque Estadual Encontro dasÁguas),目前已有超過7成範圍被大火燒毀。

如同水遇州立公園,整個大沼澤(Pantanal)地區的火勢也迅速蔓延,迄今已燒毀逾230萬公頃林地,範圍相當於這整個生物群系在巴西15%的面積。

根據巴西國家太空署(Inpe),今年1月至9月初,大沼澤共記錄1.21萬處火災,創下1999年以來同期最高紀錄。

美洲豹已被國際自然保育聯盟(IUCN)和巴西環境部視為幾乎瀕危物種。根據研究,全球90%的美洲虎集中在南美洲。

巴西生物學家波柳薩(Ricardo Boulhosa)指出,這些火災是人為的,無法控制,所以變成森林大火,危害美洲豹的生存。

非政府組織Panthera研究員托爾塔托(Fernando Tortato)強調水遇州立公園的重要性,因為那裡一直是受保護的生態走廊,確保美洲豹的優質棲息地,園內有許多美洲豹可以觀察,有利於發展生態旅遊。

生物多樣性
國際新聞
巴西
大沼澤
美洲豹
動物棲地
森林野火

本站聲明:網站內容來源環境資訊中心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/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

第一次購車,想花10來萬買豪車、用小錢買大SUV!?

58%/1。40%,而SUV可以說是市場上的寵兒,在17年全年共銷售了1007。77萬輛,同比增長了15。3%。SUV總給到我們高大威猛的感覺,而且憑藉著高底盤所帶來的高通過性以及廂式結構的高實用性,在同樣價格下,即便SUV車型的定位要低於轎車,但是在氣場上依然會比轎車更強。

人總會有第一次,而你第一次購車到底碰到過怎麼樣的經歷呢?今天咱們不妨來討論一下。

簡單來說,第一次大家都沒有太大經驗,所以在選車方面也變得比較迷茫,於是也會存在各種各樣的誤區。說起人生第一次購車,其實也是分為數人群。

第一種就是剛畢業不久出社會打滾的大學生,這部分消費者購車大多數會有家人的經濟支持(也有少部分精英通過自己的努力去買下一台心儀之車);另一種是有購車計劃但奈何購車資金遲遲不到位的消費者。

值得一提的是,無論你是哪一種情況,在選車時所碰到的情況無非只有幾種:

眼高手低這一個四字詞語也可以很好地形容我們的購車觀念,關於預算這方面的問題,也可以說是90%的消費者都躺槍的一個地方,就像有不少人一直有顆買思域的心,但卻忽視了自己只有買飛度的預算一樣。

也正因為這一種情況,你會發現像有很多豪華品牌都開始自降身份,推出極為“親民”的車型搶佔合資市場,像奧迪的A3、寶馬的1系也是一個非常成功的例子,雖然網上的吐槽聲不斷,但是買的人依然絡繹不絕,畢竟它們是豪華品牌、關鍵還賣得便宜呀!

另一個方面,預算不夠又想買更高級的車型要怎麼辦?做貸款是大多數車主的選擇,在絕大多數情況下,4S店更希望你選擇貸款購車,而且在貸款方案上也給出比較大的優惠,譬如超低首付、0利息等等的貸款政策(當然,羊毛出在羊身上,貸了多少錢,該還的還是要還的)。

花了大部分積蓄去買一台車,每一位消費者都不希望買到一台問題不斷的故障車,所以才會出現有很多消費者在選車時會出現偏向於選擇合資品牌甚至對某個品牌有極高忠誠度的情況,歸根到底的原因是這些品牌自從進入中國市場以來,通過旗下車型的極佳表現早早在消費者心中打下了“質量高”的良好口碑,讓消費者信賴。

這也是為什麼近幾年來國產品牌有着質一般的飛躍,但是很多消費者依然偏向於考慮合資,關鍵也就在於“口碑”之上(畢竟在很多消費者心中,國產品牌剛起步時口碑真心不怎麼樣),像這樣的固有思想需要用一段很長的時間去改變。

也是因為這個,所以在用樣的預算之下,有很多消費者便會選擇去購買一輛SUV車型,根據中汽協發布的數據显示,2017年我國乘用車產銷量分別為2480.67萬/2471.83萬,同比增長了1.58%/1.40%,而SUV可以說是市場上的寵兒,在17年全年共銷售了1007.77萬輛,同比增長了15.3%。

SUV總給到我們高大威猛的感覺,而且憑藉著高底盤所帶來的高通過性以及廂式結構的高實用性,在同樣價格下,即便SUV車型的定位要低於轎車,但是在氣場上依然會比轎車更強。

關於選車這一方面,其實也是一個比較尷尬的情況,譬如是靠家中的資金購車的消費者,給錢的不是你,最終你可能買到一台你家人喜愛的車型;又或者你看上了思域,可是家中的老婆大人卻看上了A3,相信…無論怎麼選,最終的選擇權估計不會在你手上。

簡單來說就是將更多的注意力放在等待新款車型上市,對於絕大部分新款車型而言,剛上市並沒有太多的優惠(基本是沒有的),這時候購車你又嫌優惠少,再等幾個月又有新款車型上市,究竟要到什麼時候你才會下訂決心買車呢?

普遍情況下,車型上市時間越久,現金優惠力度越大,而且車型價格越高優惠幅度越大。像合資的B級車或者更高級的豪華品牌,動不動就會有數萬的優惠。

簡單來說,第一次購車,大家都會像一個懵懂的少年,不知所措。你還記得當初你在首次購車的時候碰到過什麼樣的煩惱嗎?不妨在下方評論區分享出來!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※想知道最厲害的網頁設計公司"嚨底家"!

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

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

不要小看這些LOGO,分分鐘差個幾百萬!

而其標誌性的立標設計,當你坐進奔馳座艙抬頭看見發動機艙矗立的“三叉星”,心底一種莫名的自豪感,以及一種底氣油然而生,這也是E級立標的銷量遠超大標的重要因素。除此外,將安全帶融入到LOGO的沃爾沃也是一個很有特色的設計。

LOGO,在很大程度上代表着一個品牌、一件產品的形象、價值,乃至地位。一個好的LOGO,不僅能讓人過目不忘,並且能為消費者灌入一個品牌歸屬感,以及品牌忠誠度。設計出一個優秀的LOGO,十分考驗設計師的功力,以及決策層對企業和品牌文化的定調,二者缺一不可。

今天,我們就來聊聊市面上常見的汽車品牌,它們各自間由LOGO而讓人聯想到的一些有趣的故事,一起來看看:

諸多汽車品牌LOGO中,叫獸個人最欣賞奔馳的LOGO。“三叉星”簡潔有力,並象徵著奔馳向海陸空三方位發展的野心。而其標誌性的立標設計,當你坐進奔馳座艙抬頭看見發動機艙矗立的“三叉星”,心底一種莫名的自豪感,以及一種底氣油然而生,這也是E級立標的銷量遠超大標的重要因素。

除此外,將安全帶融入到LOGO的沃爾沃也是一個很有特色的設計。沃爾沃從LOGO開始,就在向世人显示自身對安全的不懈追求。

拋開品牌檔次問題,不知道各位小夥伴比較喜歡哪家品牌的LOGO呢?歡迎在評論區留言。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

15年、20多萬公里還在跑,這是別克曾經最牛的車型!

Samuel拿到駕照的那一天,他從父親手中接過了君威的方向盤。在這十多年裡,君威給Samuel留下了不少深刻的回憶。他說這台重達1。6噸的中型轎車在遇上起伏較大的路面時,車身就像一艘船般搖晃,舒適性卻是一流。他還吐槽這2。

筆者有一位叫Samuel的朋友,是一位年輕的醫生,最近他跟筆者說,他要買新車了。由於沒有找到額外的固定車位,他決定“淘汰”掉已經服役了十多年的老君威。與即將開上新車的喜悅相比,Samuel更舍不得離開這台陪伴了他多年的老君威。

那天,Smauel說:

不如你幫我的老君威拍一組照片吧

做個留念。

筆者答應了。

拍攝的地點選在了廣州大學城,在這一片江邊的林蔭小道上,我們拍攝了一組老車配落恭弘=叶 恭弘的照片。

夕陽無限好,只是近黃昏

歲月讓這台老君威的面容憔悴,身上的小毛病也越來越多

它真的不能再服務Samuel一家了

在拍攝間隙,筆者和Samuel聊起了這台老君威的故事。

Samuel表示,這台別克君威GL 2.5是他家裡第一台車。

作為別克在中國生產的第一代別克君威,同時也是別克Regal系列的第四代車型,國內消費者心中的“美系車”印象在很大程度上就是基於這款車型產生的。

雖然與別克新世紀(Reagl剛剛進入中國時被命名為新世紀)相比,君威的外觀經過修改,但也依然保留了非常原汁原味的美式設計風格。

和大部分80后、90后青年一樣

家中的第一台車往往也是自己感受最深刻的一台車。

Samuel回想起2005年家中決定買車時,市面上的合資中型轎車選擇並不多,他說當時他的父親將選擇範圍確定在了別克君威、本田雅閣和現代索納塔之間。經過一輪深思熟慮,別克君威成為了他父親的心頭好。Samuel拿到駕照后,這台君威也就同時成為了他的座駕,直至今日。

Samuel拿到駕照的那一天,他從父親手中接過了君威的方向盤。

在這十多年裡,君威給Samuel留下了不少深刻的回憶;他說這台重達1.6噸的中型轎車在遇上起伏較大的路面時,車身就像一艘船般搖晃,舒適性卻是一流。他還吐槽這2.5L自然吸氣V6發動機竟只能輸出152馬力,油耗高卻沒有足夠的動力輸出;而這就是老君威給他留下的“美式豪華轎車”的印象。不過,Samuel卻不覺得這些是缺陷,他說早前曾試駕過現款別克君越,無論是設計還是駕控表現都非常優秀,但是卻沒有了老君威這種突出的美式印象,個性稍有缺失。

十多年的服役,中央扶手已經破損。

在Samuel看來,老君威的座椅舒適性即便是在今天看來也是一流的。

2.5L自然吸氣V6發動機只能輸出152馬力,搭配4擋自動變速箱,對於超過1.5噸的整車重量來說,動力表現不盡人意,這也是Samuel對老君威唯一的“抱怨”。

Samuel跟筆者說,正如電影《桃姐》里所講述的故事一樣,一位給家裡服務了多年的保姆,當有一天她年紀大了,必須離開的時候,就犹如家人分離,那種不舍的感覺讓人難以描述;他正在努力尋找一個能夠長期停放這台老君威的地方,就像電影里劉德華把桃姐送到了護老院,他要將這台滿載回憶的車保存起來,希望他能夠早日找到這個地方吧。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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