Browse Source

first commit

master
fangyang2021 1 year ago
commit
ae8db2e68b
  1. 16
      .editorconfig
  2. 42
      .gitignore
  3. 4
      .vscode/extensions.json
  4. 5
      .vscode/launch.json
  5. 42
      .vscode/tasks.json
  6. 28
      README.md
  7. 239
      angular.json
  8. 5
      doc.md
  9. 38
      mock/index.ts
  10. 54
      package.json
  11. 7380
      pnpm-lock.yaml
  12. 24
      projects/cdk/README.md
  13. 7
      projects/cdk/ng-package.json
  14. 12
      projects/cdk/package.json
  15. 8
      projects/cdk/src/dec-module/base-href.ts
  16. 41
      projects/cdk/src/dec-module/dec.module.ts
  17. 100
      projects/cdk/src/dec-module/http.interceptor.ts
  18. 36
      projects/cdk/src/form-error-tips/form-error-tips.component.html
  19. 0
      projects/cdk/src/form-error-tips/form-error-tips.component.less
  20. 28
      projects/cdk/src/form-error-tips/form-error-tips.component.ts
  21. 24
      projects/cdk/src/input-space-error/input-space-error.directive.ts
  22. 14
      projects/cdk/src/public-api.ts
  23. 16
      projects/cdk/src/public-path/public-path.pipe.ts
  24. 0
      projects/cdk/src/quick-date-range/quick-date-range.component.css
  25. 17
      projects/cdk/src/quick-date-range/quick-date-range.component.html
  26. 94
      projects/cdk/src/quick-date-range/quick-date-range.component.ts
  27. 71
      projects/cdk/src/storage/cache.service.ts
  28. 3
      projects/cdk/src/storage/index.ts
  29. 15
      projects/cdk/src/storage/storage.module.ts
  30. 79
      projects/cdk/src/storage/storage.service.ts
  31. 5
      projects/cdk/src/table-list/index.ts
  32. 143
      projects/cdk/src/table-list/table-list-options.ts
  33. 56
      projects/cdk/src/table-list/table-list.module.ts
  34. 223
      projects/cdk/src/table-list/table-list/table-list.component.html
  35. 119
      projects/cdk/src/table-list/table-list/table-list.component.less
  36. 386
      projects/cdk/src/table-list/table-list/table-list.component.ts
  37. 30
      projects/cdk/src/table-list/table-operation/table-operation.component.html
  38. 25
      projects/cdk/src/table-list/table-operation/table-operation.component.less
  39. 61
      projects/cdk/src/table-list/table-operation/table-operation.component.ts
  40. 30
      projects/cdk/src/table-list/td-overflow.directive.ts
  41. 58
      projects/cdk/src/types/index.ts
  42. 46
      projects/cdk/src/utils/index.ts
  43. 66
      projects/cdk/src/validators/index.ts
  44. 14
      projects/cdk/tsconfig.lib.json
  45. 10
      projects/cdk/tsconfig.lib.prod.json
  46. 14
      projects/cdk/tsconfig.spec.json
  47. 15
      projects/client/proxy.conf.json
  48. 26
      projects/client/src/app/app-routing.module.ts
  49. 4
      projects/client/src/app/app.component.html
  50. 0
      projects/client/src/app/app.component.less
  51. 73
      projects/client/src/app/app.component.ts
  52. 47
      projects/client/src/app/app.module.ts
  53. 27
      projects/client/src/app/core/gaurd/auth.guard.ts
  54. 33
      projects/client/src/app/core/gaurd/permisson.guard.ts
  55. 56
      projects/client/src/app/core/services/auth.service.ts
  56. 43
      projects/client/src/app/core/services/client.interceptor.ts
  57. 269
      projects/client/src/app/core/services/detection-api.service.ts
  58. 4
      projects/client/src/app/core/services/index.ts
  59. 11
      projects/client/src/app/core/services/utils.service.ts
  60. 65
      projects/client/src/app/core/services/websocket-api.service.ts
  61. 16
      projects/client/src/app/feature/auth/auth-routing.module.ts
  62. 11
      projects/client/src/app/feature/auth/auth.module.ts
  63. 1
      projects/client/src/app/feature/auth/pages/index.ts
  64. 57
      projects/client/src/app/feature/auth/pages/login/login.component.html
  65. 23
      projects/client/src/app/feature/auth/pages/login/login.component.less
  66. 50
      projects/client/src/app/feature/auth/pages/login/login.component.ts
  67. 153
      projects/client/src/app/feature/detection/components/detection-graphics/detection-graphics.component.html
  68. 204
      projects/client/src/app/feature/detection/components/detection-graphics/detection-graphics.component.less
  69. 375
      projects/client/src/app/feature/detection/components/detection-graphics/detection-graphics.component.ts
  70. 4
      projects/client/src/app/feature/detection/components/detection-value/detection-value.component.html
  71. 0
      projects/client/src/app/feature/detection/components/detection-value/detection-value.component.less
  72. 12
      projects/client/src/app/feature/detection/components/detection-value/detection-value.component.ts
  73. 47
      projects/client/src/app/feature/detection/components/device-table/device-table.component.html
  74. 0
      projects/client/src/app/feature/detection/components/device-table/device-table.component.less
  75. 133
      projects/client/src/app/feature/detection/components/device-table/device-table.component.ts
  76. 4
      projects/client/src/app/feature/detection/components/forbidden/forbidden.component.html
  77. 0
      projects/client/src/app/feature/detection/components/forbidden/forbidden.component.less
  78. 14
      projects/client/src/app/feature/detection/components/forbidden/forbidden.component.ts
  79. 0
      projects/client/src/app/feature/detection/components/home/home.component.html
  80. 0
      projects/client/src/app/feature/detection/components/home/home.component.less
  81. 34
      projects/client/src/app/feature/detection/components/home/home.component.ts
  82. 129
      projects/client/src/app/feature/detection/components/image-player/_image-player.component.ts
  83. 52
      projects/client/src/app/feature/detection/components/image-player/image-player.component.html
  84. 44
      projects/client/src/app/feature/detection/components/image-player/image-player.component.less
  85. 176
      projects/client/src/app/feature/detection/components/image-player/image-player.component.ts
  86. 10
      projects/client/src/app/feature/detection/components/index.ts
  87. 6
      projects/client/src/app/feature/detection/components/notfound/notfound.component.html
  88. 0
      projects/client/src/app/feature/detection/components/notfound/notfound.component.less
  89. 10
      projects/client/src/app/feature/detection/components/notfound/notfound.component.ts
  90. 29
      projects/client/src/app/feature/detection/components/point-status/point-status.component.html
  91. 0
      projects/client/src/app/feature/detection/components/point-status/point-status.component.less
  92. 16
      projects/client/src/app/feature/detection/components/point-status/point-status.component.ts
  93. 41
      projects/client/src/app/feature/detection/components/point-table/point-table.component.html
  94. 0
      projects/client/src/app/feature/detection/components/point-table/point-table.component.less
  95. 115
      projects/client/src/app/feature/detection/components/point-table/point-table.component.ts
  96. 156
      projects/client/src/app/feature/detection/detection-routing.module.ts
  97. 56
      projects/client/src/app/feature/detection/detection.module.ts
  98. 94
      projects/client/src/app/feature/detection/pages/analysis/analysis.component.html
  99. 34
      projects/client/src/app/feature/detection/pages/analysis/analysis.component.less
  100. 347
      projects/client/src/app/feature/detection/pages/analysis/analysis.component.ts

16
.editorconfig

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
.gitignore

@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

4
.vscode/extensions.json

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

5
.vscode/launch.json

@ -0,0 +1,5 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": []
}

42
.vscode/tasks.json

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

28
README.md

@ -0,0 +1,28 @@
# Dec 视觉检测项目
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.2.0.
## Development server
* `npm run start:client`
* `npm run start:manage`
## Build
* `npm run build:client`
* `npm run build:manage`
# 7.10 问题
1. 相机列表需要返回 监测点id、分组id、电站id、相机型号、延时参数 或者新增详情接口
2. 系统信息管理 保存 一起保存
3. 主题管理 保存 一起保存
4. 颜色的设置
5. ui 端接口
6. 用户管理 id 、uid 、userId
7. ui 查看权限
8. 编辑算法 需要的数据 & 名称?
9. 机组管理 机组编号 2#caa
10. 相机管理列表 保存了数据过后就报错了

239
angular.json

@ -0,0 +1,239 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm",
"analytics": false
},
"newProjectRoot": "projects",
"projects": {
"client": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "less"
}
},
"root": "projects/client",
"sourceRoot": "projects/client/src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"baseHref": "/ui/",
"outputPath": "dist/client",
"index": "projects/client/src/index.html",
"main": "projects/client/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "projects/client/tsconfig.app.json",
"inlineStyleLanguage": "less",
"assets": [
"projects/client/src/favicon.ico",
"projects/client/src/assets",
{
"glob": "**/*",
"input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
"output": "/assets/"
}
],
"styles": ["projects/client/src/styles.less"],
"scripts": [],
"fileReplacements": [
{
"replace": "projects/client/src/environments/environment.ts",
"with": "projects/client/src/environments/environment.prod.ts"
}
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "10mb",
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "10mb",
"maximumError": "10mb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "client:build:production"
},
"development": {
"browserTarget": "client:build:development",
"port": 4396,
"host": "0.0.0.0"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "client:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "projects/client/tsconfig.spec.json",
"inlineStyleLanguage": "less",
"assets": ["projects/client/src/favicon.ico", "projects/client/src/assets"],
"styles": ["projects/client/src/styles.less"],
"scripts": []
}
}
}
},
"manage": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "less"
}
},
"root": "projects/manage",
"sourceRoot": "projects/manage/src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"baseHref": "/admin/",
"outputPath": "dist/manage",
"index": "projects/manage/src/index.html",
"main": "projects/manage/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "projects/manage/tsconfig.app.json",
"inlineStyleLanguage": "less",
"assets": [
"projects/manage/src/favicon.ico",
"projects/manage/src/assets",
{
"glob": "**/*",
"input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
"output": "/assets/"
}
],
"styles": ["projects/manage/src/styles.less"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "10mb",
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "10mb",
"maximumError": "10mb"
}
],
"outputHashing": "all",
"fileReplacements": [
{
"replace": "projects/manage/src/environments/environment.ts",
"with": "projects/manage/src/environments/environment.prod.ts"
}
]
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "manage:build:production"
},
"development": {
"browserTarget": "manage:build:development",
"port": 4567,
"host": "0.0.0.0"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "manage:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "projects/manage/tsconfig.spec.json",
"inlineStyleLanguage": "less",
"assets": ["projects/manage/src/favicon.ico", "projects/manage/src/assets"],
"styles": ["projects/manage/src/styles.less"],
"scripts": []
}
}
}
},
"cdk": {
"projectType": "library",
"root": "projects/cdk",
"sourceRoot": "projects/cdk/src",
"prefix": "dec",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "projects/cdk/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/cdk/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/cdk/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "projects/cdk/tsconfig.spec.json",
"polyfills": ["zone.js", "zone.js/testing"]
}
}
}
}
}
}

5
doc.md

@ -0,0 +1,5 @@
enableDetect => 0 开启 1 关闭
高级设置
数据分析

38
mock/index.ts

@ -0,0 +1,38 @@
import * as Mock from "mockjs";
Mock.setup({
timeout: "200-600",
});
const UserMock = [
{
Url: "/api/user/list",
Method: "get",
Res: {
"data|5-10": [
{
"Id|+1": "@guid",
"Name|1": "@cname(2)",
},
],
},
},
{
Url: "/api/user/add",
Method: "post",
Res: {
"Id|1": "@guid",
"Name|1": "@cname()",
},
},
];
// mock数据集
const routerList = [...UserMock];
// 循环创建mock接口拦截数据
routerList.forEach((e) => {
Mock.mock(e.Url, e.Method, e.Res);
});
// 导出Mock
export default Mock;

54
package.json

@ -0,0 +1,54 @@
{
"name": "dec-app",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start:client": "ng serve client --proxy-config ./projects/client/proxy.conf.json",
"build:client": "ng build client --vendor-chunk",
"start:manage": "ng serve manage --proxy-config ./projects/manage/proxy.conf.json",
"build:manage": "ng build manage --vendor-chunk",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.0",
"@angular/cdk": "^15.2.6",
"@angular/common": "^15.2.0",
"@angular/compiler": "^15.2.0",
"@angular/core": "^15.2.0",
"@angular/forms": "^15.2.0",
"@angular/platform-browser": "^15.2.0",
"@angular/platform-browser-dynamic": "^15.2.0",
"@angular/router": "^15.2.0",
"@ant-design/icons-angular": "^15.0.0",
"@iplab/ngx-color-picker": "15",
"date-fns": "^2.30.0",
"echarts": "^5.4.2",
"immer": "^10.0.1",
"ng-zorro-antd": "15.1.0",
"ngx-permissions": "^15.0.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.6",
"@angular/cli": "~15.2.0",
"@angular/compiler-cli": "^15.2.0",
"@types/jasmine": "~4.3.0",
"@types/mockjs": "^1.0.7",
"autoprefixer": "^10.4.14",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"mockjs": "^1.1.0",
"ng-packagr": "^15.2.2",
"postcss": "^8.4.21",
"tailwindcss": "^3.3.1",
"typescript": "~4.9.4"
}
}

7380
pnpm-lock.yaml

File diff suppressed because it is too large

24
projects/cdk/README.md

@ -0,0 +1,24 @@
# Cdk
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.2.0.
## Code scaffolding
Run `ng generate component component-name --project cdk` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project cdk`.
> Note: Don't forget to add `--project cdk` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build cdk` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build cdk`, go to the dist folder `cd dist/cdk` and run `npm publish`.
## Running unit tests
Run `ng test cdk` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

7
projects/cdk/ng-package.json

@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/cdk",
"lib": {
"entryFile": "src/public-api.ts"
}
}

12
projects/cdk/package.json

@ -0,0 +1,12 @@
{
"name": "cdk",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^15.2.0",
"@angular/core": "^15.2.0"
},
"dependencies": {
"tslib": "^2.3.0"
},
"sideEffects": false
}

8
projects/cdk/src/dec-module/base-href.ts

@ -0,0 +1,8 @@
import { PlatformLocation } from "@angular/common";
import { InjectionToken } from "@angular/core";
export const PUBLIC_PATH = new InjectionToken<string>("pablic-path");
export function getBaseHref(platformLocation: PlatformLocation): string {
return platformLocation.getBaseHrefFromDOM();
}

41
projects/cdk/src/dec-module/dec.module.ts

@ -0,0 +1,41 @@
import { InjectionToken, ModuleWithProviders, NgModule } from "@angular/core";
import { CommonModule, PlatformLocation } from "@angular/common";
import { NzMessageModule } from "ng-zorro-antd/message";
import { HttpResponse, HTTP_INTERCEPTORS } from "@angular/common/http";
import { HTTPInterceptor } from "./http.interceptor";
import { DecSafeAny } from "@cdk/types";
import { getBaseHref, PUBLIC_PATH } from "./base-href";
export const decConfigToken = new InjectionToken<DecConfig>("decConfig");
export type DecConfig = {
environment: DecSafeAny;
isClient?: boolean;
loginUrl?: string;
localStroageKey: string;
triggerError?: <T>(res: HttpResponse<T>) => void;
};
@NgModule({
declarations: [],
imports: [NzMessageModule],
providers: [{ provide: HTTP_INTERCEPTORS, useClass: HTTPInterceptor, multi: true }],
})
export class DecModule {
public static forRoot(decConfig: DecConfig): ModuleWithProviders<DecModule> {
return {
ngModule: DecModule,
providers: [
{
provide: decConfigToken,
useValue: decConfig,
},
{
provide: PUBLIC_PATH,
useFactory: getBaseHref,
deps: [PlatformLocation],
},
],
};
}
}

100
projects/cdk/src/dec-module/http.interceptor.ts

@ -0,0 +1,100 @@
import { Inject, Injectable } from "@angular/core";
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse,
HttpResponse,
} from "@angular/common/http";
import { catchError, Observable, switchMap, tap, throwError, timer } from "rxjs";
import { Router } from "@angular/router";
import { NzMessageService } from "ng-zorro-antd/message";
import { decConfigToken, DecConfig } from "./dec.module";
import { ResponseType } from "@cdk/types";
@Injectable({ providedIn: "root" })
export class HTTPInterceptor implements HttpInterceptor {
constructor(
@Inject(decConfigToken) private decConfig: Required<DecConfig>,
private router: Router,
private msg: NzMessageService
) {}
private msgFlag = false;
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const { localStroageKey } = this.decConfig;
const token = localStorage.getItem(localStroageKey);
if (token) {
req = req.clone({
// headers: req.headers.set('Authorization', `Bearer ${token}`),
headers: req.headers.set("Authorization", token),
});
}
return this.handleResult(next, req);
}
private handleResult(next: HttpHandler, authReq: HttpRequest<any>): Observable<HttpEvent<any>> {
return next.handle(authReq).pipe(
tap((res) => {
if (res instanceof HttpResponse) {
const Authorization = res.headers.get("Authorization");
if (Authorization) {
localStorage.setItem(this.decConfig.localStroageKey, Authorization);
}
if (this.decConfig.triggerError) {
this.decConfig.triggerError(res);
}
if (res.body?.success === false && res.body.desc) {
throw new HttpErrorResponse({ error: res.body });
}
}
}),
catchError((err: HttpErrorResponse) => {
const throwErr = throwError(() => err);
if (this.msgFlag) {
return throwErr;
}
const { isClient } = this.decConfig;
setTimeout(() => {
this.msgFlag = false;
}, 1500);
const error: ResponseType = err.error;
this.msgFlag = true;
if (error.success === false) {
if (isClient) {
switch (error.code) {
case 401:
break;
default:
this.msg.error(error.desc);
break;
}
} else {
this.msg.error(error.desc);
switch (error.code) {
case 401:
this.router.navigate([this.decConfig.loginUrl]);
break;
default:
break;
}
}
} else {
this.msg.error("服务器出错了!");
}
return throwErr;
})
);
}
}

36
projects/cdk/src/form-error-tips/form-error-tips.component.html

@ -0,0 +1,36 @@
<ng-container *ngFor="let item of control.errors | keyvalue">
<ng-container *ngIf="item.value?.message;else defaultTipsTpl">
{{item.value.message}}
</ng-container>
<ng-template #defaultTipsTpl>
<ng-container [ngSwitch]="item.key">
<div *ngSwitchCase="'required'">
不能为空
</div>
<div *ngSwitchCase="'inputTrim'">
首末字符不能为空格
</div>
<div *ngSwitchCase="'email'">
请输入正确的邮箱地址
</div>
<div *ngSwitchCase="'maxlength'">
最多输入{{item.value.requiredLength}}位字符
</div>
<div *ngSwitchCase="'minlength'">
最少输入{{item.value.requiredLength}}位字符
</div>
<div *ngSwitchCase="'min'">
不能小于{{item.value.min}}
</div>
<div *ngSwitchCase="'max'">
不能大于{{item.value.max}}
</div>
<div *ngSwitchCase="'pattern'">
请输入正确的内容
</div>
<div *ngSwitchDefault>
字段验证失败
</div>
</ng-container>
</ng-template>
</ng-container>

0
projects/cdk/src/form-error-tips/form-error-tips.component.less

28
projects/cdk/src/form-error-tips/form-error-tips.component.ts

@ -0,0 +1,28 @@
import { CommonModule } from "@angular/common";
import { Component, Input, OnChanges, OnInit, SimpleChanges } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
@Component({
standalone: true,
selector: "dec-form-error-tips",
templateUrl: "./form-error-tips.component.html",
styleUrls: ["./form-error-tips.component.less"],
imports: [CommonModule],
})
export class FormErrorTipsComponent implements OnInit, OnChanges {
constructor() {}
@Input() control!: FormControl;
ngOnChanges(changes: SimpleChanges): void {
// console.log("FormErrorTipsComponent changes", changes["control"]?.currentValue);
// const formControl: FormControl = changes["control"].currentValue;
// const root = formControl.root as FormGroup;
// console.log("formControl.root", formControl);
// if (formControl && !this.label) {
// if(initLabelFormControlNameMaps.has(formControl))
// }
}
ngOnInit(): void {}
}

24
projects/cdk/src/input-space-error/input-space-error.directive.ts

@ -0,0 +1,24 @@
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn } from "@angular/forms";
import { Directive } from "@angular/core";
const VALIDATE_WHITE_SPACE_REGEX = /^[\s]|[\s]$/;
export function stringWhiteSpaceForbiddenValidator(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
const isInvalid = VALIDATE_WHITE_SPACE_REGEX.test(control.value);
return isInvalid ? { inputTrim: { value: control.value } } : null;
};
}
@Directive({
standalone: true,
selector: "[nz-input]",
providers: [{ provide: NG_VALIDATORS, useExisting: InputSpaceErrorDirective, multi: true }],
})
export class InputSpaceErrorDirective implements Validator {
constructor() {}
validate(control: AbstractControl): ValidationErrors | null {
return stringWhiteSpaceForbiddenValidator()(control);
}
}

14
projects/cdk/src/public-api.ts

@ -0,0 +1,14 @@
/*
* Public API Surface of cdk
*/
export * from "./types";
export * from "./utils";
export * from "./validators";
export * from "./dec-module/dec.module";
export * from "./form-error-tips/form-error-tips.component";
export * from "./input-space-error/input-space-error.directive";
export * from "./public-path/public-path.pipe";
export * from "./quick-date-range/quick-date-range.component";
export * from "./table-list";
export * from "./storage";

16
projects/cdk/src/public-path/public-path.pipe.ts

@ -0,0 +1,16 @@
import { Inject, Pipe, PipeTransform } from "@angular/core";
import { PUBLIC_PATH } from "@cdk/dec-module/base-href";
@Pipe({
name: "publicPath",
standalone: true,
})
export class PublicPathPipe implements PipeTransform {
constructor(@Inject(PUBLIC_PATH) private publicPath: string) {}
transform(value: string): string {
if (value.startsWith("/")) {
value = value.replace("/", "");
}
return this.publicPath + value;
}
}

0
projects/cdk/src/quick-date-range/quick-date-range.component.css

17
projects/cdk/src/quick-date-range/quick-date-range.component.html

@ -0,0 +1,17 @@
<nz-space>
<nz-radio-group
*nzSpaceItem
[ngModel]="quick"
(ngModelChange)="onQuickChange($event)">
<label *ngFor="let item of dateFilterOptions"
nz-radio-button
[nzValue]="item.value">
{{item.label}}
</label>
</nz-radio-group>
<nz-range-picker
*nzSpaceItem
[ngModel]="range"
(ngModelChange)="onRangeChange($event)">
</nz-range-picker>
</nz-space>

94
projects/cdk/src/quick-date-range/quick-date-range.component.ts

@ -0,0 +1,94 @@
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from "@angular/forms";
import { NzRadioModule } from "ng-zorro-antd/radio";
import { NzSpaceModule } from "ng-zorro-antd/space";
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { NzDatePickerModule } from "ng-zorro-antd/date-picker";
import { subDays, format } from "date-fns";
@Component({
standalone: true,
selector: "dec-quick-date-range",
templateUrl: "./quick-date-range.component.html",
styleUrls: ["./quick-date-range.component.css"],
imports: [CommonModule, NzDatePickerModule, NzSpaceModule, NzRadioModule, FormsModule],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: QuickDateRangeComponent,
},
],
})
export class QuickDateRangeComponent implements OnInit, ControlValueAccessor {
constructor() {}
dateFilterOptions = [
{
label: "今日",
value: 0,
},
{
label: "近7日",
value: 7,
},
{
label: "近30日",
value: 30,
},
{
label: "近90日",
value: 90,
},
];
quick: number | null = null;
range: Date[] = [];
onQuickChange(v: number) {
this.quick = v;
this.range = this.getDateRange(v).map((d) => new Date(d));
const val = this.range.map((date) => format(date, "yyyy-MM-dd"));
// console.log("val", val);
this.onChange(val);
}
getDateRange(n: number): [string, string] {
const today = new Date();
const previousDay = subDays(today, n);
const formattedToday = format(today, "yyyy-MM-dd");
const formattedPreviousDay = format(previousDay, "yyyy-MM-dd");
return [formattedPreviousDay, formattedToday];
}
ngOnInit(): void {}
onRangeChange(v: Date[]) {
this.quick = null;
this.range = v;
const val = v.map((date) => format(date, "yyyy-MM-dd"));
this.onChange(val);
}
onChange(v: any) {}
ontouch(v: any) {}
writeValue(v: string[]): void {
// console.log("v", v);
if (v && Array.isArray(v)) {
this.range = v?.map((d) => new Date(d));
} else {
this.onQuickChange(90);
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.ontouch = fn;
}
}

71
projects/cdk/src/storage/cache.service.ts

@ -0,0 +1,71 @@
import { Injectable } from "@angular/core";
import { NavigationEnd, Router } from "@angular/router";
import { filter } from "rxjs/operators";
import { DecSafeAny } from "../types";
import { StorageService } from "./storage.service";
type IcacheUrlMap = Record<string, Array<string>>;
export class CacheItem<T = any> {
constructor(private storage: StorageService, private cacheKey: string) {}
getItem(): T {
return this.storage.get(this.cacheKey, { stroage: "session" });
}
setItem(query: T) {
this.storage.set(this.cacheKey, query, { stroage: "session" });
}
remove() {
this.storage.set(this.cacheKey, null, { stroage: "session" });
}
}
@Injectable()
export class CacheService {
constructor(private router: Router, private storage: StorageService) {}
/**
* cacheKey url
*/
private get cacheUrlMap(): IcacheUrlMap {
return this.storage.get("cacheUrlMap") ?? {};
}
private set cacheUrlMap(o: IcacheUrlMap) {
this.storage.set("cacheUrlMap", o);
}
listen() {
this.router.events.pipe(filter((f): f is NavigationEnd => f instanceof NavigationEnd)).subscribe((r) => {
const currentUrl = r.url;
for (const [api, urls] of Object.entries(this.cacheUrlMap)) {
if (!urls.some((s) => currentUrl.startsWith(s))) {
this.storage.remove(api, { stroage: "session" });
}
}
});
}
/**
*
* @param cacheKey key
* @param cacheInUrl cacheKey url中缓存url中时删除
* @return getItem & setItem
*/
initCache(cacheKey: string, cacheInUrl?: string): CacheItem;
initCache(cacheKey: string, cacheInUrl?: Array<string>): CacheItem;
initCache(cacheKey: string, cacheInUrl?: string | Array<string>): CacheItem {
if (!cacheInUrl) {
cacheInUrl = [this.router.url];
}
if (!this.cacheUrlMap[cacheKey]) {
cacheInUrl = (Array.isArray(cacheInUrl) ? cacheInUrl : [cacheInUrl]) as Array<string>;
const storageData = this.cacheUrlMap;
storageData[cacheKey] = cacheInUrl;
this.cacheUrlMap = storageData;
}
return new CacheItem(this.storage, cacheKey);
}
}

3
projects/cdk/src/storage/index.ts

@ -0,0 +1,3 @@
export * from "./storage.module";
export * from "./storage.service";
export * from "./cache.service";

15
projects/cdk/src/storage/storage.module.ts

@ -0,0 +1,15 @@
import { NgModule } from "@angular/core";
import { CacheService } from "./cache.service";
import { StorageService } from "./storage.service";
@NgModule({
declarations: [],
imports: [],
providers: [StorageService, CacheService],
exports: [],
})
export class StorageModule {
constructor(private cache: CacheService) {
this.cache.listen();
}
}

79
projects/cdk/src/storage/storage.service.ts

@ -0,0 +1,79 @@
import { Injectable } from "@angular/core";
export interface IFunc<T> {
(prev?: T): T;
}
interface Option<T = any> {
stroage?: "local" | "session";
defaultValue?: T | IFunc<T>;
serializer?: (v: T) => string;
deserializer?: (v: string) => T;
}
export const isFunction = (value: unknown): value is Function => typeof value === "function";
@Injectable()
export class StorageService {
constructor() {}
private parseOption(option?: Option) {
const storage = option?.stroage === "session" ? sessionStorage : localStorage;
const serializer = option?.serializer ? option?.serializer : JSON.stringify;
const deserializer = option?.deserializer ? option?.deserializer : JSON.parse;
let defaultValue = option?.defaultValue;
if (isFunction(option?.defaultValue)) {
defaultValue = option?.defaultValue();
}
return {
storage,
serializer,
deserializer,
defaultValue,
};
}
get<T>(key: string, option?: Option<T>) {
const { storage, deserializer, defaultValue } = this.parseOption(option);
try {
const val = storage.getItem(key);
if (val) {
return deserializer(val);
}
} catch (error) {
console.error(error);
}
return defaultValue;
}
set<T>(key: string, value: T | IFunc<T>, option?: Option<T>) {
const { storage, serializer, defaultValue } = this.parseOption(option);
const val = (isFunction(value) ? value() : value) ?? defaultValue;
if (typeof val === "undefined") {
storage.removeItem(key);
} else {
try {
storage.setItem(key, serializer(val));
} catch (error) {
console.error(error);
}
}
}
remove(key: string, option?: Pick<Option, "stroage">) {
const { storage } = this.parseOption(option);
storage.removeItem(key);
}
clear(option?: Pick<Option, "stroage">) {
const { storage } = this.parseOption(option);
storage.clear();
}
keys(option?: Pick<Option, "stroage">) {
const { storage } = this.parseOption(option);
return Object.keys(storage);
}
}

5
projects/cdk/src/table-list/index.ts

@ -0,0 +1,5 @@
export * from "./table-list.module";
export * from "./table-list-options";
export * from "./table-list/table-list.component";
export * from "./table-operation/table-operation.component";

143
projects/cdk/src/table-list/table-list-options.ts

@ -0,0 +1,143 @@
import { Observable, Subject } from "rxjs";
import { produce, immerable, setAutoFreeze } from "immer";
import { DecSafeAny, TableListColumns, TableOperation } from "../types";
import { EventEmitter } from "@angular/core";
type IfetchData = (...args: DecSafeAny[]) => Observable<DecSafeAny>;
type ITableListConfig = {
manual?: boolean;
cacheKey?: string;
cacheTo?: string[];
rowKey?: string;
columnKey?: string;
selectable?: boolean;
withOutDefaultColumns?: boolean;
theadSettable?: boolean;
pageFromZero?: boolean;
frontPagination?: boolean;
};
type TableListState = {
selectedKeys: string[];
};
type TableListPager = {
page: number;
size: number;
total: number;
loading: boolean;
sort: { [K: string]: "ascend" | "descend" };
};
setAutoFreeze(false);
export class TableListOption {
[immerable] = true;
trigger$ = new Subject<DecSafeAny>();
getState$ = new EventEmitter<TableListState>();
columns: TableListColumns[] = [];
operations!: TableOperation[];
fetchData: IfetchData;
manual?: boolean = false;
cacheKey?: string;
cacheTo?: string[];
rowKey: string;
columnKey?: string;
selectable: boolean = false;
withOutDefaultColumns?: boolean;
pageFromZero?: boolean;
theadSettable: boolean = false;
frontPagination: boolean = true;
scroll: { x?: string | null; y?: string | null } = { x: "2000px", y: null };
pager: TableListPager = {
page: 1,
size: 5,
loading: false,
total: 0,
sort: {},
};
constructor(fetchData: IfetchData, config?: ITableListConfig) {
this.fetchData = fetchData;
this.manual = config?.manual;
this.cacheKey = config?.cacheKey;
this.cacheTo = config?.cacheTo;
this.rowKey = config?.rowKey ?? "id";
this.selectable = config?.selectable ?? false;
this.withOutDefaultColumns = config?.withOutDefaultColumns;
this.theadSettable = config?.theadSettable ?? false;
this.columnKey = config?.columnKey;
this.pageFromZero = config?.pageFromZero;
this.frontPagination = config?.frontPagination ?? true;
}
/**
*
* @param columns TableListColumns
* @returns
*/
setColumns(columns: TableListColumns[]) {
return produce(this, (draft) => {
// if (!this.withOutDefaultColumns) {
// columns = columns.concat(
// { key: "createTime", title: "创建时间", sort: true },
// { key: "updateTime", title: "更新时间", sort: true, visible: false }
// );
// draft.pager.sort = { createTime: "descend" };
// }
draft.columns = columns;
});
}
/**
*
* @param columns TableListColumns
* @returns
*/
setOptions(operations: TableOperation[]) {
return produce(this, (draft) => {
draft.operations = operations;
});
}
/**
* CacheKey
* @param columns TableListColumns
* @returns
*/
setCacheKey(cacheKey: string) {
return produce(this, (draft) => {
draft.cacheKey = cacheKey;
});
}
run(e?: DecSafeAny) {
setTimeout(() => {
// 防止 还没有 subscribe 就 next 了
this.trigger$.next(e);
}, 10);
}
reset() {
this.pager.page = 1;
this.run();
}
}

56
projects/cdk/src/table-list/table-list.module.ts

@ -0,0 +1,56 @@
import { NgxPermissionsModule } from "ngx-permissions";
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";
import { DragDropModule } from "@angular/cdk/drag-drop";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { NzDrawerModule } from "ng-zorro-antd/drawer";
import { NzDropDownModule } from "ng-zorro-antd/dropdown";
import { NzCheckboxModule } from "ng-zorro-antd/checkbox";
import { NzEmptyModule } from "ng-zorro-antd/empty";
import { NzTableModule } from "ng-zorro-antd/table";
import { NzPaginationModule } from "ng-zorro-antd/pagination";
import { NzCardModule } from "ng-zorro-antd/card";
import { NzFormModule } from "ng-zorro-antd/form";
import { NzSpaceModule } from "ng-zorro-antd/space";
import { NzIconModule } from "ng-zorro-antd/icon";
import { NzSwitchModule } from "ng-zorro-antd/switch";
import { NzSkeletonModule } from "ng-zorro-antd/skeleton";
import { NzDividerModule } from "ng-zorro-antd/divider";
import { NzPopoverModule } from "ng-zorro-antd/popover";
import { NzButtonModule } from "ng-zorro-antd/button";
import { TableListComponent } from "./table-list/table-list.component";
import { TableOperationComponent } from "./table-operation/table-operation.component";
import { TdOverflowDirective } from "./td-overflow.directive";
@NgModule({
declarations: [TableListComponent, TableOperationComponent, TdOverflowDirective],
imports: [
CommonModule,
RouterModule,
FormsModule,
ReactiveFormsModule,
DragDropModule,
NzSwitchModule,
NzDividerModule,
NzCheckboxModule,
NzDrawerModule,
NzDropDownModule,
NzEmptyModule,
NzTableModule,
NzPaginationModule,
NzCardModule,
NzFormModule,
NzSpaceModule,
NzButtonModule,
NzPopoverModule,
NzIconModule,
NzSkeletonModule,
NgxPermissionsModule.forRoot(),
],
exports: [TableListComponent, TableOperationComponent],
})
export class TableListModule {
constructor() {}
}

223
projects/cdk/src/table-list/table-list/table-list.component.html

@ -0,0 +1,223 @@
<form nz-form class="query-form" [formGroup]="formGroup" [nzLayout]="searchLayout">
<nz-card nzBorderless nzSize="small"
*ngIf="search || action">
<div nz-row>
<div nz-col nzFlex="auto" nz-row [nzGutter]="[12,12]" class="search-row">
<ng-container *ngIf="search">
<ng-container [ngTemplateOutlet]="search"></ng-container>
<div nz-col nzFlex="120px">
<nz-form-item>
<nz-form-label nzNoColon></nz-form-label>
<nz-form-control>
<nz-space *ngIf="search">
<button nz-button nzGhost nzType="primary" *nzSpaceItem (click)="doQuery()">
查询
</button>
<button nz-button *nzSpaceItem (click)="reset()">
重置
</button>
</nz-space>
</nz-form-control>
</nz-form-item>
</div>
</ng-container>
</div>
<div nz-col class="flex items-center justify-end">
<ng-container *ngIf="action">
<ng-container [ngTemplateOutlet]="action"></ng-container>
</ng-container>
</div>
</div>
</nz-card>
</form>
<ng-template #renderTableTpl>
<nz-card [nzBordered]="false" class="table-card table-list shadow-sm "
[nzBodyStyle]="{padding:0}">
<div #tableEl class="overflow-auto">
<nz-table
#basicTable
[nzData]="dataSource"
[nzPageSizeOptions]="[5,10,20,]"
[nzLoading]="props.pager.loading"
[(nzPageSize)]="props.pager.size"
[(nzPageIndex)]="props.pager.page"
[nzTotal]="props.pager.total"
[nzFrontPagination]="props.frontPagination"
[nzShowPagination]="true"
[nzShowSizeChanger]="true"
[nzShowQuickJumper]="true"
(nzPageIndexChange)="onPageChange()"
(nzPageSizeChange)="onPageChange()"
[nzTableLayout]="props.scroll.x ? 'fixed' : 'auto' "
[nzScroll]="props.scroll">
<thead>
<tr>
<th
*ngIf="props.selectable"
[nzChecked]="dataSource && selection.selected.length === dataSource.length && dataSource.length !== 0"
(nzCheckedChange)="onChecked($event)"
nzWidth="40px">
</th>
<ng-container *ngFor="let col of columns">
<th *ngIf="col.visible"
[nzWidth]="col.width || null "
[nzShowSort]="col.sort"
[nzSortDirections]="['ascend', 'descend']"
[nzSortOrder]="props.pager.sort[col.key] || null"
(nzSortOrderChange)="onSort($event,col.key)">
{{col.title}}
</th>
</ng-container>
<th *ngIf="operation.length" [nzWidth]="optionWidth" [nzRight]="!!props.scroll.x">
操作
</th>
<th nzRight nzWidth="40px" *ngIf="props.theadSettable">
<span (click)="toggleColumnVisible(columnVisibleSettingTpl)"
class="cursor-pointer"
nz-icon
nzType="setting"
nzTheme="outline">
</span>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let dataItem of basicTable.data" (click)="onTrClick(dataItem)"
[ngClass]="rowClass">
<td *ngIf="props.selectable" [nzChecked]="selection.isSelected(dataItem[props.rowKey])"
(nzCheckedChange)="onChecked($event,dataItem[props.rowKey])">
</td>
<ng-container *ngFor="let col of columns">
<td *ngIf="col.visible" nzEllipsis>
<ng-container [ngSwitch]="col.key">
<ng-container *ngSwitchCase="'createTime'">
<ng-container
[ngTemplateOutlet]="dateTimeTpl"
[ngTemplateOutletContext]="{$implicit:dataItem[col.key]}">
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'updateTime'">
<ng-container
[ngTemplateOutlet]="dateTimeTpl"
[ngTemplateOutletContext]="{$implicit:dataItem[col.key]}">
</ng-container>
</ng-container>
<ng-container *ngSwitchDefault>
<ng-container *ngIf="renderColumns else defaultTdTpl"
[ngTemplateOutlet]="renderColumns"
[ngTemplateOutletContext]="{
$implicit:dataItem[col.key],
key:col.key,
row:dataItem,
column:col
}">
</ng-container>
<ng-template #defaultTdTpl>
{{dataItem[col.key]}}
</ng-template>
</ng-container>
</ng-container>
</td>
</ng-container>
<td *ngIf="operation.length" [nzRight]="!!props.scroll.x" class="operation">
<dec-table-operation [rowData]="dataItem" [options]="operation">
</dec-table-operation>
</td>
<td nzRight *ngIf="props.theadSettable"></td>
</tr>
</tbody>
</nz-table>
</div>
</nz-card>
</ng-template>
<!-- 为了计算操作栏的宽度 ---- start -->
<dec-table-operation #operationEl [rowData]="{}" [options]="operation" class="hidden-option">
</dec-table-operation>
<!-- 为了计算操作栏的宽度 ---- end -->
<ng-container *ngIf="renderItem else renderTableTpl">
<ng-container *ngIf="props.pager.loading">
<nz-card [nzBordered]="false">
<nz-skeleton [nzActive]="true"></nz-skeleton>
</nz-card>
</ng-container>
<div class="custom-render" *ngIf="!props.pager.loading">
<ng-container *ngIf="dataSource.length > 0 else emptyTpl">
<div>
<ng-container
[ngTemplateOutlet]="renderItem"
[ngTemplateOutletContext]="{$implicit:dataSource}">
</ng-container>
</div>
<div class="mt-4 flex justify-end">
<nz-pagination [nzPageSizeOptions]="[10,20,50,100]"
[(nzPageSize)]="props.pager.size"
[(nzPageIndex)]="props.pager.page"
[nzTotal]="props.pager.total"
[nzShowTotal]="totalTpl"
[nzShowSizeChanger]="true"
[nzShowQuickJumper]="true"
(nzPageIndexChange)="onPageChange()"
(nzPageSizeChange)="onPageChange()">
</nz-pagination>
</div>
</ng-container>
<ng-template #emptyTpl>
<nz-card [nzBordered]="false">
<nz-empty></nz-empty>
</nz-card>
</ng-template>
</div>
</ng-container>
<ng-template #columnVisibleSettingTpl>
<nz-checkbox-wrapper class="w-full">
<ul cdkDropList (cdkDropListDropped)="colunmsSort($event)" class="columns-list">
<li *ngFor="let item of columns"
class="table-setting-item cursor-pointer" cdkDrag>
<div class="columns-item-placeholder" *cdkDragPlaceholder></div>
<div class="flex items-center justify-between p-2">
<div class="flex-shrink-0">
<span nz-icon nzType="holder" nzTheme="outline"></span>
</div>
<div class="flex-1 pl-1">
{{item.title}}
</div>
<div class="flex-shrink-0">
<nz-switch
nzSize="small"
[disabled]="!!item.disabled"
[(ngModel)]="item.visible"
(ngModelChange)="onColumnsChange()">
</nz-switch>
</div>
</div>
</li>
</ul>
</nz-checkbox-wrapper>
</ng-template>
<ng-template #totalTpl let-total>
<ng-container *ngIf="showTotal">
<ng-template [ngTemplateOutlet]="showTotal" [ngTemplateOutletContext]="{$implicit:props.pager}"></ng-template>
</ng-container>
<ng-container *ngIf="!showTotal">
共{{total}}条
</ng-container>
</ng-template>
<ng-template #dateTimeTpl let-date>
{{date| date:'yyyy-MM-dd HH:mm:ss'}}
</ng-template>

119
projects/cdk/src/table-list/table-list/table-list.component.less

@ -0,0 +1,119 @@
.advance-search-btn {
position: relative;
.up-arrow {
position: absolute;
bottom: calc(-100% + 10px);
left: 50%;
transform: translateX(-50%);
color: #fff;
font-size: 28px;
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.1));
}
.double-arrow {
transform: rotate(90deg);
transition: transform 0.3s;
&.up {
transform: rotate(-90deg);
}
}
}
.hidden-option {
display: inline-block;
height: 0;
opacity: 0;
overflow: hidden;
pointer-events: none;
}
.table-list {
td {
word-break: break-all;
}
th {
white-space: nowrap;
}
::ng-deep {
.ant-table-pagination.ant-pagination {
padding: 0 16px;
}
// tbody {
// tr td:last-child {
// width: 0;
// }
// }
}
}
.advance-search {
::ng-deep {
.ant-card-body {
padding: 16px 24px;
}
}
}
.search-row {
::ng-deep {
> * {
padding: 6px;
}
}
}
.query-form {
::ng-deep {
.hor {
nz-form-item {
align-items: center;
}
}
nz-form-item {
margin-bottom: 0;
// flex-direction: column;
// align-items: flex-start;
// justify-content: flex-start;
// margin-right: 12px;
}
}
}
::ng-deep {
.table-settings {
.ant-drawer-body {
padding: 12px 0;
}
}
.table-setting-item {
background-color: #fff;
&:hover {
background-color: var(--bg-light);
}
}
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 4px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-animating {
transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);
}
.table-setting-item:last-child {
border: none;
}
.columns-list.cdk-drop-list-dragging .table-setting-item:not(.cdk-drag-placeholder) {
transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);
}
.columns-item-placeholder {
background: #fff;
border: 3px dotted var(--p);
min-height: 38px;
transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);
}

386
projects/cdk/src/table-list/table-list/table-list.component.ts

@ -0,0 +1,386 @@
import {
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
TemplateRef,
ViewChild,
Pipe,
PipeTransform,
AfterViewInit,
ElementRef,
ViewChildren,
QueryList,
Renderer2,
ChangeDetectorRef,
OnDestroy,
} from "@angular/core";
import { CommonModule } from "@angular/common";
import { SelectionModel } from "@angular/cdk/collections";
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
import { Router } from "@angular/router";
import { debounceTime, finalize } from "rxjs/operators";
import { FormsModule, FormControl, FormGroup, AbstractControl } from "@angular/forms";
import { NzDrawerModule, NzDrawerService } from "ng-zorro-antd/drawer";
import { DecSafeAny, PageResult, TableListColumns, TableOperation } from "../../types";
import { TableListOption } from "../table-list-options";
import { CacheItem, CacheService, StorageService } from "../../storage";
import { TableOperationComponent } from "../table-operation/table-operation.component";
@Pipe({
name: "operationFilter",
})
export class OperationPipe implements PipeTransform {
transform(operations: TableOperation[], rowItem: any): TableOperation[] {
return operations?.filter((f) => (f.visible ? f.visible(rowItem) : true)) ?? [];
}
}
const DATE_RANGE_FIELDS = ["createTime", "updateTime"];
@Component({
selector: "dec-table-list",
templateUrl: "./table-list.component.html",
styleUrls: ["./table-list.component.less"],
})
export class TableListComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
constructor(
// private modal: NzModalService,
private cdr: ChangeDetectorRef,
private el: ElementRef,
private router: Router,
private drawerService: NzDrawerService,
private storage: StorageService,
private cacheService: CacheService
) {}
@Input() props!: TableListOption;
// https://github.com/angular/angular/issues/13761
@Input() formGroup = new FormGroup<any>({});
/**
*
* $implicit:dataItem[col.key] `具体的值`
* key:col.key `字段名`
* row:dataItem `当前行数据`
* column:col `当前列`
*/
@Input() renderColumns?: TemplateRef<void>;
/**
* 使
*/
@Input() renderItem?: TemplateRef<void>;
@Input() action?: TemplateRef<void>;
@Input() search?: TemplateRef<void>;
@Input() searchLayout: "horizontal" | "vertical" = "horizontal";
/**
* @deprecated
*/
@Input() advanceSearch?: TemplateRef<void>;
@Input() showTotal?: TemplateRef<void>;
@Input() beforeReset?: Function;
@Input() rowClass?: string;
// @Input() resizeable?: boolean;
@Output() onRowClick = new EventEmitter();
dataSource: DecSafeAny[] = [];
totalPages = 0;
public selection = new SelectionModel<string>(true);
public advanceSearchVisible: boolean = false;
public optionWidth: string = "200px";
private cache?: CacheItem;
get columns(): TableListColumns[] {
return this.props.columns ?? [];
}
get operation(): TableOperation[] {
return this.props.operations ?? [];
}
get createTime() {
return this.formGroup.get("createTime");
}
ngOnInit(): void {
this.props.trigger$.pipe(debounceTime(100)).subscribe((e?: any) => {
if (this.props.fetchData) {
this.props.pager.loading = true;
this.selection.clear();
this.emitState();
const query = this.formGroup.getRawValue();
// this.saveQueryDataToCache(query);
const pager = this.parsePager();
this.props
.fetchData(pager, this.parseQueryValue(), e)
.pipe(
finalize(() => {
this.props.pager.loading = false;
})
)
.subscribe((f: PageResult) => {
this.dataSource = f.records ?? f.content;
console.log("this.dataSource", f);
this.props.pager.total = f.total;
this.totalPages = Math.ceil(f.total / this.props.pager.size);
this.checkPage();
});
}
});
setTimeout(() => {
this.emitState();
}, 10);
// 初始化的时候?
if (!this.props.manual) {
this.props.run();
}
this.getCacheData();
}
ngOnDestroy(): void {
// console.log("ngOnDestroy");
}
ngAfterViewInit(): void {
const opEl: HTMLDivElement = this.el.nativeElement.querySelector(".hidden-option");
if (opEl) {
setTimeout(() => {
const paddingX = 2 * 24;
this.optionWidth = Math.ceil(opEl.offsetWidth + paddingX) + "px";
this.cdr.detectChanges();
});
}
}
ngOnChanges(changes: SimpleChanges): void {
const props = changes["props"];
const currentProps = props?.currentValue;
const previousProps = props?.previousValue;
if (currentProps) {
if (previousProps?.["cacheKey"] !== currentProps?.["cacheKey"]) {
this.removeOldCache();
this.getCacheData();
}
this.parseColumus(currentProps?.["columns"]);
this.parseFormControls(currentProps?.["queryForm"]);
}
}
private saveQueryDataToCache(query: {}) {
if (this.cache) {
const { page, size, sort, total } = this.props.pager;
this.cache.setItem({ page, size, sort, total, ...query });
}
}
private removeOldCache() {
this.cache?.remove();
}
private getCacheData() {
let { cacheKey, cacheTo = [] } = this.props;
this.formGroup.reset();
if (!cacheKey) {
cacheKey = `DATA_CACHE_${this.formatePathname()}`;
}
if (!this.cache) {
this.cache = this.cacheService.initCache(cacheKey, [this.router.url, ...cacheTo]);
}
const cacheData = this.cache?.getItem();
if (cacheData) {
const { page, size, sort, total, ...query } = cacheData;
this.props.pager = {
...this.props.pager,
total,
page,
size,
sort,
};
this.formGroup.patchValue(query);
}
}
private parsePager() {
let { page, size } = this.props.pager;
if (this.props.pageFromZero) {
page = page - 1;
}
const pager = { page: page, current: page, size, sort: this.formatSort() };
return pager;
}
private parseQueryValue(): {} {
const o = Object.create(null);
Object.entries(this.formGroup.getRawValue()).forEach(([k, v]) => {
if (DATE_RANGE_FIELDS.includes(k) && Array.isArray(v)) {
const from = v?.[0] instanceof Date ? v?.[0]?.toISOString() : v[0];
const to = v?.[1] instanceof Date ? v?.[1]?.toISOString() : v[1];
o[k] = { from, to };
} else {
o[k] = v;
}
});
return o;
}
private checkPage() {
const maxPages = this.totalPages === 0 ? 1 : this.totalPages;
if (maxPages < this.props.pager.page) {
this.props.pager.page = maxPages;
this.reload();
}
}
private emitState() {
this.props.getState$.emit({ selectedKeys: this.selection.selected });
}
private parseColumus(currentCols: TableListColumns[]) {
if (this.props.columns.some((s) => s.coverStorage)) {
this.onColumnsChange();
}
const colsFormStorage: TableListColumns[] = this.storage.get(this.formatePathname()) ?? [];
this.props.columns = currentCols
.map((i: TableListColumns) => {
const storageCol = colsFormStorage.find((f) => f.key === i.key);
let visible = i.visible !== void 0 ? i.visible : true;
let width = i.width;
// if (["createTime", "updateTime"].includes(i.key)) {
// width = "180px";
// }
return {
...i,
visible,
width,
...(storageCol ?? {}),
};
})
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
private parseFormControls(formGroup?: FormGroup) {
// if (!this.props.withOutDefaultColumns) {
// this.formGroup.addControl("updateTime", new FormControl(null));
// this.formGroup.addControl("createTime", new FormControl(null));
// }
}
formatSort() {
const { sort } = this.props.pager;
if (!sort) {
return null;
}
const sortArr = [...Object.entries(sort)]?.[0];
if (!sortArr) {
return null;
}
return `${sortArr[0]},${sortArr[1] === "ascend" ? "asc" : "desc"}`;
}
colunmsSort(event: unknown) {
const e = event as CdkDragDrop<TableListColumns[]>;
moveItemInArray(this.props.columns, e.previousIndex, e.currentIndex);
this.onColumnsChange();
}
reload() {
this.props.run();
}
onPageChange() {
if (!this.props.frontPagination) {
this.props.run();
}
}
doQuery() {
this.props.pager.page = 1;
this.reload();
}
reset() {
this.beforeReset?.();
this.formGroup.reset();
this.doQuery();
}
onSort(v: string | null, fieldName: string) {
this.props.pager.sort = { [fieldName]: v as "ascend" | "descend" };
this.reload();
}
toggleAdvanceSearch(show?: boolean) {
if (typeof show === "boolean") {
this.advanceSearchVisible = show;
} else {
this.advanceSearchVisible = !this.advanceSearchVisible;
}
}
filteroperation(dataItem: any): TableOperation[] {
return this.operation?.filter((f) => (f.visible ? f.visible(dataItem) : true)) ?? [];
}
toggleColumnVisible(nzContent: TemplateRef<DecSafeAny>) {
this.drawerService.create({
nzTitle: "设置展示项",
nzWidth: 280,
nzContent,
nzWrapClassName: "table-settings",
});
}
private formatePathname() {
return this.props.columnKey ?? this.router.url.replace(/\//g, "_");
}
onColumnsChange() {
const columnStorageKey = `COLUMN_${this.formatePathname()}`;
this.storage.set(
columnStorageKey,
this.columns.map((i, idx) => {
const { key, width, visible } = i;
return { key, width, visible, order: idx };
})
);
}
onChecked(checked: boolean, rowKey?: DecSafeAny) {
const fn = checked ? "select" : "deselect";
const rowKeys = rowKey ? [rowKey] : this.dataSource.map((i) => i[this.props.rowKey]);
this.selection[fn](...rowKeys);
this.emitState();
}
onTrClick(dataItem: DecSafeAny) {
if (this.onRowClick) {
this.onRowClick.emit(dataItem);
}
}
}

30
projects/cdk/src/table-list/table-operation/table-operation.component.html

@ -0,0 +1,30 @@
<div class="operation">
<ng-container *ngFor="let item of options.slice(0,maximum); let i = index; let last = last">
<span [ngClass]="{disabled:item.disabled}">
<a [ngClass]="{danger:item.danger}" [href]="item.href" [routerLink]="item.link"
(click)="onClick(item)">
{{item.title}}
</a>
</span>
<nz-divider nzType="vertical" *ngIf="!last"></nz-divider>
</ng-container>
<ng-container *ngIf="options.length > maximum">
<nz-divider nzType="vertical"></nz-divider>
<a nz-dropdown [nzDropdownMenu]="menu">
更多操作
<i nz-icon nzType="down" nzTheme="outline"></i>
</a>
<nz-dropdown-menu #menu="nzDropdownMenu">
<ul nz-menu class="operation">
<li
*ngFor="let item of options.slice(maximum);"
nz-menu-item
(click)="onClick(item)"
[nzDanger]="item.danger"
[nzDisabled]="item.disabled">
{{item.title}}
</li>
</ul>
</nz-dropdown-menu>
</ng-container>
</div>

25
projects/cdk/src/table-list/table-operation/table-operation.component.less

@ -0,0 +1,25 @@
.operation {
display: inline-block;
min-width: 120px;
}
.danger {
color: var(--red);
}
.disabled {
cursor: not-allowed;
a {
pointer-events: none;
color: rgba(0, 0, 0, 0.65);
}
}
.operation {
a {
// color: rgba(0, 0, 0, 0.65);
&:hover {
color: var(--p);
text-decoration: underline;
}
}
}

61
projects/cdk/src/table-list/table-operation/table-operation.component.ts

@ -0,0 +1,61 @@
import { Component, Input, OnInit } from "@angular/core";
import { NgxPermissionsService } from "ngx-permissions";
import { DecSafeAny, TableOperation } from "../../types";
@Component({
selector: "dec-table-operation",
templateUrl: "./table-operation.component.html",
styleUrls: ["./table-operation.component.less"],
})
export class TableOperationComponent implements OnInit {
constructor(private premission: NgxPermissionsService) {}
@Input() options: TableOperation[] = [];
@Input() maximum: number = 3;
@Input() rowData: DecSafeAny;
ngOnInit(): void {
// this.options = this.options.filter(async (f) => {
// if (f.visible) {
// return f.visible(this.rowData);
// }
// if (f.premissions.length) {
// console.log("f.premissions", f.premissions, this.premission.hasPermission(f.premissions));
// return await this.premission.hasPermission(f.premissions);
// }
// return true;
// });
this.filterOption();
}
async filterOption() {
const o = [];
for (const f of this.options) {
let visible = true;
if (typeof f.visible === "function") {
visible = f.visible(this.rowData);
}
if (!visible) {
continue;
}
if (f.premissions.length) {
const r = await this.premission.hasPermission(f.premissions);
if (r) {
o.push(f);
}
continue;
}
o.push(f);
}
this.options = o;
}
onClick(item: TableOperation) {
if (item.onClick) {
item?.onClick(this.rowData);
}
}
}

30
projects/cdk/src/table-list/td-overflow.directive.ts

@ -0,0 +1,30 @@
import { AfterContentInit, AfterViewInit, ChangeDetectorRef, Directive, ElementRef, Input } from "@angular/core";
import { NzPopoverDirective } from "ng-zorro-antd/popover";
@Directive({
selector: "[jwTdOverflow]",
})
export class TdOverflowDirective implements AfterViewInit {
@Input("jwTdOverflow") content!: string;
constructor(
private elementRef: ElementRef,
private popoverDirective: NzPopoverDirective,
private cdr: ChangeDetectorRef
) {}
ngOnInit() {}
ngAfterViewInit(): void {
const element = this.elementRef.nativeElement as HTMLElement;
console.log("element", element.offsetWidth, element.scrollWidth);
// 如果元素的实际宽度大于可见宽度,就使用 nz-popover 指令来显示完整的内容
if (element.offsetWidth < element.scrollWidth) {
this.popoverDirective.content = this.content;
} else {
this.popoverDirective.trigger = null;
this.popoverDirective.content = "da";
element.textContent = this.content;
}
}
}

58
projects/cdk/src/types/index.ts

@ -0,0 +1,58 @@
export type AnyObject = { [k: string]: any };
export type DecSafeAny = any;
export type DecText = number | string;
export type Augmented<O extends object> = O & AnyObject;
export interface ResponseType<T = any> {
body: T;
code: number;
desc: string;
success: boolean;
}
export interface TableListColumns {
key: string;
title: string;
visible?: boolean;
width?: string;
sort?: boolean;
order?: number;
disabled?: boolean;
coverStorage?: boolean;
}
export interface TableOperation {
title: string;
href?: string;
link?: string[];
target?: string;
premissions: string[];
danger?: boolean;
disabled?: boolean;
onClick?: (v: DecSafeAny) => void;
visible?: (v: DecSafeAny) => boolean;
}
export interface PageResult<T = DecSafeAny> {
total: number;
content: T[];
records: T[];
}
export interface AuthInterface {
role: string;
userId: string;
userName: string;
permissionList: AuthPermissionInterface[];
}
export interface AuthPermissionInterface {
name: string;
roleId: string;
scope: 1 | 0;
type: number;
value: "true" | "false";
}

46
projects/cdk/src/utils/index.ts

@ -0,0 +1,46 @@
import { AbstractControl, FormControl, FormGroup } from "@angular/forms";
export class Utils {
static validateFormGroup(formGroup: FormGroup) {
if (!formGroup.valid) {
Object.keys(formGroup.controls).forEach((field) => {
const control = formGroup.get(field);
if (control instanceof FormControl) {
control.markAsDirty();
control.markAsTouched({ onlySelf: true });
control.updateValueAndValidity({ onlySelf: true });
} else if (control instanceof FormGroup) {
Utils.validateFormGroup(control);
}
});
}
return formGroup.valid;
}
static validateFormControl(control: AbstractControl) {
control.markAsDirty();
control.markAsTouched({ onlySelf: true });
control.updateValueAndValidity({ onlySelf: true });
return control.valid;
}
static queryify(query: {}): string {
const o = Object.create(null);
Object.entries(query).forEach(([k, v]) => {
if (v !== void 0 && v !== null) {
o[k] = v;
}
});
return new URLSearchParams(o).toString();
}
static getHostByEnvironment(prod?: boolean) {
const protocol = window.location.protocol;
const host = prod ? window.location.host : `localhost:${window.location.port}`;
return `${protocol}//${host}`;
}
// static detectionImagePath(jobId: string, imgName: string) {
// return `/record/${jobId}/detect/${imgName}`;
// }
}

66
projects/cdk/src/validators/index.ts

@ -0,0 +1,66 @@
import { ValidatorFn, Validators } from "@angular/forms";
export class DecValidators {
static required(message?: string): ValidatorFn {
return (control) => {
const error = Validators.required(control);
return error ? { ...error, required: { message } } : null;
};
}
static maxLength(maxLength: number, message?: string): ValidatorFn {
return (control) => {
const error = Validators.maxLength(maxLength)(control);
if (error) {
error["maxlength"]["message"] = message;
return error;
}
return null;
};
}
static minLength(maxLength: number, message?: string): ValidatorFn {
return (control) => {
const error = Validators.minLength(maxLength)(control);
if (error) {
error["minlength"]["message"] = message;
return error;
}
return null;
};
}
static pattern(pattern: RegExp | string, message?: string): ValidatorFn {
return (control) => {
const error = Validators.pattern(pattern)(control);
if (error) {
error["pattern"]["message"] = message;
return error;
}
return null;
};
}
static min(maxLength: number, message?: string): ValidatorFn {
return (control) => {
const error = Validators.min(maxLength)(control);
if (error) {
error["min"]["message"] = message;
return error;
}
return null;
};
}
static max(maxLength: number, message?: string): ValidatorFn {
return (control) => {
const error = Validators.max(maxLength)(control);
if (error) {
error["max"]["message"] = message;
return error;
}
return null;
};
}
static email(message?: string): ValidatorFn {
return (control) => {
const error = Validators.email(control);
return error ? { ...error, email: { message } } : null;
};
}
}

14
projects/cdk/tsconfig.lib.json

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": [
"**/*.spec.ts"
]
}

10
projects/cdk/tsconfig.lib.prod.json

@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

14
projects/cdk/tsconfig.spec.json

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

15
projects/client/proxy.conf.json

@ -0,0 +1,15 @@
{
"/api": {
"target": "http://47.109.27.8:8081",
"secure": false
},
"/record": {
"target": "http://47.109.27.8",
"secure": false
},
"/websocket": {
"target": "http://47.109.27.8:8081",
"secure": false,
"ws": true
}
}

26
projects/client/src/app/app-routing.module.ts

@ -0,0 +1,26 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { authGuard } from "./core/gaurd/auth.guard";
const routes: Routes = [
{
path: "auth",
loadChildren: () => import("./feature/auth/auth.module").then((m) => m.AuthModule),
},
{
path: "",
pathMatch: "full",
redirectTo: "detection",
},
{
path: "detection",
loadChildren: () => import("./feature/detection/detection.module").then((m) => m.DetectionModule),
canActivate: [authGuard],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

4
projects/client/src/app/app.component.html

@ -0,0 +1,4 @@
<ng-container *ngIf="!loading">
<app-bg-border></app-bg-border>
<router-outlet></router-outlet>
</ng-container>

0
projects/client/src/app/app.component.less

73
projects/client/src/app/app.component.ts

@ -0,0 +1,73 @@
import { Component, OnInit, Renderer2 } from "@angular/core";
import { AuthService } from "@client/app/core/services";
import { delay, finalize } from "rxjs";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.less"],
})
export class AppComponent implements OnInit {
constructor(private api: AuthService, private rd2: Renderer2) {}
loading = true;
ngOnInit(): void {
this.loading = true;
this.api
.getSystemInfo()
.pipe(
finalize(() => {
this.loading = false;
})
)
.subscribe((res) => {
if (res.theme) {
let style = "";
Object.entries(res.theme as Record<string, string>).forEach(([k, v]) => {
if (!["themeName", "themeId"].includes(k)) {
const key = k.replaceAll("_", "-");
style += `--${key}:${v};`;
if (k === "primary") {
const pRgba = this.parseRGB(v);
style += `--p-rgb:${pRgba.join(",")};`;
style += `--p:${v};`;
}
if (k === "text_color") {
style += `--thead-color:${v};`;
style += `--input-color:${v};`;
style += `--table-color:${v};`;
}
}
});
this.rd2.setAttribute(document.body, "style", style);
}
if (res.systemInfo) {
const { reservedField, systemInfoName } = res.systemInfo;
const title = (reservedField ? `${reservedField} · ` : "") + systemInfoName;
document.title = title;
}
});
}
parseRGB(color: string) {
color = color.replace(/\s/g, "");
const rgbRegex = /^rgb\((\d+),(\d+),(\d+)\)$/;
const rgbaRegex = /^rgba\((\d+),(\d+),(\d+),(\d+(\.\d+)?)\)$/;
let matches = color.match(rgbRegex) || color.match(rgbaRegex);
if (matches) {
const r = parseInt(matches[1], 10);
const g = parseInt(matches[2], 10);
const b = parseInt(matches[3], 10);
return [r, g, b];
} else {
return [];
}
}
}

47
projects/client/src/app/app.module.ts

@ -0,0 +1,47 @@
import { HTTP_INTERCEPTORS, HttpClientModule } from "@angular/common/http";
import { NgModule } from "@angular/core";
import { HashLocationStrategy, LocationStrategy, registerLocaleData } from "@angular/common";
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { NzConfig, NZ_CONFIG } from "ng-zorro-antd/core/config";
import { NZ_I18N } from "ng-zorro-antd/i18n";
import { zh_CN } from "ng-zorro-antd/i18n";
import zh from "@angular/common/locales/zh";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { SharedModule } from "./shared/shared.module";
import { NgxPermissionsModule } from "ngx-permissions";
import { ClientHTTPInterceptor } from "./core/services/client.interceptor";
registerLocaleData(zh);
const ngZorroConfig: NzConfig = {
pageHeader: {
nzGhost: false,
},
};
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
SharedModule,
AppRoutingModule,
NgxPermissionsModule.forRoot(),
],
providers: [
{ provide: NZ_I18N, useValue: zh_CN },
{ provide: NZ_CONFIG, useValue: ngZorroConfig },
{
provide: LocationStrategy,
useClass: HashLocationStrategy,
},
{ provide: HTTP_INTERCEPTORS, useClass: ClientHTTPInterceptor, multi: true },
],
bootstrap: [AppComponent],
})
export class AppModule {}

27
projects/client/src/app/core/gaurd/auth.guard.ts

@ -0,0 +1,27 @@
import { inject } from "@angular/core";
import { Router } from "@angular/router";
import { decConfigToken } from "@cdk/public-api";
import { map } from "rxjs";
import { DetectionApiService } from "../services";
export const authGuard = () => {
const api = inject(DetectionApiService);
const router = inject(Router);
const decConfig = inject(decConfigToken);
// const token = localStorage.getItem(decConfig.localStroageKey);
// if (!token) {
// router.navigate([decConfig.loginUrl]);
// return false;
// }
return api.getAllPoint().pipe(
map((res) => {
if (res) {
return true;
} else {
router.navigate([decConfig.loginUrl]);
return false;
}
})
);
};

33
projects/client/src/app/core/gaurd/permisson.guard.ts

@ -0,0 +1,33 @@
import { inject } from "@angular/core";
import {
ActivatedRoute,
ActivatedRouteSnapshot,
CanActivateChildFn,
CanActivateFn,
Route,
Router,
RouterStateSnapshot,
} from "@angular/router";
import { AuthInterface } from "@cdk/public-api";
import { NgxPermissionsService } from "ngx-permissions";
export const PermissionLoadGuard: CanActivateFn = async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const permissionsService = inject(NgxPermissionsService);
const auth = localStorage.getItem("auth");
if (auth) {
try {
const authData = JSON.parse(auth) as AuthInterface;
if (Array.isArray(authData.permissionList)) {
const permissionList = authData.permissionList;
const permissions = permissionList.reduce((a, c) => {
if (c.scope === 0 && c.value === "true") {
return a.concat(c.roleId);
}
return a;
}, [] as string[]);
permissionsService.loadPermissions(permissions);
}
} catch (error) {}
}
return true;
};

56
projects/client/src/app/core/services/auth.service.ts

@ -0,0 +1,56 @@
import { HttpClient, HttpParams, HttpResponse } from "@angular/common/http";
import { Inject, Injectable } from "@angular/core";
import { DecConfig, decConfigToken } from "@cdk/public-api";
import { AnyObject, ResponseType } from "@cdk/types";
import { environment } from "@client/environments/environment";
import { map, of, tap } from "rxjs";
export interface SystemInfoInterface {
theme?: Record<string, string>;
info?: { logoImg: string; systemInfoName: string };
}
@Injectable({
providedIn: "root",
})
export class AuthService {
constructor(private http: HttpClient, @Inject(decConfigToken) private decConfig: Required<DecConfig>) {}
private system: SystemInfoInterface = {};
private systemLoaded = false;
getSystemInfo() {
if (this.systemLoaded) {
return of(this.system);
}
return this.http.post<ResponseType>("/api/config/selectSystemInfoById", {}).pipe(
map((res) => {
return res.body;
}),
tap((res) => {
if (res.systemInfo) {
localStorage.setItem("systemInfo", JSON.stringify(res.systemInfo));
this.system.info = res.systemInfo;
}
if (res.theme) {
localStorage.setItem("theme", JSON.stringify(res.theme));
this.system.theme = res.theme;
}
console.log("this.system", this.system);
this.systemLoaded = true;
})
);
}
login(vals: AnyObject) {
const auth = { ...vals, clientVersion: environment.clientVersion, clientType: environment.clientType };
return this.http.post<ResponseType>("/api/user/login", auth).pipe(
tap((res) => {
if (res.success) {
localStorage.setItem("auth", JSON.stringify(res.body));
}
})
);
}
}

43
projects/client/src/app/core/services/client.interceptor.ts

@ -0,0 +1,43 @@
import { Inject, Injectable } from "@angular/core";
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse,
HttpResponse,
} from "@angular/common/http";
import { catchError, Observable, switchMap, tap, throwError, timer } from "rxjs";
import { Router } from "@angular/router";
import { NzMessageService } from "ng-zorro-antd/message";
import { ResponseType } from "@cdk/types";
import { AuthService } from "./auth.service";
@Injectable({ providedIn: "root" })
export class ClientHTTPInterceptor implements HttpInterceptor {
constructor(private auth: AuthService, private router: Router) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.handleResult(next, req);
}
private handleResult(next: HttpHandler, authReq: HttpRequest<any>): Observable<HttpEvent<any>> {
return next.handle(authReq).pipe(
catchError((err: HttpErrorResponse) => {
const throwErr = throwError(() => err);
const error: ResponseType = err.error;
if (error.success === false) {
if (error.code === 401) {
this.auth.login({ uid: "", password: "" }).subscribe(() => {
this.router.navigate(["/"]);
});
}
}
return throwErr;
})
);
}
}

269
projects/client/src/app/core/services/detection-api.service.ts

@ -0,0 +1,269 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { AnyObject, Augmented, DecText, ResponseType } from "@cdk/types";
import { Utils } from "@cdk/utils";
import { AlarmDTO, AlgorithmDTO } from "@client/dtos";
import {
DeviceDTO,
PointDTO,
PointGroupDTO,
PointStatusEnum,
PoleItemDTO,
PoleQueryDTO,
PowerStationDTO,
} from "@client/dtos/point.dto";
import { map, Observable, of, tap } from "rxjs";
@Injectable({
providedIn: "root",
})
export class DetectionApiService {
constructor(private http: HttpClient) {}
allPoint: PowerStationDTO[] = [];
imgBaseUrl: string = "";
loadImage(jobId: string, img: string) {
return this.http.post<ResponseType<string>>("/api/img/base", { jobId, img }).pipe(
map((res) => {
return "data:image/jpeg;base64," + res.body;
})
);
}
loadImages(url: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
if (url.startsWith("data:image/jpeg;base64,")) {
resolve(url);
return;
}
this.http.get(url, { responseType: "blob" }).subscribe({
next(blob: Blob) {
// setTimeout(() => {
// resolve("");
// }, 150);
const reader = new FileReader();
reader.onloadend = () => {
resolve(reader.result as string);
};
reader.readAsDataURL(blob);
},
error(error) {
reject(new Error(`Failed to load image ${url}${error}`));
},
});
});
}
getAllPoint(force?: boolean) {
if (this.allPoint.length > 0 && !force) {
return of(this.allPoint);
}
return this.http.post<ResponseType<PowerStationDTO[]>>("/api/point/getAll", {}).pipe(
map((res) => {
return res.body.map((station) => {
return {
...station,
groupList: station.groupList.map((group) => {
let status = PointStatusEnum.NORMAL;
if (group.pointList.some((s) => s.status === PointStatusEnum.ABNORMAL)) {
status = PointStatusEnum.ABNORMAL;
} else if (group.pointList.some((s) => s.status === PointStatusEnum.DISCONNECT)) {
status = PointStatusEnum.DISCONNECT;
}
return {
...group,
status,
};
}),
};
});
}),
tap((points) => {
this.allPoint = points;
})
);
}
getImageBaseUrl(force?: boolean) {
if (!force) {
if (this.imgBaseUrl) {
return of(this.imgBaseUrl);
}
}
return this.http.post<ResponseType<string>>("/api/img/path", {}).pipe(
map((res) => {
return res.body;
}),
tap((imgBaseUrl) => {
this.imgBaseUrl = imgBaseUrl;
})
);
}
getFlatPoints(): Observable<{ points: PointDTO[]; groups: PointGroupDTO[] }> {
return this.getAllPoint().pipe(
map((p) => {
const groups = p?.[0]?.groupList;
const points: PointDTO[] = [];
if (Array.isArray(groups)) {
groups.forEach((g) => {
points.push(
...g.pointList.map((p) => ({
...p,
gid: g.motorGroupId,
gname: g.name,
}))
);
});
}
return { points, groups };
})
);
}
getNotice() {
return this.http.post<ResponseType<AlarmDTO[]>>("/api/detect/alarm", null);
}
getRealtimeJob(pointId: DecText) {
return this.http.post<ResponseType<AnyObject>>("/api/detect/getRealtimeJob", { pointId });
}
getDetcttionHistoryPage(page: {}, query: {}) {
const q = Object.assign({}, page, query);
return this.http.post<ResponseType>(`/api/history/query`, q).pipe(
map((t) => {
return {
...t.body,
content: t.body.records,
};
})
);
}
getHistoryDetail(jobId: DecText) {
return this.http
.post<ResponseType<any>>(`/api/history/detail`, {
jobId,
current: 1,
size: 5,
})
.pipe(
map((r) => {
return r.body.dataList;
})
);
}
exportWord(data: {}) {
return this.http.post("/api/history/exportWord", data, {
observe: "response",
responseType: "blob" as "json",
});
}
exportExcel(data: {}) {
return this.http.post("/api/history/exportExcel", data, {
observe: "response",
responseType: "blob" as "json",
});
}
getQuery(jobId: string) {
return this.http.post<ResponseType<PoleQueryDTO>>("/api/detect/query", { jobId }).pipe(
map((r) => {
return r;
return {
body: {},
success: true,
};
})
);
// .pipe(
// map((i) => {
// i.body[5][2].value = 6;
// i.body[8][8].value = 116;
// i.body[4][8].alarm = true;
// i.body[2][8].alarm = true;
// i.body[1][8].alarm = true;
// i.body[1][10].alarm = true;
// i.body[20][10].alarm = true;
// i.body[4][8].alarm = true;
// i.body[15][9].alarm = true;
// i.body[11][1].alarm = true;
// return i;
// })
// );
}
getImg(jobId: string) {
return this.http.post<ResponseType<Record<number, string[]>>>("/api/detect/getImg", { jobId });
}
getBase64Image(o: { jobID: string; img: string }) {
return this.http.post("/api/img/base", o);
}
getFeatures(pointId?: string) {
// return of({
// body: {
// line: "引出线变形",
// bolt: "螺栓松动",
// temperature: "无线测温",
// pole: "磁极开闸",
// },
// });
return this.http.post<ResponseType<Record<string, string>>>("/api/analysis/features", { pointId });
}
manualDetection(pointId: string) {
return this.http.post<ResponseType>("/api/detect/manualDetection", { pointId });
}
getAnalysisChartData(q: {}) {
return this.http.post<ResponseType<Record<string, Augmented<PoleItemDTO>[]>>>("/api/analysis/chartData", q);
}
getVersion() {
return this.http.post<ResponseType>("/api/config/selectVersion", null);
}
getExportConfig(config: {}) {
return this.http.post<ResponseType>("/api/config/select", config);
}
updateExportConfig(config: {}) {
return this.http.post<ResponseType>("/api/config/update", config);
}
resetPointData(pointId: string) {
return this.http.post<ResponseType>("/api/point/reset", { pointId });
}
savePointEnable(data: AnyObject[]) {
return this.http.post<ResponseType>("/api/point/switch", data);
}
getAlgorithm() {
return this.http.post<ResponseType<AlgorithmDTO[]>>("/api/algorithm/selectParams", null);
}
saveAlgorithm(vals: {}) {
return this.http.post<ResponseType>("/api/algorithm/updateParams", vals);
}
saveTemperatureSetting(vals: {}) {
return this.http.post<ResponseType<AlgorithmDTO[]>>("/api/device/updateTemp", vals);
}
getTemperatureSettings() {
return this.http.post<ResponseType>("/api/device/selectTemp", null);
}
getDeviceListByPointId(pointId: string) {
return this.http.post<ResponseType<DeviceDTO>>("/api/device/select", { pointId });
}
}

4
projects/client/src/app/core/services/index.ts

@ -0,0 +1,4 @@
export * from "./auth.service";
export * from "./detection-api.service";
export * from "./websocket-api.service";
export * from "./utils.service";

11
projects/client/src/app/core/services/utils.service.ts

@ -0,0 +1,11 @@
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { ResponseType } from "@cdk/types";
import { map } from "rxjs";
@Injectable({
providedIn: "root",
})
export class UtilsService {
constructor(private http: HttpClient) {}
}

65
projects/client/src/app/core/services/websocket-api.service.ts

@ -0,0 +1,65 @@
import { Injectable } from "@angular/core";
import { WebSocketSubject } from "rxjs/webSocket";
import { environment } from "@client/environments/environment";
@Injectable({
providedIn: "root",
})
export class WebsocketApiService {
connect(wsUrl: string): WebSocketSubject<any> {
const protocol = window.location.protocol.replace("http", "ws");
const host = environment.production ? window.location.host : `localhost:${window.location.port}`;
wsUrl = `${protocol}//${host}${wsUrl}`;
console.log("wsUrl", environment, wsUrl);
return new WebSocketSubject(wsUrl);
}
}
// const CHAT_URL = "ws://localhost:8082";
// @Injectable({
// providedIn: "root",
// })
// export class WebsocketApiService {
// private subject?: AnonymousSubject<MessageEvent>;
// public messages: Subject<AnyObject>;
// constructor() {
// this.messages = <Subject<AnyObject>>this.connect(CHAT_URL).pipe(
// map((response: MessageEvent): AnyObject => {
// console.log(response.data);
// let data = JSON.parse(response.data);
// return data;
// })
// );
// }
// public connect(url: string): AnonymousSubject<MessageEvent> {
// if (!this.subject) {
// this.subject = this.create(url);
// console.log("Successfully connected: " + url);
// }
// return this.subject;
// }
// private create(url: string): AnonymousSubject<MessageEvent> {
// let ws = new WebSocket(url);
// let observable = new Observable((obs: Observer<MessageEvent>) => {
// ws.onmessage = obs.next.bind(obs);
// ws.onerror = obs.error.bind(obs);
// ws.onclose = obs.complete.bind(obs);
// return ws.close.bind(ws);
// });
// let observer = {
// error: () => {},
// complete: () => {},
// next: (data: AnyObject) => {
// console.log("Message sent to websocket: ", data, ws.readyState, WebSocket.OPEN);
// if (ws.readyState === WebSocket.OPEN) {
// ws.send(JSON.stringify(data));
// }
// },
// };
// return new AnonymousSubject<MessageEvent>(observer, observable);
// }
// }

16
projects/client/src/app/feature/auth/auth-routing.module.ts

@ -0,0 +1,16 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { LoginComponent } from "./pages";
const routes: Routes = [
{
path: "login",
component: LoginComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AuthRoutingModule {}

11
projects/client/src/app/feature/auth/auth.module.ts

@ -0,0 +1,11 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "@client/app/shared/shared.module";
import { AuthRoutingModule } from "./auth-routing.module";
import { LoginComponent } from "./pages";
@NgModule({
declarations: [LoginComponent],
imports: [SharedModule, AuthRoutingModule],
})
export class AuthModule {}

1
projects/client/src/app/feature/auth/pages/index.ts

@ -0,0 +1 @@
export * from "./login/login.component";

57
projects/client/src/app/feature/auth/pages/login/login.component.html

@ -0,0 +1,57 @@
<section class="h-full flex items-center justify-center">
<div class="login relative z-10">
<div class="text-center text-white">
<ng-container *ngIf="(system$ |async) as system">
<img
class=" h-[58px]"
[src]="system.info.logoImg ? 'data:image/png' + ';base64,' + system.info.logoImg :'/assets/imgs/logo.png' | publicPath" />
<h1 *ngIf="system.info.reservedField" class="text-white mt-[25px]">{{system.info.reservedField}}</h1>
<p>{{system.info.systemInfoName}}</p>
</ng-container>
<form nz-form [formGroup]="loginForm">
<nz-form-item>
<nz-form-control [nzErrorTip]="formErrorTipsTpl">
<nz-input-group [nzPrefix]="prefixTemplateUser">
<input nz-input placeholder="账户" formControlName="uid" />
</nz-input-group>
<ng-template #prefixTemplateUser>
<!-- <span nz-icon nzType="user" class="text-white"></span> -->
<img [src]="'/assets/icons/user.svg' | publicPath" />
<nz-divider nzType="vertical"></nz-divider>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control [nzErrorTip]="formErrorTipsTpl">
<nz-input-group [nzPrefix]="prefixTemplatePassword">
<input nz-input type="password" placeholder="密码" formControlName="password" />
</nz-input-group>
<ng-template #prefixTemplatePassword>
<!-- <span nz-icon nzType="lock" class="text-white"></span> -->
<img [src]="'/assets/icons/lock.svg' | publicPath" />
<nz-divider nzType="vertical"></nz-divider>
</ng-template>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control>
<button nz-button
nzType="primary"
nzBlock
class="btn"
(click)="onLogin()"
[nzLoading]="loading">
登录
</button>
</nz-form-control>
</nz-form-item>
</form>
</div>
</div>
</section>
<ng-template #formErrorTipsTpl let-control>
<div class="text-left">
<dec-form-error-tips [control]="control"></dec-form-error-tips>
</div>
</ng-template>

23
projects/client/src/app/feature/auth/pages/login/login.component.less

@ -0,0 +1,23 @@
.login {
width: 320px;
height: 404px;
h1 {
margin-bottom: 12px;
font-size: 40px;
font-weight: 400;
}
p {
font-size: 16px;
line-height: 24px;
letter-spacing: 0.23em;
text-shadow: 0px 8px 20px rgba(0, 0, 0, 0.1);
}
form {
margin-top: 48px;
}
.btn {
margin-top: 16px;
}
}

50
projects/client/src/app/feature/auth/pages/login/login.component.ts

@ -0,0 +1,50 @@
import { Router } from "@angular/router";
import { Component, Inject, OnInit } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { Utils } from "@cdk/utils";
import { DecValidators } from "@cdk/validators";
import { PUBLIC_PATH } from "@cdk/dec-module/base-href";
import { AuthService } from "@client/app/core/services";
import { NzMessageService } from "ng-zorro-antd/message";
import { finalize, lastValueFrom, tap } from "rxjs";
@Component({
selector: "app-login",
templateUrl: "./login.component.html",
styleUrls: ["./login.component.less"],
})
export class LoginComponent implements OnInit {
constructor(
@Inject(PUBLIC_PATH) public baseHref: string,
private msg: NzMessageService,
private api: AuthService,
private router: Router
) {}
public loginForm = new FormGroup({
uid: new FormControl("", [DecValidators.required("请输入账户")]),
password: new FormControl("", [DecValidators.required("请输入密码")]),
});
public loading: boolean = false;
system$ = this.api.getSystemInfo();
ngOnInit(): void {}
async onLogin() {
if (Utils.validateFormGroup(this.loginForm)) {
const { value } = this.loginForm;
this.loading = true;
const res = await lastValueFrom(
this.api.login(value).pipe(
finalize(() => {
this.loading = false;
})
)
);
this.msg.success(res.desc);
this.router.navigate(["/"]);
}
}
}

153
projects/client/src/app/feature/detection/components/detection-graphics/detection-graphics.component.html

@ -0,0 +1,153 @@
<div #wapper class="w-full h-full flex items-center" (wheel)="onMouseWheel($event)">
<div class="box h-full relative" id="scroll-container" #box>
<svg width="100%" height="100%"
[attr.viewBox]="'0 0 ' + size * 2 + ' ' + size * 2">
<defs>
<linearGradient id="innerBg" x1="100%" y2="100%">
<stop offset="0%" style="stop-color:#3560b0"></stop>
<stop offset="100%" style="stop-color:#072254"></stop>
</linearGradient>
</defs>
<defs>
<linearGradient id="a1">
<stop offset="50%" stop-color="#22529a"></stop>
<stop offset="100%" stop-color="#063985"></stop>
</linearGradient>
<radialGradient id="b3" x1="0" y1="0" x2="0" y2="1" xlink:href="#a1" />
</defs>
<path
class="border"
[attr.d]="borderPath"
fill="#182d61"
stroke="#475781"
stroke-width="8px" />
<circle class="inner-bg"
[attr.cx]="size"
[attr.cy]="size"
[attr.r]="size - 80 * RATIO"
fill="url(#innerBg)">
</circle>
<ng-container *ngFor="let item of pies;let i = index">
<ng-container *ngIf="poles[i] as pole">
<g class="polygons lines w">
<path [attr.d]="item.polygons[0]" [ngClass]="{alarm:pole['line'][0]['alarm']}" />
<path [attr.d]="item.polygons[1]" [ngClass]="{alarm:pole['line'][1]['alarm']}" />
</g>
<g class="points w">
<ng-container *ngFor="let p of item.points; let pointIdx = index; let first=first">
<circle
[attr.r]="p.r"
[ngClass]="{alarm:pole['bolt'][pointIdx]['alarm']}"
[attr.cx]="p.x"
[attr.cy]="p.y">
</circle>
</ng-container>
</g>
<!-- *ngIf="pole?.pole?.alarm && pole.zone === selectedZone" -->
<g class="textpath"
*ngIf="pole?.pole?.alarm "
[attr.transform]="'rotate('+ ((360 / poles.length / 2) + 1) +','+size+','+size+')'">
<path [attr.id]="'path_' + i" [attr.d]="item.textPath" />
<text class="text">
<textPath [attr.href]="'#path_' + i" class="select-none">
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;&nbsp;
&nbsp;
#{{pole.zone}}
开匝
</textPath>
</text>
</g>
</ng-container>
</ng-container>
<ng-container *ngFor="let item of pies;let i = index">
<ng-container *ngIf="poles[i] as pole">
<g class="piepath w">
<path [attr.d]="item.piePath"
(click)="onSelect(pole.zone)"
[ngClass]="{selected:pole.zone === selectedZone}" />
<ng-container *ngIf="item.points[0] as p; ">
<g class="tooltip ">
<rect
[attr.x]="p.x - 80 "
[attr.y]="p.y "
width="150"
height="70"
rx="8"
fill="#fff">
</rect>
<text [attr.x]="p.x - 60"
[attr.y]="p.y + 45 ">
#{{pole.zone}}磁极
</text>
</g>
</ng-container>
</g>
</ng-container>
</ng-container>
<circle class="center" [attr.cx]="size" [attr.cy]="size" [attr.r]="size/4" fill="url(#b3)"></circle>
</svg>
<div class="center-img" #centerImage>
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
<img [src]="'/assets/icons/pie-center.svg' | publicPath" />
</div>
</div>
</div>

204
projects/client/src/app/feature/detection/components/detection-graphics/detection-graphics.component.less

@ -0,0 +1,204 @@
:host {
display: block;
width: 100%;
height: 100%;
overflow: hidden;
}
.circle {
display: block;
width: 400px;
height: 400px;
border: 3px solid #6e5ac9;
box-sizing: border-box;
}
.inner-bg {
stroke: #183265;
stroke-width: 4px;
}
.piepath {
.tooltip {
opacity: 0;
pointer-events: none;
}
path {
// stroke: #fff;
stroke-width: 4px;
fill: transparent;
position: relative;
opacity: 0.1;
&.selected {
stroke: #68bbe9;
opacity: 1;
// & + .textpath {
// display: block;
// }
}
&:hover {
stroke: #68bbe9;
opacity: 0.6;
& + .tooltip {
opacity: 1;
}
}
}
}
.center-img {
position: absolute;
top: 50%;
left: 50%;
z-index: 100;
width: 225px;
height: 225px;
transform: translate(-50%, -50%) scale(1.5);
border: 1px solid #2b529a;
border-radius: 50%;
// background-color: rgba(255, 255, 255, 0.05);
transform-origin: center;
pointer-events: none;
img {
position: absolute;
top: 50%;
left: 50%;
transform-origin: center;
&:nth-child(1) {
transform: translate(-60%, -131%) rotate(-2deg);
}
&:nth-child(2) {
transform: translate(31%, -127%) rotate(18deg);
}
&:nth-child(3) {
transform: translate(114%, -115%) rotate(36deg);
}
&:nth-child(4) {
transform: translate(180%, -96%) rotate(54deg);
}
&:nth-child(5) {
transform: translate(219%, -73%) rotate(72deg);
}
&:nth-child(6) {
transform: translate(232%, -47%) rotate(90deg);
}
&:nth-child(7) {
transform: translate(217%, -21%) rotate(108deg);
}
&:nth-child(8) {
transform: translate(174%, 2%) rotate(126deg);
}
&:nth-child(9) {
transform: translate(109%, 20%) rotate(144deg);
}
&:nth-child(10) {
transform: translate(30%, 31%) rotate(162deg);
}
&:nth-child(11) {
transform: translate(-57%, 35%) rotate(180deg);
}
&:nth-child(12) {
transform: translate(-147%, 31%) rotate(196deg);
}
&:nth-child(13) {
transform: translate(-228%, 20%) rotate(214deg);
}
&:nth-child(14) {
transform: translate(-295%, 3%) rotate(232deg);
}
&:nth-child(15) {
transform: translate(-336%, -20%) rotate(250deg);
}
&:nth-child(16) {
transform: translate(-352%, -45%) rotate(268deg);
}
&:nth-child(17) {
transform: translate(-340%, -71%) rotate(286deg);
}
&:nth-child(18) {
transform: translate(-298%, -95%) rotate(304deg);
}
&:nth-child(19) {
transform: translate(-234%, -114%) rotate(322deg);
}
&:nth-child(20) {
transform: translate(-154%, -126%) rotate(343deg);
}
}
}
.box {
width: 500px;
height: 500px;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
z-index: 10;
.polygons {
pointer-events: none;
path {
stroke: rgba(255, 255, 255, 0.85);
stroke-width: 2px;
fill: rgba(255, 255, 255, 0.45);
}
}
svg {
display: block;
// border: 1px solid red;
transition: transform 0.3s;
transform-origin: center;
position: relative;
z-index: 10;
text {
font-size: 30px;
}
}
.action {
position: absolute;
bottom: 20px;
right: 20px;
button {
margin: 6px;
}
}
}
.lines {
.alarm {
stroke: var(--red-1) !important;
}
}
.points {
circle {
fill: #fff;
&.alarm {
fill: var(--red-1);
}
}
}
.textpath {
pointer-events: none;
path {
stroke: none;
fill: none;
}
}
.center {
fill: linear-gradient(to right, red, yellow);
opacity: 0.99;
}
.text {
fill: #5eb7e8;
}

375
projects/client/src/app/feature/detection/components/detection-graphics/detection-graphics.component.ts

@ -0,0 +1,375 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostListener,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
Renderer2,
SimpleChanges,
ViewChild,
} from "@angular/core";
import { PoleItemDTO } from "@client/dtos";
import { BehaviorSubject } from "rxjs";
class PathLine {
constructor(public cx: number, public cy: number, public r: number, public dr: number = 0) {}
x0: number = 0;
y0: number = 0;
x1: number = 0;
y1: number = 0;
angle: number = 0;
}
class Point {
constructor(public x: number, public y: number, public r: number = 10) {}
}
interface Pie {
piePath: string;
textPath: string;
polygons: [path: string, path: string];
points: [Point, Point, Point, Point, Point, Point, Point, Point];
}
export type PoleGroupInterface = {
zone: number;
pole: PoleItemDTO;
bolt: Array<PoleItemDTO>;
line: Array<PoleItemDTO>;
};
const createDefaultPole = (idx: number) => {
return {
zone: idx + 1,
pole: {
alarm: false,
img: "img1.jpg",
position: 1,
type: "pole",
value: 0,
zone: 1,
},
line: Array.from({ length: 2 }, (_, i) => {
return {
alarm: false,
img: "img1.jpg",
position: i + 1,
type: "line",
value: 0,
zone: idx,
};
}),
bolt: Array.from({ length: 8 }, (_, i) => {
return {
alarm: false,
img: "img1.jpg",
position: i + 1,
type: "bolt",
value: 0,
zone: idx,
};
}),
} as PoleGroupInterface;
};
@Component({
selector: "app-detection-graphics",
templateUrl: "./detection-graphics.component.html",
styleUrls: ["./detection-graphics.component.less"],
})
export class DetectionGraphicsComponent implements OnDestroy, OnInit, AfterViewInit, OnChanges {
constructor(private rd2: Renderer2, private cdr: ChangeDetectorRef) {}
@Input() props$!: BehaviorSubject<{ poles: Array<PoleGroupInterface> }>;
@Input() public selectedZone?: number;
poleNum: number = 0;
@Output() onPoleSelect = new EventEmitter<number>();
@ViewChild("wapper") wapper!: ElementRef<HTMLDivElement>;
@ViewChild("box") box!: ElementRef<HTMLDivElement>;
@ViewChild("centerImage") centerImage!: ElementRef<HTMLDivElement>;
public poles: Array<PoleGroupInterface> = [];
public size: number = 800;
public d: string = "";
public pies: Pie[] = [];
public rd = Math.ceil(Math.random() * 10);
public scale: number = 1;
public borderPath = "";
public RATIO = 1;
private isDragging = false;
private startX = 0;
private startY = 0;
private translateX = 0;
private translateY = 0;
private readonly MIN_SCALE = 1;
private readonly MAX_SCALE = 2.5;
ngOnChanges(changes: SimpleChanges): void {}
ngOnInit(): void {
this.RATIO = this.size / 800;
this.drawBorderPath();
this.props$.subscribe((r) => {
// console.log('graphics-poles', r)
if (r.poles.length > 0) {
this.poles = r.poles;
this.poleNum = r.poles.length;
this.createPie();
}
});
}
ngAfterViewInit(): void {
this.dragMove();
this.setBoxSize();
}
ngOnDestroy(): void {
this.wapper?.nativeElement.removeEventListener("mousedown", this.onMouseDown);
this.box?.nativeElement.removeEventListener("mousemove", this.onMouseMove);
document.removeEventListener("mouseup", this.onMouseUp);
}
onSelect(poleZone: number) {
// this.selectedZone = poleZone;
this.onPoleSelect.emit(poleZone);
}
drawBorderPath() {
const { RATIO } = this;
const d1 = 700 * RATIO;
const d2 = 60 * RATIO;
const d3 = 10 * RATIO;
const d4 = 900 * RATIO;
const d5 = 720 * RATIO;
const d7 = 1540 * RATIO;
const d8 = 1590 * RATIO;
this.borderPath = `M${d1},${d2} L${d1},${d3} L${d4},${d3} L${d4},${d2}
A ${d5} ${d5} 0 0 1 ${d7} ${d1}
L${d8},${d1} L${d8},${d4} L${d7},${d4}
A ${d5} ${d5} 0 0 1 ${d4} ${d7}
L${d4},${d8} L${d1},${d8} L${d1},${d7}
A ${d5} ${d5} 0 0 1 ${d2} ${d4}
L${d3},${d4} L${d3},${d1} L${d2},${d1}
A ${d5} ${d5} 0 0 1 ${d1} ${d2}`;
}
createPie() {
const { size, RATIO, poleNum } = this;
const cx = size;
const cy = size;
const radius = size - 80 * RATIO;
const pie = new PathLine(cx, cy, radius);
const polygon1_1 = new PathLine(cx, cy, 710 * RATIO, 0.6 * RATIO);
const polygon1_2 = new PathLine(cx, cy, 675 * RATIO, 0.7 * RATIO);
const polygon2_1 = new PathLine(cx, cy, 660 * RATIO, 0.7 * RATIO);
const polygon2_2 = new PathLine(cx, cy, 625 * RATIO, 0.75 * RATIO);
const dot1 = new PathLine(cx, cy, 700 * RATIO, 1.3 * RATIO);
const dot2 = new PathLine(cx, cy, 685 * RATIO, 1.4 * RATIO);
const dot3 = new PathLine(cx, cy, 650 * RATIO, 1.3 * RATIO);
const dot4 = new PathLine(cx, cy, 635 * RATIO, 1.4 * RATIO);
let sweep = 0;
if ((1 / poleNum) * 360 > 180) {
sweep = 1;
}
this.pies = Array.from({ length: this.poleNum }, (_, idx) => {
this.calcPos(pie, idx);
this.calcPos(polygon1_1, idx);
this.calcPos(polygon1_2, idx);
this.calcPos(polygon2_1, idx);
this.calcPos(polygon2_2, idx);
this.calcPos(dot1, idx);
this.calcPos(dot2, idx);
this.calcPos(dot3, idx);
this.calcPos(dot4, idx);
const dotSize = 4 * RATIO;
// this.poles.push(createDefaultPole(idx));
return {
textPath: `M ${cx} ${cy},L ${pie.x0} ${pie.y0}`,
piePath: `M ${cx} ${cy},L ${pie.x0} ${pie.y0} A ${pie.r} ${pie.r} 0 ${sweep} 1 ${pie.x1} ${pie.y1} Z`,
polygons: [
`M ${polygon1_1.x0} ${polygon1_1.y0} A ${polygon1_1.r} ${polygon1_1.r} 0 ${sweep} 1 ${polygon1_1.x1} ${polygon1_1.y1} L ${polygon1_2.x1} ${polygon1_2.y1} A ${polygon1_2.r} ${polygon1_2.r} 0 ${sweep} 0 ${polygon1_2.x0} ${polygon1_2.y0} Z`,
`M ${polygon2_1.x0} ${polygon2_1.y0} A ${polygon2_1.r} ${polygon2_1.r} 0 ${sweep} 1 ${polygon2_1.x1} ${polygon2_1.y1} L ${polygon2_2.x1} ${polygon2_2.y1} A ${polygon2_2.r} ${polygon2_2.r} 0 ${sweep} 0 ${polygon2_2.x0} ${polygon2_2.y0} Z`,
],
dots: [dot1, dot2, dot3, dot4],
points: [
new Point(dot1.x0, dot1.y0, dotSize),
new Point(dot1.x1, dot1.y1, dotSize),
new Point(dot2.x0, dot2.y0, dotSize),
new Point(dot2.x1, dot2.y1, dotSize),
new Point(dot3.x0, dot3.y0, dotSize),
new Point(dot3.x1, dot3.y1, dotSize),
new Point(dot4.x0, dot4.y0, dotSize),
new Point(dot4.x1, dot4.y1, dotSize),
],
};
});
}
calcPos(line: PathLine, idx: number): PathLine {
const { poleNum } = this;
const n = poleNum;
let { cx, cy, dr, r } = line;
let currentIndex = 0;
currentIndex = idx + 1;
if (idx === 0) {
line.x0 = cx + r * Math.cos((dr * Math.PI) / 180);
line.y0 = cy + r * Math.sin((dr * Math.PI) / 180);
line.angle = (currentIndex / n) * 360;
line.x1 = cx + r * Math.cos(((line.angle - dr) * Math.PI) / 180);
line.y1 = cy + r * Math.sin(((line.angle - dr) * Math.PI) / 180);
} else if (idx > 0 && idx < n - 1) {
line.x0 = cx + r * Math.cos(((line.angle + dr) * Math.PI) / 180);
line.y0 = cy + r * Math.sin(((line.angle + dr) * Math.PI) / 180);
line.angle = (currentIndex / n) * 360;
line.x1 = cx + r * Math.cos(((line.angle - dr) * Math.PI) / 180);
line.y1 = cy + r * Math.sin(((line.angle - dr) * Math.PI) / 180);
} else {
line.x0 = cx + r * Math.cos(((line.angle + dr) * Math.PI) / 180);
line.y0 = cy + r * Math.sin(((line.angle + dr) * Math.PI) / 180);
line.x1 = cx + r * Math.cos((-dr * Math.PI) / 180);
line.y1 = cy + r * Math.sin((-dr * Math.PI) / 180);
}
return line;
}
// getPole(idx: number): PoleGroupInterface {
// console.log("defaultPole", defaultPole);
// return defaultPole as PoleGroupInterface;
// }
handleNumber(m: number) {
const n = 250;
if (m < -n) {
return -n;
} else if (m > n) {
return n;
} else {
return m;
}
}
onMouseDown = (event: MouseEvent) => {
event.preventDefault();
this.isDragging = true;
this.startX = event.clientX - this.translateX;
this.startY = event.clientY - this.translateY;
};
onMouseUp = () => {
this.isDragging = false;
};
onMouseMove = (event: MouseEvent) => {
if (this.isDragging) {
event.preventDefault();
let x = event.clientX - this.startX;
let y = event.clientY - this.startY;
this.translateX = this.handleNumber(x);
this.translateY = this.handleNumber(y);
this.modifyTransform();
}
};
dragMove() {
this.wapper.nativeElement.addEventListener("mousedown", this.onMouseDown);
this.box.nativeElement.addEventListener("mousemove", this.onMouseMove);
document.addEventListener("mouseup", this.onMouseUp);
}
@HostListener("window:resize")
onResize() {
this.setBoxSize();
}
onMouseWheel(event: WheelEvent): void {
event.preventDefault();
const scaleChange = -event.deltaY / 2000;
this.scale += scaleChange;
if (this.scale < this.MIN_SCALE) {
this.scale = this.MIN_SCALE;
} else if (this.scale > this.MAX_SCALE) {
this.scale = this.MAX_SCALE;
}
this.modifyTransform();
}
modifyTransform() {
this.rd2.setStyle(
this.box.nativeElement,
"transform",
`scale(${this.scale}) translate(${this.translateX}px, ${this.translateY}px)`
);
}
setBoxSize() {
if (this.wapper.nativeElement) {
setTimeout(() => {
const { width, height } = this.wapper.nativeElement.getBoundingClientRect();
const min = Math.min(width, height);
const svg = this.box.nativeElement.querySelector("svg");
this.rd2.setStyle(this.box.nativeElement, "width", width + "px");
this.rd2.setStyle(this.box.nativeElement, "height", height + "px");
this.rd2.setStyle(svg, "width", min + "px");
this.rd2.setStyle(svg, "height", min + "px");
const scale = min / (225 * 1.35);
this.rd2.setStyle(this.centerImage.nativeElement, "transform", `translate(-50%, -50%) scale(${scale})`);
}, 70);
}
}
onScale(sc: number) {
this.scale = this.scale + sc;
}
onScroll(event: any) {
event.preventDefault();
const delta = Math.max(-1, Math.min(1, event.wheelDelta || -event.detail));
const zoom = delta > 0 ? 1.1 : 0.9; // 缩放系数
this.scale *= zoom;
}
}

4
projects/client/src/app/feature/detection/components/detection-value/detection-value.component.html

@ -0,0 +1,4 @@
<span *ngIf="value" class="txt" [ngClass]="{'c-red': value.alarm}">
{{value.value}}
</span>
<span *ngIf="!value" class="txt">-</span>

0
projects/client/src/app/feature/detection/components/detection-value/detection-value.component.less

12
projects/client/src/app/feature/detection/components/detection-value/detection-value.component.ts

@ -0,0 +1,12 @@
import { Component, Input } from "@angular/core";
@Component({
selector: "app-detection-value",
templateUrl: "./detection-value.component.html",
styleUrls: ["./detection-value.component.less"],
})
export class DetectionValueComponent {
constructor() {}
@Input() value?: { alarm: boolean; value: number };
}

47
projects/client/src/app/feature/detection/components/device-table/device-table.component.html

@ -0,0 +1,47 @@
<div class="flex justify-between">
<div class="flex-1">
<nz-space>
<nz-select *nzSpaceItem nzPlaceHolder="请选择机组" class="w-[180px]" [(ngModel)]="gid"
(ngModelChange)="onGroupChange($event)">
<nz-option *ngFor="let g of groups" [nzValue]="g.motorGroupId" [nzLabel]="g.name"></nz-option>
</nz-select>
<nz-select *nzSpaceItem nzPlaceHolder="请选择检测点" class="w-[180px]" [(ngModel)]="pid">
<nz-option *ngFor="let p of currentPointList" [nzValue]="p.pointId" [nzLabel]="p.name"></nz-option>
</nz-select>
</nz-space>
</div>
<div>
<nz-space>
<button *nzSpaceItem nz-button nzGhost type="button" (click)="onReset()">重置</button>
<button *nzSpaceItem nz-button nzGhost type="button" (click)="onSearch()">
<i nz-icon nzType="search"></i>
查询
</button>
</nz-space>
</div>
</div>
<nz-table #table [nzData]="listOfData" class="mt-6" nzShowQuickJumper [nzPageSize]="5"
(nzCurrentPageDataChange)="onCurrentPageDataChange($event)">
<thead>
<tr>
<th nzWidth="16px"
[nzChecked]="checked"
[nzIndeterminate]="indeterminate"
(nzCheckedChange)="onAllChecked($event)">
</th>
<th>
机组/检测点/设备
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of table.data">
<td [nzChecked]="setOfCheckedId.has(data['id'])"
[nzDisabled]="data['disabled']"
(nzCheckedChange)="onItemChecked(data['id'], $event)"></td>
<td>
{{currentSelectedGroupName}}/{{currentSelectedPoint?.name}}/{{data.name}}
</td>
</tr>
</tbody>
</nz-table>

0
projects/client/src/app/feature/detection/components/device-table/device-table.component.less

133
projects/client/src/app/feature/detection/components/device-table/device-table.component.ts

@ -0,0 +1,133 @@
import { Component, Input, OnInit } from "@angular/core";
import { AnyObject } from "@cdk/types";
import { DetectionApiService } from "@client/app/core/services";
import { PointDTO, PointGroupDTO, DeviceDTO } from "@client/dtos";
import { NzMessageService } from "ng-zorro-antd/message";
@Component({
selector: "app-device-table",
templateUrl: "./device-table.component.html",
styleUrls: ["./device-table.component.less"],
})
export class DeviceTableComponent implements OnInit {
constructor(private api: DetectionApiService, private msg: NzMessageService) {}
@Input() currentSelected: { id: string; name: string }[] = [];
@Input() otherSelected: { id: string; name: string }[] = [];
checked = false;
loading = false;
indeterminate = false;
groups: PointGroupDTO[] = [];
gid: string = "";
pid: string = "";
points: PointDTO[] = [];
currentPointList: PointDTO[] = [];
listOfData: DeviceDTO[] = [];
listOfCurrentPageData: readonly any[] = [];
setOfCheckedId = new Set<string>();
setOfDataFromServer: AnyObject[] = [];
public get selectedDevice(): AnyObject[] {
return this.setOfDataFromServer.filter((f) => this.setOfCheckedId.has(f["id"]));
}
public get currentSelectedGroupName(): string {
return this.groups.find((f) => f.motorGroupId === this.gid)?.name ?? "";
}
public get currentSelectedPoint(): PointDTO | undefined {
return this.points.find((f) => f.pointId === this.pid);
}
ngOnInit(): void {
this.currentSelected?.forEach((p) => {
this.updateCheckedSet(p.id, true);
});
this.api.getFlatPoints().subscribe(({ points, groups }) => {
this.groups = groups;
this.points = points;
});
}
onReset() {
this.gid = "";
this.onSearch();
}
onSearch() {
if (!this.pid) {
this.msg.error("请选择检测点");
return;
}
this.api.getDeviceListByPointId(this.pid).subscribe((res) => {
console.log("this.otherSelected", this.otherSelected);
if (Array.isArray(res.body)) {
this.listOfData = res.body.map((i) => {
return {
...i,
id: i.deviceId,
disabled: !!this.otherSelected.find((f) => f.id === i.deviceId),
};
});
} else {
this.listOfData = [];
}
this.listOfData.forEach((i) => {
if (!this.setOfDataFromServer.some((s) => s["id"] === i["id"])) {
this.setOfDataFromServer.push(i);
}
});
this.refreshCheckedStatus();
});
}
onGroupChange(gid: string) {
this.currentPointList = this.groups.find((f) => f.motorGroupId === gid)?.pointList ?? [];
this.pid = "";
}
updateCheckedSet(id: string, checked: boolean): void {
if (checked) {
this.setOfCheckedId.add(id);
} else {
this.setOfCheckedId.delete(id);
}
}
onCurrentPageDataChange(listOfCurrentPageData: readonly any[]): void {
this.listOfCurrentPageData = listOfCurrentPageData;
this.refreshCheckedStatus();
}
refreshCheckedStatus(): void {
const listOfEnabledData = this.listOfCurrentPageData.filter(({ disabled }) => !disabled);
this.checked = listOfEnabledData.length > 0 && listOfEnabledData.every(({ id }) => this.setOfCheckedId.has(id));
this.indeterminate = listOfEnabledData.some(({ id }) => this.setOfCheckedId.has(id)) && !this.checked;
}
onItemChecked(id: string, checked: boolean): void {
this.updateCheckedSet(id, checked);
this.refreshCheckedStatus();
}
onAllChecked(checked: boolean): void {
this.listOfCurrentPageData
.filter(({ disabled }) => !disabled)
.forEach(({ id }) => this.updateCheckedSet(id, checked));
this.refreshCheckedStatus();
}
}

4
projects/client/src/app/feature/detection/components/forbidden/forbidden.component.html

@ -0,0 +1,4 @@
<div class="pt-20">
<nz-result nzStatus="403"></nz-result>
<h2 class=" text-center">你没有此页面的访问权限。</h2>
</div>

0
projects/client/src/app/feature/detection/components/forbidden/forbidden.component.less

14
projects/client/src/app/feature/detection/components/forbidden/forbidden.component.ts

@ -0,0 +1,14 @@
import { ActivatedRoute, Router } from "@angular/router";
import { Component, OnInit } from "@angular/core";
import { NgxPermissionsService } from "ngx-permissions";
@Component({
selector: "app-forbidden",
templateUrl: "./forbidden.component.html",
styleUrls: ["./forbidden.component.less"],
})
export class ForbiddenComponent implements OnInit {
constructor(private perm: NgxPermissionsService, private router: Router, private route: ActivatedRoute) {}
async ngOnInit() {}
}

0
projects/client/src/app/feature/detection/components/home/home.component.html

0
projects/client/src/app/feature/detection/components/home/home.component.less

34
projects/client/src/app/feature/detection/components/home/home.component.ts

@ -0,0 +1,34 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { NgxPermissionsService } from "ngx-permissions";
@Component({
selector: "app-home",
templateUrl: "./home.component.html",
styleUrls: ["./home.component.less"],
})
export class HomeComponent {
constructor(private perm: NgxPermissionsService, private router: Router, private route: ActivatedRoute) {}
async ngOnInit() {
let children = this.route.parent?.routeConfig?.children;
const { from } = this.route.snapshot.queryParams;
if (Array.isArray(children)) {
if (from) {
children = children.find((f) => f.path === from)?.children ?? [];
}
for (const child of children) {
if (child.path === this.route.routeConfig?.path) {
continue;
}
const only = child.data?.["permissions"]?.only;
if (Array.isArray(only)) {
if (await this.perm.hasPermission(only)) {
this.router.navigate(["/detection", from, child.path].filter(Boolean));
break;
}
}
}
}
}
}

129
projects/client/src/app/feature/detection/components/image-player/_image-player.component.ts

@ -0,0 +1,129 @@
// import { environment } from "@client/environments/environment";
// import { Component, Inject, Input, OnDestroy, OnInit } from "@angular/core";
// import { BehaviorSubject, forkJoin, interval, lastValueFrom, Subject, Subscription } from "rxjs";
// import { filter, takeUntil } from "rxjs/operators";
// import { NzImageService } from "ng-zorro-antd/image";
// import { UtilsService } from "@client/app/core/services";
// export interface ImageObj {
// jobId: string;
// img: string;
// }
// @Component({
// selector: "app-image-player",
// templateUrl: "./image-player.component.html",
// styleUrls: ["./image-player.component.less"],
// })
// export class ImagePlayerComponent implements OnInit, OnDestroy {
// constructor(private img: NzImageService, private util: UtilsService) {}
// @Input() images$!: BehaviorSubject<ImageObj[]>;
// images: string[] = [];
// imageObjs: ImageObj[] = [];
// environment = environment;
// subs$?: Subscription;
// currentIndex = 0;
// played = false;
// fps: number = 1;
// ngOnInit(): void {
// this.images$.subscribe(async (imgObjs) => {
// for (let i = 0; i < imgObjs.length; i++) {
// const o = imgObjs[i];
// // if (i < 10) {
// // const res = await lastValueFrom(this.util.loadImage(o.jobId, o.img));
// // this.imageObjs.push
// // this.images.push("data:image/png;base64," + res.body);
// // }
// this.imageObjs.push(o);
// }
// this.initPlayImage();
// });
// }
// ngOnDestroy(): void {
// this.currentIndex = 0;
// this.tempIndex = 0;
// this.played = false;
// this.subs$?.unsubscribe();
// }
// jumpTo(idx: number) {
// this.played = false;
// if (idx < 0 || idx > this.images.length - 1) {
// return;
// }
// this.currentIndex = idx;
// this.tempIndex = idx;
// }
// tempIndex = 0;
// initPlayImage() {
// this.subs$?.unsubscribe();
// this.subs$ = interval(1000 / this.fps)
// .pipe(
// filter(() => {
// // return this.currentIndex === this.tempIndex;
// return true;
// })
// )
// .subscribe(async () => {
// if (this.played) {
// // this.tempIndex = this.currentIndex + 1;
// let targetImage = this.images[this.currentIndex];
// if (!targetImage) {
// const o = this.imageObjs[this.tempIndex];
// const base64 = await lastValueFrom(this.util.loadImage(o.jobId, o.img));
// targetImage = base64;
// this.images.push(base64);
// }
// this.currentIndex++;
// console.log("targetImage", targetImage);
// // if (!targetImage) {
// // this.currentIndex = this.images.length - 1;
// // this.tempIndex = this.currentIndex;
// // this.togglePaly();
// // } else {
// // const bs = await this.util.loadImages(targetImage);
// // this.currentIndex++;
// // this.images[this.currentIndex] = bs;
// // }
// } else {
// this.subs$?.unsubscribe();
// }
// });
// }
// togglePaly() {
// this.played = !this.played;
// if (this.played) {
// if (this.currentIndex === this.images.length - 1) {
// this.currentIndex = 0;
// this.tempIndex = 0;
// }
// this.initPlayImage();
// }
// }
// onFpsChange(fps: number) {
// this.fps = fps;
// this.initPlayImage();
// }
// showImg() {
// if (this.images.length > 0) {
// const imgRef = this.img.preview(this.images.map((src) => ({ src })));
// imgRef.switchTo(this.currentIndex);
// }
// }
// }

52
projects/client/src/app/feature/detection/components/image-player/image-player.component.html

@ -0,0 +1,52 @@
<div class="flex-1 flex justify-center items-center h-full relative">
<ng-container *ngIf="images[currentIndex] as img">
<!-- <h1 class="idx">{{images.length}} / {{currentIndex}}</h1> -->
<img *ngIf="img.loaded" class="w-full" [src]="img.url">
</ng-container>
<img *ngIf="images.length === 0" [src]="'/assets/icons/no-image.png' | publicPath" />
</div>
<div class="controls flex items-center justify-between absolute z-10 left-0 right-0 bottom-0 pl-4">
<div class="flex-1 ">
<nz-space>
<button *nzSpaceItem type="button" (click)="jumpTo(0)">
<span nz-icon nzType="fast-backward" nzTheme="outline"></span>
</button>
<button *nzSpaceItem type="button" (click)="jumpTo(currentIndex - 1)">
<span nz-icon nzType="backward" nzTheme="outline"></span>
</button>
<button *nzSpaceItem type="button" (click)="togglePlay()">
<span *ngIf="!autoPlay" nz-icon nzType="caret-right" nzTheme="fill"></span>
<span *ngIf="autoPlay" nz-icon nzType="pause" nzTheme="outline"></span>
</button>
<button *nzSpaceItem type="button" (click)="jumpTo(currentIndex + 1)">
<span nz-icon nzType="forward" nzTheme="outline"></span>
</button>
<button *nzSpaceItem type="button" (click)="jumpTo(images.length - 1)">
<span nz-icon nzType="fast-forward" nzTheme="outline"></span>
</button>
</nz-space>
</div>
<div class="pr-4 flex items-center">
<button
type="button"
class="fps mr-4 px-3 inline-block"
nz-dropdown
nzTrigger="click"
[nzDropdownMenu]="fpsSelect">
{{fps}} FPS
</button>
<button type="button" (click)="showImg()">
<span nz-icon nzType="fullscreen" nzTheme="outline"></span>
<!-- <img [src]="'/assets/icons/fullscreen.svg' | publicPath" /> -->
</button>
</div>
</div>
<nz-dropdown-menu #fpsSelect="nzDropdownMenu">
<ul nz-menu nzSelectable>
<li nz-menu-item (click)="onFpsChange(1)">1 FPS</li>
<li nz-menu-item (click)="onFpsChange(2)">2 FPS</li>
<li nz-menu-item (click)="onFpsChange(3)">3 FPS</li>
<li nz-menu-item (click)="onFpsChange(5)">5 FPS</li>
</ul>
</nz-dropdown-menu>

44
projects/client/src/app/feature/detection/components/image-player/image-player.component.less

@ -0,0 +1,44 @@
:host {
display: flex;
height: 100%;
background-color: var(--black-10);
position: relative;
}
.idx {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 60px;
color: #fff;
}
.controls {
background-color: rgba(0, 0, 0, .25);
color: var(--text-color);
::ng-deep {
.ant-space-item {
margin-right: 0;
}
}
button {
appearance: none;
border: none;
outline: none;
color: var(--text-color);
font-size: 20px;
background-color: transparent;
cursor: pointer;
}
.fps {
appearance: none;
border: none;
border-radius: 2px;
background-color: var(--white-10);
font-size: 14px;
}
}

176
projects/client/src/app/feature/detection/components/image-player/image-player.component.ts

@ -0,0 +1,176 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { AnyObject } from "@cdk/types";
import { DetectionApiService, UtilsService } from "@client/app/core/services";
import { NzImageService } from "ng-zorro-antd/image";
import {
BehaviorSubject,
Subject,
Subscription,
interval,
lastValueFrom,
switchMap,
takeUntil,
takeWhile,
tap,
} from "rxjs";
export interface ImageObj {
jobId: string;
img: string;
}
@Component({
selector: "app-image-player",
templateUrl: "./image-player.component.html",
styleUrls: ["./image-player.component.less"],
})
export class ImagePlayerComponent implements OnInit {
constructor(private nzImg: NzImageService, private api: DetectionApiService) {}
@Input() props$!: BehaviorSubject<ImageObj[]>;
images: ({ url: string; loaded: boolean } & Partial<ImageObj>)[] = [];
currentIndex = -1;
autoPlay = false;
fps = 5;
private destroy$ = new Subject<void>();
private trigger$ = new Subject<void>();
private subs$?: Subscription;
ngOnInit(): void {
const { origin } = window.location;
this.props$.subscribe((imgs) => {
if (imgs.length === 0) {
return;
}
// console.log("imgs", imgs);
this.api.getImageBaseUrl().subscribe((baseUrl) => {
imgs.forEach((item) => {
const url = `${origin}${baseUrl}${item.jobId}/detect/${item.img}`;
this.images.push({ url, loaded: false, ...item });
});
if (this.currentIndex < 0 && this.images.length > 0) {
this.jumpTo(0);
}
});
});
}
reset() {
this.images = [];
this.currentIndex = -1;
}
destroy() {
this.destroy$.next();
this.destroy$.complete();
}
ngOnDestroy() {
this.destroy();
}
togglePlay() {
this.autoPlay = !this.autoPlay;
if (this.autoPlay && this.currentIndex === this.images.length - 1) {
this.currentIndex = 0;
}
if (this.images.length === 0) {
this.autoPlay = false;
}
if (this.autoPlay) {
this.subs$?.unsubscribe();
this.subs$ = this.trigger$
.pipe(
switchMap(() => interval(1000 / this.fps)),
takeUntil(this.destroy$),
takeWhile(() => this.autoPlay),
tap((i) => {
// console.log(i);
})
)
.subscribe(() => {
this.jumpTo(this.currentIndex + 1);
// console.log("played");
});
this.trigger$.next();
}
}
async jumpTo(idx: number) {
// console.log("idx", idx);
if (idx < 0 || idx > this.images.length - 1) {
return;
}
const image = this.images[idx];
if (!image.loaded) {
// await this.loadBase64(image as ImageObj);
await this.loadImage(image);
}
this.currentIndex = idx;
if (this.autoPlay && this.currentIndex === this.images.length - 1) {
this.togglePlay();
}
}
async loadBase64(item: ImageObj & AnyObject) {
const base64 = await lastValueFrom(this.api.loadImage(item.jobId, item.img));
item["loaded"] = true;
item["url"] = base64;
}
loadImage(image: { url: string; loaded: boolean }) {
return new Promise((r, j) => {
if (!image.loaded) {
const img = new Image();
img.src = image.url;
img.onload = () => {
image.loaded = true;
r(image);
};
} else {
r(image);
}
});
}
onFpsChange(fps: number) {
this.fps = fps;
this.trigger$.next();
// this.initPlayImage();
}
showImg() {
if (this.images.length > 0) {
this.togglePlay();
const imgRef = this.nzImg.preview(this.images.map((img) => ({ src: img.url })));
imgRef.switchTo(this.currentIndex);
}
}
preLoad() {
this.images.forEach((image) => {
if (!image.loaded) {
const img = new Image();
img.src = image.url;
img.onload = () => {
image.loaded = true;
};
}
});
if (this.currentIndex < 0) {
this.currentIndex = 0;
}
}
}

10
projects/client/src/app/feature/detection/components/index.ts

@ -0,0 +1,10 @@
export * from "./detection-graphics/detection-graphics.component";
export * from "./image-player/image-player.component";
export * from "./point-status/point-status.component";
export * from "./detection-value/detection-value.component";
export * from "./point-table/point-table.component";
export * from "./device-table/device-table.component";
export * from "./home/home.component";
export * from "./forbidden/forbidden.component";
export * from "./notfound/notfound.component";

6
projects/client/src/app/feature/detection/components/notfound/notfound.component.html

@ -0,0 +1,6 @@
<div class="mt-20">
<nz-result nzStatus="404"></nz-result>
<h2 class="text-center">
此页面未找到。
</h2>
</div>

0
projects/client/src/app/feature/detection/components/notfound/notfound.component.less

10
projects/client/src/app/feature/detection/components/notfound/notfound.component.ts

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-notfound',
templateUrl: './notfound.component.html',
styleUrls: ['./notfound.component.less']
})
export class NotfoundComponent {
}

29
projects/client/src/app/feature/detection/components/point-status/point-status.component.html

@ -0,0 +1,29 @@
<ng-container *ngIf="!textVisible" [ngSwitch]="status">
<img *ngSwitchCase="PointStatusEnum.ABNORMAL"
[src]="'/assets/icons/alert.svg' | publicPath"
nz-tooltip="异常" />
<img *ngSwitchCase="PointStatusEnum.DISCONNECT"
[src]="'/assets/icons/disconnect.svg' | publicPath"
nz-tooltip="掉线" />
<img *ngSwitchDefault
[src]="'/assets/icons/success.svg' | publicPath"
nz-tooltip="正常" />
</ng-container>
<div class="flex items-center " *ngIf="textVisible" [ngSwitch]="status">
<ng-container *ngSwitchCase="PointStatusEnum.ABNORMAL">
<img class="mr-2"
[src]="'/assets/icons/alert.svg' | publicPath" />
异常
</ng-container>
<ng-container *ngSwitchCase="PointStatusEnum.DISCONNECT">
<img class="mr-2"
[src]="'/assets/icons/disconnect.svg' | publicPath" />
掉线
</ng-container>
<ng-container *ngSwitchDefault>
<img class="mr-2"
[src]="'/assets/icons/success.svg' | publicPath" />
正常
</ng-container>
</div>

0
projects/client/src/app/feature/detection/components/point-status/point-status.component.less

16
projects/client/src/app/feature/detection/components/point-status/point-status.component.ts

@ -0,0 +1,16 @@
import { Component, Input, ViewEncapsulation } from "@angular/core";
import { PointStatusEnum } from "@client/dtos";
@Component({
selector: "app-point-status",
templateUrl: "./point-status.component.html",
styleUrls: ["./point-status.component.less"],
encapsulation: ViewEncapsulation.None,
})
export class PointStatusComponent {
@Input() status: PointStatusEnum = PointStatusEnum.NORMAL;
@Input() textVisible: boolean = false;
PointStatusEnum = PointStatusEnum;
}

41
projects/client/src/app/feature/detection/components/point-table/point-table.component.html

@ -0,0 +1,41 @@
<div class="flex justify-between">
<div class="flex-1">
<nz-select nzPlaceHolder="请选择机组" class="w-[180px]" [(ngModel)]="gid">
<nz-option *ngFor="let g of groups" [nzValue]="g.motorGroupId" [nzLabel]="g.name"></nz-option>
</nz-select>
</div>
<div>
<nz-space>
<button *nzSpaceItem nz-button nzGhost type="button" (click)="onReset()">重置</button>
<button *nzSpaceItem nz-button nzGhost type="button" (click)="onSearch()">
<i nz-icon nzType="search"></i>
查询
</button>
</nz-space>
</div>
</div>
<nz-table #table [nzData]="listOfData" class="mt-6" nzShowQuickJumper [nzPageSize]="5"
(nzCurrentPageDataChange)="onCurrentPageDataChange($event)">
<thead>
<tr>
<th nzWidth="16px"
[nzChecked]="checked"
[nzIndeterminate]="indeterminate"
(nzCheckedChange)="onAllChecked($event)">
</th>
<th>
机组/检测点
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of table.data">
<td [nzChecked]="setOfCheckedId.has(data.pointId)"
[nzDisabled]="data['disabled']"
(nzCheckedChange)="onItemChecked(data.pointId, $event)"></td>
<td>
{{data.name}}
</td>
</tr>
</tbody>
</nz-table>

0
projects/client/src/app/feature/detection/components/point-table/point-table.component.less

115
projects/client/src/app/feature/detection/components/point-table/point-table.component.ts

@ -0,0 +1,115 @@
import { Component, Input, OnInit } from "@angular/core";
import { DetectionApiService } from "@client/app/core/services";
import { PointDTO, PointGroupDTO } from "@client/dtos";
@Component({
selector: "app-point-table",
templateUrl: "./point-table.component.html",
styleUrls: ["./point-table.component.less"],
})
export class PointTableComponent implements OnInit {
constructor(private api: DetectionApiService) {}
@Input() currentSelected: PointDTO[] = [];
@Input() otherSelected: PointDTO[] = [];
checked = false;
loading = false;
indeterminate = false;
groups: PointGroupDTO[] = [];
gid: string = "";
points: PointDTO[] = [];
listOfData: PointDTO[] = [];
listOfCurrentPageData: readonly any[] = [];
setOfCheckedId = new Set<string>();
arrOfChecked: PointDTO[] = [];
ngOnInit(): void {
this.currentSelected?.forEach((p) => {
this.updateCheckedSet(p.pointId, true);
});
this.api.getFlatPoints().subscribe(({ groups, points }) => {
points = points.map((p) => ({ ...p, disabled: this.otherSelected.some((s) => s.pointId === p.pointId) }));
this.points = points;
this.listOfData = points;
this.groups = groups;
this.refreshCheckedStatus();
});
// this.api.getAllPoint().subscribe((p) => {
// const groups = p?.[0]?.groupList;
// const points: PointDTO[] = [];
// console.log("this.currentSelected", this.currentSelected, this.otherSelected);
// if (groups) {
// this.groups = groups;
// groups.forEach((g) => {
// points.push(
// ...g.pointList.map((p) => ({
// ...p,
// gid: g.motorGroupId,
// gname: g.name,
// }))
// );
// });
// this.points = points;
// this.listOfData = points;
// this.refreshCheckedStatus();
// }
// });
}
updateCheckedSet(id: string, checked: boolean): void {
if (checked) {
this.setOfCheckedId.add(id);
} else {
this.setOfCheckedId.delete(id);
}
this.arrOfChecked = this.points.filter((f) => this.setOfCheckedId.has(f.pointId));
}
onCurrentPageDataChange(listOfCurrentPageData: readonly any[]): void {
this.listOfCurrentPageData = listOfCurrentPageData;
this.refreshCheckedStatus();
}
refreshCheckedStatus(): void {
const listOfEnabledData = this.listOfCurrentPageData.filter(({ disabled }) => !disabled);
this.checked =
listOfEnabledData.length > 0 && listOfEnabledData.every(({ pointId }) => this.setOfCheckedId.has(pointId));
this.indeterminate = listOfEnabledData.some(({ pointId }) => this.setOfCheckedId.has(pointId)) && !this.checked;
}
onItemChecked(id: string, checked: boolean): void {
this.updateCheckedSet(id, checked);
this.refreshCheckedStatus();
}
onAllChecked(checked: boolean): void {
this.listOfCurrentPageData
.filter(({ disabled }) => !disabled)
.forEach(({ pointId }) => this.updateCheckedSet(pointId, checked));
this.refreshCheckedStatus();
}
onReset() {
this.gid = "";
this.onSearch();
}
onSearch() {
if (this.gid) {
this.listOfData = this.points.filter((f) => f["gid"] === this.gid);
} else {
this.listOfData = this.points;
}
}
}

156
projects/client/src/app/feature/detection/detection-routing.module.ts

@ -0,0 +1,156 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthorizationLayoutComponent } from "@client/app/shared/components";
import {
AnalysisComponent,
DetectionIndexComponent,
HistoryDetailComponent,
HistoryListComponent,
PointSettingComponent,
SettingsLayoutComponent,
AlgorithmSettingComponent,
TemperatureSettingComponent,
SystemSettingComponent,
} from "./pages";
import { PermissionLoadGuard } from "@client/app/core/gaurd/permisson.guard";
import { ForbiddenComponent, HomeComponent, NotfoundComponent } from "./components";
import { NgxPermissionsGuard } from "ngx-permissions";
const routes: Routes = [
{
path: "",
component: AuthorizationLayoutComponent,
canActivateChild: [PermissionLoadGuard],
children: [
{
path: "",
pathMatch: "full",
redirectTo: "home",
},
{
path: "home",
component: HomeComponent,
},
{
path: "index",
component: DetectionIndexComponent,
canActivate: [NgxPermissionsGuard],
data: {
permissions: {
only: ["role_pole"],
redirectTo: "/detection/forbidden",
},
},
},
{
path: "history",
component: HistoryListComponent,
canActivate: [NgxPermissionsGuard],
data: {
permissions: {
only: ["role_history"],
redirectTo: "/detection/forbidden",
},
},
},
{
path: "history/:id",
component: HistoryDetailComponent,
canActivate: [NgxPermissionsGuard],
data: {
permissions: {
only: ["role_history"],
redirectTo: "/detection/forbidden",
},
},
},
{
path: "analysis",
component: AnalysisComponent,
canActivate: [NgxPermissionsGuard],
data: {
permissions: {
only: ["role_analysis"],
redirectTo: "/detection/forbidden",
},
},
},
{
path: "settings",
component: SettingsLayoutComponent,
canActivateChild: [NgxPermissionsGuard],
data: {
permissions: {
only: ["role_settings_show", "role_settings_bolt", "role_settings_temp", "role_settings_sys"],
redirectTo: "/detection/forbidden",
},
},
children: [
{
path: "",
pathMatch: "full",
redirectTo: "/detection/home?from=settings",
},
{
path: "point",
component: PointSettingComponent,
canActivate: [NgxPermissionsGuard],
data: {
permissions: {
only: ["role_settings_show"],
redirectTo: "/detection/forbidden",
},
},
},
{
path: "algorithm",
component: AlgorithmSettingComponent,
canActivate: [NgxPermissionsGuard],
data: {
permissions: {
only: ["role_settings_bolt"],
redirectTo: "/detection/forbidden",
},
},
},
{
path: "temperature",
component: TemperatureSettingComponent,
canActivate: [NgxPermissionsGuard],
data: {
permissions: {
only: ["role_settings_temp"],
redirectTo: "/detection/forbidden",
},
},
},
{
path: "system",
component: SystemSettingComponent,
canActivate: [NgxPermissionsGuard],
data: {
permissions: {
only: ["role_settings_sys"],
redirectTo: "/detection/forbidden",
},
},
},
],
},
{
path: "forbidden",
component: ForbiddenComponent,
},
{
path: "**",
component: NotfoundComponent,
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class DetectionRoutingModule {}

56
projects/client/src/app/feature/detection/detection.module.ts

@ -0,0 +1,56 @@
import { FormsModule } from "@angular/forms";
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { DetectionRoutingModule } from "./detection-routing.module";
import {
DetectionGraphicsComponent,
ImagePlayerComponent,
PointStatusComponent,
DetectionValueComponent,
PointTableComponent,
DeviceTableComponent,
ForbiddenComponent,
NotfoundComponent,
HomeComponent,
} from "./components";
import {
AnalysisComponent,
DetectionIndexComponent,
HistoryDetailComponent,
HistoryListComponent,
SettingsLayoutComponent,
PointSettingComponent,
AlgorithmSettingComponent,
TemperatureSettingComponent,
SystemSettingComponent,
} from "./pages";
import { SharedModule } from "../../shared/shared.module";
const components = [
DetectionGraphicsComponent,
ImagePlayerComponent,
PointStatusComponent,
DetectionValueComponent,
PointTableComponent,
DeviceTableComponent,
ForbiddenComponent,
NotfoundComponent,
HomeComponent,
];
const pages = [
AnalysisComponent,
DetectionIndexComponent,
HistoryDetailComponent,
HistoryListComponent,
SettingsLayoutComponent,
PointSettingComponent,
AlgorithmSettingComponent,
TemperatureSettingComponent,
SystemSettingComponent,
];
@NgModule({
declarations: [...components, ...pages],
imports: [SharedModule, DetectionRoutingModule],
})
export class DetectionModule {}

94
projects/client/src/app/feature/detection/pages/analysis/analysis.component.html

@ -0,0 +1,94 @@
<div nz-row nzGutter="12" class="mt-[1px]">
<div nz-col nzFlex="12rem">
<nz-cascader class="w-full"
[(ngModel)]="query.pointId"
(ngModelChange)="onPointChange($event)"
[nzOptions]="points"
nzPlaceHolder="请选择机组/监测点">
</nz-cascader>
</div>
<div nz-col nzFlex="12rem">
<nz-select nzPlaceHolder="请选择磁极"
[(ngModel)]="query.zone"
nzAllowClear
class="min-w-[12rem]">
<nz-option *ngFor="let f of poles" [nzValue]="f" [nzLabel]="f + '#'"></nz-option>
</nz-select>
</div>
<div nz-col nzFlex="auto">
<nz-select nzPlaceHolder="请选择特征量"
nzMode="multiple"
[(ngModel)]="query.features"
nzAllowClear
class="mr-4 min-w-[200px]">
<nz-option *ngFor="let f of features|keyvalue" [nzValue]="f.key" [nzLabel]="f.value"></nz-option>
</nz-select>
<dec-quick-date-range [(ngModel)]="query.range"></dec-quick-date-range>
</div>
<div nz-col>
<ng-template #btnSplitTpl>
<nz-divider nzType="vertical"></nz-divider>
</ng-template>
<nz-space>
<button *nzSpaceItem nz-button nzGhost type="button" (click)="reset()">
重置
</button>
<button *nzSpaceItem nz-button nzType="primary" (click)="search()">
查询
</button>
</nz-space>
</div>
</div>
<div class="analysis grid flex-1 mt-6">
<div *ngIf="prevSelectFeatures.includes('line')">
<nz-card decCorner nzTitle="引出线变形趋势图" [nzExtra]="extraLineBtnTpl">
<ng-template #extraLineBtnTpl>
<button nz-button *ngIf="featureChartMap.get('line')" nzSize="small" nzGhost
(click)="downloadChart('line')">
保存为图片
</button>
</ng-template>
<div class="w-full h-full" #chart1></div>
<!-- <nz-empty *ngIf="!featureChartMap.get('line')"></nz-empty> -->
</nz-card>
</div>
<div *ngIf="prevSelectFeatures.includes('bolt')">
<nz-card decCorner nzTitle="螺栓松动变化趋势图" [nzExtra]="extraBoltBtnTpl">
<ng-template #extraBoltBtnTpl>
<button nz-button *ngIf="featureChartMap.get('bolt')" nzSize="small" nzGhost
(click)="downloadChart('bolt')">
保存为图片
</button>
</ng-template>
<div class="w-full h-full" #chart2></div>
</nz-card>
</div>
<div *ngIf="prevSelectFeatures.includes('pole')">
<nz-card decCorner nzTitle="磁极开匝变化趋势图" [nzExtra]="extraPoleBtnTpl">
<ng-template #extraPoleBtnTpl>
<button nz-button *ngIf="featureChartMap.get('pole')" nzSize="small" nzGhost
(click)="downloadChart('pole')">
保存为图片
</button>
</ng-template>
<div class="w-full h-full" #chart3></div>
</nz-card>
</div>
<div *ngIf="prevSelectFeatures.includes('temperature')">
<nz-card decCorner nzTitle="检测点温度变化趋势图" [nzExtra]="extraTemperatureBtnTpl">
<ng-template #extraTemperatureBtnTpl>
<button *ngIf="featureChartMap.get('temperature')" nz-button nzSize="small" nzGhost
(click)="downloadChart('temperature')">
保存为图片
</button>
</ng-template>
<div class="w-full h-full" #chart4></div>
</nz-card>
</div>
</div>

34
projects/client/src/app/feature/detection/pages/analysis/analysis.component.less

@ -0,0 +1,34 @@
:host {
display: flex;
height: calc(100% - 1px);
flex-direction: column;
overflow-y: auto;
}
.visible {
visibility: visible !important;
}
.analysis {
gap: 16px;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 500px);
& > div {
&:nth-child(1) {
grid-column: 1 / span 1;
grid-row: 1 / span 1;
}
&:nth-child(2) {
grid-column: 2 / span 1;
grid-row: 1 / span 1;
}
&:nth-child(3) {
grid-column: 1 / span 1;
grid-row: 2 / span 1;
}
&:nth-child(4) {
grid-column: 2 / span 1;
grid-row: 2 / span 1;
}
}
}

347
projects/client/src/app/feature/detection/pages/analysis/analysis.component.ts

@ -0,0 +1,347 @@
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from "@angular/core";
import { AnyObject } from "@cdk/types";
import { DetectionApiService } from "@client/app/core/services";
import { PoleItemDTO } from "@client/dtos";
import { format } from "date-fns";
import { init, EChartsType } from "echarts";
import { NzCascaderOption } from "ng-zorro-antd/cascader";
import { NzMessageService } from "ng-zorro-antd/message";
import { forkJoin } from "rxjs";
type QueryInterface = {
range: string[] | null;
pointId: null | string[];
features: string[] | null;
zone: number | null;
};
const initQuery = {
range: null,
pointId: null,
features: null,
zone: null,
};
@Component({
selector: "app-analysis",
templateUrl: "./analysis.component.html",
styleUrls: ["./analysis.component.less"],
})
export class AnalysisComponent implements OnInit, AfterViewInit {
constructor(private api: DetectionApiService, private msg: NzMessageService) {}
points: NzCascaderOption[] = [];
features: Record<string, string> = {};
query: QueryInterface = JSON.parse(JSON.stringify(initQuery));
prevSelectFeatures: string[] = [];
poles: number[] = [];
featureChartMap = new Map<string, boolean>();
@ViewChild("chart1") line!: ElementRef<HTMLDivElement>;
@ViewChild("chart2") bolt!: ElementRef<HTMLDivElement>;
@ViewChild("chart3") pole!: ElementRef<HTMLDivElement>;
@ViewChild("chart4") temperature!: ElementRef<HTMLDivElement>;
echart_line?: EChartsType;
echart_bolt?: EChartsType;
echart_pole?: EChartsType;
echart_temperature?: EChartsType;
ngOnInit(): void {
this.forkjoinPointAndFeatures();
}
ngAfterViewInit(): void {
// if (this.chart1.nativeElement) {
// this.echart1 = this.createChart(this.chart1.nativeElement);
// }
// if (this.chart2.nativeElement) {
// this.echart2 = this.createChart(this.chart2.nativeElement);
// }
// if (this.chart3.nativeElement) {
// this.echart3 = this.createChart(this.chart3.nativeElement);
// }
// if (this.chart4.nativeElement) {
// this.echart4 = this.createChart(this.chart4.nativeElement);
// }
}
onPointChange(point: Array<string>) {
let poleNumbers = 0;
if (Array.isArray(point) && point.length === 2) {
const selectPoint = this.points.find((f) => f.value === point[0])?.children?.find((f) => f.value === point[1]);
poleNumbers = selectPoint?.["poleNum"] ?? 0;
const pid = selectPoint?.["pointId"];
if (pid) {
this.api.getFeatures(pid).subscribe((featureRes) => {
this.features = featureRes.body;
this.query.features = Object.keys(this.features);
setTimeout(() => {
this.getData();
}, 70);
});
}
}
this.poles = Array.from({ length: poleNumbers }, (_, idx) => idx + 1);
}
forkjoinPointAndFeatures() {
forkJoin([this.api.getAllPoint()]).subscribe(([pointData]) => {
const stationGroups = pointData[0];
this.points = stationGroups.groupList.map((g) => {
return {
label: g.name,
value: g.motorGroupId,
children: g.pointList.map((p) => {
return {
label: p.name,
value: p.pointId,
isLeaf: true,
...p,
};
}),
};
});
this.query.pointId = [this.points[0].value, this.points[0]?.children?.[0]?.value];
this.onPointChange(this.query.pointId);
this.query.zone = 1;
});
}
ngModelChange(v: any) {}
getData() {
const { query } = this;
const q = Object.create(null);
Object.entries(query).forEach(([k, v]) => {
q[k] = v;
if (k === "range" && Array.isArray(v) && v.length === 2) {
const startTime = typeof v[0] === "string" ? v[0] : format(v[0], "yyyy-MM-dd");
const endTime = typeof v[1] === "string" ? v[1] : format(v[1], "yyyy-MM-dd");
q.startTime = startTime;
q.endTime = endTime;
}
if (k === "pointId" && Array.isArray(v) && v.length === 2) {
q.pointId = v[1];
}
});
if (!q.pointId) {
this.msg.error("请选择机组/监测点");
return;
}
if (!q.features) {
this.msg.error("请选特征量");
return;
}
this.prevSelectFeatures = q.features;
this.featureChartMap.clear();
this.api.getAnalysisChartData(q).subscribe((res) => {
Object.entries(res.body).forEach(([category, data]) => {
let echartDataSet: AnyObject[] = [];
data.forEach((item) => {
let time = echartDataSet.find((f) => f["time"] === item["time"]);
if (time) {
time[item["position"]] = item.value;
} else {
echartDataSet.push({ time: item["time"], [item["position"]]: item.value });
}
});
this.featureChartMap.set(category, true);
this.createChart(category, echartDataSet);
});
});
}
search() {
this.getData();
}
reset() {
this.query = JSON.parse(JSON.stringify(initQuery));
}
echartTitleMap = new Map([
["line", { left: "最高变化量:#-@mm", right: `最高变化量产生日期:@` }],
["bolt", { left: "最大转动角度:#-@°", right: `最大转动角度产生日期:@` }],
["pole", { left: "开匝磁极:#", right: `最多开匝磁极产生日期:@` }],
["temperature", { left: "最高变化量:@℃", right: `最高变化量产生日期:@` }],
]);
findMaxValue(arr: AnyObject[]) {
arr.forEach((obj) => {
Object.entries(obj).forEach(([k, v], idx) => {
if (idx === 0) {
obj["value"] = v;
obj["valueIdx"] = k;
}
if (k !== "value" && v > obj["value"]) {
obj["value"] = v;
obj["valueIdx"] = k;
}
});
// obj["value"] = Math.max(...Object.values(obj).filter((f) => typeof f === "number"));
});
return arr.reduce((max, current) => {
return current["value"] > max["value"] ? current : max;
});
}
createChart(category: string, data: AnyObject[]) {
console.log("category", category, data);
//@ts-expect-error
const el = this[category];
const echartRef = "echart_" + category;
if (el && el?.nativeElement) {
const div = el.nativeElement as HTMLDivElement;
//@ts-expect-error
const old: EChartsType = this[echartRef];
if (old) {
old.dispose();
}
const echart = init(el.nativeElement, "dark");
const dimensions = Object.keys(data[0]).filter((f) => f !== "time");
//@ts-expect-error
this[echartRef] = echart;
const txt = this.echartTitleMap.get(category)!;
const max = this.findMaxValue(data);
// console.log("max", max);
let text = [
`{a|${txt.left.replace("#", "#" + max["valueIdx"]).replace("@", max["value"])}}`,
`{b|${txt.right.replace("@", max["time"])}}`,
].join("");
echart.setOption({
title: {
text,
show: !!max,
textStyle: {
width: div.clientWidth - 20,
fontWeight: "normal",
rich: {
a: {
fontSize: 14,
color: "#fff",
align: "left",
},
b: {
align: "right",
color: "#fff",
fontSize: 14,
},
},
},
},
backgroundColor: "transparent",
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
label: {
backgroundColor: "#6a7985",
},
},
},
legend: {
bottom: 0,
left: 0,
formatter: "{name} #",
},
dataset: {
dimensions: ["time", ...dimensions],
source: data,
},
grid: {
top: "12%",
left: "35",
right: "35",
bottom: "10%",
containLabel: true,
},
xAxis: [
{
type: "category",
boundaryGap: false,
},
],
yAxis: [
{
type: "value",
splitLine: {
show: true,
lineStyle: {
color: "rgba(255, 255, 255, 0.1)",
},
},
},
],
series: dimensions.reduce((a, c) => {
return a.concat({
name: c,
type: "line",
areaStyle: {
opacity: "0.2",
},
emphasis: {
focus: "series",
},
});
}, [] as AnyObject[]),
});
}
}
downloadChart(type: string) {
const echart = <EChartsType>(<unknown>this[`echart_${type}` as keyof this]);
console.log("type", type, echart);
if (!echart) {
return;
}
const base64String = echart.getDataURL();
const filename = `image_${type}.png`;
const byteString = atob(base64String.split(",")[1]);
const mimeString = base64String.split(",")[0].split(":")[1].split(";")[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ab], { type: mimeString });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, 0);
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save