commit
ae8db2e68b
282 changed files with 22276 additions and 0 deletions
@ -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 |
||||
@ -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 |
||||
@ -0,0 +1,4 @@ |
|||||
|
{ |
||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 |
||||
|
"recommendations": ["angular.ng-template"] |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
{ |
||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 |
||||
|
"version": "0.2.0", |
||||
|
"configurations": [] |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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. 相机管理列表 保存了数据过后就报错了 |
||||
@ -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"] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
enableDetect => 0 开启 1 关闭 |
||||
|
|
||||
|
高级设置 |
||||
|
|
||||
|
数据分析 |
||||
@ -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; |
||||
@ -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" |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -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. |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json", |
||||
|
"dest": "../../dist/cdk", |
||||
|
"lib": { |
||||
|
"entryFile": "src/public-api.ts" |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
@ -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], |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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,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 {} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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"; |
||||
@ -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,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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
export * from "./storage.module"; |
||||
|
export * from "./storage.service"; |
||||
|
export * from "./cache.service"; |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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"; |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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() {} |
||||
|
} |
||||
@ -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> |
||||
@ -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); |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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"; |
||||
|
} |
||||
@ -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}`;
|
||||
|
// }
|
||||
|
} |
||||
@ -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; |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -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" |
||||
|
] |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
|
} |
||||
@ -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" |
||||
|
] |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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 {} |
||||
@ -0,0 +1,4 @@ |
|||||
|
<ng-container *ngIf="!loading"> |
||||
|
<app-bg-border></app-bg-border> |
||||
|
<router-outlet></router-outlet> |
||||
|
</ng-container> |
||||
@ -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 []; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 {} |
||||
@ -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; |
||||
|
} |
||||
|
}) |
||||
|
); |
||||
|
}; |
||||
@ -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; |
||||
|
}; |
||||
@ -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)); |
||||
|
} |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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 }); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
export * from "./auth.service"; |
||||
|
export * from "./detection-api.service"; |
||||
|
export * from "./websocket-api.service"; |
||||
|
export * from "./utils.service"; |
||||
@ -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) {} |
||||
|
} |
||||
@ -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);
|
||||
|
// }
|
||||
|
// }
|
||||
@ -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 {} |
||||
@ -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 {} |
||||
@ -0,0 +1 @@ |
|||||
|
export * from "./login/login.component"; |
||||
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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(["/"]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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"> |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
#{{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> |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
<span *ngIf="value" class="txt" [ngClass]="{'c-red': value.alarm}"> |
||||
|
{{value.value}} |
||||
|
</span> |
||||
|
<span *ngIf="!value" class="txt">-</span> |
||||
@ -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 }; |
||||
|
} |
||||
@ -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,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(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
<div class="pt-20"> |
||||
|
<nz-result nzStatus="403"></nz-result> |
||||
|
<h2 class=" text-center">你没有此页面的访问权限。</h2> |
||||
|
</div> |
||||
@ -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,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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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);
|
||||
|
// }
|
||||
|
// }
|
||||
|
// }
|
||||
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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"; |
||||
@ -0,0 +1,6 @@ |
|||||
|
<div class="mt-20"> |
||||
|
<nz-result nzStatus="404"></nz-result> |
||||
|
<h2 class="text-center"> |
||||
|
此页面未找到。 |
||||
|
</h2> |
||||
|
</div> |
||||
@ -0,0 +1,10 @@ |
|||||
|
import { Component } from '@angular/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-notfound', |
||||
|
templateUrl: './notfound.component.html', |
||||
|
styleUrls: ['./notfound.component.less'] |
||||
|
}) |
||||
|
export class NotfoundComponent { |
||||
|
|
||||
|
} |
||||
@ -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,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; |
||||
|
} |
||||
@ -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,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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 {} |
||||
@ -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 {} |
||||
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue