ABP故障诊断系统开发文档
本系列旨在以实验室大屏智能监测系统为原型,将开发中使用的技术归纳梳理,记录从头开发类似项目的过程。原型项目实际开发过程中存在很多试错勉强能用的代码,在本次开发过程中尽可能考虑周全,在了解各技术原理的基础上选择合适的方式开发
本系列主要分成前端与后端两个部分。前端技术栈主要包含:Angular (opens new window),NG-ZORRO (opens new window),ECharts (opens new window),fabric (opens new window),signalR。后端技术栈主要包含:cs,Python,后端使用的框架包括ASP.Net Core (opens new window),ABP,IdentityServer (opens new window),部署技术使用Docker (opens new window),操作系统包括Windows和Linux,后端开发主要框架为ABP (opens new window)。同时本系列预计还包括系统开发前的功能梳理、UI设计以及系统开发后的打包上线部分。
本系列旨在以实验室大屏智能监测系统为原型,将开发中使用的技术归纳梳理,记录从头开发类似项目的过程。原型项目实际开发过程中存在很多试错勉强能用的代码,在本次开发过程中尽可能考虑周全,在了解各技术原理的基础上选择合适的方式开发
本系列主要分成前端与后端两个部分。前端技术栈主要包含:Angular (opens new window),NG-ZORRO (opens new window),ECharts (opens new window),fabric (opens new window),signalR。后端技术栈主要包含:C#,Python,后端使用的框架包括ASP.Net Core (opens new window),ABP,IdentityServer (opens new window),部署技术使用Docker (opens new window),操作系统包括Windows和Linux,后端开发主要框架为ABP (opens new window)。同时本系列预计还包括系统开发前的功能梳理、UI设计以及系统开发后的打包上线部分。
# 1 项目基础内容
# 1.1 启动项目
# 1.1.1 项目模板下载
dotnet tool install -g Volo.Abp.Cli
下载abp框架,需要dotnet。这部分下载可以使用蒋师兄的装机代码从chocolatey上下载abp new 项目名称 -u angular -dbms MySQL -csf
建立一个包含abp内容的前端模板,以及使用MySQL为数据库的后端模板
下载完毕后会生成两个文件夹,angular是前端文件夹,aspnet-core是后端文件夹
# 1.1.2 项目配置修改
docker run --name 容器名 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=数据库密码 -d mysql:latest
通过docker启动数据库,其中3306:3306前一个是本机访问mysql的端口,后一个是docker镜像内部端口,不建议改- 修改后端中数据库连接配置
在 HTTPApi.Host文件夹里的appsetting.json修改以下内容
"ConnectionStrings": {
"Default": "Server=localhost;Port=3306;Database=数据库名称;Uid=root;Pwd=数据库密码;"
},
2
3
在DbMigrator里的appsetting.json数据库修改以下内容
"ConnectionStrings": {
"Default": "Server=localhost;Port=3306;Database=数据库名称;Uid=root;Pwd=数据库密码;"
},
2
3
cd Dashboard.DbMigrator
然后执行dotnet run
迁移数据库
- 启动HTTPApi.Host就可以开启Swagger,证明后端已开启
- 进去前端文件夹
yarn
从package.json中下载依赖项 yarn satrt
开启项目,在本机4200端口就可以看到启动的前端网页
如果你和我一样有其他的angular项目,就需要修改一下端口。因为Angular项目的默认端口都是4200,会存在冲突问题。
- 在前端
src/enviroments
里的两个ts文件做同样的修改
const baseUrl = 'http://localhost:你希望的端口号';
在package.json文件中修改
"start": "ng serve --open --port 你希望的端口号",
保存之后你会发现你的前端报错了,这是因为后端写的端口号是原先默认的4200,所有我们需要对后端做同样的处理。找到之前修改过的两个appsetting.json文件,将其中的4200改成你修改后的端口号,重新启动项目。前端网页刷新后又会显示原先的页面,证明你已经将端口号修改好了
# 1.2 项目目录介绍
# 1.2.1 前端项目目录介绍
本人对webpack打包还有前端自动化测试方面的内容一知半解,所以这里只介绍一些与内容直接相关的文件
- node_modules
是你yarn之后下载下来的依赖项,开发过程中可以不用管,你对这个文件夹的修改一般只会来自npm包下载和卸载,不要手动去修改里面的内容
src:前端开发的内容都在里面
app:前端开发的组件,服务等实际的页面内容和页面逻辑
home:abp帮你写好的首页组件
shared:一般多个组件共用的东西放在里面,比如之后会用到的服务和UI库的自定义主题
app的三个文件:app的路由设置,组件依赖注入和app这个挂载自身ts
route.provider.ts:abp提供的导航栏中的路由设置
assets:静态文件存储的地方,一般放一些项目所需的图片、json文件
environments:项目的环境配置,可以在这里设置项目所用到的所有后端url以及自身的输入端口。其中prod是打包后使用的环境配置,另一个是开发阶段使用的环境配置
index.html:整个项目的入口文件,主要用来修改网页head的内容
main.ts:整个项目的ts文件,基本没改过
styles.scss:项目的总scss文件
package.json:其他的文件基本都是angular自己生成的,不会做修改。比较重要的就是这个文件,你所有的npm包操作都会反应到这个文件里,这里会记载该项目的依赖项,以及你项目开启,打包的命令。
# 1.2.2 后端项目目录介绍
后端项目大方向分为两个部分,一部分为C#语言包含的后端逻辑功能,一部分为Python语言包含的数据诊断与发送功能
# C#
首先是权限管理后端,其项目文件目录结构如下所示:
其中src为源文件文件夹,test为功能测试文件夹。使用Rider打开*.sln项目,目录结构如下所示:
其中:
- Application:后端逻辑实现,实体间映射目录
- Contracts:接口定义以及权限定义目录
- Domain:实体定义、权限种子数据、密码格式设置目录
- Shared:本地化配置文件目录
- EFCore:后端与MySQL数据表联系建立
- DbMigrations:EF数据库管理迁移目录
- Host:项目模块配置以及项目启动项
# 1.3 项目需求分析
# 1.3.1 前端项目需求
- 登录、注册界面
- 通过signalR获取数据,进行阈值判断,并显示状态信息
- 切换数据源
- 通过监视的状态信息,直接反馈到二维平面图上
- 系统基础的登录、注销、权限管理
- 生成报告单,并提供打印功能
- 通过二维平面图获取单个数据的长期信息和诊断信息
- 系统基础的查看数据库,下载数据库信息
- 提供设置算法参数,并绘制算法结果图接口
- 界面平面图与报警信息的前端修改
# 1.3.2 后端项目需求
- 用户登录
- 实时数据
- 配置文件
- 算法接入
- 检测报告输出
# 1.4 UI设计
不是专业做UI设计的,但一般做前端开发应该先在脑子里有大致的页面分配,主要页面布局,每个功能在哪个部分实现。这里为了全面展示一个系统的开发流程,就做了比较细致的UI设计。
# 1.4.1 页面分配
首先根据需求确定页面的个数,这里因为有原型系统,所以这里很确定一共是4个界面:登录和注册、主监视界面、检测报告界面和统计分析界面。还有一个权限管理界面,主要是应用abp框架自带的。
# 风格确定
页面首先应该确定颜色风格,可以先在dribbble上搜索相关的设计方案,比如我这里就搜索了dashboard,这里参考了下面这个
# 具体页面参考
像比较具体且通用的页面,如登录等可以在b站上搜索一下例子。一是可以快速了解流行的页面样式;二是这种一般是纯HTML和CSS写成的,各种框架都可以应用;三是这种一般会带着你写,让你更容易理解和修改代码。
我这里参考了
这两个登录界面,我主要关注的前端样式up有总监日记、山羊の前端小窝、阿阳热爱前端,还有一个设计类的up主子牧说。
# 页面设计
我这里使用的figma,主要考虑到它可以直接导出css样式,可以有效减少前端的工作。另外就是可以多人协作,发送连接给别人就可以共享,比较方便。figma的操作还是比较简单的,有些基础的原理可以参考原先邓师兄的PS教程。如果需要可以看一下UI教程 | 手把手教你用Figma做UI界面设计_哔哩哔哩_bilibili (opens new window),基本你需要什么到里面找就可以了
最后的设计方案
![登录界面](https://pic.imgdb.cn/item/61bf258a2ab3f51d91a1c27a.jpg
设计的连接在这里Dashboard – Figma (opens new window)
# 主题颜色
背景色#F2F7FF
主题色#9590C7
装饰色 #C4D3F9 #51459E #F2F7FF
状态色 #84E8F4 #FCBE30 #41B674 文字色 #000000 #9CA0AB #FFFFFF
本篇文章内容全部是前端开发内容,主要包括监测主界面生成、项目初始化开发与登录界面的开发。
主要的技术点有:Angular新页面生成、ui库依赖使用、响应式布局基础、Angular组件、登录界面CSS、修改ui库样式、Angular表单、Angular http请求、Angular/ABP用户登录、Angular服务
因为系列是前端开发的第一篇文章,命令行与CSS都会介绍得比较详细,后续文章没有很特殊的不会再做介绍
# 2 Angular页面开发基础
# 2.1 监测主界面生成
介绍如何一步步在Angular中生成一个新页面,了解Angular的依赖注入
# 2.1.1 Angular页面包含内容
参考home文件夹,可以发现angular一个页面需要至少有5个文件,其中有component存在的html、scss、ts属于home组件的内容。home.module.ts是保存home页面使用的组件的文件。home-routing.module.ts是app通过路由确定到home页面的文件。
# 2.1.2Angular Cli生成命令
# 生成dashboard.module.ts文件
我们直接在醒目的路径下使用ng generate module dashboard
,就会生成一个叫dashboard的文件夹,里面包含一个dashboard.module.ts文件。使用命令生成的module会自动生成一些依赖内容。
# 生成dashboard.component的三个文件
使用cd src\app\dashboard
命令,转到对应的dashboard文件夹,再使用ng generate component dashboardLayout
生成一个dashboardLayout组件。这里为了方便理解使用了不同的组件名称,为了方便一个页面多组件之后查看我也一般会把一个页面的根组件命名为Layout
# 生成dashboard-routing.module.ts文件
在dashboard目录下,使用ng generate module dashboardRouting
生成dashboard-routing.module.ts,可以把其从文件夹中取出来
# 2.1.3 angular各文件之间的依赖、引入、声明补充
# dashboard.module.ts
将dashboard-routing.module引入
import { DashboardRoutingModule } from './dashboard-routing.module';
@NgModule({
declarations: [
DashboardLayoutComponent
],
imports: [
CommonModule,
DashboardRoutingModule,
]
})
2
3
4
5
6
7
8
9
10
11
这里的DashboardLayoutComponent是生成的时候会自动补充的
# dashboard-routing.module.ts
按照home-routing.module.ts修改
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DashboardLayoutComponent } from './dashboard-layout/dashboard-layout.component';
const routes: Routes = [{ path: '', component: DashboardLayoutComponent }];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class DashboardRoutingModule { }
2
3
4
5
6
7
8
9
10
11
# app.module.ts
将dashboard import进来
@NgModule({
imports: [
此处省略
DashboardModule,
],
此处省略
})
2
3
4
5
6
7
# app-routing.module.ts
添加app路由
{
path: 'dashboard',
pathMatch: 'full',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
},
2
3
4
5
完成以上步骤就可以通过理由跳转到新的页面了
# route.provider.ts
这里添加主页上的跳转按钮,修改初始样式。这里为了方便后续布局使用的是空样式
routesService.add([
{
path: '/dashboard',
name: '::监视主屏',
iconClass: 'fas fa-home',
order: 1,
layout: eLayoutType.empty
},
]);
2
3
4
5
6
7
8
9
# 2.2 页面尺寸响应式基础
这种监测大屏不存在响应式适配手机的需求,只需要适配不同分辨率的显示屏以及pad。所以这里没有考虑手机的适配,基本按照16:9等比例缩放。以后可以做手机适配的优化
在这样的前提下,项目使用rem对宽高、字体大小进行限值,通过设备窗口对项目HTML的font-size进行调节即可。
我们在main.ts里添加如下内容即可
function remSize() {
const deviceWidth = document.body.clientWidth
// 在1920px宽的设计稿里10px=1rem
document.documentElement.style.fontSize = (deviceWidth / 192) + 'px'
}
//已启动项目就执行remSize
remSize()
//项目窗口变化就执行remSize
window.onresize = () => {
remSize()
}
2
3
4
5
6
7
8
9
10
11
在dashboard-layout.component.html里验证响应式,
<div style="font-size: 3rem;height: 96rem;width: 96rem;background-color: aqua;">
新页面测试代码
</div>
2
3
该方块会一直是一个占据半边宽度的正方形
# 2.3 引入ui库
原本是准备使用Vue、React都可以使用的Element ui的,但其适配Angular的版本是8以前的,目前Angular已经更新到12了。所以使用了官方推荐且适配较好的NG-ZORRO (opens new window)。
各类ui库都会提供大量的交互组件,避免你从头开发。在开发具体页面前建议先看一下使用的ui库都提供了哪些组件,组件效果是什么样的,这样可以在你拿到页面需求的时候能快速反应有没有现成的组件可用。
# 2.3.1 安装NG-ZORRO
yarn add ng-zorro-antd
ng add ng-zorro-antd
使用上述命令后会自动配置一些需要的内容
# 2.3.2 全局配置
在angular.json中添加"src/theme.less"
"styles": [
"src/theme.less",
此后省略
2
3
# 2.3.3 模块配置
在全局配置的NG-ZORRO无法直接使用,还是需要在对应的模块进行配置。这里为了检验NG-ZORRO引入成功在dashboard.module.ts文件中引入NzButtonModule
import { NzButtonModule } from 'ng-zorro-antd/button';
@NgModule({
declarations: [
DashboardLayoutComponent
],
imports: [
CommonModule,
DashboardRoutingModule,
NzButtonModule
]
})
2
3
4
5
6
7
8
9
10
11
在dashboard-layout.component.html里测试NG-ZORRO的使用
<button nz-button nzType="primary">Primary Button</button>
这里截的图是修改主题色之后的,原本应该是蓝色。按钮自带颜色,有点击动效证明ng-zorro的NzButtonModule可以使用了
# 2.3.4 自定义主题
在theme.less文件中可以自定义主题
@import "../node_modules/ng-zorro-antd/ng-zorro-antd.less";
@primary-color: #9590C7;
@info-color:#9CA0AB;
@body-background:#F2F7FF;
@error-color:#FC5B5D;
@warning-color:#FCBE30;
@processing-color:#84E8F4;
@success-color:#41B674;
@normal-color:#51459E;
@text-color:#9CA0AB;
2
3
4
5
6
7
8
9
10
11
这里根据ui设计定义了一下颜色,后续可能还会根据需要进行调整。像vue比较常用的element ui是可以可视化定义主题的,会比ng-zorro要更好用一些。
这个网址 (opens new window)可以看到所有可以自定义的内容
# 3 登录页面开发
# 3.1 登录页面布局开发
# 3.1.1 登录页面模块划分
根据业务逻辑将登录页面划分成了四个部分,1是登录的表单,2是注册的表单,3是用来遮盖的翻转图片,4是点击后浮动到页面上的重置密码弹窗。于是将登录页面划分成了四个组件:reset-modal负责重置密码弹窗、login-form负责登录表单、sign-up-form负责注册表单、loginlayout是登录页面的根,负责整体布局和遮盖翻转图片逻辑
# 3.1.2 登录页面生成
按照前文主页面生成的方法生成登录页面,一并生成登录表单与注册表单组件
# 3.1.3 loginlayout组件开发
该组件要实现的功能是登录页面整体布局:登录框水平垂直居中,图片翻转遮盖
# 整体布局
页面通过.backgroud的div实现设计图背景颜色和后续子元素.container div页面水平垂直居中
.background {
//flex布局,子元素水平垂直居中
display: flex;
align-items: center;
justify-content: center;
//背景颜色
background-color: #C4D3F9;
//保证页面大小占满屏幕
width: 100%;
height: 100%;
}
.container {
//登录页面核心部分颜色,大小,阴影
background-color: #F2F7FF;
width: 115rem;
height: 60rem;
box-shadow: 0.5rem 1rem 1rem 0.2rem rgba(0, 0, 0, 0.3);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
通过.cover的div放置遮挡用的图片,以及上面的文字。与.container组成子绝父相
.container {
position: relative;
}
.container .cover{
//确定图片初始位置
position: absolute;
top: 0;
left: 0;
width: 80rem;
height: 100%;
z-index: 98;
//图片层样式,颜色后续可以删掉
background-color: aquamarine;
filter: drop-shadow(0.5rem 0px 1rem rgba(0, 0, 0, 0.25));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
通过.forms的div嵌套两个div,分别嵌入两个组件,将用于翻转图片的内容放在组件外面。这里设置background-color是为了在初步构建代码时直观看到各个盒子的位置
.forms .signupform{
position: absolute;
top: 0;
left: 0;
width: 35rem;
height: 100%;
background-color: blueviolet;
}
.forms .loginform{
position: absolute;
top: 0;
left: 80rem;
width: 35rem;
height: 100%;
background-color: blueviolet;
}
.forms .signupform, .forms .loginform {
//flex布局,子元素水平垂直居中
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 图片翻转
图片翻转是通过一个不在页面上显示的#flip的checkbox完成的
.cover整体翻转。.container #flip:checked是选择#flip这个元素被选中;~ 选择前面选择元素的后续兄弟元素;这样再.cover就是#flip这个元素被选中时.cover元素要执行的CSS样式了
.container {
//元素距离视图的距离,翻转时z轴最高高度
perspective: 280rem;
}
.container .cover{
//动画样式相关设置
transition: all 0.8s ease;
//翻转轴位置
transform-origin: 71.875%;
transform-style: preserve-3d;
}
.container #flip:checked ~ .cover{
transform: rotateY(180deg);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
但这样直接翻转会导致,图片和上面的文字翻转后颠倒。所以要把.cover分成两个面,分别显示。本来应该使用backface-visibility: hidden;
来隐藏背面,但不知为何我的浏览器显示有这个样式,但没有这个效果。所以只能退而求其次,使背面元素display:none
不渲染
//正面设置
.container .cover .front {
width: 100%;
height: 100%;
background-image: url(~src/assets/login.jpg);
background-size: 100% 100%;
background-repeat:no-repeat;
}
.container #flip:checked ~ .cover .front {
display:none
}
//背面设置
.container .cover .back{
width: 100%;
height: 100%;
background-image: url(~src/assets/login.jpg);
background-size: 100% 100%;
background-repeat:no-repeat;
transform: rotateY(180deg);
display:none
}
.container #flip:checked ~ .cover .back {
display:flex
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
还有一些文字样式就不一一介绍了
# 3.2.4 login-form组件开发
表单组件在NG-ZZORO有相应的组件了,使用和前文一样的方法把NzFormModule配置进来。但为了更好的自定义样式,没有引入NzInputModule和NzButtonModule,而是自己写的样式
# 表单
在对应的module导入import { FormsModule, ReactiveFormsModule } from '@angular/forms'
;
对应ts中引入import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
并定义表单
loginForm = new FormGroup({
userName: new FormControl('', [Validators.required]),
password: new FormControl('', [Validators.required]),
})
2
3
4
<form nz-form [formGroup]="loginForm">
<nz-form-item>
<nz-form-control nzErrorTip="用户名不能为空">
<input type="text" formControlName="userName" placeholder="用户名" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control nzErrorTip="密码至少6位且包含小写字母与数字">
<input type="password" formControlName="password" </nz-form-control>
</nz-form-item>
<button>登录</button>
</form>
2
3
4
5
6
7
8
9
10
11
12
13
14
[formGroup]设置整个表单名称,在ts需要定义这个名称,后续也用这个名称接收整个表单数据
formControlName设置单个输入对应的键,实现双向绑定
# 修改ui库样式
除去在自定义主题里修改ui库里的样式,我们遇到具体的ui库样式不合要求时可以直接不配置ui库,比如下面的input,也可以使用ng-deep修改,比如下面的.ant-form-item-explain.ant-form-item-explain-error。
input{
width: 100%;
outline: none;
border: 0;
border-bottom: 0.25rem solid rgba(81, 69, 158, 0.58);
background-color: transparent;
}
:host ::ng-deep .ant-form-item-explain.ant-form-item-explain-error{
font-size: 1.2rem;
color: #F9896B;
}
2
3
4
5
6
7
8
9
10
11
遇到引入ui库导致的样式不合适时,使用F12,查看是哪个部分的样式在起作用,用浏览器更改后有效果,就可以复制其css选择器,在前面添上
深度选择器
:host ::ng-deep就可以在本文件作用域下修改ui组件库样式,添上**::ng-deep**就会使修改的样式全局生效
# 伪类选择器添加图形
在登录两个字下面添加了一个小方块儿,使用伪类选择器也是常规操作了
.name::before{
content: "";
position: absolute;
left: 0;
bottom: 0rem;
height: 0.5rem;
width: 3rem;
background-color: #B1ABED;
}
2
3
4
5
6
7
8
9
# 3.2.5 sign-up-form组件
这个组件内容与login-form组件基本一致,就是添加了一个email输入。后续考虑是否将两个合并到一个里面去,使用传参来动态修改组件内容
# 3.2.6 reset-modal组件
由于这个组件的点击文字分配在了登录按钮之上,所以把该组件作为login-form组件的子组件开发。组件内容与sign-up-form组件一致,其部分样式与在login-form打开弹窗里定义,在介绍业务逻辑时进行纤细说明。
# 3.2 登录页面业务逻辑
# 3.2.1 打开重置密码弹窗
这部分使用NG-ZORRO提供的服务,将弹窗里的内容抽象成一个组件打开
首先要在拥有打开弹窗逻辑的组件里引入依赖import { NzModalService } from 'ng-zorro-antd/modal';
然后在对应打开弹窗的地方绑定事件逻辑,并编写处理的逻辑
resetModal() {
const modal = this.modal.create({
//定义弹窗标题
nzTitle: '找回密码',
//定义弹窗主体的组件
nzContent: ResetModalComponent,
//定义弹窗尾部
nzFooter: [
//定义取消按钮,点击后摧毁弹窗
{
label: '取消',
onClick: () => modal.destroy()
},
//定义确认按钮,点击确认后执行组件里的reset方法
{
label: '确认',
type: 'primary',
loading: false,
onClick(component): void {
this.loading = true;// 让提交按钮显示加载动画,防止重复提交
setTimeout(() => {
this.loading = false
}, 3000)
component.reset();
}
},
],
nzWidth: 40 * this.rem
});
}
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
# 3.2.2 确定表单内容
这个方面需要和后端进行协同,首先要弄清楚登录、注册、找回密码都需要向后端传输哪些信息。这些信息里哪些是用户输入的,这一部分需要在界面上提供输入;哪些是系统自动传输的,这一部分需要自己在前端代码中定义。而且沟通的时候也要确定好数据的类型和要求,方便后续进行表单验证。
# 3.2.3 表单验证逻辑
# angular自带的表单验证
angular自带的表单验证可以验证常规的邮箱、电话号码和正则表达式,在创建表单组的时候定义其需要验证的逻辑即可。以注册表单为例,实例化FormControl对象时传入的第一个参数是这个量的初始值,第二个参数是采取的表单验证逻辑。Validators.required是这个量不能为空,Validators.email是这个量格式为邮箱,Validators.pattern则是这个量要满足后面的正则表达式
signupForm = new FormGroup({
userName: new FormControl(null, [Validators.required]),
email: new FormControl(null, [Validators.required, Validators.email]),
password: new FormControl(null, [Validators.required, Validators.pattern(/^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,10}$/)]),
})
2
3
4
5
# 自定义的表单验证
业务上存在angular自带的表单验证无法覆盖的内容,比如重置密码的时候第二次输入的密码应该与第一次一致。这个时候就需要自定义一个表单验证逻辑。这里就定义了一个新的表单验证逻辑confirmValidator,为了能够展示不同的两种信息所以设计了两种状态值error和required。
confirmValidator = (control: FormControl): { [s: string]: boolean } => {
if (!control.value) {
return { error: true, required: true };
} else if (control.value !== this.resetForm.controls.password.value) {
return { confirm: true, error: true };
}
return {};
};
resetForm = new FormGroup({
userName: new FormControl(null, [Validators.required]),
email: new FormControl(null, [Validators.required, Validators.email]),
password: new FormControl(null, [Validators.required, Validators.pattern(/^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,10}$/)]),
password1: new FormControl(null, [this.confirmValidator]),
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.2.4 表单提交逻辑
# 表单数据回收
因为和后端定义的关于权限部分的http请求全部使用post,所以这里都使用直接定义对象来回收表单数据,将表单组的对应值直接幅值到后端定义的对象中。这里因为this.resetForm是一个观察对象,和vue里的ref类似,需要使用value来获取值。
let resetForm = {
username: this.resetForm.value.userName,
userEmail: this.resetForm.value.email,
newPassword: this.resetForm.value.password,
}
2
3
4
5
# 数据提交与返回状态处理
angular的自己封装了http请求的服务,不需要像vue那样引入,只要在组件里注入import { HttpClient, HttpHeaders } from '@angular/common/http';
的依赖就可以使用。对于post请求需要定义请求头。
由于很多地方都需要用到后端的网址,为了应对项目落地调试时频繁修改IP的问题,这里将后端的IP定义在environment里面,通过import { accountUrl } from 'src/environments/environment'
来使用
请求返回的内容是与后端协商确定的,根据statusCode的不同来确定后续启用的逻辑,这里成功就提示成功并关闭弹窗;不成功则提示错误信息,保留弹窗
interface responseProps {
statusCode: number
message: string
}
const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }
let api = accountUrl + '/api/app/reset-password'
this.http.post(api, resetForm, httpOptions).subscribe((response: responseProps) => {
if (response.statusCode === 200) {
this.alert.MessageAlert('success', response.message, 3000)
this.nzModalRef.destroy(true);
} else {
this.alert.MessageAlert('error', response.message, 3000)
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.2.5 提示信息服务
由于浏览器自带的alert很丑,且必须手动关闭会阻塞代码运行。结合NG-ZORRO封装的全局信息提示,项目创建了一个提示的服务,供全局使用
使用ng generate service alert
命令创建提示信息服务,将
NG-ZORRO封装的全局信息提示注入import { NzMessageService } from 'ng-zorro-antd/message';
,并且在构造函数里定义private message: NzMessageService
。
MessageAlert(type: string, message: string, duration: number): void {
this.message.create(type, `${message}`, { nzDuration: duration });
}
2
3
这样在其他组件里注入这个服务import { AlertService } from 'src/app/service/alert.service'
,构造函数里定义private *alert*: AlertService
,就可以直接使用this.alert.MessageAlert('success', response.message, 3000)
# 3.2.6 登录获取保存token
一般来说登录之后后端会返回token,你需要将其保存在LocalStorage或者sessionStorage里面。但Angular与abp给你做了封装只要拿过来用就可以了。
首先是要设置好登录的方式,以及给后端传的值。这部分内容在environment.ts和environment.prod.ts里设置。前者是开发的时候使用的,后者是打包的时候使用,在不涉及打包上线的时候后者完全不起作用。但为了防止你到了打包上线的时候忘记修改,最好两个一起修改
oAuthConfig: {
//验证信息往哪里传
issuer: accountUrl,
//获取token的api
tokenEndpoint: accountUrl + '/connect/token',
//使用token方法验证,abp默认的是跳转到后端页面上验证
requestAccessToken: true,
//这是后端需要给你的前端设备标识和密码
clientId: 'Dashboard_App',
dummyClientSecret: '1q2w3e*',
//这个是目前还没做https的授权那一套先给他关掉,免得报不安全
requireHttps: false,
//你前端设备名称,是什么不重要
scope: 'offline_access Dashboard',
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
然后将登录相关的服务注入import { OAuthService } from 'angular-oauth2-oidc';
import { AuthService } from '@abp/ng.core';
。构造函数定义,之后直接使用接口就可以了
this.authService.login(user).toPromise()
.then(data => {
this.alert.MessageAlert('success', "恭喜您登录成功!", 1000)
setTimeout(() => {
this.router.navigateByUrl('/dashboard');
}, 1000)
})
.catch(() => {
this.alert.MessageAlert('error', "您输入的用户名或密码不正确!", 3000)
});
2
3
4
5
6
7
8
9
10
如果你发现你登录之后在LocalStorage里多出了很多信息证明登录获取保存token就成功了。但我们做的时候出现了一些问题,本来不注销账号刷新页面是token是会一直保留的,但我们当时出现了token不保留的bug,打印后端返回的数据后发现,只是返回了token但没有返回用户信息,修改后端代码后解决。
# 4 导航栏与主界面布局开发
三个主要的功能界面共用同一导航栏,所以将导航栏的开发放在share目录下
# 4.1 导航栏开发
# 4.1.1 导航栏布局开发
导航栏(.nav)从左到右分为名称区(.title)、路由区(.router)和菜单区(.menu)。通过NG-ZORRO (opens new window)的Grid栅格布局,使其分别占据页面的8/24、5/25、8/24。通过nzJustify="space-between"
使其合理分隔显示
# 4.1.2 导航栏名称区开发
名称区功能很简单,显示名称与当前时间。在组件初始化钩子函数里添加了一个1秒刷新一次的定时器用于给界面显示当前时间,html中直接使用插值语法更新显示时间
# 4.1.3 导航栏路由区开发
通过routerLink="/dashboard"
控制路由跳转,如果直接通过a标签的url跳转会出现系统页面直接刷新全部重启的问题。这会导致与后端数据接收通道的断开。
# 4.1.4 导航栏菜单区开发
菜单区分为算法、文件、设置和用户头像4个主体,每个主体分别存在各自的下拉菜单,实现不同的功能
# 4.2 主界面布局开发
主界面通过dashboard-layout文件夹来实现主界面布局分配,首先<app-nav></app-nav>
将导航栏引入,其余按照设计图将主界面纵向分成三个部分:左侧特殊显示区(.left)、中间主体显示区(.middle)、右侧多功能区(.right)
# 4.2.1 左侧特殊显示区
左侧特殊显示区从上至下分为传感器仪表盘(.mainMonitor)、统计饼图(.pieChart)、文字显示区(.Word)
# 传感器仪表盘
以2X2矩阵形式排布4个传感器的信息,通过[index]
参数输入绑定不同的传感器以复用status-panel组件
status-panel组件从上至下分为状态颜色块(.status)、数字(.value)、描述(.description)
# 统计饼图
通过statistics组件引入一个echarts图,通过[options]="chartOption"
在ts文件中设置图中显示内容
# 文字显示区
通过description组件完成布局,在description组件里使用cardtab,将显示内容通过tab标签切换为异常参数列表(abnormal-parameters)、诊断结果(diagnostic-results)、操作建议(recommendation)
# 4.2.2 中间主体显示区
中间显示区包括一个类导航部分以及下方的fabric的canvas。类导航区左侧是一个通过this.fabricService.editMode参数标志canvas状态显示提示,中间是一个切换数据源的tab、右侧是用来切换右侧多功能区的按钮。下方canvas通过tab和[source]="source"
达到复用main-view组件显示3个不同转向架的效果。
# 4.2.3 右侧多功能区
通过*ngIf"
与this.fabricService.function参数判断右侧多功能区渲染单个传感器信息(single-parameter)还是全部传感器信息(overall-parameters)
# 全部传感器信息
overall-parameters组件通过一个自动列表将所有传感器的信息(状态、名称、数值、单位)渲染出来
# 单个传感器信息
single-parameter组件分为传感器信息绑定显示区、时域图(time-domain-chart组件)、频域图组件(frequency-domain-chart)和两个功能按钮组成
# 5 fabric二维图像显示服务
Fabric.js (opens new window) 是一个提供多种二维图像交互显示的库,项目中用以显示传感器的二维位置以及一系列传感器相关的交互
# 5.1 canvas初始化
initialize(*source*: string, *width*: number, *height*: number){}
方法用以初始化canvas,在main-view组件初始化中调用并传入数据源、canvas需要的自适应长宽,通过*ngFor="let i of this.sensor.sourceList"
与*ngIf="source == i"
选择当前渲染的canvas
因为使用Circle类作为传感器,需要该类增加一个name属性,于是在constructor中添加以下代码。
constructor(public sensor: SensorInfoService, private http: HttpClient, private alert: AlertService) {
this.editMode = false
// 获取之前添加的name信息
fabric.Circle.prototype.toObject = ((toObject) => {
return function () {
return fabric.util.object.extend(toObject.call(this), {
name: this.name
});
};
})(fabric.Circle.prototype.toObject);
}
2
3
4
5
6
7
8
9
10
11
# 5.2 设置背景图片
setBackground(*imgSrc*: string, *width*: number, *height*: number){}
方法用于设置背景图片,目前在single-parameter组件中被imageSetting按钮事件调用。之后考虑在该按钮产生的弹窗组件被调用。
函数中首先根据自适应要求调整长宽,将图片设置为canvas背景并渲染,生成对应Image实例
# 5.3 传感器增删改查与选中
# 5.3.1 选中传感器
selectSensor(*element*){}
方法用以将传感器选中,使其他组件得知当前选中的传感器且在视觉上特殊显示。通过该服务的target存储选中的传感器,并在选中传感器方法中恢复之前target目标的显示以及突出显示当前target,重新渲染canvas。
# 5.3.2 查找传感器
inquireSensor(*name*: string){}
通过遍历canvas里的object,比照name属性,调用selectSensor(element)
以选中传感器。在single-parameter组件中被inquireSensor按钮事件调用,通过返回值进行查询结果提示。
# 5.3.3 添加传感器
addSensor(*name*?: string) {}
方法用以增加传感器,并选中刚创建的传感器。在single-parameter组件中被createSensor按钮事件调用。在确定当前数据源传感器信息存在该传感器ID以及查询该传感器不存在于canvas中后调用该方法。
# 5.3.4 删除传感器
deleteSensor(){}
方法用以删除传感器,因为选中的传感器信息存储于fabric服务中,直接通过this.target
获取,所以不用传入参数。在single-parameter组件中被deleteSensor按钮事件调用。
# 5.3.5 修改传感器
modifySensor(*name*: string){}
方法用以修改传感器的name属性,在single-parameter组件中被saveSensor按钮事件调用。
# 5.4 鼠标传感器事件
# 5.4.1 双击选中事件
this.canvas.on('mouse:dblclick', (*options*) =>{}
双击选中时间不仅要调用选中传感器的selectSensor(*options*.target)
,同时要把editMode调整为false,从编辑状态改为查看状态
# 5.4.2 滑动触发名字事件
this.canvas.on('mouse:over', (*options*) => {}
因为背景图片也是一个对象所以要判断对象类型。在判断对象类型后添加一个sensorName
和sensorParam
的Text类实例对象显示传感器ID与中文名,重新渲染canvas
this.canvas.on('mouse:out', (*options*) =>{}
删除之前添加的实例对象,重新渲染canvas
# 5.5 保存canvas信息
saveCanvas(){}
首先确定保存时的显示设备尺寸是否标准,之后可能存在的多余文字显示与选中状态移除,然后存入浏览器的localStorage中。后续存入后端数据库则在别的地方实现
# 5.6 读取保存canvas信息
readCanvas(*source*){}
首先判断浏览器localStorage是否存在该数据源的canvas信息,如果不存在读取项目自带的JSON,否则则读取浏览器中JSON。
由于从项目或者后端获取的JSON存在显示设备尺寸不一的情况,adaptiveJson(*json*) {}
用以读取内容后自适应屏幕尺寸。
# 6 sensor-info 传感器信息处理服务
为了能够动态配置存储使用传感器的一系列相关信息(中文名、单位、报警信息等)开发了sensor-info服务,该服务作为整个项目的底层为后续数据接收的signalR、界面渲染、以及传感器信息修改提供基础
# 6.1 从项目获取传感器信息
reset(){}
目前是从前端项目的打包文件里获取,将全部传感器信息存放在sensorInfo中,将数据源列表存在sourceList中,之后调用getInfo(){}
# 6.2 传感器信息整理存储
getInfo(){}
将不同数据源的传感器信息、状态传感器列表、算法传感器列表、高频传感器列表变为对象分别存在sensorDict、statusList、algorithmList、highFrequencyList中
# 6.3 确定当前输出信息源
setSource(source:string) {}
通过传入的source确定用于输出的source、currentSensorDict、currentStatusList、currentAlgorithmList、currentHighFrequencyList。在组件DashboardLayoutComponent
中通过switchSource(source){}
触发已获得tab切换时同时切换其他地方数据源的功能
# 6.4 更新传感器信息
refesh(info:Object){}
通过传入的info改变sensorInfo与sourceList,然后调用getInfo()
重新存储传感器信息,并将传感器信息固化存储在服务器中。在SensorInfoModalComponent
组件中通过子组件上传事件触发的refreshSensorInfo(*info*: Object) {}
触发
# 7 algorithm阈值算法处理服务
为了分离数据接收的signalR服务和各传感器的阈值报警分析,开发了AlgorithmService
服务,该服务需要SensorInfoService
提供服务,并给后续的SignalRService
提供服务
# 7.1 ParamsList类
interface ParamsList {
paraName: string; //传感器型号
chineseName: string; //传感器中文名
time: string; //时间
unit: string; //传感器单元
numericalValue: number; //当前时刻传感器数据
statusBar: string; //传感器状态(颜色)
description: string; //报警信息
result: any; //诊断结果
operation: any; //操作建议
}
2
3
4
5
6
7
8
9
10
11
将传感器信息按照ParamsList类存储
# 7.2 单个传感器阈值判断
judgeStatus(*value*: number, *ID*: string, *SensorDict*: Object) {}
迭代输入ID对应的传感器的报警列表,根据limitType对输入的值与limitValue进行对比,返回一个列表[传感器状态,报警信息,诊断结果,操作建议]
# 7.3 诊断结果、决策去重累计显示
judgeAdd(*showList*: any, *addList*: any, *num*: number) {}
showList是用于存储显示的列表,addList是需要增加进去的列表,num是showList能够存储的个数
# 7.4 数据总处理
dataProcess(*key*, *value*: number, *time*: string, *SensorDict*: Object, *statusList*: Array<string>,*dataList*: ParamsList[], *panelData*: Object, *errorList*: ParamsList[],*errorIDList*: Object, *resultList*: Array<string>, *operationList*:Array<string>, *ERROR*: Array<string>) {}
key为传感器型号,value为当前传感器值,time为时间,SensorDict为传感器信息字典,statusList为常现传感器列表,dataList为全部传感器信息列表,panelData为常现传感器数据列表,errorList报警信息数据列表,errorIDList报警传感器列表,resultList诊断结果显示列表,operationList操作建议显示列表,ERROR为历史故障信息列表
# 8 signalR实时数据接收服务
# 8.1 定期固化上传error信息
uploadERROR() {}
通过http的post请求把存储的ERROR传给后端,并且将其清零
# 8.2 连接signalR与持续监听的数据
connect() {}
目前只有传感器数据传输通过for (const source of this.sensor.sourceList) {}
迭代数据源,连接到对应数据源的signalR通道,迭代每次传来的数据中的传感器ID,调用algorithm.dataProcess(key, RawData[key], RawData["Time"],this.sensor.sensorDict[source], this.sensor.statusList[source] , dataList, panelData,errorList, errorIDList, resultList, operationList,this.ERROR)
,并将数据存入sensorData、ListOfData、PanelData、errorID、ListOfError、ListOfResult、ListOfOperation
# 8.3 constructor(){}
构造器中首先获取之前存在浏览器中的固化历史抱紧信息,然后建立signalR的hub,然后调用connect() {}
# 8.4 输出数据源选择
setSource(*source*) {}
通过传入的source,切换用于输出的currentSensorData、currentListOfData、currentPanelData、currentErrorID、currentListOfError、currentListOfResult、currentListOfOperation。在组件DashboardLayoutComponent
中通过switchSource(source){}
触发已获得tab切换时同时切换其他地方数据源的功能