├── src
├── assets
│ ├── .gitkeep
│ ├── icon.png
│ └── icons
│ │ ├── icon-72x72.png
│ │ ├── icon-96x96.png
│ │ ├── icon-128x128.png
│ │ ├── icon-144x144.png
│ │ ├── icon-152x152.png
│ │ ├── icon-192x192.png
│ │ ├── icon-384x384.png
│ │ └── icon-512x512.png
├── app
│ ├── app.component.scss
│ ├── app.component.html
│ ├── module
│ │ ├── generic
│ │ │ ├── component
│ │ │ │ ├── generic
│ │ │ │ │ ├── generic.component.scss
│ │ │ │ │ ├── generic.component.spec.ts
│ │ │ │ │ ├── generic.component.html
│ │ │ │ │ └── generic.component.ts
│ │ │ │ ├── attribute
│ │ │ │ │ ├── attribute.component.scss
│ │ │ │ │ ├── attribute.component.spec.ts
│ │ │ │ │ ├── attribute.component.html
│ │ │ │ │ └── attribute.component.ts
│ │ │ │ ├── log
│ │ │ │ │ ├── log.component.html
│ │ │ │ │ ├── log.component.scss
│ │ │ │ │ ├── log.component.spec.ts
│ │ │ │ │ └── log.component.ts
│ │ │ │ ├── result
│ │ │ │ │ ├── result.component.scss
│ │ │ │ │ ├── result.component.html
│ │ │ │ │ ├── result.component.spec.ts
│ │ │ │ │ └── result.component.ts
│ │ │ │ └── attribute-item
│ │ │ │ │ ├── attribute-item.component.scss
│ │ │ │ │ ├── attribute-item.component.spec.ts
│ │ │ │ │ ├── attribute-item.component.ts
│ │ │ │ │ └── attribute-item.component.html
│ │ │ └── generic.module.ts
│ │ ├── index
│ │ │ ├── index.module.ts
│ │ │ ├── index-routing.module.ts
│ │ │ └── component
│ │ │ │ └── index
│ │ │ │ ├── index.component.ts
│ │ │ │ ├── index.component.spec.ts
│ │ │ │ ├── index.component.scss
│ │ │ │ └── index.component.html
│ │ └── shared
│ │ │ └── shared.module.ts
│ ├── http
│ │ ├── index.ts
│ │ └── ResponseErrorInterceptor.ts
│ ├── app.component.ts
│ ├── service
│ │ ├── pwa.service.spec.ts
│ │ ├── utils.service.spec.ts
│ │ ├── generic.service.spec.ts
│ │ ├── persistence.service.spec.ts
│ │ ├── pwa.service.ts
│ │ ├── persistence.service.ts
│ │ ├── utils.service.ts
│ │ └── generic.service.ts
│ ├── app-routing.module.ts
│ ├── icons-provider.module.ts
│ ├── app.component.spec.ts
│ ├── app.module.ts
│ └── theme.service.ts
├── styles
│ ├── themes
│ │ ├── base.less
│ │ ├── dark.less
│ │ ├── default.less
│ │ └── mixin.less
│ ├── dark.less
│ └── default.less
├── favicon.ico
├── styles.scss
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── main.ts
├── index.html
├── test.ts
├── manifest.webmanifest
└── polyfills.ts
├── .editorconfig
├── e2e
├── src
│ ├── app.po.ts
│ └── app.e2e-spec.ts
├── tsconfig.json
└── protractor.conf.js
├── tsconfig.app.json
├── tsconfig.spec.json
├── tsconfig.json
├── ngsw-config.json
├── .gitignore
├── .browserslistrc
├── CHANGELOG.md
├── karma.conf.js
├── package.json
├── .github
└── workflows
│ └── main.yml
├── README.md
├── tslint.json
├── angular.json
└── LICENSE
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/styles/themes/base.less:
--------------------------------------------------------------------------------
1 | @margin-md: 17px;
2 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/generic/generic.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/attribute/attribute.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-web/master/src/favicon.ico
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/src/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-web/master/src/assets/icon.png
--------------------------------------------------------------------------------
/src/styles/dark.less:
--------------------------------------------------------------------------------
1 | @import '../../node_modules/ng-zorro-antd/ng-zorro-antd';
2 | @import "./themes/dark";
3 |
--------------------------------------------------------------------------------
/src/styles/default.less:
--------------------------------------------------------------------------------
1 | @import '../../node_modules/ng-zorro-antd/ng-zorro-antd';
2 | @import "./themes/default";
3 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-web/master/src/assets/icons/icon-72x72.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-web/master/src/assets/icons/icon-96x96.png
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | baseUrl: '127.0.0.1:8868'
4 | };
5 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-web/master/src/assets/icons/icon-128x128.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-web/master/src/assets/icons/icon-144x144.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-web/master/src/assets/icons/icon-152x152.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-web/master/src/assets/icons/icon-192x192.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-web/master/src/assets/icons/icon-384x384.png
--------------------------------------------------------------------------------
/src/assets/icons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-web/master/src/assets/icons/icon-512x512.png
--------------------------------------------------------------------------------
/src/app/module/generic/component/log/log.component.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/result/result.component.scss:
--------------------------------------------------------------------------------
1 | pre {
2 | overflow: unset;
3 | }
4 |
5 | .result-box {
6 | max-height: 500px;
7 | overflow: auto;
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/log/log.component.scss:
--------------------------------------------------------------------------------
1 | pre {
2 | overflow: unset;
3 | margin: 0;
4 | }
5 |
6 | .log-box {
7 | max-height: 500px;
8 | overflow: auto;
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/themes/dark.less:
--------------------------------------------------------------------------------
1 | @import (multiple) '../../../node_modules/ng-zorro-antd/src/style/themes/dark';
2 | @import './base';
3 | @layout-sider-background: #001529;
4 | @menu-dark-bg: #001529;
5 |
6 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/result/result.component.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/styles/themes/default.less:
--------------------------------------------------------------------------------
1 | @import (multiple) '../../../node_modules/ng-zorro-antd/src/style/themes/default';
2 | @import './base';
3 |
4 | @layout-header-background: #fff;
5 | @layout-sider-background: #001529;
6 | @menu-dark-bg: #001529;
7 |
--------------------------------------------------------------------------------
/src/styles/themes/mixin.less:
--------------------------------------------------------------------------------
1 | .themeMixin(@rules) {
2 | html {
3 | &.default {
4 | @import './default.less';
5 | @rules();
6 | }
7 | &.dark {
8 | @import './dark.less';
9 | @rules();
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/http/index.ts:
--------------------------------------------------------------------------------
1 | import {HTTP_INTERCEPTORS} from '@angular/common/http';
2 | import {ResponseErrorInterceptor} from './ResponseErrorInterceptor';
3 | import {Provider} from '@angular/core';
4 |
5 | export const httpInterceptorProviders: Provider[] = [
6 | {provide: HTTP_INTERCEPTORS, useClass: ResponseErrorInterceptor, multi: true},
7 | ];
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/e2e/src/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class AppPage {
4 | navigateTo(): Promise {
5 | return browser.get(browser.baseUrl) as Promise;
6 | }
7 |
8 | getTitleText(): Promise {
9 | return element(by.css('app-root .content span')).getText() as Promise;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "../tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "../out-tsc/e2e",
6 | "module": "commonjs",
7 | "target": "es2018",
8 | "types": [
9 | "jasmine",
10 | "jasminewd2",
11 | "node"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": []
7 | },
8 | "files": [
9 | "src/main.ts",
10 | "src/polyfills.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/attribute-item/attribute-item.component.scss:
--------------------------------------------------------------------------------
1 | .box-item {
2 | margin: 12px 0;
3 | }
4 |
5 | .box-item-button{
6 | margin-top: 4px;
7 | }
8 |
9 | nz-select {
10 | width: 84px;
11 | }
12 |
13 | .box-item-select {
14 | text-align: center;
15 | line-height: 31px;
16 | }
17 |
18 | .box-item-checkbox {
19 | margin-left: 6px;
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {PwaService} from './service/pwa.service';
3 |
4 | @Component({
5 | selector: 'app-root',
6 | templateUrl: './app.component.html',
7 | styleUrls: ['./app.component.scss']
8 | })
9 | export class AppComponent {
10 | constructor(private pwaService: PwaService) {
11 | pwaService.start();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/spec",
6 | "types": [
7 | "jasmine"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts",
12 | "src/polyfills.ts"
13 | ],
14 | "include": [
15 | "src/**/*.spec.ts",
16 | "src/**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/src/app/service/pwa.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { PwaService } from './pwa.service';
4 |
5 | describe('PwaService', () => {
6 | let service: PwaService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(PwaService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/service/utils.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { UtilsService } from './utils.service';
4 |
5 | describe('UtilsService', () => {
6 | let service: UtilsService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(UtilsService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/module/index/index.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 |
3 | import {IndexRoutingModule} from './index-routing.module';
4 | import {SharedModule} from '../shared/shared.module';
5 | import {IndexComponent} from './component/index/index.component';
6 |
7 |
8 | @NgModule({
9 | declarations: [IndexComponent],
10 | imports: [
11 | SharedModule,
12 | IndexRoutingModule
13 | ]
14 | })
15 | export class IndexModule {
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/service/generic.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { GenericService } from './generic.service';
4 |
5 | describe('GenericService', () => {
6 | let service: GenericService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(GenericService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/service/persistence.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { PersistenceService } from './persistence.service';
4 |
5 | describe('PersistenceService', () => {
6 | let service: PersistenceService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(PersistenceService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 测试工具箱
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "sourceMap": true,
8 | "declaration": false,
9 | "downlevelIteration": true,
10 | "experimentalDecorators": true,
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "target": "es2015",
14 | "module": "es2020",
15 | "lib": [
16 | "es2018",
17 | "dom"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { Routes, RouterModule } from '@angular/router';
3 | import {IndexComponent} from './module/index/component/index/index.component';
4 |
5 | const routes: Routes = [
6 | {path: '', component: IndexComponent},
7 | /* { path: 'welcome', loadChildren: () => import('./pages/welcome/welcome.module').then(m => m.WelcomeModule) }*/
8 | ];
9 |
10 | @NgModule({
11 | imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
12 | exports: [RouterModule]
13 | })
14 | export class AppRoutingModule { }
15 |
--------------------------------------------------------------------------------
/src/app/icons-provider.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { NZ_ICONS, NzIconModule } from 'ng-zorro-antd/icon';
3 |
4 | import {
5 | MenuFoldOutline,
6 | MenuUnfoldOutline,
7 | FormOutline,
8 | DashboardOutline
9 | } from '@ant-design/icons-angular/icons';
10 |
11 | const icons = [MenuFoldOutline, MenuUnfoldOutline, DashboardOutline, FormOutline];
12 |
13 | @NgModule({
14 | imports: [NzIconModule],
15 | exports: [NzIconModule],
16 | providers: [
17 | { provide: NZ_ICONS, useValue: icons }
18 | ]
19 | })
20 | export class IconsProviderModule {
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/module/index/index-routing.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {RouterModule, Routes} from '@angular/router';
3 | import {IndexComponent} from './component/index/index.component';
4 | import {GenericComponent} from '../generic/component/generic/generic.component';
5 |
6 | const routes: Routes = [
7 | {
8 | path: '',
9 | component: IndexComponent,
10 | children: [
11 | {path: '', redirectTo: 'generic', pathMatch: 'full'},
12 | {path: 'generic', component: GenericComponent},
13 | ]
14 | }
15 | ];
16 |
17 | @NgModule({
18 | imports: [RouterModule.forChild(routes)],
19 | exports: [RouterModule]
20 | })
21 | export class IndexRoutingModule {
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/module/index/component/index/index.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit} from '@angular/core';
2 | import {ThemeService, ThemeType} from '../../../../theme.service';
3 |
4 | @Component({
5 | selector: 'app-index',
6 | templateUrl: './index.component.html',
7 | styleUrls: ['./index.component.scss']
8 | })
9 | export class IndexComponent implements OnInit {
10 |
11 | isCollapsed = true;
12 |
13 | isDarkMode: boolean;
14 |
15 | constructor(private themeService: ThemeService) {
16 | }
17 |
18 | ngOnInit(): void {
19 | this.isDarkMode = this.themeService.currentTheme === ThemeType.dark;
20 | }
21 |
22 | toggleTheme(): void {
23 | this.themeService.toggleTheme().then();
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/ngsw-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json",
3 | "index": "/index.html",
4 | "assetGroups": [
5 | {
6 | "name": "app",
7 | "installMode": "prefetch",
8 | "resources": {
9 | "files": [
10 | "/favicon.ico",
11 | "/index.html",
12 | "/manifest.webmanifest",
13 | "/*.css",
14 | "/*.js"
15 | ]
16 | }
17 | },
18 | {
19 | "name": "assets",
20 | "installMode": "lazy",
21 | "updateMode": "prefetch",
22 | "resources": {
23 | "files": [
24 | "/assets/**",
25 | "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
26 | ]
27 | }
28 | }
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/log/log.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { LogComponent } from './log.component';
4 |
5 | describe('LogComponent', () => {
6 | let component: LogComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ LogComponent ]
12 | })
13 | .compileComponents();
14 | });
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(LogComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/e2e/src/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppPage } from './app.po';
2 | import { browser, logging } from 'protractor';
3 |
4 | describe('workspace-project App', () => {
5 | let page: AppPage;
6 |
7 | beforeEach(() => {
8 | page = new AppPage();
9 | });
10 |
11 | it('should display welcome message', () => {
12 | page.navigateTo();
13 | expect(page.getTitleText()).toEqual('generic-service-client-web app is running!');
14 | });
15 |
16 | afterEach(async () => {
17 | // Assert that there are no errors emitted from the browser
18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER);
19 | expect(logs).not.toContain(jasmine.objectContaining({
20 | level: logging.Level.SEVERE,
21 | } as logging.Entry));
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/module/index/component/index/index.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { IndexComponent } from './index.component';
4 |
5 | describe('IndexComponent', () => {
6 | let component: IndexComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ IndexComponent ]
12 | })
13 | .compileComponents();
14 | });
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(IndexComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | baseUrl: 'localhost:8868'
8 | };
9 |
10 | /*
11 | * For easier debugging in development mode, you can import the following file
12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
13 | *
14 | * This import should be commented out in production mode because it will have a negative impact
15 | * on performance if an error is thrown.
16 | */
17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
18 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/result/result.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ResultComponent } from './result.component';
4 |
5 | describe('ResultComponent', () => {
6 | let component: ResultComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ ResultComponent ]
12 | })
13 | .compileComponents();
14 | });
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(ResultComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/generic/generic.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { GenericComponent } from './generic.component';
4 |
5 | describe('GenericComponent', () => {
6 | let component: GenericComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ GenericComponent ]
12 | })
13 | .compileComponents();
14 | });
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(GenericComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/attribute/attribute.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { AttributeComponent } from './attribute.component';
4 |
5 | describe('AttributeComponent', () => {
6 | let component: AttributeComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ AttributeComponent ]
12 | })
13 | .compileComponents();
14 | });
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(AttributeComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/module/generic/generic.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {GenericComponent} from './component/generic/generic.component';
4 | import {SharedModule} from '../shared/shared.module';
5 | import {AttributeItemComponent} from './component/attribute-item/attribute-item.component';
6 | import {AttributeComponent} from './component/attribute/attribute.component';
7 | import {ResultComponent} from './component/result/result.component';
8 | import {LogComponent} from './component/log/log.component';
9 |
10 |
11 | @NgModule({
12 | declarations: [GenericComponent, AttributeItemComponent, AttributeComponent, ResultComponent, LogComponent],
13 | imports: [
14 | CommonModule,
15 | SharedModule
16 | ]
17 | })
18 | export class GenericModule {
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/attribute-item/attribute-item.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { AttributeItemComponent } from './attribute-item.component';
4 |
5 | describe('AttributeItemComponent', () => {
6 | let component: AttributeItemComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async () => {
10 | await TestBed.configureTestingModule({
11 | declarations: [ AttributeItemComponent ]
12 | })
13 | .compileComponents();
14 | });
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(AttributeItemComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone-testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: {
11 | context(path: string, deep?: boolean, filter?: RegExp): {
12 | keys(): string[];
13 | (id: string): T;
14 | };
15 | };
16 |
17 | // First, initialize the Angular testing environment.
18 | getTestBed().initTestEnvironment(
19 | BrowserDynamicTestingModule,
20 | platformBrowserDynamicTesting()
21 | );
22 | // Then we find all the tests.
23 | const context = require.context('./', true, /\.spec\.ts$/);
24 | // And load the modules.
25 | context.keys().map(context);
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events*.json
15 | speed-measure-plugin*.json
16 |
17 | # IDEs and editors
18 | /.idea
19 | .project
20 | .classpath
21 | .c9/
22 | *.launch
23 | .settings/
24 | *.sublime-workspace
25 |
26 | # IDE - VSCode
27 | .vscode/*
28 | !.vscode/settings.json
29 | !.vscode/tasks.json
30 | !.vscode/launch.json
31 | !.vscode/extensions.json
32 | .history/*
33 |
34 | # misc
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | yarn-error.log
41 | testem.log
42 | /typings
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 | .wakatime-project
48 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/log/log.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input, OnInit, EventEmitter} from '@angular/core';
2 | import {GenericService} from '../../../../service/generic.service';
3 |
4 | @Component({
5 | selector: 'app-log',
6 | templateUrl: './log.component.html',
7 | styleUrls: ['./log.component.scss']
8 | })
9 | export class LogComponent implements OnInit {
10 | resultData: string[] = [];
11 | @Input()
12 | clearEvent: EventEmitter;
13 |
14 | constructor(private genericService: GenericService) {
15 | }
16 |
17 | ngOnInit(): void {
18 | this.genericService.connectionLogWebSocket().subscribe(message => {
19 | this.resultData.push(message);
20 | // message.split('\n').forEach(line => this.resultData.push(line));
21 | });
22 | this.clearEvent.subscribe(() => this.resultData = []);
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # For the full list of supported browsers by the Angular framework, please see:
6 | # https://angular.io/guide/browser-support
7 |
8 | # You can see what browsers were selected by your queries by running:
9 | # npx browserslist
10 |
11 | last 1 Chrome version
12 | last 1 Firefox version
13 | last 2 Edge major versions
14 | last 2 Safari major versions
15 | last 2 iOS major versions
16 | Firefox ESR
17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
19 |
--------------------------------------------------------------------------------
/e2e/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Protractor configuration file, see link for more information
3 | // https://github.com/angular/protractor/blob/master/lib/config.ts
4 |
5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
6 |
7 | /**
8 | * @type { import("protractor").Config }
9 | */
10 | exports.config = {
11 | allScriptsTimeout: 11000,
12 | specs: [
13 | './src/**/*.e2e-spec.ts'
14 | ],
15 | capabilities: {
16 | browserName: 'chrome'
17 | },
18 | directConnect: true,
19 | baseUrl: 'http://localhost:4200/',
20 | framework: 'jasmine',
21 | jasmineNodeOpts: {
22 | showColors: true,
23 | defaultTimeoutInterval: 30000,
24 | print: function() {}
25 | },
26 | onPrepare() {
27 | require('ts-node').register({
28 | project: require('path').join(__dirname, './tsconfig.json')
29 | });
30 | jasmine.getEnv().addReporter(new SpecReporter({
31 | spec: {
32 | displayStacktrace: StacktraceOption.PRETTY
33 | }
34 | }));
35 | }
36 | };
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 1.2.18
2 | - 升级依赖
3 | # 1.2.17
4 | - 新增夜间模式
5 | # 1.2.16
6 | - 新增环境信息保存
7 | # 1.2.15
8 | - 新增支持对参数禁用启用
9 | # 1.2.13
10 | - nexus支持
11 | - 升级Angular版本
12 | # 1.2.11
13 | - 导入导出优化
14 | # 1.2.10
15 | - 用户提示更加明确
16 | # 1.2.9
17 | - 升级Angular版本
18 | - 升级ng-zorro-antd版本
19 | # 1.2.8
20 | - 升级Angular版本
21 | - 升级dayjs版本
22 | # 1.2.7
23 | - 升级Angular版本
24 | - 新增检查参数是否正确
25 | # 1.2.6
26 | - 修复多方法弹出不弹的问题
27 | - 支持Nacos注册中心
28 | # 1.2.5
29 | # 依赖
30 | - 升级Angular版本到11.0.7
31 | - 升级dayjs版本到1.10.2
32 | - 升级jsoneditor版本到9.1.7
33 | # 1.2.4
34 | # 依赖
35 | - 升级Angular版本到[11.0.6](https://github.com/angular/angular/blob/master/CHANGELOG.md#1106-2021-01-06)
36 | # 1.2.3
37 | ## 功能
38 | - TAB支持重命名
39 | # 1.2.2
40 | ## 功能
41 | - 多环境打包
42 | # 1.2.1
43 | ## 功能
44 | - 新增日期格式
45 | # 1.2.0
46 | ## 修复
47 | - 修复上传文件进度条进度不准确BUG
48 | - 修复按钮高度不一致问题
49 | - 新增自动完成提示枚举值
50 | ## 功能
51 | - 新增ChangeLog文件
52 | - 新增展开关闭功能
53 | - 新增解析日期自动填充功能
54 | # 1.0.1
55 | - 升级ng-zorro-antd至11.0.1
56 | # 1.0.0
57 | - 升级Angular至11.0.5
58 | - 升级ng-zorro-antd至11.0.0
59 | - 添加PWA支持
60 | # 0.0.0
61 | - 初始化项目
62 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, './coverage/generic-service-client-web'),
20 | reports: ['html', 'lcovonly', 'text-summary'],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false,
30 | restartOnFileChange: true
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 | import { AppComponent } from './app.component';
4 |
5 | describe('AppComponent', () => {
6 | beforeEach(async () => {
7 | await TestBed.configureTestingModule({
8 | imports: [
9 | RouterTestingModule
10 | ],
11 | declarations: [
12 | AppComponent
13 | ],
14 | }).compileComponents();
15 | });
16 |
17 | it('should create the app', () => {
18 | const fixture = TestBed.createComponent(AppComponent);
19 | const app = fixture.componentInstance;
20 | expect(app).toBeTruthy();
21 | });
22 |
23 | it(`should have as title 'generic-service-client-web'`, () => {
24 | const fixture = TestBed.createComponent(AppComponent);
25 | const app = fixture.componentInstance;
26 | expect(app.title).toEqual('generic-service-client-web');
27 | });
28 |
29 | it('should render title', () => {
30 | const fixture = TestBed.createComponent(AppComponent);
31 | fixture.detectChanges();
32 | const compiled = fixture.nativeElement;
33 | expect(compiled.querySelector('.content span').textContent).toContain('generic-service-client-web app is running!');
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/app/service/pwa.service.ts:
--------------------------------------------------------------------------------
1 | import {ApplicationRef, Injectable} from '@angular/core';
2 | import {environment} from '../../environments/environment';
3 | import {SwUpdate} from '@angular/service-worker';
4 | import {concat, interval} from 'rxjs';
5 | import {first} from 'rxjs/operators';
6 |
7 | @Injectable({
8 | providedIn: 'root'
9 | })
10 | export class PwaService {
11 |
12 | constructor(private appRef: ApplicationRef, private updates: SwUpdate) {
13 | }
14 |
15 | start(): void {
16 | if (!environment.production) {
17 | console.log('non production environment');
18 | return;
19 | }
20 | console.log('pwa service running...');
21 | this.updates.available.subscribe(event => {
22 | console.log('current version is', event.current);
23 | console.log('available version is', event.available);
24 | });
25 | this.updates.activated.subscribe(event => {
26 | console.log('old version was', event.previous);
27 | console.log('new version is', event.current);
28 | });
29 |
30 | const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true));
31 | const everySixHours$ = interval(6 * 60 * 60 * 1000);
32 | const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
33 |
34 | everySixHoursOnceAppIsStable$.subscribe(() => this.updates.checkForUpdate());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/module/index/component/index/index.component.scss:
--------------------------------------------------------------------------------
1 | .menu-sidebar {
2 | position: relative;
3 | z-index: 10;
4 | min-height: 100vh;
5 | box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
6 | }
7 |
8 | .header-trigger {
9 | height: 64px;
10 | padding: 20px 24px;
11 | font-size: 20px;
12 | cursor: pointer;
13 | transition: all .3s, padding 0s;
14 | }
15 |
16 | .header-right {
17 | float: right;
18 | margin-right: 32px;
19 | }
20 |
21 | .trigger:hover {
22 | color: #1890ff;
23 | }
24 |
25 | .sidebar-logo {
26 | position: relative;
27 | height: 64px;
28 | padding-left: 24px;
29 | overflow: hidden;
30 | line-height: 64px;
31 | background: #001529;
32 | transition: all .3s;
33 | }
34 |
35 | .sidebar-logo img {
36 | display: inline-block;
37 | height: 32px;
38 | width: 32px;
39 | vertical-align: middle;
40 | }
41 |
42 | .sidebar-logo h1 {
43 | display: inline-block;
44 | margin: 0 0 0 20px;
45 | color: #fff;
46 | font-weight: 600;
47 | font-size: 14px;
48 | font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
49 | vertical-align: middle;
50 | }
51 |
52 | nz-header {
53 | padding: 0;
54 | width: 100%;
55 | z-index: 2;
56 | }
57 |
58 | .app-header {
59 | position: relative;
60 | height: 64px;
61 | padding: 0;
62 | box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
63 | }
64 |
65 | nz-content {
66 | margin: 24px;
67 | }
68 |
69 | .inner-content {
70 | padding: 24px;
71 | }
72 |
73 | .white-back{
74 | background: #fff;
75 | }
76 |
--------------------------------------------------------------------------------
/src/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Dubbo泛化客户端",
3 | "short_name": "Dubbo泛化客户端",
4 | "theme_color": "#1976d2",
5 | "background_color": "#fafafa",
6 | "display": "standalone",
7 | "scope": "./",
8 | "start_url": "./",
9 | "icons": [
10 | {
11 | "src": "assets/icons/icon-72x72.png",
12 | "sizes": "72x72",
13 | "type": "image/png",
14 | "purpose": "maskable any"
15 | },
16 | {
17 | "src": "assets/icons/icon-96x96.png",
18 | "sizes": "96x96",
19 | "type": "image/png",
20 | "purpose": "maskable any"
21 | },
22 | {
23 | "src": "assets/icons/icon-128x128.png",
24 | "sizes": "128x128",
25 | "type": "image/png",
26 | "purpose": "maskable any"
27 | },
28 | {
29 | "src": "assets/icons/icon-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image/png",
32 | "purpose": "maskable any"
33 | },
34 | {
35 | "src": "assets/icons/icon-152x152.png",
36 | "sizes": "152x152",
37 | "type": "image/png",
38 | "purpose": "maskable any"
39 | },
40 | {
41 | "src": "assets/icons/icon-192x192.png",
42 | "sizes": "192x192",
43 | "type": "image/png",
44 | "purpose": "maskable any"
45 | },
46 | {
47 | "src": "assets/icons/icon-384x384.png",
48 | "sizes": "384x384",
49 | "type": "image/png",
50 | "purpose": "maskable any"
51 | },
52 | {
53 | "src": "assets/icons/icon-512x512.png",
54 | "sizes": "512x512",
55 | "type": "image/png",
56 | "purpose": "maskable any"
57 | }
58 | ]
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/module/index/component/index/index.component.html:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "generic-service-client-web",
3 | "version": "1.2.18",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build --prod",
8 | "test": "ng test",
9 | "lint": "ng lint",
10 | "e2e": "ng e2e"
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/animations": "~11.2.6",
15 | "@angular/common": "~11.2.6",
16 | "@angular/compiler": "~11.2.6",
17 | "@angular/core": "~11.2.6",
18 | "@angular/forms": "~11.2.6",
19 | "@angular/platform-browser": "~11.2.6",
20 | "@angular/platform-browser-dynamic": "~11.2.6",
21 | "@angular/router": "~11.2.6",
22 | "@angular/service-worker": "~11.2.6",
23 | "dayjs": "^1.10.4",
24 | "js-base64": "^3.6.0",
25 | "jsoneditor": "^9.2.0",
26 | "ng-zorro-antd": "^11.3.0",
27 | "rxjs": "~6.6.6",
28 | "tslib": "^2.0.0",
29 | "uuid": "^8.3.2",
30 | "zone.js": "~0.10.2"
31 | },
32 | "devDependencies": {
33 | "@angular-devkit/build-angular": "~0.1102.5",
34 | "@angular/cli": "~11.2.5",
35 | "@angular/compiler-cli": "~11.2.6",
36 | "@types/jasmine": "~3.6.0",
37 | "@types/jasminewd2": "~2.0.3",
38 | "@types/node": "^12.11.1",
39 | "codelyzer": "^6.0.0",
40 | "jasmine-core": "~3.6.0",
41 | "jasmine-spec-reporter": "~5.0.0",
42 | "karma": "~5.2.3",
43 | "karma-chrome-launcher": "~3.1.0",
44 | "karma-coverage-istanbul-reporter": "~3.0.2",
45 | "karma-jasmine": "~4.0.0",
46 | "karma-jasmine-html-reporter": "^1.5.0",
47 | "protractor": "~7.0.0",
48 | "ts-node": "~8.3.0",
49 | "tslint": "~6.1.0",
50 | "typescript": "~4.0.2"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Npm Web project with Node.JS and create release
2 | # author: https://github.com/itning
3 |
4 | name: Auto Build
5 |
6 | on:
7 | push:
8 | # Sequence of patterns matched against refs/tags
9 | tags:
10 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Npm Install And Build
20 | uses: actions/setup-node@v2
21 | with:
22 | node-version: '14'
23 | - run: npm install
24 | - run: npm run build
25 | - name: Archive Release
26 | uses: papeloto/action-zip@v1
27 | with:
28 | files: dist/
29 | dest: release.zip
30 | - name: Create Release
31 | id: create_release
32 | uses: actions/create-release@v1
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
35 | with:
36 | tag_name: ${{ github.ref }}
37 | release_name: Release ${{ github.ref }}
38 | body: |
39 | - This Release Build By Github Action.
40 | - [Click Me To See Change Log File.](https://github.com/${{ github.repository }}/blob/master/CHANGELOG.md)
41 | draft: false
42 | prerelease: false
43 | - name: Upload Release Asset
44 | id: upload-release-asset
45 | uses: actions/upload-release-asset@v1
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | with:
49 | upload_url: ${{ steps.create_release.outputs.upload_url }}
50 | asset_path: ./release.zip
51 | asset_name: release.zip
52 | asset_content_type: application/zip
53 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import {BrowserModule} from '@angular/platform-browser';
2 | import {APP_INITIALIZER, NgModule} from '@angular/core';
3 |
4 | import {AppRoutingModule} from './app-routing.module';
5 | import {AppComponent} from './app.component';
6 | import {FormsModule} from '@angular/forms';
7 | import {HttpClientModule} from '@angular/common/http';
8 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
9 | import {NZ_I18N, zh_CN} from 'ng-zorro-antd/i18n';
10 | import {registerLocaleData} from '@angular/common';
11 | import zh from '@angular/common/locales/zh';
12 | import {SharedModule} from './module/shared/shared.module';
13 | import {IndexModule} from './module/index/index.module';
14 | import {GenericModule} from './module/generic/generic.module';
15 | import {httpInterceptorProviders} from './http';
16 | import {ServiceWorkerModule} from '@angular/service-worker';
17 | import {environment} from '../environments/environment';
18 | import {ThemeService} from './theme.service';
19 |
20 | registerLocaleData(zh);
21 |
22 | export const AppInitializerProvider = {
23 | provide: APP_INITIALIZER,
24 | useFactory: (themeService: ThemeService) => () => {
25 | return themeService.loadTheme();
26 | },
27 | deps: [ThemeService],
28 | multi: true,
29 | };
30 |
31 | @NgModule({
32 | declarations: [
33 | AppComponent
34 | ],
35 | imports: [
36 | GenericModule,
37 | IndexModule,
38 | SharedModule,
39 | BrowserModule,
40 | AppRoutingModule,
41 | FormsModule,
42 | HttpClientModule,
43 | BrowserAnimationsModule,
44 | ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production})
45 | ],
46 | providers: [AppInitializerProvider, {provide: NZ_I18N, useValue: zh_CN}, httpInterceptorProviders],
47 | bootstrap: [AppComponent]
48 | })
49 | export class AppModule {
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/http/ResponseErrorInterceptor.ts:
--------------------------------------------------------------------------------
1 | import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
2 | import {EMPTY, Observable} from 'rxjs';
3 | import {Injectable} from '@angular/core';
4 | import {catchError} from 'rxjs/operators';
5 | import {NzNotificationService} from 'ng-zorro-antd/notification';
6 |
7 | /**
8 | * 响应错误处理
9 | */
10 | @Injectable()
11 | export class ResponseErrorInterceptor implements HttpInterceptor {
12 | private static IS_SHOW_NOTIFICATION_NOW = false;
13 |
14 | constructor(private notification: NzNotificationService) {
15 | }
16 |
17 | private showErrorNotificationOnceOnView(title: string, content: string, id: string): void {
18 | if (ResponseErrorInterceptor.IS_SHOW_NOTIFICATION_NOW) {
19 | return;
20 | } else {
21 | this.notification.error(title, content, {nzKey: id})
22 | .onClose.subscribe(() => ResponseErrorInterceptor.IS_SHOW_NOTIFICATION_NOW = false);
23 | }
24 | }
25 |
26 | private handleError(): (error: HttpErrorResponse) => Observable {
27 | return (error: HttpErrorResponse) => {
28 | if (error.error instanceof ErrorEvent) {
29 | // 发生客户端或网络错误。
30 | console.error('An error occurred:', error.error.message);
31 | this.showErrorNotificationOnceOnView('客户端错误:', error.error.message, 'client');
32 | } else {
33 | console.error(
34 | `Backend returned code ${error.status}, ` +
35 | `body was: ${JSON.stringify(error.error)}`);
36 | if (error.status === 0) {
37 | this.showErrorNotificationOnceOnView('网络错误:', '请检查网络连接后再试', 'no-net');
38 | } else {
39 | this.showErrorNotificationOnceOnView('错误:', error.message, 'backend');
40 | }
41 | }
42 | return EMPTY;
43 | };
44 | }
45 |
46 | intercept(req: HttpRequest, next: HttpHandler): Observable> {
47 | return next.handle(req)
48 | .pipe(
49 | catchError(this.handleError()),
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dubbo 泛化调用客户端-前端
2 |
3 | [](https://github.com/itning/generic-service-client-web/stargazers)
4 | [](https://github.com/itning/generic-service-client-web/network/members)
5 | [](https://github.com/itning/generic-service-client-web/watchers)
6 | [](https://github.com/itning?tab=followers)
7 |
8 | [](https://github.com/itning/generic-service-client-web/actions/workflows/main.yml)
9 | [](https://github.com/itning/generic-service-client-web/issues)
10 | [](https://github.com/itning/generic-service-client-web/blob/master/LICENSE)
11 | [](https://github.com/itning/generic-service-client-web/commits)
12 | [](https://github.com/itning/generic-service-client-web/releases)
13 | [](https://github.com/itning/generic-service-client-web)
14 | [](http://hits.dwyl.com/itning/generic-service-client-web)
15 | [](https://github.com/itning/generic-service-client-web)
16 |
17 | [Click Me To Open Readme](https://github.com/itning/generic-service-client)
18 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/result/result.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
2 | import {GenericService, TabInfo, WebSocketMessageType, WebSocketResultModel} from '../../../../service/generic.service';
3 | import {NzNotificationService} from 'ng-zorro-antd/notification';
4 | import {UtilsService} from '../../../../service/utils.service';
5 |
6 | @Component({
7 | selector: 'app-result',
8 | templateUrl: './result.component.html',
9 | styleUrls: ['./result.component.scss']
10 | })
11 | export class ResultComponent implements OnInit {
12 | @Input()
13 | tabs: TabInfo[] = [];
14 | @Input()
15 | nowSelectedTab: number;
16 | @Output()
17 | lastJsonInfo: EventEmitter = new EventEmitter();
18 | @ViewChild('resultBox', {static: true})
19 | resultBoxElementRef: ElementRef;
20 |
21 | constructor(private genericService: GenericService,
22 | private notification: NzNotificationService,
23 | private util: UtilsService) {
24 | }
25 |
26 | ngOnInit(): void {
27 | this.genericService.connectionResultWebSocketReply()
28 | .subscribe(model => {
29 | const tabInfo = this.tabs.find(it => it.id === model.echo);
30 | if (tabInfo) {
31 | this.renderResultView(model, tabInfo);
32 | } else {
33 | this.tabs.forEach(it => it.resultData.push(model.message));
34 | }
35 | setTimeout(() => this.util.scrollToEndSmooth(this.resultBoxElementRef.nativeElement as Element), 250);
36 | }, error => {
37 | this.notification.error('网络错误', `WebSocket连接出现错误!`);
38 | console.error(error);
39 | });
40 | }
41 |
42 | private renderResultView(resultModel: WebSocketResultModel, tab: TabInfo): void {
43 | switch (resultModel.type) {
44 | case WebSocketMessageType.PLAINTEXT:
45 | resultModel.message.split('\n').forEach(item => tab.resultData.push(item));
46 | break;
47 | case WebSocketMessageType.JSON:
48 | this.lastJsonInfo.emit(resultModel.message);
49 | tab.resultData.push(`${resultModel.message}`);
50 | break;
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/theme.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {PersistenceService} from './service/persistence.service';
3 |
4 | export enum ThemeType {
5 | dark = 'dark',
6 | default = 'default',
7 | }
8 |
9 | @Injectable({
10 | providedIn: 'root',
11 | })
12 | export class ThemeService {
13 | private static readonly DEFAULT_THEME = 'DEFAULT_THEME';
14 |
15 | currentTheme = ThemeType.default;
16 |
17 | constructor(private persistenceService: PersistenceService) {
18 | const defaultTheme = this.persistenceService.getMetaInfo(ThemeService.DEFAULT_THEME);
19 | if (ThemeType.default === defaultTheme) {
20 | this.currentTheme = ThemeType.default;
21 | } else if (ThemeType.dark === defaultTheme) {
22 | this.currentTheme = ThemeType.dark;
23 | } else {
24 | this.currentTheme = ThemeType.default;
25 | this.persistenceService.saveMetaInfo(ThemeService.DEFAULT_THEME, ThemeType.default);
26 | }
27 | }
28 |
29 | private reverseTheme(theme: string): ThemeType {
30 | return theme === ThemeType.dark ? ThemeType.default : ThemeType.dark;
31 | }
32 |
33 | private removeUnusedTheme(theme: ThemeType): void {
34 | document.documentElement.classList.remove(theme);
35 | const removedThemeStyle = document.getElementById(theme);
36 | if (removedThemeStyle) {
37 | document.head.removeChild(removedThemeStyle);
38 | }
39 | }
40 |
41 | private loadCss(href: string, id: string): Promise {
42 | return new Promise((resolve, reject) => {
43 | const style = document.createElement('link');
44 | style.rel = 'stylesheet';
45 | style.href = href;
46 | style.id = id;
47 | style.onload = resolve;
48 | style.onerror = reject;
49 | document.head.append(style);
50 | });
51 | }
52 |
53 | public loadTheme(firstLoad = true): Promise {
54 | const theme = this.currentTheme;
55 | if (firstLoad) {
56 | document.documentElement.classList.add(theme);
57 | }
58 | return new Promise((resolve, reject) => {
59 | this.loadCss(`${theme}.css`, theme).then(
60 | (e) => {
61 | if (!firstLoad) {
62 | document.documentElement.classList.add(theme);
63 | }
64 | this.removeUnusedTheme(this.reverseTheme(theme));
65 | resolve(e);
66 | },
67 | (e) => reject(e)
68 | );
69 | });
70 | }
71 |
72 | public toggleTheme(): Promise {
73 | this.currentTheme = this.reverseTheme(this.currentTheme);
74 | return this.loadTheme(false).then(it => {
75 | this.persistenceService.saveMetaInfo(ThemeService.DEFAULT_THEME, this.currentTheme);
76 | return it;
77 | });
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/app/module/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {NzTabsModule} from 'ng-zorro-antd/tabs';
4 | import {NzLayoutModule} from 'ng-zorro-antd/layout';
5 | import {NzMenuModule} from 'ng-zorro-antd/menu';
6 | import {IconsProviderModule} from '../../icons-provider.module';
7 | import {FormsModule, ReactiveFormsModule} from '@angular/forms';
8 | import {NzButtonModule} from 'ng-zorro-antd/button';
9 | import {NzInputModule} from 'ng-zorro-antd/input';
10 | import {NzFormModule} from 'ng-zorro-antd/form';
11 | import {NzGridModule} from 'ng-zorro-antd/grid';
12 | import {NzToolTipModule} from 'ng-zorro-antd/tooltip';
13 | import {NzCollapseModule} from 'ng-zorro-antd/collapse';
14 | import {NzPopconfirmModule} from 'ng-zorro-antd/popconfirm';
15 | import {NzModalModule} from 'ng-zorro-antd/modal';
16 | import {NzTypographyModule} from 'ng-zorro-antd/typography';
17 | import {NzMessageModule} from 'ng-zorro-antd/message';
18 | import {NzNotificationModule} from 'ng-zorro-antd/notification';
19 | import {NzAutocompleteModule} from 'ng-zorro-antd/auto-complete';
20 | import {NzSelectModule} from 'ng-zorro-antd/select';
21 | import {NzUploadModule} from 'ng-zorro-antd/upload';
22 | import {NzDatePickerModule} from 'ng-zorro-antd/date-picker';
23 | import {NzProgressModule} from 'ng-zorro-antd/progress';
24 | import {NzCheckboxModule} from 'ng-zorro-antd/checkbox';
25 | import {NzSwitchModule} from 'ng-zorro-antd/switch';
26 |
27 | @NgModule({
28 | declarations: [],
29 | imports: [
30 | CommonModule,
31 | ReactiveFormsModule,
32 | FormsModule,
33 | NzTabsModule,
34 | NzLayoutModule,
35 | NzMenuModule,
36 | NzButtonModule,
37 | NzInputModule,
38 | NzFormModule,
39 | NzGridModule,
40 | NzToolTipModule,
41 | NzCollapseModule,
42 | NzPopconfirmModule,
43 | NzModalModule,
44 | NzTypographyModule,
45 | NzMessageModule,
46 | NzNotificationModule,
47 | NzAutocompleteModule,
48 | NzSelectModule,
49 | NzUploadModule,
50 | NzDatePickerModule,
51 | IconsProviderModule,
52 | NzProgressModule,
53 | NzCheckboxModule,
54 | NzSwitchModule
55 | ],
56 | exports: [
57 | CommonModule,
58 | ReactiveFormsModule,
59 | FormsModule,
60 | NzTabsModule,
61 | NzLayoutModule,
62 | NzMenuModule,
63 | NzButtonModule,
64 | NzInputModule,
65 | NzFormModule,
66 | NzGridModule,
67 | NzToolTipModule,
68 | NzCollapseModule,
69 | NzPopconfirmModule,
70 | NzModalModule,
71 | NzTypographyModule,
72 | NzMessageModule,
73 | NzNotificationModule,
74 | NzAutocompleteModule,
75 | NzSelectModule,
76 | NzUploadModule,
77 | NzDatePickerModule,
78 | IconsProviderModule,
79 | NzProgressModule,
80 | NzCheckboxModule,
81 | NzSwitchModule
82 | ]
83 | })
84 | export class SharedModule {
85 | }
86 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/attribute-item/attribute-item.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
2 | import {AttributeValueType, Item, Type} from '../../../../service/generic.service';
3 | import {UtilsService} from '../../../../service/utils.service';
4 |
5 | @Component({
6 | selector: 'app-attribute-item',
7 | templateUrl: './attribute-item.component.html',
8 | styleUrls: ['./attribute-item.component.scss']
9 | })
10 | export class AttributeItemComponent implements OnInit {
11 | /**
12 | * 某一参数列表
13 | */
14 | @Input()
15 | data: Item;
16 | /**
17 | * 偏移量:用于页面区分子属性
18 | */
19 | @Input()
20 | offset: number;
21 |
22 | @Input()
23 | parentData: Item;
24 | /**
25 | * 删除事件
26 | */
27 | @Output()
28 | delete: EventEmitter = new EventEmitter();
29 | /**
30 | * 新增事件
31 | */
32 | @Output()
33 | add: EventEmitter = new EventEmitter();
34 |
35 | constructor(private utils: UtilsService) {
36 | }
37 |
38 | ngOnInit(): void {
39 | }
40 |
41 | /**
42 | * 删除属性
43 | * @param id 属性唯一ID
44 | */
45 | deleteAttribute(id: string): void {
46 | this.delete.emit(id);
47 | }
48 |
49 | /**
50 | * 添加属性
51 | * @param data 要添加所在的父属性
52 | */
53 | addAttribute(data: AttributeValueType): void {
54 | this.data.use = true;
55 | this.add.emit(data);
56 | }
57 |
58 | isPlain(type: Type): boolean {
59 | return type !== Type.OBJECT && type !== Type.ARRAY;
60 | }
61 |
62 | isDate(type: Type): boolean {
63 | return type !== Type.DATE && type !== Type.DATE_8601;
64 | }
65 |
66 | /**
67 | * 属性类型改变回调
68 | * @param data 某一个属性改变了
69 | */
70 | onTypeChange(data: Item): void {
71 | data.autoComplete = [];
72 | switch (data.type) {
73 | case Type.OBJECT:
74 | case Type.ARRAY:
75 | data.attributeValue = [];
76 | (data.attributeValue as Item[]).push(Item.generateString('', ''));
77 | break;
78 | case Type.BOOLEAN:
79 | data.autoComplete = ['true', 'false'];
80 | data.attributeValue = false;
81 | break;
82 | default:
83 | data.attributeValue = '';
84 | }
85 | }
86 |
87 | onDatePickerChange(result: Date, data: Item): void {
88 | if (result) {
89 | data.attributeValue = data.type === Type.DATE ?
90 | this.utils.formatDate2DateString(result) : this.utils.formatDate2Date_8301String(result);
91 | data.attributeValueDate = result;
92 | }
93 | }
94 |
95 | onUseChange($event: boolean, data: Item): void {
96 | if ($event && this.parentData) {
97 | this.parentData.use = true;
98 | }
99 | if (!this.isPlain(data.type)) {
100 | (data.attributeValue as Item[]).forEach(it => it.use = $event);
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
22 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
23 |
24 | /**
25 | * Web Animations `@angular/platform-browser/animations`
26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
28 | */
29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
30 |
31 | /**
32 | * By default, zone.js will patch all possible macroTask and DomEvents
33 | * user can disable parts of macroTask/DomEvents patch by setting following flags
34 | * because those flags need to be set before `zone.js` being loaded, and webpack
35 | * will put import in the top of bundle, so user need to create a separate file
36 | * in this directory (for example: zone-flags.ts), and put the following flags
37 | * into that file, and then add the following code before importing zone.js.
38 | * import './zone-flags';
39 | *
40 | * The flags allowed in zone-flags.ts are listed here.
41 | *
42 | * The following flags will work for all browsers.
43 | *
44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
47 | *
48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
50 | *
51 | * (window as any).__Zone_enable_cross_context_check = true;
52 | *
53 | */
54 |
55 | /***************************************************************************************************
56 | * Zone JS is required by default for Angular itself.
57 | */
58 | import 'zone.js/dist/zone'; // Included with Angular CLI.
59 |
60 |
61 | /***************************************************************************************************
62 | * APPLICATION IMPORTS
63 | */
64 |
--------------------------------------------------------------------------------
/src/app/service/persistence.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Base64} from 'js-base64';
3 | import {FormBuilder, Validators} from '@angular/forms';
4 | import {EnvInfo, Item, TabInfo} from './generic.service';
5 |
6 | @Injectable({
7 | providedIn: 'root'
8 | })
9 | export class PersistenceService {
10 | /**
11 | * 保存泛化调用参数信息的持久化唯一键
12 | * @private
13 | */
14 | private static readonly GENERIC_PARAM_INFO_KEY = 'TEST_BOX_PERSISTENCE_GENERIC_PARAM_INFO';
15 |
16 | /**
17 | * 保存选择的TAB页的持久化唯一键
18 | * @private
19 | */
20 | private static readonly GENERIC_META_INFO_KEY = 'TEST_BOX_PERSISTENCE_META_INFO';
21 |
22 | constructor(private fb: FormBuilder) {
23 | }
24 |
25 | saveMetaInfo(key: string, value: any): void {
26 | const base64Str = window.localStorage.getItem(PersistenceService.GENERIC_META_INFO_KEY);
27 | let info;
28 | if (null === base64Str) {
29 | info = {};
30 | } else {
31 | const json = Base64.decode(base64Str);
32 | info = JSON.parse(json);
33 | }
34 |
35 | info[key] = value;
36 | const encode = Base64.encode(JSON.stringify(info));
37 | window.localStorage.setItem(PersistenceService.GENERIC_META_INFO_KEY, encode);
38 | }
39 |
40 | getMetaInfo(key: string): any {
41 | return this.getMetaInfos()[key];
42 | }
43 |
44 | getMetaInfos(): any {
45 | const base64Str = window.localStorage.getItem(PersistenceService.GENERIC_META_INFO_KEY);
46 | if (null === base64Str) {
47 | return {};
48 | }
49 | try {
50 | const json = Base64.decode(base64Str);
51 | return JSON.parse(json);
52 | } catch (e) {
53 | window.localStorage.removeItem(PersistenceService.GENERIC_META_INFO_KEY);
54 | return {};
55 | }
56 | }
57 |
58 | /**
59 | * 保存泛化调用参数信息
60 | * @param tabs TAB页
61 | */
62 | saveGenericParamInfo(tabs: TabInfo[]): void {
63 | const save: PersistenceGenericParamInfo = tabs.map(tab => {
64 | return {
65 | id: tab.id,
66 | tabName: tab.tabName,
67 | formParamsValue: tab.formParams.value as FormParamsInfo,
68 | parameterValue: tab.parameterValue,
69 | selectEnv: tab.selectEnv
70 | };
71 | });
72 | const encode = Base64.encode(JSON.stringify(save));
73 | window.localStorage.setItem(PersistenceService.GENERIC_PARAM_INFO_KEY, encode);
74 | }
75 |
76 | /**
77 | * 获取保存的泛化调用参数信息
78 | */
79 | getGenericParamInfo(): TabInfo[] {
80 | const base64Str = window.localStorage.getItem(PersistenceService.GENERIC_PARAM_INFO_KEY);
81 | if (null === base64Str) {
82 | return [];
83 | }
84 | try {
85 | const json = Base64.decode(base64Str);
86 | const info: PersistenceGenericParamInfo = JSON.parse(json);
87 | return info.map(item => {
88 | const formGroup = this.fb.group({
89 | url: [item.formParamsValue.url, [Validators.required]],
90 | interfaceName: [item.formParamsValue.interfaceName, [Validators.required]],
91 | method: [item.formParamsValue.method, [Validators.required]],
92 | version: [item.formParamsValue.version, []],
93 | group: [item.formParamsValue.group, []],
94 | path: [item.formParamsValue.path]
95 | });
96 | return new TabInfo(item.id, item.tabName, formGroup, item.parameterValue, [], item.selectEnv);
97 | });
98 | } catch (e) {
99 | window.localStorage.removeItem(PersistenceService.GENERIC_PARAM_INFO_KEY);
100 | return [];
101 | }
102 | }
103 | }
104 |
105 | export type PersistenceGenericParamInfo =
106 | { id: string, tabName: string, formParamsValue: FormParamsInfo, parameterValue: Item[], selectEnv: EnvInfo }[];
107 | export type FormParamsInfo = { url: string, interfaceName: string, method: string, version: string, group: string, path: string };
108 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "rulesDirectory": [
4 | "codelyzer"
5 | ],
6 | "rules": {
7 | "align": {
8 | "options": [
9 | "parameters",
10 | "statements"
11 | ]
12 | },
13 | "array-type": false,
14 | "arrow-return-shorthand": true,
15 | "curly": true,
16 | "deprecation": {
17 | "severity": "warning"
18 | },
19 | "eofline": true,
20 | "import-blacklist": [
21 | true,
22 | "rxjs/Rx"
23 | ],
24 | "import-spacing": true,
25 | "indent": {
26 | "options": [
27 | "spaces"
28 | ]
29 | },
30 | "max-classes-per-file": false,
31 | "max-line-length": [
32 | true,
33 | 140
34 | ],
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-console": [
47 | true,
48 | "debug",
49 | "info",
50 | "time",
51 | "timeEnd",
52 | "trace"
53 | ],
54 | "no-empty": false,
55 | "no-inferrable-types": [
56 | true,
57 | "ignore-params"
58 | ],
59 | "no-non-null-assertion": true,
60 | "no-redundant-jsdoc": true,
61 | "no-switch-case-fall-through": true,
62 | "no-var-requires": false,
63 | "object-literal-key-quotes": [
64 | true,
65 | "as-needed"
66 | ],
67 | "quotemark": [
68 | true,
69 | "single"
70 | ],
71 | "semicolon": {
72 | "options": [
73 | "always"
74 | ]
75 | },
76 | "space-before-function-paren": {
77 | "options": {
78 | "anonymous": "never",
79 | "asyncArrow": "always",
80 | "constructor": "never",
81 | "method": "never",
82 | "named": "never"
83 | }
84 | },
85 | "typedef": [
86 | true,
87 | "call-signature"
88 | ],
89 | "typedef-whitespace": {
90 | "options": [
91 | {
92 | "call-signature": "nospace",
93 | "index-signature": "nospace",
94 | "parameter": "nospace",
95 | "property-declaration": "nospace",
96 | "variable-declaration": "nospace"
97 | },
98 | {
99 | "call-signature": "onespace",
100 | "index-signature": "onespace",
101 | "parameter": "onespace",
102 | "property-declaration": "onespace",
103 | "variable-declaration": "onespace"
104 | }
105 | ]
106 | },
107 | "variable-name": {
108 | "options": [
109 | "ban-keywords",
110 | "check-format",
111 | "allow-pascal-case"
112 | ]
113 | },
114 | "whitespace": {
115 | "options": [
116 | "check-branch",
117 | "check-decl",
118 | "check-operator",
119 | "check-separator",
120 | "check-type",
121 | "check-typecast"
122 | ]
123 | },
124 | "component-class-suffix": true,
125 | "contextual-lifecycle": true,
126 | "directive-class-suffix": true,
127 | "no-conflicting-lifecycle": true,
128 | "no-host-metadata-property": true,
129 | "no-input-rename": true,
130 | "no-inputs-metadata-property": true,
131 | "no-output-native": true,
132 | "no-output-on-prefix": true,
133 | "no-output-rename": true,
134 | "no-outputs-metadata-property": true,
135 | "template-banana-in-box": true,
136 | "template-no-negated-async": true,
137 | "use-lifecycle-interface": true,
138 | "use-pipe-transform-interface": true,
139 | "directive-selector": [
140 | true,
141 | "attribute",
142 | "app",
143 | "camelCase"
144 | ],
145 | "component-selector": [
146 | true,
147 | "element",
148 | "app",
149 | "kebab-case"
150 | ]
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/generic/generic.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
13 |
18 |
23 |
28 |
29 |
30 |
32 |
33 |
34 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/attribute-item/attribute-item.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
42 |
43 |
44 |
45 |
46 |
47 |
52 |
53 |
57 |
58 |
59 |
65 |
66 |
--------------------------------------------------------------------------------
/src/app/service/utils.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Artifact, Type} from './generic.service';
3 | import {NzMessageService} from 'ng-zorro-antd/message';
4 | import * as dayjs from 'dayjs';
5 |
6 | @Injectable({
7 | providedIn: 'root'
8 | })
9 | export class UtilsService {
10 |
11 | static readonly DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
12 |
13 | static readonly DATE_8301_FORMAT = 'YYYY-MM-DDTHH:mm:ss';
14 |
15 | constructor(private message: NzMessageService) {
16 | }
17 |
18 | /**
19 | * 获取URL的查询参数值
20 | * @param url URL
21 | * @param key KEY
22 | * @private
23 | */
24 | getParamValue(url: string, key: string): string | null {
25 | const regex = new RegExp(key + '=([^&]*)', 'i');
26 | const matchResult = url.match(regex);
27 | if (!matchResult || matchResult.length < 1) {
28 | return null;
29 | }
30 | return url.match(regex)[1];
31 | }
32 |
33 |
34 | /**
35 | * 获取对象的类型
36 | * @param o 对象
37 | */
38 | getObjectType(o: any): Type {
39 | const type = Object.prototype.toString.call(o);
40 | switch (type) {
41 | case '[object Array]':
42 | return Type.ARRAY;
43 | case '[object Object]':
44 | return Type.OBJECT;
45 | case '[object Number]':
46 | return Type.NUMBER;
47 | case '[object Boolean]':
48 | return Type.BOOLEAN;
49 | case '[object String]':
50 | return Type.STRING;
51 | default:
52 | console.error(`Not Supported Type:${type}`);
53 | }
54 | }
55 |
56 | /**
57 | * 复制到粘贴板
58 | * @param info 信息
59 | */
60 | copyToClip(info: string): void {
61 | const aux = document.createElement('input');
62 | aux.setAttribute('value', info);
63 | document.body.appendChild(aux);
64 | aux.select();
65 | document.execCommand('copy');
66 | document.body.removeChild(aux);
67 | this.message.success('复制成功');
68 | }
69 |
70 | /**
71 | * 滚动
72 | * @param el 元素
73 | * @param top 上
74 | * @param left 左
75 | */
76 | scrollToWithSmooth(el: Element, top: number, left: number): void {
77 | el.scrollTo({top, left, behavior: 'smooth'});
78 | }
79 |
80 | /**
81 | * 滚动到末尾
82 | * @param el 元素
83 | */
84 | scrollToEndSmooth(el: Element): void {
85 | const scrollHeight = el.scrollHeight;
86 | this.scrollToWithSmooth(el, scrollHeight, 0);
87 | }
88 |
89 | isMatchDateString(value: string): boolean {
90 | return /([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8])))\s([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/.test(value);
91 | }
92 |
93 | isMatchDate_8301String(value: string): boolean {
94 | return /([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8])))[\s,T]([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/.test(value);
95 | }
96 |
97 | getNowDate2String(): string {
98 | return dayjs().format(UtilsService.DATE_FORMAT);
99 | }
100 |
101 | getNowDate2_8301String(): string {
102 | return dayjs().format(UtilsService.DATE_8301_FORMAT);
103 | }
104 |
105 | formatDate2DateString(date: Date): string {
106 | return dayjs(date).format(UtilsService.DATE_FORMAT);
107 | }
108 |
109 | formatDate2Date_8301String(date: Date): string {
110 | return dayjs(date).format(UtilsService.DATE_8301_FORMAT);
111 | }
112 |
113 | formatDateString2Date(value: string): Date {
114 | return dayjs(value, UtilsService.DATE_FORMAT).toDate();
115 | }
116 |
117 | formatDate_8301String2Date(value: string): Date {
118 | return dayjs(value, UtilsService.DATE_8301_FORMAT).toDate();
119 | }
120 |
121 | genericMavenDependencyXml(artifact: Artifact): string {
122 | return `
123 | ${artifact.groupId}
124 | ${artifact.artifactId}
125 | ${artifact.version}
126 | `;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "generic-service-client-web": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | }
12 | },
13 | "root": "",
14 | "sourceRoot": "src",
15 | "prefix": "app",
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:browser",
19 | "options": {
20 | "outputPath": "dist/generic-service-client-web",
21 | "index": "src/index.html",
22 | "main": "src/main.ts",
23 | "polyfills": "src/polyfills.ts",
24 | "tsConfig": "tsconfig.app.json",
25 | "aot": true,
26 | "assets": [
27 | "src/favicon.ico",
28 | "src/assets",
29 | {
30 | "glob": "**/*",
31 | "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
32 | "output": "/assets/"
33 | },
34 | "src/manifest.webmanifest"
35 | ],
36 | "styles": [
37 | "src/styles.scss",
38 | "node_modules/jsoneditor/dist/jsoneditor.min.css",
39 | {
40 | "input": "src/styles/default.less",
41 | "bundleName": "default",
42 | "inject": false
43 | },
44 | {
45 | "input": "src/styles/dark.less",
46 | "bundleName": "dark",
47 | "inject": false
48 | }
49 | ],
50 | "stylePreprocessorOptions": {
51 | "includePaths": [
52 | "src/styles/themes"
53 | ]
54 | },
55 | "scripts": []
56 | },
57 | "configurations": {
58 | "production": {
59 | "fileReplacements": [
60 | {
61 | "replace": "src/environments/environment.ts",
62 | "with": "src/environments/environment.prod.ts"
63 | }
64 | ],
65 | "optimization": true,
66 | "outputHashing": "all",
67 | "sourceMap": false,
68 | "namedChunks": false,
69 | "extractLicenses": true,
70 | "vendorChunk": false,
71 | "buildOptimizer": true,
72 | "budgets": [
73 | {
74 | "type": "initial",
75 | "maximumWarning": "2mb",
76 | "maximumError": "5mb"
77 | },
78 | {
79 | "type": "anyComponentStyle",
80 | "maximumWarning": "6kb",
81 | "maximumError": "10kb"
82 | }
83 | ],
84 | "serviceWorker": true,
85 | "ngswConfigPath": "ngsw-config.json"
86 | }
87 | }
88 | },
89 | "serve": {
90 | "builder": "@angular-devkit/build-angular:dev-server",
91 | "options": {
92 | "browserTarget": "generic-service-client-web:build"
93 | },
94 | "configurations": {
95 | "production": {
96 | "browserTarget": "generic-service-client-web:build:production"
97 | }
98 | }
99 | },
100 | "extract-i18n": {
101 | "builder": "@angular-devkit/build-angular:extract-i18n",
102 | "options": {
103 | "browserTarget": "generic-service-client-web:build"
104 | }
105 | },
106 | "test": {
107 | "builder": "@angular-devkit/build-angular:karma",
108 | "options": {
109 | "main": "src/test.ts",
110 | "polyfills": "src/polyfills.ts",
111 | "tsConfig": "tsconfig.spec.json",
112 | "karmaConfig": "karma.conf.js",
113 | "assets": [
114 | "src/favicon.ico",
115 | "src/assets",
116 | "src/manifest.webmanifest"
117 | ],
118 | "styles": [
119 | "src/styles.scss"
120 | ],
121 | "scripts": []
122 | }
123 | },
124 | "lint": {
125 | "builder": "@angular-devkit/build-angular:tslint",
126 | "options": {
127 | "tsConfig": [
128 | "tsconfig.app.json",
129 | "tsconfig.spec.json",
130 | "e2e/tsconfig.json"
131 | ],
132 | "exclude": [
133 | "**/node_modules/**"
134 | ]
135 | }
136 | },
137 | "e2e": {
138 | "builder": "@angular-devkit/build-angular:protractor",
139 | "options": {
140 | "protractorConfig": "e2e/protractor.conf.js",
141 | "devServerTarget": "generic-service-client-web:serve"
142 | },
143 | "configurations": {
144 | "production": {
145 | "devServerTarget": "generic-service-client-web:serve:production"
146 | }
147 | }
148 | }
149 | }
150 | }
151 | },
152 | "defaultProject": "generic-service-client-web"
153 | }
154 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/generic/generic.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, EventEmitter, OnInit} from '@angular/core';
2 | import {FormParamsInfo, PersistenceService} from '../../../../service/persistence.service';
3 | import {RequestModel} from '../attribute/attribute.component';
4 | import {GenericService, Item, TabInfo} from '../../../../service/generic.service';
5 | import {NzMessageService} from 'ng-zorro-antd/message';
6 | import {Base64} from 'js-base64';
7 | import {v4 as uuidv4} from 'uuid';
8 | import {UtilsService} from '../../../../service/utils.service';
9 |
10 | @Component({
11 | selector: 'app-generic',
12 | templateUrl: './generic.component.html',
13 | styleUrls: ['./generic.component.scss']
14 | })
15 | export class GenericComponent implements OnInit {
16 | tabs: TabInfo[] = [];
17 | nowSelectedTabIndex = 0;
18 | clearLogEvent: EventEmitter = new EventEmitter();
19 | lastJsonInfo: string;
20 | isShowImportModal: boolean;
21 | isShowExportModal: boolean;
22 | importTabBase64Str: string;
23 | exportInfo: string;
24 |
25 | constructor(private persistenceService: PersistenceService,
26 | private genericService: GenericService,
27 | private message: NzMessageService,
28 | private util: UtilsService) {
29 | }
30 |
31 | ngOnInit(): void {
32 | this.tabs = this.persistenceService.getGenericParamInfo();
33 | if (!this.tabs || this.tabs.length === 0) {
34 | this.tabs.push(new TabInfo(uuidv4(), 'Unnamed Tab', this.genericService.generateFormParams(), [], []));
35 | }
36 | const index = this.persistenceService.getMetaInfo('nowSelectedTabIndex');
37 | if (index && !Number.isNaN(index)) {
38 | this.nowSelectedTabIndex = index > this.tabs.length - 1 ? this.tabs.length - 1 : index;
39 | }
40 | }
41 |
42 | private filterUseAttributeNotTrue(items: Item[]): Item[] {
43 | return items.filter(item => item.use).map(item => {
44 | if (item.attributeValue instanceof Array) {
45 | item.attributeValue = this.filterUseAttributeNotTrue(item.attributeValue as Item[]);
46 | }
47 | return item;
48 | });
49 | }
50 |
51 | handleRequest(tab: TabInfo): void {
52 | this.persistenceService.saveGenericParamInfo(this.tabs);
53 | const parameterValue = this.filterUseAttributeNotTrue(JSON.parse(JSON.stringify(tab.parameterValue)));
54 | const resultObj = this.genericService.conversionRequest(parameterValue);
55 | const result: RequestModel = Object.assign(tab.formParams.value as FormParamsInfo, {params: resultObj});
56 | const newResult: RequestModel = JSON.parse(JSON.stringify(result));
57 | if (newResult.path) {
58 | newResult.interfaceName = newResult.path;
59 | }
60 | newResult.url = `dubbo://${newResult.url}`;
61 | tab.isRequestLoading = this.genericService.sendGenericRequest(newResult, tab.id);
62 | }
63 |
64 | handleTabSelect(index: number): void {
65 | this.nowSelectedTabIndex = index;
66 | this.persistenceService.saveMetaInfo('nowSelectedTabIndex', index);
67 | }
68 |
69 | handleClearResult($event: MouseEvent): void {
70 | $event.stopPropagation();
71 | this.tabs[this.nowSelectedTabIndex].resultData = [];
72 | }
73 |
74 | handleClearLog($event: MouseEvent): void {
75 | $event.stopPropagation();
76 | this.clearLogEvent.emit();
77 | }
78 |
79 | handleCopyJsonResult($event: MouseEvent): void {
80 | $event.stopPropagation();
81 | if (!this.lastJsonInfo) {
82 | this.message.warning('暂无JSON结果,请发起调用成功后再试!');
83 | return;
84 | }
85 | this.util.copyToClip(this.lastJsonInfo);
86 | }
87 |
88 | handleImportAllTags($event: MouseEvent): void {
89 | $event.stopPropagation();
90 | this.importTabBase64Str = '';
91 | this.isShowImportModal = true;
92 | }
93 |
94 | handleExportNowTag($event: MouseEvent): void {
95 | $event.stopPropagation();
96 | this.exportInfo = '';
97 | if (!this.tabs || this.tabs.length === 0 || !this.tabs[this.nowSelectedTabIndex]) {
98 | this.message.warning('没有可导出的TAB!');
99 | return;
100 | }
101 | const tab = this.tabs[this.nowSelectedTabIndex];
102 | const encode = Base64.encode(JSON.stringify([{
103 | tabName: tab.tabName,
104 | formParamsValue: tab.formParams.value as FormParamsInfo,
105 | parameterValue: tab.parameterValue,
106 | selectEnv: tab.selectEnv
107 | }]));
108 | this.exportInfo = encode;
109 | this.isShowExportModal = true;
110 | this.util.copyToClip(encode);
111 | }
112 |
113 | handleExportAllTags($event: MouseEvent): void {
114 | $event.stopPropagation();
115 | this.exportInfo = '';
116 | if (!this.tabs || this.tabs.length === 0) {
117 | this.message.warning('没有可导出的TAB!');
118 | return;
119 | }
120 | const save = this.tabs.map(tab => {
121 | return {
122 | tabName: tab.tabName,
123 | formParamsValue: tab.formParams.value as FormParamsInfo,
124 | parameterValue: tab.parameterValue,
125 | selectEnv: tab.selectEnv
126 | };
127 | });
128 | const encode = Base64.encode(JSON.stringify(save));
129 | this.exportInfo = encode;
130 | this.isShowExportModal = true;
131 | this.util.copyToClip(encode);
132 | }
133 |
134 | handleLastJsonInfoChange(json: string): void {
135 | this.lastJsonInfo = json;
136 | }
137 |
138 | doImport(): void {
139 | if (!this.importTabBase64Str) {
140 | this.message.error('请输入要导入的TAB信息');
141 | return;
142 | }
143 | try {
144 | const decode = Base64.decode(this.importTabBase64Str);
145 | const parse = JSON.parse(decode);
146 | const importTabs = parse.map(item => {
147 | const formGroup = this.genericService.generateFormParams(
148 | item.formParamsValue.url,
149 | item.formParamsValue.interfaceName,
150 | item.formParamsValue.method,
151 | item.formParamsValue.version,
152 | item.formParamsValue.group,
153 | item.formParamsValue.path
154 | );
155 | return new TabInfo(uuidv4(), item.tabName ? item.tabName : 'Unnamed Tab', formGroup, item.parameterValue, [], item.selectEnv);
156 | });
157 | importTabs.forEach(tab => this.tabs.push(tab));
158 | this.isShowImportModal = false;
159 | this.importTabBase64Str = '';
160 | this.persistenceService.saveGenericParamInfo(this.tabs);
161 | this.nowSelectedTabIndex = this.tabs.length - 1;
162 | this.message.success('导入成功');
163 | } catch (e) {
164 | console.error(e);
165 | this.message.error('导入失败');
166 | }
167 | }
168 |
169 | handleCopyNowTag($event: MouseEvent): void {
170 | $event.stopPropagation();
171 | if (!this.tabs || this.tabs.length === 0 || !this.tabs[this.nowSelectedTabIndex]) {
172 | this.message.warning('没有可复制的TAB!');
173 | return;
174 | }
175 | const needCopyTabInfo = this.tabs[this.nowSelectedTabIndex];
176 | const newNeedCopyTabInfo = JSON.parse(JSON.stringify({
177 | formParamsValue: needCopyTabInfo.formParams.value as FormParamsInfo,
178 | parameterValue: needCopyTabInfo.parameterValue
179 | }));
180 | const formGroup = this.genericService.generateFormParams(
181 | newNeedCopyTabInfo.formParamsValue.url,
182 | newNeedCopyTabInfo.formParamsValue.interfaceName,
183 | newNeedCopyTabInfo.formParamsValue.method,
184 | newNeedCopyTabInfo.formParamsValue.version,
185 | newNeedCopyTabInfo.formParamsValue.group,
186 | newNeedCopyTabInfo.formParamsValue.path
187 | );
188 | const newTabInfo = new TabInfo(uuidv4(), 'Unnamed Tab', formGroup, newNeedCopyTabInfo.parameterValue, []);
189 | this.tabs.push(newTabInfo);
190 | this.persistenceService.saveGenericParamInfo(this.tabs);
191 | this.nowSelectedTabIndex = this.tabs.length - 1;
192 | this.message.success('复制成功!');
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/attribute/attribute.component.html:
--------------------------------------------------------------------------------
1 |
2 |
5 | 1" [nzTitle]="tab.tabName"
6 | (nzClick)="handleTabClick(i)">
7 |
90 |
95 |
96 |
97 |
98 |
101 |
102 |
103 |
104 |
107 |
108 |
109 |
110 |
111 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
142 |
143 |
144 |
145 |
146 |
147 |
151 |
152 |
153 |
154 | version版本信息可以不填
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
178 |
179 | 下载速度:{{getGoodProgress()}}
180 |
181 |
182 |
183 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2020 itning
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/app/service/generic.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {RequestModel} from '../module/generic/component/attribute/attribute.component';
3 | import {HttpClient, HttpEvent, HttpParams} from '@angular/common/http';
4 | import {Observable, Subject} from 'rxjs';
5 | import {map, mergeMap} from 'rxjs/operators';
6 | import {FormBuilder, FormGroup, Validators} from '@angular/forms';
7 | import {environment} from '../../environments/environment';
8 | import {v4 as uuidv4} from 'uuid';
9 | import {NzMessageService} from 'ng-zorro-antd/message';
10 | import {AutocompleteDataSource} from 'ng-zorro-antd/auto-complete/autocomplete.component';
11 |
12 | @Injectable({
13 | providedIn: 'root'
14 | })
15 | export class GenericService {
16 |
17 | private token: string;
18 | private resultSubject: Subject;
19 | private textDecoder: TextDecoder;
20 | private wsInstance: WebSocket;
21 |
22 | constructor(private http: HttpClient,
23 | private message: NzMessageService,
24 | private fb: FormBuilder) {
25 | this.textDecoder = new TextDecoder();
26 | }
27 |
28 | sendGenericRequest(requestModel: RequestModel, echo: string): boolean {
29 | if (!this.wsInstance) {
30 | this.message.error('WebSocket连接实例不可用,请稍后再试!');
31 | return false;
32 | }
33 | if (this.wsInstance.readyState !== WebSocket.OPEN) {
34 | this.message.error('WebSocket连接不可用,请刷新页面后再试!');
35 | return false;
36 | }
37 | this.getWebSocketToken().subscribe(token => {
38 | // tslint:disable-next-line
39 | requestModel['token'] = token;
40 | // tslint:disable-next-line
41 | requestModel['echo'] = echo;
42 | this.wsInstance.send(new TextEncoder().encode(JSON.stringify(requestModel)));
43 | });
44 | return true;
45 | }
46 |
47 | sendMavenRequest(mavenRequest: MavenRequest): Observable> {
48 | return this.getWebSocketToken().pipe(mergeMap(token => {
49 | mavenRequest.token = token;
50 | return this.http.post>(`http://${environment.baseUrl}/nexus/dependency/download`, mavenRequest);
51 | }));
52 | }
53 |
54 | sendMavenParse(dep: string): Observable {
55 | return this.http.post>(`http://${environment.baseUrl}/nexus/dependency/parse`,
56 | new HttpParams({fromObject: {dependency: dep}}),
57 | {headers: {'Content-Type': 'application/x-www-form-urlencoded'}})
58 | .pipe(map(it => {
59 | if (!it || !it.success) {
60 | this.message.error(it.message);
61 | return null;
62 | } else {
63 | return it.data;
64 | }
65 | }));
66 | }
67 |
68 | cancelDownload(cancelToken: string): Observable {
69 | return this.http.get(`http://${environment.baseUrl}/nexus/dependency/download/cancel?token=${cancelToken}`);
70 | }
71 |
72 | getAvailableEnv(): Observable {
73 | return this.http.get(`http://${environment.baseUrl}/service/env`);
74 | }
75 |
76 | getAvailableInterFaces(tag: string, env: string): Observable {
77 | return this.http.get(`http://${environment.baseUrl}/service/providers?env=${env}&tag=${tag}`);
78 | }
79 |
80 | getURL(env: string, tag: string, interfaceName: string): Observable {
81 | return this.http
82 | .get(`http://${environment.baseUrl}/service/provideDetail?env=${env}&tag=${tag}&interfaceName=${interfaceName}`);
83 | }
84 |
85 | uploadJar(file: Blob, interfaceName: string, methodName: string): Observable> {
86 | const formData: FormData = new FormData();
87 | formData.append('file', file);
88 | formData.append('interfaceName', interfaceName);
89 | formData.append('methodName', methodName);
90 | return this.http.post(`http://${environment.baseUrl}/jar/upload`, formData, {reportProgress: true, observe: 'events'});
91 | }
92 |
93 | getWebSocketToken(): Observable {
94 | if (!this.token) {
95 | return this.http.get(`http://${environment.baseUrl}/socket_token`, {responseType: 'text'}).pipe(
96 | map(i => {
97 | if (this.token) {
98 | return this.token;
99 | }
100 | this.token = i;
101 | return i;
102 | })
103 | );
104 | } else {
105 | return new Observable(subscriber => {
106 | subscriber.next(this.token);
107 | subscriber.complete();
108 | });
109 | }
110 | }
111 |
112 | generateFormParams(url = '',
113 | interfaceName = '',
114 | method = '',
115 | version = '',
116 | group = '',
117 | path = ''): FormGroup {
118 | return this.fb.group({
119 | url: [url, [Validators.required]],
120 | interfaceName: [interfaceName, [Validators.required]],
121 | method: [method, [Validators.required]],
122 | version: [version, []],
123 | group: [group, []],
124 | path: [path]
125 | });
126 | }
127 |
128 | private initWebSocket(url: string): WsInstanceAndResult {
129 | const ws = new WebSocket(url);
130 | window.onbeforeunload = () => {
131 | if (ws) {
132 | ws.close();
133 | }
134 | };
135 | return new WsInstanceAndResult(ws, new Observable(
136 | observer => {
137 | ws.onopen = () => observer.next(WebSocketResultWrap.local('服务器连接成功!'));
138 | ws.onmessage = (event) => {
139 | (event.data as Blob).arrayBuffer()
140 | .then(it => observer.next(WebSocketResultWrap.wrap(it)))
141 | .catch(err => observer.error(err));
142 | };
143 | ws.onerror = (event) => observer.error(event);
144 | ws.onclose = () => {
145 | observer.next(WebSocketResultWrap.local('服务器连接已断开,请刷新页面后再试!'));
146 | observer.complete();
147 | };
148 | }));
149 | }
150 |
151 | private getRealValue(item: Item): string | number | boolean | null {
152 | switch (item.type) {
153 | case Type.STRING:
154 | case Type.DATE:
155 | case Type.DATE_8601:
156 | return item.attributeValue as string;
157 | case Type.NUMBER:
158 | const value = Number(item.attributeValue);
159 | if (Number.isNaN(value)) {
160 | this.message.error(`${item.attributeValue}非数字!`);
161 | throw new Error(`${item.attributeValue}非数字!`);
162 | }
163 | return value;
164 | case Type.BOOLEAN:
165 | return item.attributeValue === 'true';
166 | default:
167 | console.error(`非法调用 Type:${item.type}`);
168 | return null;
169 | }
170 | }
171 |
172 | connectionResultWebSocket(token: string): Observable {
173 | const wsInstanceAndResult = this.initWebSocket(`ws://${environment.baseUrl}/p?token=${token}`);
174 | this.wsInstance = wsInstanceAndResult.instance;
175 | return wsInstanceAndResult.observable
176 | .pipe(map((it) => {
177 | if (it.localMessage) {
178 | return new WebSocketResultModel(0, '', it.message);
179 | }
180 | const data = it.data;
181 | const type: number = new DataView(data.slice(0, 1)).getUint8(0);
182 | const echo: string = this.textDecoder.decode(data.slice(1, 37));
183 | const message: string = this.textDecoder.decode(data.slice(37));
184 | return new WebSocketResultModel(type, echo, message);
185 | }));
186 | }
187 |
188 | connectionResultWebSocketReply(): Subject {
189 | if (this.resultSubject) {
190 | return this.resultSubject;
191 | }
192 | this.resultSubject = new Subject();
193 | this.getWebSocketToken().pipe(mergeMap(token => this.connectionResultWebSocket(token))).subscribe(this.resultSubject);
194 | return this.resultSubject;
195 | }
196 |
197 | connectionLogWebSocket(): Observable {
198 | return this.initWebSocket(`ws://${environment.baseUrl}/log`).observable.pipe(map((it) => {
199 | return it.localMessage ? it.message : this.textDecoder.decode(it.data);
200 | }));
201 | }
202 |
203 | /**
204 | * 转换请求
205 | * @param items 参数列表
206 | * @private
207 | */
208 | conversionRequest(items: Item[]): any {
209 | const r = [];
210 | items.forEach(item => {
211 | const re = {};
212 | if (item.type === Type.OBJECT) {
213 | re[item.attributeName] = this.conversionRequestForItem(item.attributeValue as Item[]);
214 | } else if (item.type === Type.ARRAY) {
215 | re[item.attributeName] = this.conversionRequestForItemArray(item.attributeValue as Item[]);
216 | } else {
217 | re[item.attributeName] = this.getRealValue(item);
218 | }
219 | r.push(re);
220 | });
221 | return r;
222 | }
223 |
224 | conversionRequestForItem(items: Item[]): any {
225 | const result = {};
226 | items.map(item => {
227 | if (item.type === Type.OBJECT) {
228 | result[item.attributeName] = this.conversionRequestForItem(item.attributeValue as Item[]);
229 | } else if (item.type === Type.ARRAY) {
230 | result[item.attributeName] = (item.attributeValue as Item[]).map(it => {
231 | if (it.type === Type.OBJECT) {
232 | return this.conversionRequestForItem(it.attributeValue as Item[]);
233 | } else if (it.type === Type.ARRAY) {
234 | return this.conversionRequestForItemArray(it.attributeValue as Item[]);
235 | } else {
236 | return this.getRealValue(it);
237 | }
238 | });
239 | } else {
240 | result[item.attributeName] = this.getRealValue(item);
241 | }
242 | });
243 | return result;
244 | }
245 |
246 | conversionRequestForItemArray(items: Item[]): any {
247 | return items.map(it => {
248 | if (it.type === Type.OBJECT) {
249 | return this.conversionRequestForItem(it.attributeValue as Item[]);
250 | } else if (it.type === Type.ARRAY) {
251 | return this.conversionRequestForItemArray(it.attributeValue as Item[]);
252 | } else {
253 | return this.getRealValue(it);
254 | }
255 | });
256 | }
257 | }
258 |
259 | class WsInstanceAndResult {
260 | instance: WebSocket;
261 | observable: Observable;
262 |
263 | constructor(instance: WebSocket, observable: Observable) {
264 | this.instance = instance;
265 | this.observable = observable;
266 | }
267 | }
268 |
269 | class WebSocketResultWrap {
270 | localMessage: boolean;
271 | message: string;
272 | data: ArrayBuffer;
273 |
274 | static local(message: string): WebSocketResultWrap {
275 | const w = new WebSocketResultWrap();
276 | w.localMessage = true;
277 | w.message = message;
278 | return w;
279 | }
280 |
281 | static wrap(data: ArrayBuffer): WebSocketResultWrap {
282 | const w = new WebSocketResultWrap();
283 | w.localMessage = false;
284 | w.data = data;
285 | return w;
286 | }
287 | }
288 |
289 | export class Artifact {
290 | groupId: string;
291 | artifactId: string;
292 | version: string;
293 | }
294 |
295 | export class MavenRequest {
296 | token: string;
297 | echo: string;
298 | dependency: string;
299 | interfaceName: string;
300 | methodName: string;
301 | }
302 |
303 | export class MavenResponse {
304 | success: boolean;
305 | message: string;
306 | data: T;
307 | }
308 |
309 | export class WebSocketResultModel {
310 | type: WebSocketMessageType;
311 | message: string;
312 | echo: string;
313 |
314 | constructor(type: WebSocketMessageType, echo: string, message: string) {
315 | this.type = type;
316 | this.message = message;
317 | this.echo = echo;
318 | }
319 | }
320 |
321 | export enum WebSocketMessageType {
322 | PLAINTEXT,
323 | JSON,
324 | NEXUS_DOWNLOAD_CANCEL_TOKEN,
325 | NEXUS_DOWNLOAD_PROGRESS,
326 | NEXUS_DOWNLOAD_SUCCESS,
327 | NEXUS_DOWNLOAD_FAILED
328 | }
329 |
330 | export type AttributeNameType = string | undefined;
331 | export type AttributeValueType = Item | number | string | boolean | number[] | string[] | Item[] | boolean[] | undefined;
332 |
333 | /**
334 | * 参数每一项
335 | */
336 | export class Item {
337 | id: string;
338 | attributeName: AttributeNameType;
339 | attributeValue: AttributeValueType;
340 | type: Type;
341 | placeholder: string;
342 | autoComplete: AutocompleteDataSource = [];
343 | show: boolean;
344 | use: boolean;
345 | attributeValueDate: Date;
346 |
347 | static generate(type: Type,
348 | attributeName: AttributeNameType,
349 | attributeValue: AttributeValueType,
350 | placeholder?: string,
351 | autoComplete?: AutocompleteDataSource,
352 | attributeValueDate?: Date): Item {
353 | const item = new Item();
354 | item.id = uuidv4();
355 | item.type = type;
356 | item.attributeName = attributeName;
357 | item.attributeValue = attributeValue;
358 | item.placeholder = placeholder;
359 | item.autoComplete = autoComplete;
360 | item.show = true;
361 | item.use = true;
362 | item.attributeValueDate = attributeValueDate;
363 | return item;
364 | }
365 |
366 | static generateObject(attributeName: AttributeNameType, attributeValue: AttributeValueType): Item {
367 | return Item.generate(Type.OBJECT, attributeName, attributeValue);
368 | }
369 |
370 | static generateArray(attributeName: AttributeNameType, attributeValue: AttributeValueType): Item {
371 | return Item.generate(Type.ARRAY, attributeName, attributeValue);
372 | }
373 |
374 | static generateString(attributeName: AttributeNameType,
375 | attributeValue: AttributeValueType,
376 | placeholder = '',
377 | autoComplete: AutocompleteDataSource = []): Item {
378 | return Item.generate(Type.STRING, attributeName, attributeValue, placeholder, autoComplete);
379 | }
380 |
381 | static generateNumber(attributeName: AttributeNameType,
382 | attributeValue: AttributeValueType,
383 | placeholder = '',
384 | autoComplete: AutocompleteDataSource = []): Item {
385 | return Item.generate(Type.NUMBER, attributeName, attributeValue, placeholder);
386 | }
387 |
388 | static generateBoolean(attributeName: AttributeNameType,
389 | attributeValue: AttributeValueType,
390 | placeholder = '',
391 | autoComplete: AutocompleteDataSource = []): Item {
392 | return Item.generate(Type.BOOLEAN, attributeName, attributeValue, placeholder);
393 | }
394 |
395 | static generateDate(attributeName: AttributeNameType,
396 | attributeValue: AttributeValueType,
397 | placeholder = '',
398 | autoComplete: AutocompleteDataSource = [],
399 | attributeValueDate: Date): Item {
400 | return Item.generate(Type.DATE, attributeName, attributeValue, placeholder, autoComplete, attributeValueDate);
401 | }
402 |
403 | static generateDATE_8601(attributeName: AttributeNameType,
404 | attributeValue: AttributeValueType,
405 | placeholder = '',
406 | autoComplete: AutocompleteDataSource = [],
407 | attributeValueDate: Date): Item {
408 | return Item.generate(Type.DATE_8601, attributeName, attributeValue, placeholder, autoComplete, attributeValueDate);
409 | }
410 | }
411 |
412 | export enum Type {
413 | STRING,
414 | NUMBER,
415 | BOOLEAN,
416 | ARRAY,
417 | OBJECT,
418 | DATE,
419 | DATE_8601
420 | }
421 |
422 | /**
423 | * TAB页信息
424 | */
425 | export class TabInfo {
426 | id: string;
427 | tabName: string;
428 | formParams: FormGroup;
429 | parameterValue: Item[] = [];
430 | resultData: string[] = [];
431 | selectEnv: EnvInfo;
432 | availableInterface: string[];
433 | availableMethod: string[];
434 | isRequestLoading: boolean;
435 |
436 | constructor(id: string, tabName: string, formParams: FormGroup, parameterValue: Item[], resultData: string[], selectEnv?: EnvInfo) {
437 | this.id = id;
438 | this.tabName = tabName;
439 | this.formParams = formParams;
440 | this.parameterValue = parameterValue;
441 | this.resultData = resultData;
442 | this.selectEnv = selectEnv;
443 | }
444 | }
445 |
446 | /**
447 | * 环境信息
448 | */
449 | export class EnvInfo {
450 | tag: string;
451 | env: string;
452 |
453 | constructor(info: string) {
454 | const splitIndex = info.indexOf('||');
455 | this.tag = info.substring(0, splitIndex);
456 | this.env = info.substring(splitIndex + 2);
457 | }
458 | }
459 |
460 | export class ServiceInfo {
461 | success: boolean;
462 | regConnected: boolean;
463 | updateTime: string;
464 | env: string;
465 | data: string[];
466 | }
467 |
468 | export class MethodInfo {
469 | signature: string;
470 | paramClassName: string[];
471 | property: { [key: string]: any }[];
472 | }
473 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/attribute/attribute.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
2 | import {NzModalService} from 'ng-zorro-antd/modal';
3 | import {v4 as uuidv4} from 'uuid';
4 | import {FormParamsInfo, PersistenceService} from '../../../../service/persistence.service';
5 | import {
6 | Artifact,
7 | AttributeValueType,
8 | EnvInfo,
9 | GenericService,
10 | Item,
11 | MavenRequest,
12 | MethodInfo,
13 | TabInfo,
14 | Type,
15 | WebSocketMessageType,
16 | WebSocketResultModel
17 | } from '../../../../service/generic.service';
18 | import JSONEditor from 'jsoneditor';
19 | import {NzMessageService} from 'ng-zorro-antd/message';
20 | import {UtilsService} from '../../../../service/utils.service';
21 | import {NzAutocompleteOptionComponent} from 'ng-zorro-antd/auto-complete';
22 | import {NzUploadFile, NzUploadXHRArgs} from 'ng-zorro-antd/upload/interface';
23 | import {EMPTY, Observable, Subscription} from 'rxjs';
24 | import {filter, map} from 'rxjs/operators';
25 | import {HttpEventType, HttpResponse} from '@angular/common/http';
26 | import {AutocompleteDataSource} from 'ng-zorro-antd/auto-complete/autocomplete.component';
27 | import {AbstractControl} from '@angular/forms';
28 |
29 | @Component({
30 | selector: 'app-attribute',
31 | templateUrl: './attribute.component.html',
32 | styleUrls: ['./attribute.component.scss']
33 | })
34 | export class AttributeComponent implements OnInit {
35 |
36 | /**
37 | * 每个TAB页的实例
38 | */
39 | @Input()
40 | tabs: TabInfo[] = [];
41 |
42 | /**
43 | * 当前选择的TAB页
44 | */
45 | @Input()
46 | selectedIndex = 0;
47 |
48 | /**
49 | * 发送请求
50 | */
51 | @Output()
52 | request: EventEmitter = new EventEmitter();
53 |
54 | /**
55 | * TAB选择改变事件
56 | */
57 | @Output()
58 | tabSelectChange: EventEmitter = new EventEmitter();
59 |
60 | // noinspection JSUnusedGlobalSymbols
61 | private options = {
62 | mode: 'code',
63 | modes: ['code', 'form', 'text', 'tree', 'view', 'preview'], // allowed modes
64 | onError: (err) => {
65 | alert(err.toString());
66 | },
67 | onModeChange: (newMode, oldMode) => {
68 | console.log('Mode switched from', oldMode, 'to', newMode);
69 | }
70 | };
71 |
72 | /**
73 | * JSONEditor实例
74 | */
75 | editor: any;
76 |
77 | @ViewChild('jsonEditor') set content(content: ElementRef) {
78 | if (content && this.needEditParamArray) {
79 | const editor = new JSONEditor(content.nativeElement, this.options);
80 | const conversionRequest2 = this.genericService.conversionRequest(this.needEditParamArray);
81 | editor.set(conversionRequest2);
82 | this.editor = editor;
83 | }
84 | }
85 |
86 | /**
87 | * 需要修改的参数
88 | */
89 | needEditParamArray: Item[];
90 |
91 | /**
92 | * 模态框显示
93 | */
94 | modalShow = {
95 | editInterfaceParam: false,
96 | resolveUrl: false,
97 | resolveUrlSelect: false,
98 | methodOverloading: false,
99 | tabReName: false,
100 | maven: false,
101 | download: false,
102 | mavenVersion: false
103 | };
104 |
105 | /**
106 | * 自动完成
107 | */
108 | autocomplete = {
109 | interfaces: {
110 | availableFilter: []
111 | },
112 | methods: {
113 | availableFilter: []
114 | }
115 | };
116 |
117 | /**
118 | * 用户填的解析URL值
119 | */
120 | resolveURLValue: string;
121 |
122 | /**
123 | * 所有可用的环境
124 | */
125 | availableEnv: EnvInfo[] = [];
126 |
127 | /**
128 | * 多个提供者信息
129 | */
130 | providerInfoArray: ProviderInfo[] = [];
131 |
132 | /**
133 | * 文件上传列表
134 | */
135 | uploadFileList: NzUploadFile[];
136 |
137 | /**
138 | * 方法重载信息数组
139 | */
140 | methodInfoArray: MethodInfo[];
141 |
142 | /**
143 | * 选择的方法重载信息
144 | */
145 | resolveMethodInfo: MethodInfo;
146 |
147 | /**
148 | * 原来的TAB页信息,用于修改TAB名展示
149 | */
150 | oldTab: TabInfo;
151 |
152 | /**
153 | * TAB的新名称
154 | */
155 | tabNameForReName: string;
156 |
157 | /**
158 | * 填写的maven XML信息
159 | */
160 | mavenXml: string = '\n' +
161 | ' \n' +
162 | ' \n' +
163 | ' \n' +
164 | '';
165 |
166 | /**
167 | * 填写的maven信息
168 | */
169 | mavenXmlInputValue: Artifact = {
170 | groupId: '',
171 | artifactId: '',
172 | version: ''
173 | };
174 |
175 | /**
176 | * maven坐标填写后的加载条显示
177 | */
178 | mavenLoading: boolean;
179 |
180 | /**
181 | * 下载进度信息
182 | */
183 | downloadProgress: number;
184 |
185 | /**
186 | * 下载速度信息
187 | */
188 | downloadSpeed: number;
189 |
190 | /**
191 | * 取消下载时的TOKEN信息
192 | */
193 | cancelToken: string;
194 |
195 | /**
196 | * 可选的maven版本信息
197 | */
198 | mavenArtifacts: Artifact[];
199 |
200 | /**
201 | * 选择的maven版本信息
202 | */
203 | resolveMavenArtifact: Artifact;
204 |
205 | /**
206 | * 填写maven信息时选择的TAB页
207 | */
208 | mavenXmlTabIndex = 0;
209 |
210 | constructor(private modal: NzModalService,
211 | private persistenceService: PersistenceService,
212 | private genericService: GenericService,
213 | private message: NzMessageService,
214 | private util: UtilsService) {
215 | }
216 |
217 | ngOnInit(): void {
218 | this.genericService.getAvailableEnv().subscribe((availableEnv) => {
219 | if (availableEnv && availableEnv.length > 0) {
220 | this.availableEnv = availableEnv.map(item => new EnvInfo(item));
221 | const selectEnv = this.tabs[this.selectedIndex].selectEnv;
222 | if (selectEnv && availableEnv.includes(selectEnv.env)) {
223 | this.initAvailableInterFace(selectEnv.tag, selectEnv.env);
224 | } else {
225 | if (selectEnv && selectEnv.env !== '') {
226 | this.message.warning('Zookeeper环境信息已变化!');
227 | this.tabs[this.selectedIndex].selectEnv = new EnvInfo('||');
228 | }
229 | }
230 | } else {
231 | console.log('注册中心当前未连接请稍后再试!');
232 | }
233 | });
234 | this.genericService.connectionResultWebSocketReply().subscribe(model => {
235 | const tabInfo = this.tabs.find(it => it.id === model.echo);
236 | if (tabInfo) {
237 | if (model.type === WebSocketMessageType.PLAINTEXT || model.type === WebSocketMessageType.JSON) {
238 | tabInfo.isRequestLoading = false;
239 | }
240 | this.parseDownloadMessage(model);
241 | }
242 | });
243 | }
244 |
245 | /**
246 | * 发起maven请求
247 | * @param mavenRequest maven请求
248 | * @private
249 | */
250 | private sendMavenRequest(mavenRequest: MavenRequest): void {
251 | this.genericService.sendMavenRequest(mavenRequest)
252 | .subscribe(response => {
253 | this.mavenLoading = false;
254 | this.modalShow.maven = false;
255 | if (!response.success) {
256 | this.message.error(response.message);
257 | }
258 | }, () => {
259 | this.mavenLoading = false;
260 | this.modalShow.maven = false;
261 | });
262 | }
263 |
264 | /**
265 | * 解析下载事件
266 | * @param model 事件模型
267 | * @private
268 | */
269 | private parseDownloadMessage(model: WebSocketResultModel): void {
270 | switch (model.type) {
271 | case WebSocketMessageType.NEXUS_DOWNLOAD_CANCEL_TOKEN:
272 | this.cancelToken = model.message;
273 | this.downloadSpeed = 0;
274 | this.downloadProgress = 0;
275 | this.modalShow.download = true;
276 | break;
277 | case WebSocketMessageType.NEXUS_DOWNLOAD_PROGRESS:
278 | const progressArray = model.message.split('-');
279 | const downloadBytes = Number(progressArray[0]);
280 | const totalBytes = Number(progressArray[1]);
281 | this.downloadProgress = Math.round(downloadBytes / totalBytes * 100);
282 | this.downloadSpeed = Number(progressArray[2]);
283 | break;
284 | case WebSocketMessageType.NEXUS_DOWNLOAD_FAILED:
285 | this.message.error(model.message);
286 | this.modalShow.download = false;
287 | break;
288 | case WebSocketMessageType.NEXUS_DOWNLOAD_SUCCESS:
289 | const info: MethodInfo[] = JSON.parse(model.message);
290 | if (info && info.length > 0) {
291 | if (info.length === 1) {
292 | this.parsingParameters(info[0]);
293 | } else {
294 | // 有重载
295 | this.methodInfoArray = info;
296 | this.modalShow.methodOverloading = true;
297 | }
298 | } else {
299 | this.message.warning('在上传的文件中没有找到该方法!');
300 | }
301 | this.modalShow.download = false;
302 | this.message.success('解析完成');
303 | break;
304 | }
305 | }
306 |
307 | /**
308 | * 验证接口名和方法信息是否填写
309 | * @param tabInfo TAB页
310 | * @private
311 | */
312 | private validInterfaceNameAndMethod(tabInfo: TabInfo): { interfaceName: AbstractControl, method: AbstractControl } | null {
313 | const formParams = tabInfo.formParams;
314 | const interfaceName = formParams.get('interfaceName');
315 | const method = formParams.get('method');
316 | interfaceName.markAsDirty();
317 | interfaceName.updateValueAndValidity();
318 | method.markAsDirty();
319 | method.updateValueAndValidity();
320 | if (!interfaceName.valid || !method.valid) {
321 | this.message.warning('请先填写接口名和方法名!');
322 | return null;
323 | }
324 | return {interfaceName, method};
325 | }
326 |
327 | /**
328 | * 递归删除某一个参数
329 | * @param items 接口参数信息
330 | * @param id 要删除的参数ID
331 | * @private
332 | */
333 | private delArrayItemById(items: Item[], id: string): void {
334 |
335 | for (let i = 0; i < items.length; i++) {
336 | if (items[i].id === id) {
337 | items.splice(i, 1);
338 | return;
339 | }
340 | if (items[i].attributeValue instanceof Array) {
341 | this.delArrayItemById(items[i].attributeValue as Item[], id);
342 | }
343 | }
344 | }
345 |
346 | /**
347 | * 解析参数
348 | * @param methodInfo 方法信息
349 | * @private
350 | */
351 | private parsingParameters(methodInfo: MethodInfo): void {
352 | const result = [];
353 | for (let i = 0; i < methodInfo.paramClassName.length; i++) {
354 | const re = {};
355 | const prop = methodInfo.property[i];
356 | re[methodInfo.paramClassName[i]] = prop[Object.keys(prop)[0]];
357 | result.push(re);
358 | }
359 | this.tabs[this.selectedIndex].parameterValue = this.parseTheModifiedParameters(result, true);
360 | }
361 |
362 | /**
363 | * 获取友好的进度信息
364 | */
365 | getGoodProgress(): string {
366 | let downloadSpeed = this.downloadSpeed;
367 | if (!downloadSpeed || Number.isNaN(downloadSpeed)) {
368 | downloadSpeed = 0;
369 | }
370 | if (downloadSpeed <= 1024) {
371 | return `${downloadSpeed.toFixed(2)}KB/s`;
372 | } else if (downloadSpeed <= 1048576) {
373 | return `${(downloadSpeed / 1024).toFixed(2)}MB/s`;
374 | } else {
375 | return `${(downloadSpeed / 1024 / 1024).toFixed(2)}GB/s`;
376 | }
377 | }
378 |
379 | /**
380 | * 关闭TAB页
381 | * @param index 索引
382 | */
383 | closeTab({index}: { index: number }): void {
384 | this.modal.confirm({
385 | nzTitle: '确定关闭吗?',
386 | nzOnOk: () => {
387 | this.tabs.splice(index, 1);
388 | this.persistenceService.saveGenericParamInfo(this.tabs);
389 | this.persistenceService.saveMetaInfo('nowSelectedTabIndex', this.selectedIndex);
390 | }
391 | });
392 | }
393 |
394 | /**
395 | * 新打开TAB页
396 | */
397 | newTab(): void {
398 | this.tabs.push(new TabInfo(
399 | uuidv4(), 'Unnamed Tab',
400 | this.genericService.generateFormParams(), [], []));
401 | this.selectedIndex = this.tabs.length;
402 | this.persistenceService.saveGenericParamInfo(this.tabs);
403 | }
404 |
405 | /**
406 | * 添加接口参数
407 | * @param parameterValue 当前页的接口参数信息
408 | */
409 | addInterfaceParam(parameterValue: Item[]): void {
410 | parameterValue.push(Item.generateString('', ''));
411 | }
412 |
413 | /**
414 | * 处理参数删除事件
415 | * @param id 要删除的参数ID
416 | * @param parameterValue 当前页的接口参数信息
417 | */
418 | handleDeleteEvent(id: string, parameterValue: Item[]): void {
419 | this.delArrayItemById(parameterValue, id);
420 | }
421 |
422 | /**
423 | * 处理新增参数事件
424 | * @param data 某一项参数信息
425 | */
426 | handleAddEvent(data: AttributeValueType): void {
427 | (data as Item[]).push(Item.generateString('', ''));
428 | }
429 |
430 | /**
431 | * 发送请求;持久化参数信息
432 | * @param tab 哪个TAB页调用的
433 | */
434 | sendRequest(tab: TabInfo): void {
435 | for (const i in tab.formParams.controls) {
436 | if (tab.formParams.controls.hasOwnProperty(i)) {
437 | tab.formParams.controls[i].markAsDirty();
438 | tab.formParams.controls[i].updateValueAndValidity();
439 | }
440 | }
441 | if (tab.formParams.valid) {
442 | this.request.emit(tab);
443 | }
444 | }
445 |
446 | /**
447 | * 处理TAB切换
448 | */
449 | handleTabChange(): void {
450 | const tabInfo = this.tabs[this.selectedIndex];
451 | this.autocomplete.methods.availableFilter = tabInfo.availableMethod;
452 | this.autocomplete.interfaces.availableFilter = tabInfo.availableInterface;
453 | this.tabSelectChange.emit(this.selectedIndex);
454 | }
455 |
456 | /**
457 | * 修改参数
458 | * @param parameterValue 参数
459 | */
460 | editInterfaceParam(parameterValue: Item[]): void {
461 | this.needEditParamArray = parameterValue;
462 | this.modalShow.editInterfaceParam = true;
463 | }
464 |
465 | /**
466 | * 修改参数确认
467 | */
468 | doEditInterfaceParam(): void {
469 | if (this.editor) {
470 | try {
471 | const editorItems = this.editor.get();
472 | if (!this.checkEditDataOrderly(editorItems)) {
473 | return;
474 | }
475 | this.tabs[this.selectedIndex].parameterValue = this.parseTheModifiedParameters(editorItems);
476 | } catch (e) {
477 | console.warn(e);
478 | this.message.error('解析失败!');
479 | }
480 | this.needEditParamArray = null;
481 | this.modalShow.editInterfaceParam = false;
482 | }
483 | }
484 |
485 | /**
486 | * 检查修改的数据是否正确
487 | * @param data 数据
488 | * @private
489 | */
490 | private checkEditDataOrderly(data: any): boolean {
491 | if (Type.ARRAY !== this.util.getObjectType(data)) {
492 | this.message.error('最外层必须是个数组!');
493 | return false;
494 | }
495 | for (const item of data) {
496 | if (Type.OBJECT !== this.util.getObjectType(item)) {
497 | this.message.error('最外层数组中每一项必须是对象!');
498 | return false;
499 | }
500 | if (Object.keys(item).length > 1) {
501 | this.message.error('最外层数组中每一项的对象只能有一个KEY!');
502 | return false;
503 | }
504 | }
505 | return true;
506 | }
507 |
508 | /**
509 | * 解析修改的参数
510 | * @param editorItems 修改后的参数
511 | * @param isUpload 上传调用
512 | * @private
513 | */
514 | private parseTheModifiedParameters(editorItems: any, isUpload = false): Item[] {
515 | const result: Item[] = [];
516 | for (const item of editorItems) {
517 | const key = Object.keys(item)[0];
518 | const value = item[key];
519 | result.push(this.generateItem(key, value, isUpload));
520 | }
521 | return result;
522 | }
523 |
524 | /**
525 | * 根据值类型生成Item
526 | * @param name Item名字
527 | * @param value Item值
528 | * @param needTransform 需要将文本转换到对应的类型
529 | * @private
530 | */
531 | private generateItem(name: string, value: any, needTransform = false): Item {
532 | let valueType = this.util.getObjectType(value);
533 | if (valueType === Type.ARRAY) {
534 | return Item.generateArray(name, this.generateItemForArray(value, needTransform));
535 | } else if (valueType === Type.OBJECT) {
536 | return Item.generateObject(name, this.generateItemForObject(value, needTransform));
537 | } else {
538 | const autoComplete: AutocompleteDataSource = [];
539 | let originalValue = '';
540 | if (needTransform) {
541 | originalValue = `参数类型:${value}`;
542 | if (value.startsWith('enum|')) {
543 | const start = value.indexOf('|');
544 | const temp = value.substring(start + 1);
545 | const classNameSplit = temp.indexOf('|');
546 | const className = temp.substring(0, classNameSplit);
547 | const json = temp.substring(classNameSplit + 1);
548 | const enums = JSON.parse(json);
549 | autoComplete.push(...enums);
550 | originalValue = `参数类型:${className}`;
551 | }
552 | switch (value) {
553 | case 'java.lang.Integer':
554 | case 'java.lang.Long':
555 | case 'java.lang.Short':
556 | case 'byte':
557 | case 'short':
558 | case 'int':
559 | case 'long' :
560 | valueType = Type.NUMBER;
561 | value = '';
562 | break;
563 | case 'java.lang.Double':
564 | case 'java.lang.Float':
565 | case 'float':
566 | case 'double':
567 | valueType = Type.NUMBER;
568 | value = '';
569 | break;
570 | case 'java.lang.Character':
571 | case 'java.lang.String':
572 | valueType = Type.STRING;
573 | value = '';
574 | break;
575 | case 'java.lang.Boolean':
576 | case 'boolean':
577 | valueType = Type.BOOLEAN;
578 | value = '';
579 | autoComplete.push('true', 'false');
580 | break;
581 | case 'java.util.Date':
582 | case 'java.sql.Date':
583 | case 'java.sql.Timestamp':
584 | case 'java.sql.Time':
585 | valueType = Type.DATE;
586 | value = this.util.getNowDate2String();
587 | originalValue = '日期格式:yyyy-MM-dd HH:mm:ss';
588 | break;
589 | case 'java.time.LocalDate':
590 | case 'java.time.LocalTime':
591 | case 'java.time.LocalDateTime':
592 | valueType = Type.DATE_8601;
593 | value = this.util.getNowDate2_8301String();
594 | originalValue = '日期格式:yyyy-MM-ddTHH:mm:ss';
595 | break;
596 | default:
597 | valueType = Type.STRING;
598 | value = '';
599 | }
600 | }
601 | switch (valueType) {
602 | case Type.NUMBER:
603 | return Item.generateNumber(name, value, originalValue, autoComplete);
604 | case Type.BOOLEAN:
605 | return Item.generateBoolean(name, value, originalValue, autoComplete);
606 | case Type.DATE:
607 | return Item.generateDate(name, value, originalValue, autoComplete, new Date());
608 | case Type.DATE_8601:
609 | return Item.generateDATE_8601(name, value, originalValue, autoComplete, new Date());
610 | default:
611 | if (this.util.isMatchDateString(value)) {
612 | const attributeValueDate = this.util.formatDateString2Date(value);
613 | return Item.generateDate(name, value, '日期格式:yyyy-MM-dd HH:mm:ss', autoComplete, attributeValueDate);
614 | } else if (this.util.isMatchDate_8301String(value)) {
615 | const attributeValueDate = this.util.formatDate_8301String2Date(value);
616 | return Item.generateDATE_8601(name, value, '日期格式:yyyy-MM-ddTHH:mm:ss', autoComplete, attributeValueDate);
617 | } else {
618 | return Item.generateString(name, value, originalValue, autoComplete);
619 | }
620 | }
621 | }
622 | }
623 |
624 | /**
625 | * 处理自动填充参数
626 | * @param interfaceName 接口名
627 | * @private
628 | */
629 | private handleAutoFillingParam(interfaceName: string): void {
630 | const tabInfo = this.tabs[this.selectedIndex];
631 | this.genericService.getURL(tabInfo.selectEnv.env, tabInfo.selectEnv.tag, interfaceName).subscribe(url => {
632 | if (url && url.success && url.data && url.data.length > 0) {
633 | if (url.data.length === 1) {
634 | this.resolveURLValue = decodeURIComponent(url.data[0]);
635 | this.resolveURL();
636 | } else {
637 | this.providerInfoArray = url.data.map(item => {
638 | item = decodeURIComponent(item);
639 | const host = item.substring(8, item.indexOf('/', 8));
640 | const group = this.util.getParamValue(item, 'group');
641 | const version = this.util.getParamValue(item, 'version');
642 | return new ProviderInfo(item, `主机:${host} 分组:${group} 版本:${version}`);
643 | });
644 | this.message.info('有多个提供者,请选择一个!');
645 | this.resolveURLValue = '';
646 | this.modalShow.resolveUrlSelect = true;
647 | }
648 | } else {
649 | tabInfo.availableMethod = this.autocomplete.methods.availableFilter = [];
650 | if (url && !url.regConnected) {
651 | this.message.warning('注册中心当前未连接请稍后再试!');
652 | } else {
653 | this.message.warning('服务没有提供者!');
654 | }
655 | }
656 | });
657 | }
658 |
659 | /**
660 | * 生成对象类型
661 | * @param items 每一项
662 | * @param needTransform 需要将文本转换到对应的类型
663 | */
664 | generateItemForObject(items: any, needTransform = false): Item[] {
665 | const result: Item[] = [];
666 | for (const key in items) {
667 | if (items.hasOwnProperty(key)) {
668 | result.push(this.generateItem(key, items[key], needTransform));
669 | }
670 | }
671 | return result;
672 | }
673 |
674 | /**
675 | * 生成数组类型
676 | * @param items 每一项
677 | * @param needTransform 需要将文本转换到对应的类型
678 | */
679 | generateItemForArray(items: any, needTransform = false): Item[] {
680 | const result: Item[] = [];
681 | for (const item of items) {
682 | result.push(this.generateItem('', item, needTransform));
683 | }
684 | return result;
685 | }
686 |
687 | /**
688 | * 清空
689 | */
690 | clearAll(): void {
691 | const tabInfo = this.tabs[this.selectedIndex];
692 | if (tabInfo) {
693 | tabInfo.parameterValue = [];
694 | tabInfo.formParams.reset({url: '', interfaceName: '', method: '', version: '', group: ''});
695 | }
696 | }
697 |
698 | /**
699 | * 解析URL
700 | */
701 | resolveURL(): void {
702 | if (!this.resolveURLValue) {
703 | this.message.warning('URL不能为空!');
704 | return;
705 | }
706 | if (!this.resolveURLValue.startsWith('dubbo://')) {
707 | this.message.warning('URL必须以dubbo://开头!');
708 | return;
709 | }
710 | const host = this.resolveURLValue.substring(8, this.resolveURLValue.indexOf('/', 8));
711 | const interfaceName = this.util.getParamValue(this.resolveURLValue, 'interface');
712 | const group = this.util.getParamValue(this.resolveURLValue, 'group');
713 | const version = this.util.getParamValue(this.resolveURLValue, 'version');
714 | const methods = this.util.getParamValue(this.resolveURLValue, 'methods');
715 | const path = this.util.getParamValue(this.resolveURLValue, 'path');
716 | const tabInfo = this.tabs[this.selectedIndex];
717 | tabInfo.formParams.setValue({url: host, interfaceName, group, version, method: '', path});
718 | this.autocomplete.methods.availableFilter = tabInfo.availableMethod = methods.split(',');
719 | this.message.success('解析完成!');
720 | this.modalShow.resolveUrl = false;
721 | }
722 |
723 | /**
724 | * 可用方法过滤
725 | * @param $event Event
726 | */
727 | availableMethodsAutoCompleteFilter($event: Event): void {
728 | $event.preventDefault();
729 | const value = ($event.target as HTMLInputElement).value;
730 | this.autocomplete.methods.availableFilter = this.tabs[this.selectedIndex].availableMethod.filter(item => item.indexOf(value) !== -1);
731 | }
732 |
733 |
734 | /**
735 | * 可用方法过滤
736 | * @param $event Event
737 | */
738 | availableInterFacesAutoCompleteFilter($event: Event): void {
739 | $event.preventDefault();
740 | const value = ($event.target as HTMLInputElement).value;
741 | this.autocomplete.interfaces.availableFilter =
742 | this.tabs[this.selectedIndex].availableInterface.filter(item => item.indexOf(value) !== -1);
743 | }
744 |
745 | /**
746 | * 初始化可用接口信息
747 | * @param tag 标签
748 | * @param env 环境
749 | */
750 | initAvailableInterFace(tag: string, env: string): void {
751 | this.genericService.getAvailableInterFaces(tag, env).subscribe((availableInterface) => {
752 | if (availableInterface && availableInterface.success) {
753 | this.autocomplete.interfaces.availableFilter = this.tabs[this.selectedIndex].availableInterface = availableInterface.data;
754 | this.message.success('接口信息获取成功!');
755 | } else {
756 | if (availableInterface && !availableInterface.regConnected) {
757 | this.message.warning('注册中心当前未连接请稍后再试!');
758 | } else {
759 | this.message.warning('可用接口信息获取失败!');
760 | }
761 | }
762 | });
763 | }
764 |
765 | /**
766 | * 处理环境改变事件
767 | */
768 | handleEnvChange(): void {
769 | const tabInfo = this.tabs[this.selectedIndex];
770 | this.autocomplete.interfaces.availableFilter = tabInfo.availableInterface = [];
771 | this.initAvailableInterFace(tabInfo.selectEnv.tag, tabInfo.selectEnv.env);
772 | const interfaceName = tabInfo.formParams.get('interfaceName');
773 | interfaceName.markAsDirty();
774 | interfaceName.updateValueAndValidity();
775 | if (interfaceName.valid && interfaceName.value) {
776 | tabInfo.formParams.reset({url: '', interfaceName: interfaceName.value, method: '', version: '', group: ''});
777 | this.handleAutoFillingParam(interfaceName.value);
778 | }
779 | }
780 |
781 | /**
782 | * 处理自动完成选择事件
783 | * @param $event 事件
784 | */
785 | handleInterfaceAutocompleteSelect($event: NzAutocompleteOptionComponent): void {
786 | if ($event.nzValue) {
787 | this.handleAutoFillingParam($event.nzValue);
788 | }
789 | }
790 |
791 | /**
792 | * 解析选择的URL
793 | */
794 | resolveSelectURL(): void {
795 | this.resolveURL();
796 | this.modalShow.resolveUrlSelect = false;
797 | }
798 |
799 | /**
800 | * 上传之前检查
801 | * @param file NzUploadFile
802 | */
803 | beforeUpload = (file: NzUploadFile): boolean | Observable => {
804 | const formParams = this.tabs[this.selectedIndex].formParams;
805 | const interfaceName = formParams.get('interfaceName');
806 | const method = formParams.get('method');
807 | interfaceName.markAsDirty();
808 | interfaceName.updateValueAndValidity();
809 | method.markAsDirty();
810 | method.updateValueAndValidity();
811 | if (!interfaceName.valid || !method.valid) {
812 | this.message.info('请先填写接口名和方法名!');
813 | return false;
814 | }
815 | // tslint:disable-next-line
816 | const exName = file.name.slice((file.name.lastIndexOf('.') - 1 >>> 0) + 2);
817 | if (exName === 'jar' || exName === 'zip' || exName === 'war') {
818 | return true;
819 | } else {
820 | this.message.error(`${file.name}不是正确的文件,支持的扩展名:jar或zip`);
821 | return false;
822 | }
823 | // tslint:disable-next-line
824 | };
825 |
826 | /**
827 | * 文件上传
828 | * @param item NzUploadXHRArgs
829 | */
830 | uploadRequest = (item: NzUploadXHRArgs): Subscription => {
831 | const formParams = this.tabs[this.selectedIndex].formParams;
832 | const interfaceName = formParams.get('interfaceName');
833 | const method = formParams.get('method');
834 | interfaceName.markAsDirty();
835 | interfaceName.updateValueAndValidity();
836 | method.markAsDirty();
837 | method.updateValueAndValidity();
838 | if (!interfaceName.valid || !method.valid) {
839 | this.message.info('请先填写接口名和方法名!');
840 | item.onError(null, item.file);
841 | return EMPTY.subscribe();
842 | }
843 | return this.genericService.uploadJar(item.postFile as Blob, interfaceName.value, method.value)
844 | .pipe(
845 | filter(event => {
846 | switch (event.type) {
847 | case HttpEventType.Response: {
848 | return true;
849 | }
850 | case HttpEventType.UploadProgress: {
851 | item.onProgress({percent: Math.round(event.loaded / event.total * 100).toFixed(2)}, item.file);
852 | return false;
853 | }
854 | default:
855 | return false;
856 | }
857 | }),
858 | map(it => (it as HttpResponse).body))
859 | .subscribe(info => {
860 | item.onSuccess(info, item.file, null);
861 | this.uploadFileList = [];
862 | if (info && info.length > 0) {
863 | if (info.length === 1) {
864 | this.parsingParameters(info[0]);
865 | } else {
866 | // 有重载
867 | this.methodInfoArray = info;
868 | this.modalShow.methodOverloading = true;
869 | }
870 | } else {
871 | this.message.warning('在上传的文件中没有找到该方法!');
872 | }
873 | },
874 | err => {
875 | item.onError(err, item.file);
876 | console.warn(err);
877 | },
878 | () => {
879 | item.onSuccess(null, item.file, null);
880 | });
881 | // tslint:disable-next-line
882 | };
883 |
884 | /**
885 | * 解析选择的方法
886 | */
887 | resolveSelectMethod(): void {
888 | if (this.resolveMethodInfo) {
889 | this.parsingParameters(this.resolveMethodInfo);
890 | this.message.success('选择成功');
891 | }
892 | this.modalShow.methodOverloading = false;
893 | }
894 |
895 | /**
896 | * Tab单击事件
897 | */
898 | handleTabClick(index: number): void {
899 | if (index !== this.selectedIndex) {
900 | return;
901 | }
902 | this.oldTab = this.tabs[index];
903 | this.modalShow.tabReName = true;
904 | }
905 |
906 | /**
907 | * TAB重命名
908 | */
909 | doTabReName(): void {
910 | if (!this.tabNameForReName) {
911 | this.message.warning('输入不能为空!');
912 | return;
913 | }
914 | this.oldTab.tabName = this.tabNameForReName;
915 | this.tabNameForReName = '';
916 | this.persistenceService.saveGenericParamInfo(this.tabs);
917 | this.modalShow.tabReName = false;
918 | }
919 |
920 | /**
921 | * maven请求
922 | */
923 | mavenRequest(): void {
924 | if (!this.validInterfaceNameAndMethod(this.tabs[this.selectedIndex])) {
925 | return;
926 | }
927 | this.modalShow.maven = true;
928 | }
929 |
930 | /**
931 | * 发起maven请求
932 | */
933 | doMaven(): void {
934 | const tabInfo = this.tabs[this.selectedIndex];
935 | const tabParam = this.validInterfaceNameAndMethod(tabInfo);
936 | if (!tabParam) {
937 | return;
938 | }
939 | if (this.mavenXmlTabIndex === 1) {
940 | this.mavenXml = this.util.genericMavenDependencyXml(this.mavenXmlInputValue);
941 | }
942 | if (!this.mavenXml || this.mavenXml === '') {
943 | this.message.warning('Maven坐标信息必填!');
944 | return;
945 | }
946 | this.mavenLoading = true;
947 | this.genericService.sendMavenParse(this.mavenXml).subscribe(result => {
948 | if (!result) {
949 | this.mavenLoading = false;
950 | return;
951 | }
952 | if (result.length > 0) {
953 | if (result.length === 1) {
954 | const mavenRequest = new MavenRequest();
955 | mavenRequest.echo = tabInfo.id;
956 | mavenRequest.interfaceName = tabParam.interfaceName.value;
957 | mavenRequest.methodName = tabParam.method.value;
958 | mavenRequest.dependency = this.util.genericMavenDependencyXml(result[0]);
959 | this.sendMavenRequest(mavenRequest);
960 | } else {
961 | this.mavenArtifacts = result;
962 | this.modalShow.mavenVersion = true;
963 | }
964 | } else {
965 | this.mavenLoading = false;
966 | this.message.info('没有找到可用的版本信息');
967 | }
968 | });
969 | }
970 |
971 | /**
972 | * 取消下载
973 | */
974 | cancelDownload(): void {
975 | this.modal.confirm({
976 | nzTitle: '确定取消下载?',
977 | nzContent: '取消后已经下载的进度将删除。',
978 | nzOnOk: () => this.genericService.cancelDownload(this.cancelToken)
979 | .subscribe(() => {
980 | this.modalShow.download = false;
981 | this.downloadSpeed = 0;
982 | this.downloadProgress = 0;
983 | })
984 | });
985 | }
986 |
987 | /**
988 | * 解析选择的maven信息
989 | */
990 | resolveSelectMavenVersion(): void {
991 | if (!this.resolveMavenArtifact) {
992 | this.message.warning('必须选择个版本号');
993 | }
994 | const tabInfo = this.tabs[this.selectedIndex];
995 | const result = this.validInterfaceNameAndMethod(tabInfo);
996 | if (!result) {
997 | return;
998 | }
999 | const mavenRequest = new MavenRequest();
1000 | mavenRequest.echo = tabInfo.id;
1001 | mavenRequest.interfaceName = result.interfaceName.value;
1002 | mavenRequest.methodName = result.method.value;
1003 | mavenRequest.dependency = this.util.genericMavenDependencyXml(this.resolveMavenArtifact);
1004 | this.modalShow.mavenVersion = false;
1005 | this.sendMavenRequest(mavenRequest);
1006 | }
1007 |
1008 | /**
1009 | * maven模态框关闭
1010 | */
1011 | onMavenClose(): void {
1012 | this.mavenLoading = false;
1013 | this.modalShow.maven = false;
1014 | }
1015 |
1016 | /**
1017 | * maven版本模态框关闭
1018 | */
1019 | onMavenVersionClose(): void {
1020 | this.mavenLoading = false;
1021 | this.modalShow.mavenVersion = false;
1022 | }
1023 |
1024 | /**
1025 | * 重新加载提示
1026 | */
1027 | reloadPrompt(): void {
1028 | const tabInfo = this.tabs[this.selectedIndex];
1029 | if (tabInfo.selectEnv && tabInfo.selectEnv.env !== '') {
1030 | this.handleEnvChange();
1031 | } else {
1032 | this.message.warning('请先选择环境!');
1033 | }
1034 | }
1035 | }
1036 |
1037 | /**
1038 | * 接口提供者信息
1039 | */
1040 | class ProviderInfo {
1041 | url: string;
1042 | info: string;
1043 |
1044 | constructor(url: string, info: string) {
1045 | this.url = url;
1046 | this.info = info;
1047 | }
1048 | }
1049 |
1050 | /**
1051 | * 请求参数模型
1052 | */
1053 | export type RequestParamModel = { [name: string]: RequestParamModel | string | string[] }[];
1054 |
1055 | /**
1056 | * 请求模型
1057 | */
1058 | export type RequestModel = FormParamsInfo & { params: RequestParamModel };
1059 |
--------------------------------------------------------------------------------