Abp vNext - 应用开发系列之用户界面

在系列教程中,我们会构建一个基于ABP的Web应用程序,用于管理书籍及其作者列表

使用到的技术:

  • Entity Framework Core
  • Angular

本教程分为以下部分:

Author管理

angular目录下,使用以下命令创建新的模块

1
2
3
4
5
6
7
8
9
10
11
➜ yarn ng generate module author --module app --routing --route authors
yarn run v1.22.19
$ ng generate module author --module app --routing --route authors
CREATE src/app/author/author-routing.module.ts (354 bytes)
CREATE src/app/author/author.module.ts (372 bytes)
CREATE src/app/author/author.component.html (22 bytes)
CREATE src/app/author/author.component.spec.ts (624 bytes)
CREATE src/app/author/author.component.ts (210 bytes)
CREATE src/app/author/author.component.scss (0 bytes)
UPDATE src/app/app-routing.module.ts (2284 bytes)
Done in 1.21s.

AuthorModule

打开/src/app/author/author.module.ts,新增NgbDatepickerModule 导入:

1
2
3
4
5
6
7
8
9
10
11
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 {}

添加菜单

src/app/route.provider.ts 中添加菜单:

1
2
3
4
5
6
7
{
path: '/authors',
name: '::Menu:Authors',
parentName: '::Menu:BookStore',
layout: eLayoutType.application,
requiredPolicy: 'BookStore.Authors',
}

调整/book-store菜单权限,新增作者权限

1
2
3
4
5
6
7
8
{
path: '/book-store',
name: '::Menu:BookStore',
iconClass: 'fas fa-book',
order: 2,
layout: eLayoutType.application,
requiredPolicy: 'BookStore.Books || BookStore.Authors',
},

完整菜单配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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',
},
]);
};
}

生成代理类

ng目录下执行

1
abp generate-proxy -t ng

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜ abp generate-proxy -t ng
ABP CLI 8.1.4
A newer stable version of the ABP CLI is available: 8.2.0.
Update Command:
dotnet tool update -g Volo.Abp.Cli
DELETE src/app/proxy/books
CREATE src/app/proxy/authors/author.service.ts (1731 bytes)
CREATE src/app/proxy/authors/models.ts (509 bytes)
CREATE src/app/proxy/authors/index.ts (60 bytes)
UPDATE src/app/proxy/generate-proxy.json (992802 bytes)
UPDATE src/app/proxy/README.md (1000 bytes)
UPDATE src/app/proxy/books/book.service.ts (1715 bytes)
UPDATE src/app/proxy/books/models.ts (375 bytes)
UPDATE src/app/proxy/books/book-type.enum.ts (299 bytes)
UPDATE src/app/proxy/books/index.ts (92 bytes)
UPDATE src/app/proxy/index.ts (99 bytes)

service proxy

AuthorComponent

打开/src/app/author/author.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { Component, OnInit } 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;

constructor(
public readonly list: ListService,
private authorService: AuthorService,
private fb: FormBuilder,
private confirmation: 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 || '']
});
}

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 {
debugger;
var input = this.form.value
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<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>
</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-shortBio">ShortBio</label>
<input type="text" id="author-shortBio" class="form-control" formControlName="shortBio" />
</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 的文件,新增以下键值:

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

运行

image-20240726160527937

如果在定义新权限后运行 .DbMigrator 控制台应用程序,会自动将这些新权限授予管理员角色,不需要在自己手动授予权限

下一节

教程下一节