项目

本文档有多个版本。请选择最适合您的选项。

UI
Database

Web 应用开发教程 - 第 9 部分:作者:用户界面

简介

本部分介绍如何为前几部分引入的 Author 实体创建 CRUD 页面。

作者管理页面

在 angular 应用程序的根目录下运行以下命令行以创建一个名为 AuthorModule 的新模块:

yarn ng generate module author --module app --routing --route authors

此命令应产生以下输出:

> yarn ng generate module author --module app --routing --route authors

yarn run v1.19.1
$ ng generate module author --module app --routing --route authors
CREATE src/app/author/author-routing.module.ts (344 bytes)
CREATE src/app/author/author.module.ts (349 bytes)
CREATE src/app/author/author.component.html (21 bytes)
CREATE src/app/author/author.component.spec.ts (628 bytes)
CREATE src/app/author/author.component.ts (276 bytes)
CREATE src/app/author/author.component.scss (0 bytes)
UPDATE src/app/app-routing.module.ts (1396 bytes)
Done in 2.22s.

AuthorModule

打开 /src/app/author/author.module.ts 并按如下所示替换内容:

import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { AuthorRoutingModule } from './author-routing.module';
import { AuthorComponent } from './author.component';
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap';

@NgModule({
  declarations: [AuthorComponent],
  imports: [SharedModule, AuthorRoutingModule, NgbDatepickerModule],
})
export class AuthorModule {}
  • 添加了 SharedModuleSharedModule 导出了一些创建用户界面所需的常用模块。
  • SharedModule 已经导出了 CommonModule,因此我们移除了 CommonModule
  • 添加了 NgbDatepickerModule,稍后将在作者创建和编辑表单中使用。

菜单定义

打开 src/app/route.provider.ts 文件并添加以下菜单定义:

{
  path: '/authors',
  name: '::Menu:Authors',
  parentName: '::Menu:BookStore',
  layout: eLayoutType.application,
  requiredPolicy: 'BookStore.Authors',
}

打开 /src/app/route.provider.ts 并将 'BookStore.Books || BookStore.Authors' 添加到 /book-store 路由。/book-store 路由块应如下所示:

{
  path: '/book-store',
  name: '::Menu:BookStore',
  iconClass: 'fas fa-book',
  order: 2,
  layout: eLayoutType.application,
  requiredPolicy: 'BookStore.Books || BookStore.Authors',
},

最终的 configureRoutes 函数声明应如下所示:

function configureRoutes(routes: RoutesService) {
  return () => {
    routes.add([
      {
        path: '/',
        name: '::Menu:Home',
        iconClass: 'fas fa-home',
        order: 1,
        layout: eLayoutType.application,
      },
      {
        path: '/book-store',
        name: '::Menu:BookStore',
        iconClass: 'fas fa-book',
        order: 2,
        layout: eLayoutType.application,
        requiredPolicy: 'BookStore.Books || BookStore.Authors',
      },
      {
        path: '/books',
        name: '::Menu:Books',
        parentName: '::Menu:BookStore',
        layout: eLayoutType.application,
        requiredPolicy: 'BookStore.Books',
      },
      {
        path: '/authors',
        name: '::Menu:Authors',
        parentName: '::Menu:BookStore',
        layout: eLayoutType.application,
        requiredPolicy: 'BookStore.Authors',
      },
    ]);
  };
}

服务代理生成

ABP CLI 提供了 generate-proxy 命令,可为你的 HTTP API 生成客户端代理,以便轻松从客户端使用你的 HTTP API。在运行 generate-proxy 命令之前,你的宿主必须已启动并运行。

angular 文件夹中运行以下命令:

abp generate-proxy -t ng

此命令为作者服务及相关模型(DTO)类生成了服务代理:

bookstore-angular-service-proxy-author

AuthorComponent

打开 /src/app/author/author.component.ts 文件并按如下所示替换内容:

import { Component, OnInit, inject } from '@angular/core';
import { ListService, PagedResultDto } from '@abp/ng.core';
import { AuthorService, AuthorDto } from '@proxy/authors';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared';

@Component({
  selector: 'app-author',
  templateUrl: './author.component.html',
  styleUrls: ['./author.component.scss'],
  providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class AuthorComponent implements OnInit {
  author = { items: [], totalCount: 0 } as PagedResultDto<AuthorDto>;

  isModalOpen = false;

  form: FormGroup;

  selectedAuthor = {} as AuthorDto;

  public readonly list = inject(ListService);
  private readonly authorService = inject(AuthorService);
  private readonly fb = inject(FormBuilder);
  private readonly confirmation = inject(ConfirmationService);

  ngOnInit(): void {
    const authorStreamCreator = (query) => this.authorService.getList(query);

    this.list.hookToQuery(authorStreamCreator).subscribe((response) => {
      this.author = response;
    });
  }

  createAuthor() {
    this.selectedAuthor = {} as AuthorDto;
    this.buildForm();
    this.isModalOpen = true;
  }

  editAuthor(id: string) {
    this.authorService.get(id).subscribe((author) => {
      this.selectedAuthor = author;
      this.buildForm();
      this.isModalOpen = true;
    });
  }

  buildForm() {
    this.form = this.fb.group({
      name: [this.selectedAuthor.name || '', Validators.required],
      birthDate: [
        this.selectedAuthor.birthDate ? new Date(this.selectedAuthor.birthDate) : null,
        Validators.required,
      ],
      shortBio: [
        this.selectedAuthor.shortBio ? this.selectedAuthor.shortBio : null,
        Validators.required,
      ],
    });
  }

  save() {
    if (this.form.invalid) {
      return;
    }

    if (this.selectedAuthor.id) {
      this.authorService
        .update(this.selectedAuthor.id, this.form.value)
        .subscribe(() => {
          this.isModalOpen = false;
          this.form.reset();
          this.list.get();
        });
    } else {
      this.authorService.create(this.form.value).subscribe(() => {
        this.isModalOpen = false;
        this.form.reset();
        this.list.get();
      });
    }
  }

  delete(id: string) {
    this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure')
        .subscribe((status) => {
          if (status === Confirmation.Status.confirm) {
            this.authorService.delete(id).subscribe(() => this.list.get());
          }
     });
  }
}

打开 /src/app/author/author.component.html 并按如下所示替换内容:

<div class="card">
  <div class="card-header">
    <div class="row">
      <div class="col col-md-6">
        <h5 class="card-title">
          {{ '::Menu:Authors' | abpLocalization }}
        </h5>
      </div>
      <div class="text-end col col-md-6">
        <div class="text-lg-end pt-2">
          <button *abpPermission="'BookStore.Authors.Create'" id="create" class="btn btn-primary" type="button" (click)="createAuthor()">
            <i class="fa fa-plus me-1"></i>
            <span>{{ '::NewAuthor' | abpLocalization }}</span>
          </button>
        </div>
      </div>
    </div>
  </div>
  <div class="card-body">
    <ngx-datatable [rows]="author.items" [count]="author.totalCount" [list]="list" default>
      <ngx-datatable-column
        [name]="'::Actions' | abpLocalization"
        [maxWidth]="150"
        [sortable]="false"
      >
        <ng-template let-row="row" ngx-datatable-cell-template>
          <div ngbDropdown container="body" class="d-inline-block">
            <button
              class="btn btn-primary btn-sm dropdown-toggle"
              data-toggle="dropdown"
              aria-haspopup="true"
              ngbDropdownToggle
            >
              <i class="fa fa-cog me-1"></i>{{ '::Actions' | abpLocalization }}
            </button>
            <div ngbDropdownMenu>
              <button *abpPermission="'BookStore.Authors.Edit'" ngbDropdownItem (click)="editAuthor(row.id)">
                {{ '::Edit' | abpLocalization }}
              </button>
              <button *abpPermission="'BookStore.Authors.Delete'" ngbDropdownItem (click)="delete(row.id)">
                {{ '::Delete' | abpLocalization }}
              </button>
            </div>
          </div>
        </ng-template>
      </ngx-datatable-column>
      <ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
      <ngx-datatable-column [name]="'::BirthDate' | abpLocalization">
        <ng-template let-row="row" ngx-datatable-cell-template>
          {{ row.birthDate | date }}
        </ng-template>
      </ngx-datatable-column>
      <ngx-datatable-column [name]="'::ShortBio' | abpLocalization" prop="shortBio"></ngx-datatable-column>
    </ngx-datatable>
  </div>
</div>

<abp-modal [(visible)]="isModalOpen">
  <ng-template #abpHeader>
    <h3>{{ (selectedAuthor.id ? '::Edit' : '::NewAuthor') | abpLocalization }}</h3>
  </ng-template>

  <ng-template #abpBody>
    <form [formGroup]="form" (ngSubmit)="save()">
      <div class="form-group">
        <label for="author-name">Name</label><span> * </span>
        <input type="text" id="author-name" class="form-control" formControlName="name" autofocus />
      </div>

      <div class="mt-2">
        <label>Birth date</label><span> * </span>
        <input
          #datepicker="ngbDatepicker"
          class="form-control"
          name="datepicker"
          formControlName="birthDate"
          ngbDatepicker
          (click)="datepicker.toggle()"
        />
      </div>
      <div class="form-group">
        <label for="author-short-bio">{{ '::Short Bio' | abpLocalization }}</label><span> * </span>
        <textarea id="author-short-bio" class="form-control" formControlName="shortBio" rows="12"></textarea>
      </div>
    </form>
  </ng-template>

  <ng-template #abpFooter>
    <button type="button" class="btn btn-secondary" abpClose>
      {{ '::Close' | abpLocalization }}
    </button>

    <button class="btn btn-primary" (click)="save()" [disabled]="form.invalid">
      <i class="fa fa-check mr-1"></i>
      {{ '::Save' | abpLocalization }}
    </button>
  </ng-template>
</abp-modal>

本地化配置

此页面使用了一些我们需要声明的本地化键。打开 Acme.BookStore.Domain.Shared 项目 Localization/BookStore 文件夹下的 en.json 文件,并添加以下条目:

"Menu:Authors": "Authors",
"Authors": "Authors",
"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?",
"BirthDate": "Birth date",
"NewAuthor": "New author"

运行应用程序

运行并登录到应用程序。由于你还没有权限,所以看不到菜单项。 进入 identity/roles 页面,点击 *Actions* 按钮,并为 admin 角色选择 *Permissions* 操作:

bookstore-author-permissions

如你所见,admin 角色目前还没有 *Author Management* 权限。点击复选框并保存模态窗体以授予必要的权限。刷新页面后,你将在主菜单的 *书店* 下看到 *Authors* 菜单项:

bookstore-authors-page

就是这样!这是一个功能完整的 CRUD 页面,你可以创建、编辑和删除作者。

提示:在定义新权限后,如果运行 .DbMigrator 控制台应用程序,它会自动将这些新权限授予 admin 角色,你无需手动授予权限。


在本文档中