├── 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 |
2 |
{{i}}
3 |
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 |
2 |

3 | 
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 | 7 | 13 | 22 | 23 | 24 | 25 |
26 | 27 | 31 | 32 |
33 | 35 |
36 |
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 | [![GitHub stars](https://img.shields.io/github/stars/itning/generic-service-client-web.svg?style=social&label=Stars)](https://github.com/itning/generic-service-client-web/stargazers) 4 | [![GitHub forks](https://img.shields.io/github/forks/itning/generic-service-client-web.svg?style=social&label=Fork)](https://github.com/itning/generic-service-client-web/network/members) 5 | [![GitHub watchers](https://img.shields.io/github/watchers/itning/generic-service-client-web.svg?style=social&label=Watch)](https://github.com/itning/generic-service-client-web/watchers) 6 | [![GitHub followers](https://img.shields.io/github/followers/itning.svg?style=social&label=Follow)](https://github.com/itning?tab=followers) 7 | 8 | [![Auto Build](https://github.com/itning/generic-service-client-web/actions/workflows/main.yml/badge.svg)](https://github.com/itning/generic-service-client-web/actions/workflows/main.yml) 9 | [![GitHub issues](https://img.shields.io/github/issues/itning/generic-service-client-web.svg)](https://github.com/itning/generic-service-client-web/issues) 10 | [![GitHub license](https://img.shields.io/github/license/itning/generic-service-client-web.svg)](https://github.com/itning/generic-service-client-web/blob/master/LICENSE) 11 | [![GitHub last commit](https://img.shields.io/github/last-commit/itning/generic-service-client-web.svg)](https://github.com/itning/generic-service-client-web/commits) 12 | [![GitHub release](https://img.shields.io/github/release/itning/generic-service-client-web.svg)](https://github.com/itning/generic-service-client-web/releases) 13 | [![GitHub repo size in bytes](https://img.shields.io/github/repo-size/itning/generic-service-client-web.svg)](https://github.com/itning/generic-service-client-web) 14 | [![HitCount](http://hits.dwyl.com/itning/generic-service-client-web.svg)](http://hits.dwyl.com/itning/generic-service-client-web) 15 | [![language](https://img.shields.io/badge/language-Angular-green.svg)](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 |
60 | 61 | 64 |
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 | 7 |
8 | 9 | 弱类型接口名(全限定类名) 10 | 11 | 13 | 14 | 18 | 19 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 直连提供者URL 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 调用方法(不需要写括号) 45 | 46 | 47 | 49 | 51 | 52 | 53 | 54 | 版本 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 分组 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 上传JAR自动填充参数 78 | 79 | 80 | 81 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
90 |
91 | 94 |
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 | --------------------------------------------------------------------------------