项目

组件替换

您可以用自定义组件替换某些ABP组件。

可以替换无法自定义默认ABP组件的原因是:禁用或更改该组件的某些部分可能会导致问题。因此,我们将这些组件称为可替换组件

如何替换组件

创建一个您想要使用的新组件来代替某个ABP组件。

然后,打开 app.component.ts 文件,执行 ReplaceableComponentsServiceadd 方法,将您的组件替换为ABP组件,如下所示:

import { ReplaceableComponentsService } from '@abp/ng.core'; // 导入 ReplaceableComponentsService
import { eIdentityComponents } from '@abp/ng.identity'; // 导入 eIdentityComponents 枚举
import { Component, inject } from '@angular/core';
//...

@Component(/* 组件元数据 */)
export class AppComponent {
  private replaceableComponents = inject(ReplaceableComponentsService);

  constructor() {
    this.replaceableComponents.add({
      component: YourNewRoleComponent,
      key: eIdentityComponents.Roles,
    });
  }
}

示例用法

如何替换布局

每个ABP主题包都有3个名为 ApplicationLayoutComponentAccountLayoutComponentEmptyLayoutComponent 的布局。这些布局可以用相同的方式替换。

布局组件模板应包含 <router-outlet></router-outlet> 元素。

以下示例描述了如何替换 ApplicationLayoutComponent

angular 文件夹中运行以下命令以生成一个布局:

yarn ng generate component my-application-layout

在您希望页面加载的位置,将以下代码添加到布局模板 (my-application-layout.component.html) 中。

<router-outlet></router-outlet>

打开 src/app 文件夹中的 app.component.ts 文件,并按如下所示进行修改:

import { ReplaceableComponentsService } from '@abp/ng.core'; // 导入 ReplaceableComponentsService
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // 导入用于组件键的 eThemeBasicComponents 枚举
import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // 导入 MyApplicationLayoutComponent
import { Component, inject } from '@angular/core';

@Component(/* 组件元数据 */)
export class AppComponent {
  private replaceableComponents = inject(ReplaceableComponentsService);

  constructor() {
    this.replaceableComponents.add({
      component: MyApplicationLayoutComponent,
      key: eThemeBasicComponents.ApplicationLayout,
    });
  }
}

如果您希望在运行时替换布局组件(例如:通过按按钮更改布局),请将 ReplaceableComponentsServiceadd 方法的第二个参数传递为 true。DynamicLayoutComponent 使用 router-outlet 加载内容。当 add 方法的第二个参数为 true 时,路由将被刷新,因此请谨慎使用。您的组件状态将消失,并且任何初始化逻辑(包括 HTTP 请求)都将重复执行。

布局组件

布局组件

如何添加新的布局组件

要添加新的布局组件,您需要遵循以下步骤:

步骤 1:创建一个新的 Angular 组件

此组件应具有用于动态内容加载的 router-outlet。您可以使用 Angular CLI 创建一个新组件。在终端中运行以下命令:

ng generate component new-layout

此命令将创建一个名为 new-layout 的新组件。现在,打开 new-layout.component.html 文件并向其中添加一个 router-outlet

<router-outlet></router-outlet>

这个 router-outlet 将充当一个占位符,Angular 会根据当前路由器状态动态填充内容。

步骤 2:为布局组件定义一个变量

虽然此步骤是可选的,但如果您打算多次使用布局组件的值,它可能会很有用。您可以像这样为布局组件定义一个变量:

export const eCustomLayout = {
  key: "CustomLayout",
  component: "CustomLayoutComponent",
};

在此变量中,key 是布局组件的唯一标识符,component 是布局组件的名称。 当您需要引用布局组件时,可以使用此变量。

步骤 3:将布局组件添加到 ABP 可替换系统中

接下来,您需要将新的布局组件添加到 ReplaceableComponentsService 中。此服务允许您动态地将一个组件替换为另一个组件。

您可以通过为 provideAppInitializer 定义一个使用工厂函数的提供者来实现。在此函数中,您注入 ReplaceableComponentsService 并使用其 add 方法来添加新的布局组件。

操作方法如下:

export const CUSTOM_LAYOUT_PROVIDERS = [
  provideAppInitializer(()=>{
    configureLayoutFn();
  }),
];

function configureLayoutFn() {
  const service = inject(ReplaceableComponentsService);
  service.add({
    key: eCustomLayout.component,
    component: CustomLayoutComponent,
  });
}

在此代码中,configureLayoutFn 是一个将新布局组件添加到 ReplaceableComponentsService 的工厂函数。provideAppInitializer 提供者在应用程序启动时运行此函数。

注意:(别忘了:您应该在 app.config.ts 文件中添加 CUSTOM_LAYOUT_PROVIDERS)

步骤 4:定义应用程序的动态布局

最后,您需要定义应用程序的动态布局。这是一个映射,其中键是布局键,值是布局组件。

您可以像这样将新布局添加到现有布局中:

export const myDynamicLayouts = new Map<string, string>([...DEFAULT_DYNAMIC_LAYOUTS, [eCustomLayout.key, eCustomLayout.component]]);

步骤 5:将动态布局传递给 Core 提供者

最后一步是使用 withOptions 方法将动态布局传递给 provideAbpCore。此方法允许您使用静态方法配置提供者。

操作方法如下:

export const appConfig: ApplicationConfig = {
  providers: [
    // ...
    provideAbpCore(
      withOptions({
        dynamicLayouts: myDynamicLayouts,
        environment,
        registerLocaleFn: registerLocale(),
      }),
    ),
  ],
};

在此代码中,myDynamicLayouts 是您之前定义的动态布局映射。我们使用 withOptions 方法将此映射传递给 provideAbpCore

现在您已经定义了新布局,可以在路由器定义中使用它。您可以通过添加使用新布局的新路由来实现。

操作方法如下:

// route.provider.ts
import { eCustomLayout } from './custom-layout/custom-layout.provider';
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { provideAppInitializer } from '@angular/core';

export const APP_ROUTE_PROVIDER = [
  provideAppInitializer(() => {
    configureRoutes();
  }),
];

function configureRoutes() {
  const routes = inject(RoutesService);
  routes.add([
    {
      path: '/',
      name: '::Menu:Home',
      iconClass: 'fas fa-home',
      order: 1,
      layout: eLayoutType.application,
    },
    {
      path: '/dashboard',
      name: '::Menu:Dashboard',
      iconClass: 'fas fa-chart-line',
      order: 2,
      layout: eCustomLayout.key as eLayoutType,
      requiredPolicy: 'MyProjectName.Dashboard.Host  || MyProjectName.Dashboard.Tenant',
    },
  ]);
}

如何替换 LogoComponent

LogoComponent

注意

  • 如果您的目标仅仅是更改徽标图像或应用程序名称,则无需替换组件。建议通过 @abp/ng.theme.shared 提供徽标,以便所有主题/组件都能一致地使用它:
// app.config.ts
import { provideLogo, withEnvironmentOptions } from '@abp/ng.theme.shared';
import { environment } from './environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    provideLogo(withEnvironmentOptions(environment)),
  ],
};

如果您仍然希望完全替换徽标组件的 UI,请按照以下步骤操作:

angular 文件夹中运行以下命令,创建一个名为 LogoComponent 的新组件。

yarn ng generate component logo --inlineTemplate --inlineStyle

打开 src/app/logo 文件夹中生成的 logo.component.ts,并将其内容替换为以下内容:

import { Component } from "@angular/core";

@Component({
  selector: "app-logo",
  template: `
    <a class="navbar-brand" routerLink="/">
      <!-- 更改 img src -->
      <img
        src="https://via.placeholder.com/100x50/343a40/FF0000?text=MyLogo"
        alt="logo"
        width="100%"
        height="auto"
      />
    </a>
  `,
})
export class LogoComponent {}

打开 src/app 文件夹中的 app.component.ts,并按如下所示进行修改:

import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // 导入 ReplaceableComponentsService
import { LogoComponent } from './logo/logo.component'; // 导入 LogoComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // 导入 eThemeBasicComponents
//...

@Component(/* 组件元数据 */)
export class AppComponent implements OnInit {
  private replaceableComponents = inject(ReplaceableComponentsService);

  ngOnInit() {
    //...

    this.replaceableComponents.add({
      component: LogoComponent,
      key: eThemeBasicComponents.Logo,
    });
  }
}

最终的 UI 如下所示:

新徽标

如何替换 RoutesComponent

RoutesComponent

angular 文件夹中运行以下命令,创建一个名为 RoutesComponent 的新组件。

yarn ng generate component routes

打开 src/app/routes 文件夹中生成的 routes.component.ts,并将其内容替换为以下内容:

import { Component, HostBinding } from "@angular/core";
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { LocalizationPipe, PermissionDirective } from "@abp/ng.core";
import { EllipsisDirective } from '@abp/ng.theme.shared';

@Component({
  selector: "app-routes",
  templateUrl: "routes.component.html",
  imports: [
    CommonModule,
    RouterModule,
    NgbDropdownModule,
    PermissionDirective,
    EllipsisDirective,
    LocalizationPipe,
  ]
})
export class RoutesComponent {
  @HostBinding("class.mx-auto")
  marginAuto = true;

  get smallScreen() {
    return window.innerWidth < 992;
  }
}

打开 src/app/routes 文件夹中生成的 routes.component.html,并将其内容替换为以下内容:

<ul class="navbar-nav">
  <li class="nav-item">
    <a class="nav-link" routerLink="/"
      ><i class="fas fa-home"></i> {{ '::Menu:Home' | abpLocalization
      }}</a
    >
  </li>
  <li class="nav-item">
    <a class="nav-link" routerLink="/my-page"
      ><i class="fas fa-newspaper mr-1"></i>My Page</a
    >
  </li>
  <li
    #navbarRootDropdown
    [abpVisibility]="routeContainer"
    class="nav-item dropdown"
    display="static"
    (click)="
      navbarRootDropdown.expand
        ? (navbarRootDropdown.expand = false)
        : (navbarRootDropdown.expand = true)
    "
  >
    <a
      class="nav-link dropdown-toggle"
      data-toggle="dropdown"
      href="javascript:void(0)"
    >
      <i class="fas fa-wrench"></i>
      {{ 'AbpUiNavigation::Menu:Administration' | abpLocalization }}
    </a>
    <div
      #routeContainer
      class="dropdown-menu border-0 shadow-sm"
      (click)="$event.preventDefault(); $event.stopPropagation()"
      [class.d-block]="smallScreen && navbarRootDropdown.expand"
    >
      <div
        class="dropdown-submenu"
        ngbDropdown
        #dropdownSubmenu="ngbDropdown"
        placement="right-top"
        [autoClose]="true"
        *abpPermission="'AbpIdentity.Roles || AbpIdentity.Users'"
      >
        <div ngbDropdownToggle [class.dropdown-toggle]="false">
          <a
            abpEllipsis="210px"
            [abpEllipsisEnabled]="!smallScreen"
            role="button"
            class="btn d-block text-start dropdown-toggle"
          >
            <i class="fa fa-id-card-o"></i>
            {{ 'AbpIdentity::Menu:IdentityManagement' | abpLocalization }}
          </a>
        </div>
        <div
          #childrenContainer
          class="dropdown-menu border-0 shadow-sm"
          [class.d-block]="smallScreen && dropdownSubmenu.isOpen()"
        >
          <div class="dropdown-submenu" *abpPermission="'AbpIdentity.Roles'">
            <a class="dropdown-item" routerLink="/identity/roles">
              {{ 'AbpIdentity::Roles' | abpLocalization }}</a
            >
          </div>
          <div class="dropdown-submenu" *abpPermission="'AbpIdentity.Users'">
            <a class="dropdown-item" routerLink="/identity/users">
              {{ 'AbpIdentity::Users' | abpLocalization }}</a
            >
          </div>
        </div>
      </div>

      <div
        class="dropdown-submenu"
        ngbDropdown
        #dropdownSubmenu="ngbDropdown"
        placement="right-top"
        [autoClose]="true"
        *abpPermission="'AbpTenantManagement.Tenants'"
      >
        <div ngbDropdownToggle [class.dropdown-toggle]="false">
          <a
            abpEllipsis="210px"
            [abpEllipsisEnabled]="!smallScreen"
            role="button"
            class="btn d-block text-start dropdown-toggle"
          >
            <i class="fa fa-users"></i>
            {{ 'AbpTenantManagement::Menu:TenantManagement' | abpLocalization
            }}
          </a>
        </div>
        <div
          #childrenContainer
          class="dropdown-menu border-0 shadow-sm"
          [class.d-block]="smallScreen && dropdownSubmenu.isOpen()"
        >
          <div
            class="dropdown-submenu"
            *abpPermission="'AbpTenantManagement.Tenants'"
          >
            <a class="dropdown-item" routerLink="/tenant-management/tenants">
              {{ 'AbpTenantManagement::Tenants' | abpLocalization }}</a
            >
          </div>
        </div>
      </div>
    </div>
  </li>
</ul>

打开 src/app 文件夹中的 app.component.ts,并按如下所示进行修改:

import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // 导入 ReplaceableComponentsService
import { RoutesComponent } from './routes/routes.component'; // 导入 RoutesComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // 导入 eThemeBasicComponents
//...

@Component(/* 组件元数据 */)
export class AppComponent implements OnInit {
  private replaceableComponents = inject(ReplaceableComponentsService);

  ngOnInit() {
    //...

    this.replaceableComponents.add({
      component: RoutesComponent,
      key: eThemeBasicComponents.Routes,
    });
  }
}

最终的 UI 如下所示:

新路由

如何替换 NavItemsComponent

NavItemsComponent

angular 文件夹中运行以下命令,创建一个名为 NavItemsComponent 的新组件。

yarn ng generate component nav-items

打开 src/app/nav-items 文件夹中生成的 nav-items.component.ts,并将其内容替换为以下内容:

import {
  AuthService,
  ConfigStateService,
  CurrentUserDto,
  LanguageInfo,
  NAVIGATE_TO_MANAGE_PROFILE,
  SessionStateService,
  LocalizationPipe
} from '@abp/ng.core';
import { Component, inject, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import snq from 'snq';

@Component({
  selector: 'app-nav-items',
  templateUrl: 'nav-items.component.html',
  imports: [
    CommonModule,
    FormsModule,
    NgbDropdownModule,
    LocalizationPipe
  ]
})
export class NavItemsComponent {
  private configState = inject(ConfigStateService);
  private authService = inject(AuthService);
  private sessionState = inject(SessionStateService);
  @Inject(NAVIGATE_TO_MANAGE_PROFILE) public navigateToManageProfile: any;

  currentUser$: Observable<CurrentUserDto> = this.configState.getOne$('currentUser');
  selectedTenant$ = this.sessionState.getTenant$();
  languages$: Observable<LanguageInfo[]> = this.configState.getDeep$('localization.languages');

  get smallScreen(): boolean {
    return window.innerWidth < 992;
  }

  get defaultLanguage$(): Observable<string> {
    return this.languages$.pipe(
      map(
        languages =>
          snq(
            () => languages.find(lang => lang.cultureName === this.selectedLangCulture).displayName
          ),
        ''
      )
    );
  }

  get dropdownLanguages$(): Observable<LanguageInfo[]> {
    return this.languages$.pipe(
      map(
        languages =>
          snq(() => languages.filter(lang => lang.cultureName !== this.selectedLangCulture)),
        []
      )
    );
  }

  get selectedLangCulture(): string {
    return this.sessionState.getLanguage();
  }

  onChangeLang(cultureName: string) {
    this.sessionState.setLanguage(cultureName);
  }

  navigateToLogin() {
    this.authService.navigateToLogin();
  }

  logout() {
    this.authService.logout().subscribe();
  }
}

打开 src/app/nav-items 文件夹中生成的 nav-items.component.html,并将其内容替换为以下内容:

<ul class="navbar-nav">
  <input
    type="search"
    placeholder="Search"
    class="bg-transparent border-0 text-white"
  />
  <li class="nav-item d-flex align-items-center">
    <div
      *ngIf="(dropdownLanguages$ | async)?.length > 0"
      class="dropdown"
      ngbDropdown
      #languageDropdown="ngbDropdown"
      display="static"
    >
      <a
        ngbDropdownToggle
        class="nav-link"
        href="javascript:void(0)"
        role="button"
        id="dropdownMenuLink"
        data-toggle="dropdown"
        aria-haspopup="true"
        aria-expanded="false"
      >
        {{ defaultLanguage$ | async }}
      </a>
      <div
        class="dropdown-menu dropdown-menu-right border-0 shadow-sm"
        aria-labelledby="dropdownMenuLink"
        [class.d-block]="smallScreen && languageDropdown.isOpen()"
      >
        <a
          *ngFor="let lang of dropdownLanguages$ | async"
          href="javascript:void(0)"
          class="dropdown-item"
          (click)="onChangeLang(lang.cultureName)"
          >{{ lang?.displayName }}</a
        >
      </div>
    </div>
  </li>
  <li class="nav-item d-flex align-items-center">
    <ng-template #loginBtn>
      <a role="button" class="nav-link pointer" (click)="navigateToLogin()"
        >{{ 'AbpAccount::Login' | abpLocalization }}</a
      >
    </ng-template>
    <div
      *ngIf="(currentUser$ | async)?.isAuthenticated; else loginBtn"
      ngbDropdown
      class="dropdown"
      #currentUserDropdown="ngbDropdown"
      display="static"
    >
      <a
        ngbDropdownToggle
        class="nav-link"
        href="javascript:void(0)"
        role="button"
        id="dropdownMenuLink"
        data-toggle="dropdown"
        aria-haspopup="true"
        aria-expanded="false"
      >
        <small *ngIf="(selectedTenant$ | async)?.name as tenantName"
          ><i>{{ tenantName }}</i>\</small
        >
        <strong>{{ (currentUser$ | async)?.userName }}</strong>
      </a>
      <div
        class="dropdown-menu dropdown-menu-right border-0 shadow-sm"
        aria-labelledby="dropdownMenuLink"
        [class.d-block]="smallScreen && currentUserDropdown.isOpen()"
      >
        <a class="dropdown-item pointer" (click)="navigateToManageProfile()"
          ><i class="fa fa-cog mr-1"></i>{{ 'AbpAccount::MyAccount' |
          abpLocalization }}</a
        >
        <a class="dropdown-item" href="javascript:void(0)" (click)="logout()"
          ><i class="fa fa-power-off mr-1"></i>{{ 'AbpUi::Logout' |
          abpLocalization }}</a
        >
      </div>
    </div>
  </li>
</ul>

打开 src/app 文件夹中的 app.component.ts,并按如下所示进行修改:

import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // 导入 ReplaceableComponentsService
import { NavItemsComponent } from './nav-items/nav-items.component'; // 导入 NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // 导入 eThemeBasicComponents
//...

@Component(/* 组件元数据 */)
export class AppComponent implements OnInit {
  private replaceableComponents = inject(ReplaceableComponentsService);

  ngOnInit() {
    //...

    this.replaceableComponents.add({
      component: NavItemsComponent,
      key: eThemeBasicComponents.NavItems,
    });
  }
}

最终的 UI 如下所示:

新导航项

另请参阅


# 组件替换

您可以用自定义组件替换某些ABP组件。

您**可以替换**但**无法自定义**默认ABP组件的原因是:禁用或更改该组件的某些部分可能会导致问题。因此,我们将这些组件称为*可替换组件*。

## 如何替换组件

创建一个您想要使用的新组件来代替某个ABP组件。

然后,打开 `app.component.ts` 文件,执行 `ReplaceableComponentsService` 的 `add` 方法,将您的组件替换为ABP组件,如下所示:

```js
import { ReplaceableComponentsService } from '@abp/ng.core'; // 导入 ReplaceableComponentsService
import { eIdentityComponents } from '@abp/ng.identity'; // 导入 eIdentityComponents 枚举
import { Component, inject } from '@angular/core';
//...

@Component(/* 组件元数据 */)
export class AppComponent {
  private replaceableComponents = inject(ReplaceableComponentsService);

  constructor() {
    this.replaceableComponents.add({
      component: YourNewRoleComponent,
      key: eIdentityComponents.Roles,
    });
  }
}

示例用法

如何替换布局

每个ABP主题包都有3个名为 ApplicationLayoutComponentAccountLayoutComponentEmptyLayoutComponent 的布局。这些布局可以用相同的方式替换。

布局组件模板应包含 <router-outlet></router-outlet> 元素。

以下示例描述了如何替换 ApplicationLayoutComponent

angular 文件夹中运行以下命令以生成一个布局:

yarn ng generate component my-application-layout

在您希望页面加载的位置,将以下代码添加到布局模板 (my-application-layout.component.html) 中。

<router-outlet></router-outlet>

打开 src/app 文件夹中的 app.component.ts 文件,并按如下所示进行修改:

import { ReplaceableComponentsService } from '@abp/ng.core'; // 导入 ReplaceableComponentsService
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // 导入用于组件键的 eThemeBasicComponents 枚举
import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // 导入 MyApplicationLayoutComponent
import { Component, inject } from '@angular/core';

@Component(/* 组件元数据 */)
export class AppComponent {
  private replaceableComponents = inject(ReplaceableComponentsService);

  constructor() {
    this.replaceableComponents.add({
      component: MyApplicationLayoutComponent,
      key: eThemeBasicComponents.ApplicationLayout,
    });
  }
}

如果您希望在运行时替换布局组件(例如:通过按按钮更改布局),请将 ReplaceableComponentsServiceadd 方法的第二个参数传递为 true。DynamicLayoutComponent 使用 router-outlet 加载内容。当 add 方法的第二个参数为 true 时,路由将被刷新,因此请谨慎使用。您的组件状态将消失,并且任何初始化逻辑(包括 HTTP 请求)都将重复执行。

布局组件

布局组件

如何添加新的布局组件

要添加新的布局组件,您需要遵循以下步骤:

步骤 1:创建一个新的 Angular 组件

此组件应具有用于动态内容加载的 router-outlet。您可以使用 Angular CLI 创建一个新组件。在终端中运行以下命令:

ng generate component new-layout

此命令将创建一个名为 new-layout 的新组件。现在,打开 new-layout.component.html 文件并向其中添加一个 router-outlet

<router-outlet></router-outlet>

这个 router-outlet 将充当一个占位符,Angular 会根据当前路由器状态动态填充内容。

步骤 2:为布局组件定义一个变量

虽然此步骤是可选的,但如果您打算多次使用布局组件的值,它可能会很有用。您可以像这样为布局组件定义一个变量:

export const eCustomLayout = {
  key: "CustomLayout",
  component: "CustomLayoutComponent",
};

在此变量中,key 是布局组件的唯一标识符,component 是布局组件的名称。 当您需要引用布局组件时,可以使用此变量。

步骤 3:将布局组件添加到 ABP 可替换系统中

接下来,您需要将新的布局组件添加到 ReplaceableComponentsService 中。此服务允许您动态地将一个组件替换为另一个组件。

您可以通过为 provideAppInitializer 定义一个使用工厂函数的提供者来实现。在此函数中,您注入 ReplaceableComponentsService 并使用其 add 方法来添加新的布局组件。

操作方法如下:

export const CUSTOM_LAYOUT_PROVIDERS = [
  provideAppInitializer(()=>{
    configureLayoutFn();
  }),
];

function configureLayoutFn() {
  const service = inject(ReplaceableComponentsService);
  service.add({
    key: eCustomLayout.component,
    component: CustomLayoutComponent,
  });
}

在此代码中,configureLayoutFn 是一个将新布局组件添加到 ReplaceableComponentsService 的工厂函数。provideAppInitializer 提供者在应用程序启动时运行此函数。

注意:(别忘了:您应该在 app.config.ts 文件中添加 CUSTOM_LAYOUT_PROVIDERS)

步骤 4:定义应用程序的动态布局

最后,您需要定义应用程序的动态布局。这是一个映射,其中键是布局键,值是布局组件。

您可以像这样将新布局添加到现有布局中:

export const myDynamicLayouts = new Map<string, string>([...DEFAULT_DYNAMIC_LAYOUTS, [eCustomLayout.key, eCustomLayout.component]]);

步骤 5:将动态布局传递给 Core 提供者

最后一步是使用 withOptions 方法将动态布局传递给 provideAbpCore。此方法允许您使用静态方法配置提供者。

操作方法如下:

export const appConfig: ApplicationConfig = {
  providers: [
    // ...
    provideAbpCore(
      withOptions({
        dynamicLayouts: myDynamicLayouts,
        environment,
        registerLocaleFn: registerLocale(),
      }),
    ),
  ],
};

在此代码中,myDynamicLayouts 是您之前定义的动态布局映射。我们使用 withOptions 方法将此映射传递给 provideAbpCore

现在您已经定义了新布局,可以在路由器定义中使用它。您可以通过添加使用新布局的新路由来实现。

操作方法如下:

// route.provider.ts
import { eCustomLayout } from './custom-layout/custom-layout.provider';
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { provideAppInitializer } from '@angular/core';

export const APP_ROUTE_PROVIDER = [
  provideAppInitializer(() => {
    configureRoutes();
  }),
];

function configureRoutes() {
  const routes = inject(RoutesService);
  routes.add([
    {
      path: '/',
      name: '::Menu:Home',
      iconClass: 'fas fa-home',
      order: 1,
      layout: eLayoutType.application,
    },
    {
      path: '/dashboard',
      name: '::Menu:Dashboard',
      iconClass: 'fas fa-chart-line',
      order: 2,
      layout: eCustomLayout.key as eLayoutType,
      requiredPolicy: 'MyProjectName.Dashboard.Host  || MyProjectName.Dashboard.Tenant',
    },
  ]);
}

如何替换 LogoComponent

LogoComponent

注意

  • 如果您的目标仅仅是更改徽标图像或应用程序名称,则无需替换组件。建议通过 @abp/ng.theme.shared 提供徽标,以便所有主题/组件都能一致地使用它:
// app.config.ts
import { provideLogo, withEnvironmentOptions } from '@abp/ng.theme.shared';
import { environment } from './environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    provideLogo(withEnvironmentOptions(environment)),
  ],
};

如果您仍然希望完全替换徽标组件的 UI,请按照以下步骤操作:

angular 文件夹中运行以下命令,创建一个名为 LogoComponent 的新组件。

yarn ng generate component logo --inlineTemplate --inlineStyle

打开 src/app/logo 文件夹中生成的 logo.component.ts,并将其内容替换为以下内容:

import { Component } from "@angular/core";

@Component({
  selector: "app-logo",
  template: `
    <a class="navbar-brand" routerLink="/">
      <!-- 更改 img src -->
      <img
        src="https://via.placeholder.com/100x50/343a40/FF0000?text=MyLogo"
        alt="logo"
        width="100%"
        height="auto"
      />
    </a>
  `,
})
export class LogoComponent {}

打开 src/app 文件夹中的 app.component.ts,并按如下所示进行修改:

import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // 导入 ReplaceableComponentsService
import { LogoComponent } from './logo/logo.component'; // 导入 LogoComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // 导入 eThemeBasicComponents
//...

@Component(/* 组件元数据 */)
export class AppComponent implements OnInit {
  private replaceableComponents = inject(ReplaceableComponentsService);

  ngOnInit() {
    //...

    this.replaceableComponents.add({
      component: LogoComponent,
      key: eThemeBasicComponents.Logo,
    });
  }
}

最终的 UI 如下所示:

新徽标

如何替换 RoutesComponent

RoutesComponent

angular 文件夹中运行以下命令,创建一个名为 RoutesComponent 的新组件。

yarn ng generate component routes

打开 src/app/routes 文件夹中生成的 routes.component.ts,并将其内容替换为以下内容:

import { Component, HostBinding } from "@angular/core";
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { LocalizationPipe, PermissionDirective } from "@abp/ng.core";
import { EllipsisDirective } from '@abp/ng.theme.shared';

@Component({
  selector: "app-routes",
  templateUrl: "routes.component.html",
  imports: [
    CommonModule,
    RouterModule,
    NgbDropdownModule,
    PermissionDirective,
    EllipsisDirective,
    LocalizationPipe,
  ]
})
export class RoutesComponent {
  @HostBinding("class.mx-auto")
  marginAuto = true;

  get smallScreen() {
    return window.innerWidth < 992;
  }
}

打开 src/app/routes 文件夹中生成的 routes.component.html,并将其内容替换为以下内容:

<ul class="navbar-nav">
  <li class="nav-item">
    <a class="nav-link" routerLink="/"
      ><i class="fas fa-home"></i> {{ '::Menu:Home' | abpLocalization
      }}</a
    >
  </li>
  <li class="nav-item">
    <a class="nav-link" routerLink="/my-page"
      ><i class="fas fa-newspaper mr-1"></i>My Page</a
    >
  </li>
  <li
    #navbarRootDropdown
    [abpVisibility]="routeContainer"
    class="nav-item dropdown"
    display="static"
    (click)="
      navbarRootDropdown.expand
        ? (navbarRootDropdown.expand = false)
        : (navbarRootDropdown.expand = true)
    "
  >
    <a
      class="nav-link dropdown-toggle"
      data-toggle="dropdown"
      href="javascript:void(0)"
    >
      <i class="fas fa-wrench"></i>
      {{ 'AbpUiNavigation::Menu:Administration' | abpLocalization }}
    </a>
    <div
      #routeContainer
      class="dropdown-menu border-0 shadow-sm"
      (click)="$event.preventDefault(); $event.stopPropagation()"
      [class.d-block]="smallScreen && navbarRootDropdown.expand"
    >
      <div
        class="dropdown-submenu"
        ngbDropdown
        #dropdownSubmenu="ngbDropdown"
        placement="right-top"
        [autoClose]="true"
        *abpPermission="'AbpIdentity.Roles || AbpIdentity.Users'"
      >
        <div ngbDropdownToggle [class.dropdown-toggle]="false">
          <a
            abpEllipsis="210px"
            [abpEllipsisEnabled]="!smallScreen"
            role="button"
            class="btn d-block text-start dropdown-toggle"
          >
            <i class="fa fa-id-card-o"></i>
            {{ 'AbpIdentity::Menu:IdentityManagement' | abpLocalization }}
          </a>
        </div>
        <div
          #childrenContainer
          class="dropdown-menu border-0 shadow-sm"
          [class.d-block]="smallScreen && dropdownSubmenu.isOpen()"
        >
          <div class="dropdown-submenu" *abpPermission="'AbpIdentity.Roles'">
            <a class="dropdown-item" routerLink="/identity/roles">
              {{ 'AbpIdentity::Roles' | abpLocalization }}</a
            >
          </div>
          <div class="dropdown-submenu" *abpPermission="'AbpIdentity.Users'">
            <a class="dropdown-item" routerLink="/identity/users">
              {{ 'AbpIdentity::Users' | abpLocalization }}</a
            >
          </div>
        </div>
      </div>

      <div
        class="dropdown-submenu"
        ngbDropdown
        #dropdownSubmenu="ngbDropdown"
        placement="right-top"
        [autoClose]="true"
        *abpPermission="'AbpTenantManagement.Tenants'"
      >
        <div ngbDropdownToggle [class.dropdown-toggle]="false">
          <a
            abpEllipsis="210px"
            [abpEllipsisEnabled]="!smallScreen"
            role="button"
            class="btn d-block text-start dropdown-toggle"
          >
            <i class="fa fa-users"></i>
            {{ 'AbpTenantManagement::Menu:TenantManagement' | abpLocalization
            }}
          </a>
        </div>
        <div
          #childrenContainer
          class="dropdown-menu border-0 shadow-sm"
          [class.d-block]="smallScreen && dropdownSubmenu.isOpen()"
        >
          <div
            class="dropdown-submenu"
            *abpPermission="'AbpTenantManagement.Tenants'"
          >
            <a class="dropdown-item" routerLink="/tenant-management/tenants">
              {{ 'AbpTenantManagement::Tenants' | abpLocalization }}</a
            >
          </div>
        </div>
      </div>
    </div>
  </li>
</ul>

打开 src/app 文件夹中的 app.component.ts,并按如下所示进行修改:

import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // 导入 ReplaceableComponentsService
import { RoutesComponent } from './routes/routes.component'; // 导入 RoutesComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // 导入 eThemeBasicComponents
//...

@Component(/* 组件元数据 */)
export class AppComponent implements OnInit {
  private replaceableComponents = inject(ReplaceableComponentsService);

  ngOnInit() {
    //...

    this.replaceableComponents.add({
      component: RoutesComponent,
      key: eThemeBasicComponents.Routes,
    });
  }
}

最终的 UI 如下所示:

新路由

如何替换 NavItemsComponent

NavItemsComponent

angular 文件夹中运行以下命令,创建一个名为 NavItemsComponent 的新组件。

yarn ng generate component nav-items

打开 src/app/nav-items 文件夹中生成的 nav-items.component.ts,并将其内容替换为以下内容:

import {
  AuthService,
  ConfigStateService,
  CurrentUserDto,
  LanguageInfo,
  NAVIGATE_TO_MANAGE_PROFILE,
  SessionStateService,
  LocalizationPipe
} from '@abp/ng.core';
import { Component, inject, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import snq from 'snq';

@Component({
  selector: 'app-nav-items',
  templateUrl: 'nav-items.component.html',
  imports: [
    CommonModule,
    FormsModule,
    NgbDropdownModule,
    LocalizationPipe
  ]
})
export class NavItemsComponent {
  private configState = inject(ConfigStateService);
  private authService = inject(AuthService);
  private sessionState = inject(SessionStateService);
  @Inject(NAVIGATE_TO_MANAGE_PROFILE) public navigateToManageProfile: any;

  currentUser$: Observable<CurrentUserDto> = this.configState.getOne$('currentUser');
  selectedTenant$ = this.sessionState.getTenant$();
  languages$: Observable<LanguageInfo[]> = this.configState.getDeep$('localization.languages');

  get smallScreen(): boolean {
    return window.innerWidth < 992;
  }

  get defaultLanguage$(): Observable<string> {
    return this.languages$.pipe(
      map(
        languages =>
          snq(
            () => languages.find(lang => lang.cultureName === this.selectedLangCulture).displayName
          ),
        ''
      )
    );
  }

  get dropdownLanguages$(): Observable<LanguageInfo[]> {
    return this.languages$.pipe(
      map(
        languages =>
          snq(() => languages.filter(lang => lang.cultureName !== this.selectedLangCulture)),
        []
      )
    );
  }

  get selectedLangCulture(): string {
    return this.sessionState.getLanguage();
  }

  onChangeLang(cultureName: string) {
    this.sessionState.setLanguage(cultureName);
  }

  navigateToLogin() {
    this.authService.navigateToLogin();
  }

  logout() {
    this.authService.logout().subscribe();
  }
}

打开 src/app/nav-items 文件夹中生成的 nav-items.component.html,并将其内容替换为以下内容:

<ul class="navbar-nav">
  <input
    type="search"
    placeholder="Search"
    class="bg-transparent border-0 text-white"
  />
  <li class="nav-item d-flex align-items-center">
    <div
      *ngIf="(dropdownLanguages$ | async)?.length > 0"
      class="dropdown"
      ngbDropdown
      #languageDropdown="ngbDropdown"
      display="static"
    >
      <a
        ngbDropdownToggle
        class="nav-link"
        href="javascript:void(0)"
        role="button"
        id="dropdownMenuLink"
        data-toggle="dropdown"
        aria-haspopup="true"
        aria-expanded="false"
      >
        {{ defaultLanguage$ | async }}
      </a>
      <div
        class="dropdown-menu dropdown-menu-right border-0 shadow-sm"
        aria-labelledby="dropdownMenuLink"
        [class.d-block]="smallScreen && languageDropdown.isOpen()"
      >
        <a
          *ngFor="let lang of dropdownLanguages$ | async"
          href="javascript:void(0)"
          class="dropdown-item"
          (click)="onChangeLang(lang.cultureName)"
          >{{ lang?.displayName }}</a
        >
      </div>
    </div>
  </li>
  <li class="nav-item d-flex align-items-center">
    <ng-template #loginBtn>
      <a role="button" class="nav-link pointer" (click)="navigateToLogin()"
        >{{ 'AbpAccount::Login' | abpLocalization }}</a
      >
    </ng-template>
    <div
      *ngIf="(currentUser$ | async)?.isAuthenticated; else loginBtn"
      ngbDropdown
      class="dropdown"
      #currentUserDropdown="ngbDropdown"
      display="static"
    >
      <a
        ngbDropdownToggle
        class="nav-link"
        href="javascript:void(0)"
        role="button"
        id="dropdownMenuLink"
        data-toggle="dropdown"
        aria-haspopup="true"
        aria-expanded="false"
      >
        <small *ngIf="(selectedTenant$ | async)?.name as tenantName"
          ><i>{{ tenantName }}</i>\</small
        >
        <strong>{{ (currentUser$ | async)?.userName }}</strong>
      </a>
      <div
        class="dropdown-menu dropdown-menu-right border-0 shadow-sm"
        aria-labelledby="dropdownMenuLink"
        [class.d-block]="smallScreen && currentUserDropdown.isOpen()"
      >
        <a class="dropdown-item pointer" (click)="navigateToManageProfile()"
          ><i class="fa fa-cog mr-1"></i>{{ 'AbpAccount::MyAccount' |
          abpLocalization }}</a
        >
        <a class="dropdown-item" href="javascript:void(0)" (click)="logout()"
          ><i class="fa fa-power-off mr-1"></i>{{ 'AbpUi::Logout' |
          abpLocalization }}</a
        >
      </div>
    </div>
  </li>
</ul>

打开 src/app 文件夹中的 app.component.ts,并按如下所示进行修改:

import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // 导入 ReplaceableComponentsService
import { NavItemsComponent } from './nav-items/nav-items.component'; // 导入 NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // 导入 eThemeBasicComponents
//...

@Component(/* 组件元数据 */)
export class AppComponent implements OnInit {
  private replaceableComponents = inject(ReplaceableComponentsService);

  ngOnInit() {
    //...

    this.replaceableComponents.add({
      component: NavItemsComponent,
      key: eThemeBasicComponents.NavItems,
    });
  }
}

最终的 UI 如下所示:

新导航项

另请参阅

在本文档中