.net core3.1+angular+is4 项目记录:2 我的会议模块(上)
.net core3.1+angular+is4 项目记录:2 我的会议模块(上)
目录
- 页面搭建
- 路由配置
- 添加会议页面
- 我的会议页面
- 取消会议功能
页面搭建
首先搭建页面的主体,在 main 模块中的 MainComponent 下搭建页面结构,app 模块只放个
具体代码:
<nz-layout class="full-screen">
<nz-sider nzCollapsible [(nzCollapsed)]="isRetraction" [nzWidth] ="240">
<!-- 侧边栏 -->
<app-sider [isRetraction]="isRetraction"></app-sider>
</nz-sider>
<nz-layout>
<nz-content class="container">
<!-- 面包屑部分 -->
<div class="container_head">
<nz-breadcrumb [nzAutoGenerate]="true"></nz-breadcrumb>
</div>
<!-- 页面主体部分 -->
<div class="container_box">
<router-outlet></router-outlet>
</div>
<!-- 页脚部分 -->
<div class="container_footer">
Graduation Project ©2020 By 7
</div>
</nz-content>
</nz-layout>
</nz-layout>
路由配置
页面主体既然已经搭建完了,那么接下来该配置路由了,配置之前先看一下之前规划的模块结构:
通过图可以观察到,主要路由都放在 main.router 中。
app-routing 代码:
const routes: Routes = [
{
//token相关
path: "signin-oidc",
component: SignInComponent
},
{
//token相关
path: "refresh-oidc",
component: RefreshOidcComponent
},
{
path: "",
//添加认证守卫
canActivate: [AuthGuard],
loadChildren: "./main/ main.module#MainModule"
},
{
path: "**",
redirectTo: ""
}
];
添加了两个和 token 相关的路由,这里不做记录,如果忘了可以看:https://www.cnblogs.com/zyz-Notes/p/12097826.html。因为app.router一定是连接到main模块,所以为了缩短地址的长度使用""作为path的值。
main-routing 代码
const routes: Routes = [
{
path: "main",
component: MainComponent,
children: [
{
path: "Home",
loadChildren: "./home/ home.module#HomeModule",
data: {
breadcrumb: "首页"
}
},
{
path: "myMeeting",
loadChildren: "./my-meeting/ my-meeting. module#MyMeetingModule",
data: {
breadcrumb: "我的会议"
}
},
{
path: "myAudience",
loadChildren: "./my-audience/ my-audience. module#MyAudienceModule",
data: {
breadcrumb: "参加的会议"
}
},
{
path: "statistics",
loadChildren: "./statistics/ statistics. module#StatisticsModule",
data: {
breadcrumb: "统计"
}
},
{
path: "**",
redirectTo: "Home"
}
]
},
{
path: "**",
redirectTo: "main"
}
];
我选择路径以 main 开头。因为我们搭建的页面的主体在 MainComponent 中,所以对应组件为 MainComponent。我们通过配置 main 的子路由来激活 MainComponent 中的 router-outlet ,所以下面 4 条子路由分别对应着 4 个模块,最后一条作为路径错误时候的重定向。
my-meeting-routing 代码
const routes: Routes = [
{
path: "add",
component: AddMeetingComponent,
data: {
breadcrumb: "发起会议"
}
},
{
path: "modify/:meetingId",
component: ModifyMeetingComponent,
data: {
breadcrumb: "修改会议"
}
},
{
path: "details/:meetingId",
component: DetailsMeetingComponent,
data: {
breadcrumb: "会议详情"
}
},
{
//模块首页
path: "",
component: MyMeetingComponent
},
{
path: "**",
redirectTo: ""
}
];
各个组件的通过 cli 来进行生成。各个组件对应功能:
- AddMeetingComponent-添加会议
- ModifyMeetingComponent-修改会议
- DetailsMeetingComponent-会议详情
- MyMeetingComponent-会议列表和会议取消
添加会议页面
my-meetingComponent.html
因为我们默认路由为"",所以单击我的会议项,跳转到 MyMeetingComponent 组件中,所以我们需要在 MyMeetingComponent 添加一个按钮跳转到 AddMeetingComponent。
<button nz-button nzType="primary" routerLink="add" class="add_button">
发起会议
</button>
这个时候应该是没有图中的表格的,注意 routerLink="add"要和路由中的 path 对应起来
add-meeting.component.html
我们需要在这个页面上写一个表单,让用户来填写会议的信息。由于这个页面代码较多,所以我这里只展示一个 input 和一个日期选择器,其他的都只是改下名字。
<form
nz-form
[formGroup]="validateForm"
(ngSubmit)="submitForm($event, validateForm.value)"
>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzRequired>会议名称</ nz-form-label>
<nz-form-control [nzSpan]="12" nzHasFeedback>
<input
nz-input
formControlName="meetingName"
placeholder="请输入会议名称"
[(ngModel)]="meeting.meetingName"
/>
<nz-form-explain
*ngIf="
validateForm.get('meetingName')?.dirty &&
validateForm.get('meetingName')?.errors
"
>
<ng-container
*ngIf="validateForm.get('meetingName')? .hasError('required')"
>
会议名称不能为空
</ng-container>
</nz-form-explain>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzRequired>日期</ nz-form-label>
<nz-form-control [nzSpan]="12">
<nz-date-picker
formControlName="datePicker"
[(ngModel)]="datetime.date"
></nz-date-picker>
</nz-form-control>
</nz-form-item>
</form>
我们使用 nz-zorro 提供的表单样式配合 FormBuilder 类来进行表单验证。
FormBuilder 生成表单控件:
this.validateForm = this.fb.group({
meetingName: ["", [Validators.required]],
address: ["", [Validators.required]],
content: ["", [Validators.required]],
datePicker: ["", [Validators.required]],
timePicker: [null, [Validators.required]]
});
用到的 nz-zorro 组件,具体 API 请查阅官方文档。
nz-zorro 提供的组件来调整表单的样式。
nz-form:一个启用 nz 表单。
nz-form-item:一个表单组(包括文字,input,扩展信息)。 nz-form-label:左边的文字。nz-form-explain:扩展信息通常配合 控件状态显示不同的信息
笔记:
创建组和控件:
this.fb.group({
控件名:["","同步验证器","异步验证器"]
})
获取控件的状态:
是否被污染(用户有没有动过)
validateForm.get('meetingName')?.dirty
是否有错误
validateForm.get('meetingName')?.errors
是否有一个必须的错误
validateForm.get('meetingName')? .hasError('required')
是不是所有验证都通过了
!validateForm.valid
form 标签:
[formGroup]="组名:validateForm"
(ngSubmit)="提交事件:submitForm($event,validateForm.value)"
input 标签:
formControlName="控件名:meetingName"
[(ngModel)]="双向绑定对象:datetime.date"
最后结果应该是这样的:
效果图已经出来了,那么我们现在需要写逻辑了。写前先想一下我们需要做什么。
- 在提交按钮事件中调用一个服务将请求发出(需要提交按钮事件,需要一个服务)
- 后端把数据存进数据库(一个 Action 和一个保存数据的方法)
Meeting 与 MeetingService
//meeting对象
export class meeting {
public id: number;
public userId: string;
public userName: string;
public meetingName: string;
public address: string;
public content: string;
public dateTime: string;
public state: meetingStatsEnum;
public inviteCode: string;
}
//创建一个会议
public addMeeting(meetingDto: meeting) :Observable<meeting> {
return this.httpclient.post<meeting>("/api/Meeting",meetingDto);
}
提交事件
//提交按钮
submitForm = () => {
//整合数据
this.compose();
//发送
this.meetingServer.addMeeting(this.meeting).subscribe(x => {
console.log(x);
this.createMessage("success", "会议创建成功!");
this.router.navigate(["/main/myMeeting"]);
});
//重置控件状态
this.dirtyForm();
};
//组合post的对象
public compose(): void {
this.meeting.dateTime =
this.datetimeService.getDate(this.datetime.date) +
" " +
this.datetimeService.getHour(this.datetime.time);
this.meeting.state = meetingStatsEnum.NotStarted;
this.meeting.userId = this.oidc.user.profile.sub;
}
//重置控件状态
private dirtyForm(): void {
for (const key in this.validateForm.controls) {
this.validateForm.controls[key].markAsDirty();
this.validateForm.controls[key].updateValueAndValidity();
}
}
我们需要在 API 中进行接收,这个就比较简单了。
dto 与 model:
public class MeetingOutputDto
{
public int Id { get; set; }
public string UserId { get; set; }
public string UserName { get; set; }
public string MeetingName { get; set; }
public string Address { get; set; }
public string Content { get; set; }
public MeetingStatsEnum State { get; set; }
public string DateTime { get; set; }
public string InviteCode { get; set; }
}
public class MeetingInputDto {
public int Id { get; set; }
public string UserId { get; set; }
public string MeetingName { get; set; }
public string Address { get; set; }
public string Content { get; set; }
public MeetingStatsEnum State { get; set; }
public string DateTime { get; set; }
}
public class Meeting
{
public int Id { get; set; }
public string UserId { get; set; }
public string MeetingName { get; set; }
public string Address { get; set; }
public string Content { get; set; }
public DateTime DateTime { get; set; }
public MeetingStatsEnum State { get; set; }
public string InviteCode { get; set; }
public List<User_Meeting> User_Meetings { get; set; }
}
Action
[HttpPost]
public async Task<IActionResult> Post([FromBody]MeetingInputDto meetingAddDto)
{
_logger.LogDebug("创建会议");
var meeting= _mapper.Map<MeetingInputDto, Meeting(meetingAddDto);
_meetingRepository.addMeeting(meeting);
if (! await _unitOfWork.save()) {
_logger.LogError("创建会议失败!");
throw new Exception();
}
_logger.LogDebug("会议创建成功");
//创建邀请码
await _inviteCodeHelper.create(meeting);
var resultDto = _mapper.Map<Meeting, MeetingOutputDto(meeting);
return Created("location:5000/api/Meeting/"+resultDto.Id, resultDto);
}
public void addMeeting(Meeting meeting) {
_dbContext.Meeting.Add(meeting);
}
public async Task<bool> save() {
return await dbContext.SaveChangesAsync() > 0;
}
写好 automapper 映射关系。这里要注意,我这里写的邀请码就是把会议报名的 url 进行加密,具体加密过程参考:https://www.cnblogs.com/tianma3798/p/8807816.html。其实写到这里的时候我发现在写的过程中大部分时间都在写angular(应该是我太菜了)。。还有一些是在改设计错误。。真正API使用的时间不多。到这里添加功能就完成了,源码地址放在文章结尾。
我的会议页面
这个页面比较简单,使用 nz-zorro 的 table 组件。先想想需要做什么:
- 前端 页面加载请求会议列表(需要一个请求列表方法,展示到页面上)
- 前端 单击按钮取消会议(需要取消会议按钮和方法,成功后显示提示信息)
- 后端 返回列表方法(get)更新会议状态(patch)
获取用户的会议列表
//页面加载时请求数据
//ts文件中
ngOnInit() {
this.meetingService.getMeetings().subscribe(x => {
this.meetings = x;
console.log(this.meetings);
});
}
//查询该用户下的所有会议
//meetingService中
public getMeetings(): Observable<meeting[]> {
return this.httpclient.get<meeting[]>(`/api/Meeting`, {
params: { userId: this.oidc.user.profile.sub }
});
}
//展示数据
//html中
<nz-table #basicTable [nzData]="meetings">
<thead>
<tr>
<th>名称</th>
<th>会议日期</th>
<th>地址</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of basicTable.data; let i = index">
<td>{{ data.meetingName }}</td>
<td>{{ data.dateTime }}</td>
<td>{{ data.address }}</td>
</tr>
</tbody>
</nz-table>
返回列表 Action
[HttpGet]
public async Task<IActionResult> Get([FromQuery]stringuserId) {
var list= await _meetingRepository.getMeetings(userId);
var result= _mapper.Map<IEnumerable<Meeting>IEnumerable<MeetingOutputDto>>(list);
return Ok(result);
}
public async Task<IEnumerable<Meeting>> getMeetings(string userId)
{
return await _dbContext.Meeting.Wher(x=>x.UserId==userId).ToListAsync();
}
这个页面比较简单,没什么好记的,看取消会议功能。
取消会议按钮
//分别添加详情、修改、取消按钮
//html中
<td>
<a [routerLink]="['details', data.id]">详情</a>
<nz-divider
nzType="vertical"
*ngIf="data.state == meetingStats.NotStarted"
></nz-divider>
<a
*ngIf="data.state == meetingStats.NotStarted"
[routerLink]="['modify', data.id]"
>修改</a
>
<nz-divider
nzType="vertical"
*ngIf="data.state == meetingStats.NotStarted"
></nz-divider>
<a
*ngIf="data.state == meetingStats.NotStarted"
nz-popconfirm
//使用nz-zorro添加一个取人框
nzTitle="确定取消这个会议?"
nzPopconfirmPlacement="top"
(nzOnConfirm)="cancelMeeting(data.id, i)"
>取消</a
>
</td>
取消会议事件
//按钮单击事件
//ts中
public cancelMeeting(meetingId: number): void {
this.meetingService
.updateMeetingsState(meetingId, [
{ op: "replace", path: "/State", value: "已完成" }
])
.subscribe();
}
//局部更新会议
//service中
ublic updateMeetingsState(meetingId: number, body: any): bservable<{}> {
return this.httpclient.patch(`/api/Meeting/${meetingId}`, body, {
headers: new HttpHeaders({
"Content-type": "application/json"
})
});
修改状态 Action
[HttpPatch("{meetingId}")]
public async Task<IActionResult> Patch([FromRoute]intmeetingId, [FromBody]JsonPatchDocument<MeetingInputDto> meetingDoc)
{
//根据id查询出会议
var entity = await _meetingRepository.getMeetin(meetingId);
if (entity == null)
return NotFound("未找到该会议");
//将会议传转成dto
var meetingdto= _mapper.Map<Meeting,MeetingInputDto>(entity);
//修补操作到dto
meetingDoc.ApplyTo(meetingdto, ModelState);
//重新验证模型状态
TryValidateModel(meetingdto);
if (!ModelState.IsValid) {
return BadRequest("传送数据错误");
}
//转换回module
_mapper.Map(meetingdto,entity);
//更新操作
_meetingRepository.UpdateMeeting(entity);
//验证更新结果
if (!await _unitOfWork.save())
{
throw new Exception("更新失败!");
}
return NoContent();
}
这里需要注意:我们使用 JsonPatchDocument<修补的对象>这个类,这个类针对于修补对象进行修补操作,具体形式为:
[{ op: "操作符", path: "/对象名", value: "修改后的值" },{...}]
操作符一共由五种:Add(增加)、Move(移动)、Replace(替换)、Remove(删除)、Copy(复制)。只需要使用 meetingDoc.ApplyTo(meetingdto, ModelState) 就可以将具体改动操作到类中。
特别注意:使用JsonPatchDocument需要安装Microsoft.VisualStudio.Web.CodeGeneration.Design这个包,不然接收不到数据
总结
虽然有点水,但是写的手腕疼。源码地址:https://github.com/netLearner7/graduation。