├── CHANGELOG.md
├── src
├── assets
│ ├── .gitkeep
│ ├── icon.png
│ └── icons
│ │ ├── favicon.ico
│ │ ├── favicon.png
│ │ ├── favicon.icns
│ │ ├── favicon.256x256.png
│ │ └── favicon.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
│ │ │ │ ├── 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
│ ├── app.component.ts
│ ├── http
│ │ ├── index.ts
│ │ └── ResponseErrorInterceptor.ts
│ ├── service
│ │ ├── electron.service.spec.ts
│ │ ├── utils.service.spec.ts
│ │ ├── generic.service.spec.ts
│ │ ├── persistence.service.spec.ts
│ │ ├── electron.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
├── polyfills-test.ts
├── styles.scss
├── styles
│ ├── dark.less
│ ├── default.less
│ └── themes
│ │ ├── base.less
│ │ ├── dark.less
│ │ ├── default.less
│ │ └── mixin.less
├── environments
│ ├── environment.ts
│ ├── environment.dev.ts
│ ├── environment.prod.ts
│ └── environment.web.ts
├── typings.d.ts
├── main.ts
├── tsconfig.spec.json
├── tsconfig.app.json
├── index.html
├── test.ts
├── karma.conf.js
└── polyfills.ts
├── .wakatime-project
├── .npmrc
├── _config.yml
├── e2e
├── tsconfig.e2e.json
├── main.e2e.ts
└── common-setup.ts
├── .editorconfig
├── tsconfig.serve.json
├── electron-builder.json
├── tsconfig.json
├── angular.webpack.js
├── .gitignore
├── LICENSE.md
├── .github
└── workflows
│ └── main.yml
├── README.md
├── package.json
├── tslint.json
├── main.ts
└── angular.json
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.wakatime-project:
--------------------------------------------------------------------------------
1 | Bitter Union 55
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save=true
2 | save-exact=true
3 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-architect
--------------------------------------------------------------------------------
/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/polyfills-test.ts:
--------------------------------------------------------------------------------
1 | import 'core-js/es/reflect';
2 | import 'zone.js/dist/zone';
3 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/src/styles/dark.less:
--------------------------------------------------------------------------------
1 | @import '../../node_modules/ng-zorro-antd/ng-zorro-antd';
2 | @import "./themes/dark";
3 |
--------------------------------------------------------------------------------
/src/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-electron/master/src/assets/icon.png
--------------------------------------------------------------------------------
/src/styles/default.less:
--------------------------------------------------------------------------------
1 | @import '../../node_modules/ng-zorro-antd/ng-zorro-antd';
2 | @import "./themes/default";
3 |
--------------------------------------------------------------------------------
/src/assets/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-electron/master/src/assets/icons/favicon.ico
--------------------------------------------------------------------------------
/src/assets/icons/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-electron/master/src/assets/icons/favicon.png
--------------------------------------------------------------------------------
/src/assets/icons/favicon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-electron/master/src/assets/icons/favicon.icns
--------------------------------------------------------------------------------
/src/styles/themes/base.less:
--------------------------------------------------------------------------------
1 | @margin-md: 17px;
2 |
3 | .ant-btn > .anticon + span, .ant-btn > span + .anticon {
4 | margin-left: 0;
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/icons/favicon.256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-electron/master/src/assets/icons/favicon.256x256.png
--------------------------------------------------------------------------------
/src/assets/icons/favicon.512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itning/generic-service-client-electron/master/src/assets/icons/favicon.512x512.png
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | export const AppConfig = {
2 | production: false,
3 | environment: 'LOCAL',
4 | baseUrl:'116.62.37.140:8868'
5 | };
6 |
--------------------------------------------------------------------------------
/src/environments/environment.dev.ts:
--------------------------------------------------------------------------------
1 | export const AppConfig = {
2 | production: false,
3 | environment: 'DEV',
4 | baseUrl:'116.62.37.140:8868'
5 | };
6 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const AppConfig = {
2 | production: true,
3 | environment: 'PROD',
4 | baseUrl:'116.62.37.140:8868'
5 | };
6 |
--------------------------------------------------------------------------------
/src/environments/environment.web.ts:
--------------------------------------------------------------------------------
1 | export const AppConfig = {
2 | production: false,
3 | environment: 'WEB',
4 | baseUrl:'116.62.37.140:8868'
5 | };
6 |
--------------------------------------------------------------------------------
/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/styles/themes/dark.less:
--------------------------------------------------------------------------------
1 | @import (multiple) '../../../node_modules/ng-zorro-antd/src/style/themes/dark';
2 | @import './base';
3 | @layout-sider-background: #001529;
4 | @menu-dark-bg: #001529;
5 |
6 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/result/result.component.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | /* SystemJS module definition */
2 | declare const nodeModule: NodeModule;
3 | interface NodeModule {
4 | id: string;
5 | }
6 | interface Window {
7 | process: any;
8 | require: any;
9 | }
10 |
--------------------------------------------------------------------------------
/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/app.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.scss']
7 | })
8 | export class AppComponent {
9 | constructor() {
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "module": "commonjs",
6 | "types": [
7 | "mocha",
8 | "node"
9 | ]
10 | },
11 | "include": [
12 | "**.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://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 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 { AppConfig } from './environments/environment';
6 |
7 | if (AppConfig.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/src/app/service/electron.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { ElectronService } from './electron.service';
4 |
5 | describe('ElectronService', () => {
6 | beforeEach(() => TestBed.configureTestingModule({}));
7 |
8 | it('should be created', () => {
9 | const service: ElectronService = TestBed.get(ElectronService);
10 | expect(service).toBeTruthy();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/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/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "module": "commonjs",
6 | "types": [
7 | "jasmine",
8 | "node"
9 | ]
10 | },
11 | "files": [
12 | "test.ts",
13 | "polyfills-test.ts"
14 | ],
15 | "include": [
16 | "**/*.spec.ts",
17 | "**/*.d.ts"
18 | ],
19 | "exclude": [
20 | "dist",
21 | "release",
22 | "node_modules"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/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/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "module": "es2015",
6 | "baseUrl": "",
7 | "types": []
8 | },
9 | "include": [
10 | "main.ts",
11 | "polyfill.ts"
12 | ],
13 | "exclude": [
14 | "**/*.spec.ts"
15 | ],
16 | "angularCompilerOptions": {
17 | "fullTemplateTypeCheck": true,
18 | "strictInjectionParameters": true,
19 | "preserveWhitespaces": true,
20 | "allowSyntheticDefaultImports": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tsconfig.serve.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "declaration": false,
5 | "moduleResolution": "node",
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "target": "es5",
9 | "types": [
10 | "node"
11 | ],
12 | "lib": [
13 | "es2017",
14 | "es2016",
15 | "es2015",
16 | "dom"
17 | ]
18 | },
19 | "files": [
20 | "main.ts"
21 | ],
22 | "exclude": [
23 | "node_modules",
24 | "**/*.spec.ts"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/electron-builder.json:
--------------------------------------------------------------------------------
1 | {
2 | "productName": "generic-service-client-electron",
3 | "directories": {
4 | "output": "release/"
5 | },
6 | "files": [
7 | "dist",
8 | "file",
9 | "main.js"
10 | ],
11 | "win": {
12 | "icon": "dist/assets/icons",
13 | "target": [
14 | "portable"
15 | ]
16 | },
17 | "mac": {
18 | "icon": "dist/assets/icons",
19 | "target": [
20 | "dmg"
21 | ]
22 | },
23 | "linux": {
24 | "icon": "dist/assets/icons",
25 | "target": [
26 | "AppImage"
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 测试工具箱
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/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 | import {IconDefinition} from '@ant-design/icons-angular';
4 | import * as AllIcons from '@ant-design/icons-angular/icons';
5 |
6 | const antDesignIcons = AllIcons as {
7 | [key: string]: IconDefinition;
8 | };
9 | const icons: IconDefinition[] = Object.keys(antDesignIcons).map(key => antDesignIcons[key])
10 |
11 | @NgModule({
12 | imports: [NzIconModule],
13 | exports: [NzIconModule],
14 | providers: [
15 | {provide: NZ_ICONS, useValue: icons}
16 | ]
17 | })
18 | export class IconsProviderModule {
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "module": "es2020",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "moduleResolution": "node",
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "target": "es5",
12 | "typeRoots": [
13 | "node_modules/@types"
14 | ],
15 | "lib": [
16 | "es2017",
17 | "es2016",
18 | "es2015",
19 | "dom"
20 | ]
21 | },
22 | "files": [
23 | "src/main.ts",
24 | "src/polyfills.ts"
25 | ],
26 | "include": [
27 | "src/**/*.d.ts"
28 | ],
29 | "exclude": [
30 | "node_modules"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting()
16 | );
17 | // Then we find all the tests.
18 | const context = require.context('./', true, /\.spec\.ts$/);
19 | // And load the modules.
20 | context.keys().map(context);
21 |
--------------------------------------------------------------------------------
/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 |
9 | @NgModule({
10 | declarations: [GenericComponent, AttributeItemComponent, AttributeComponent, ResultComponent],
11 | imports: [
12 | CommonModule,
13 | SharedModule
14 | ]
15 | })
16 | export class GenericModule {
17 | }
18 |
--------------------------------------------------------------------------------
/angular.webpack.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Custom angular webpack configuration
3 | */
4 |
5 | module.exports = (config, options) => {
6 | config.target = 'electron-renderer';
7 |
8 |
9 | if (options.fileReplacements) {
10 | for(let fileReplacement of options.fileReplacements) {
11 | if (fileReplacement.replace !== 'src/environments/environment.ts') {
12 | continue;
13 | }
14 |
15 | let fileReplacementParts = fileReplacement['with'].split('.');
16 | if (fileReplacementParts.length > 1 && ['web'].indexOf(fileReplacementParts[1]) >= 0) {
17 | config.target = 'web';
18 | }
19 | break;
20 | }
21 | }
22 |
23 | return config;
24 | }
25 |
--------------------------------------------------------------------------------
/e2e/main.e2e.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { SpectronClient } from 'spectron';
3 |
4 | import commonSetup from './common-setup';
5 |
6 | describe('generic-service-client-electron App', function () {
7 |
8 | commonSetup.apply(this);
9 |
10 | let client: SpectronClient;
11 |
12 | beforeEach(function() {
13 | client = this.app.client;
14 | });
15 |
16 | it('creates initial windows', async function () {
17 | const count = await client.getWindowCount();
18 | expect(count).to.equal(1);
19 | });
20 |
21 | it('should display message saying App works !', async function () {
22 | const elem = await client.$('app-home h1');
23 | const text = await elem.getText();
24 | expect(text).to.equal('App works !');
25 | });
26 |
27 | });
28 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 | /app-builds
8 | /release
9 | main.js
10 | src/**/*.js
11 | !src/karma.conf.js
12 | *.js.map
13 |
14 | # dependencies
15 | /node_modules
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 |
33 | # misc
34 | /.sass-cache
35 | /connect.lock
36 | /coverage
37 | /libpeerconnection.log
38 | npm-debug.log
39 | testem.log
40 | /typings
41 |
42 | # e2e
43 | /e2e/*.js
44 | !/e2e/protractor.conf.js
45 | /e2e/*.map
46 |
47 | # System Files
48 | .DS_Store
49 | Thumbs.db
50 | error.log
51 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2020 - Maxime GRIS
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/e2e/common-setup.ts:
--------------------------------------------------------------------------------
1 | const Application = require('spectron').Application;
2 | const electronPath = require('electron'); // Require Electron from the binaries included in node_modules.
3 | const path = require('path');
4 |
5 | export default function setup(): void {
6 | beforeEach(async function () {
7 | this.app = new Application({
8 | // Your electron path can be any binary
9 | // i.e for OSX an example path could be '/Applications/MyApp.app/Contents/MacOS/MyApp'
10 | // But for the sake of the example we fetch it from our node_modules.
11 | path: electronPath,
12 |
13 | // Assuming you have the following directory structure
14 |
15 | // |__ my project
16 | // |__ ...
17 | // |__ main.js
18 | // |__ package.json
19 | // |__ index.html
20 | // |__ ...
21 | // |__ test
22 | // |__ spec.js <- You are here! ~ Well you should be.
23 |
24 | // The following line tells spectron to look and use the main.js file
25 | // and the package.json located 1 level above.
26 | args: [path.join(__dirname, '..')],
27 | webdriverOptions: {}
28 | });
29 |
30 | await this.app.start();
31 | });
32 |
33 | afterEach(async function () {
34 | if (this.app && this.app.isRunning()) {
35 | await this.app.stop();
36 | }
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/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/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/0.13/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-electron'),
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'),
20 | reports: [ 'html', 'lcovonly' ],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | browsers: ['AngularElectron'],
28 | customLaunchers: {
29 | AngularElectron: {
30 | base: 'Electron',
31 | flags: [
32 | '--remote-debugging-port=9222'
33 | ],
34 | browserWindowOptions: {
35 | webPreferences: {
36 | nodeIntegration: true,
37 | nodeIntegrationInSubFrames: true,
38 | allowRunningInsecureContent: true,
39 | enableRemoteModule: true
40 | }
41 | }
42 | }
43 | }
44 | });
45 | };
46 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import {BrowserModule} from '@angular/platform-browser';
2 | import {APP_INITIALIZER, NgModule} from '@angular/core';
3 | import {AppRoutingModule} from './app-routing.module';
4 | import {AppComponent} from './app.component';
5 | import {FormsModule} from '@angular/forms';
6 | import {HttpClientModule} from '@angular/common/http';
7 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
8 | import {NZ_I18N, zh_CN} from 'ng-zorro-antd/i18n';
9 | import {registerLocaleData} from '@angular/common';
10 | import zh from '@angular/common/locales/zh';
11 | import {SharedModule} from './module/shared/shared.module';
12 | import {IndexModule} from './module/index/index.module';
13 | import {GenericModule} from './module/generic/generic.module';
14 | import {httpInterceptorProviders} from './http';
15 | import {ThemeService} from './theme.service';
16 |
17 | registerLocaleData(zh);
18 |
19 | export const AppInitializerProvider = {
20 | provide: APP_INITIALIZER,
21 | useFactory: (themeService: ThemeService) => () => {
22 | return themeService.loadTheme();
23 | },
24 | deps: [ThemeService],
25 | multi: true,
26 | };
27 |
28 | @NgModule({
29 | declarations: [
30 | AppComponent
31 | ],
32 | imports: [
33 | GenericModule,
34 | IndexModule,
35 | SharedModule,
36 | BrowserModule,
37 | AppRoutingModule,
38 | FormsModule,
39 | HttpClientModule,
40 | BrowserAnimationsModule
41 | ],
42 | providers: [AppInitializerProvider, {provide: NZ_I18N, useValue: zh_CN}, httpInterceptorProviders],
43 | bootstrap: [AppComponent]
44 | })
45 | export class AppModule {
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/module/index/component/index/index.component.html:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/.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: ${{ matrix.os }}
16 |
17 | strategy:
18 | matrix:
19 | os: [macos-latest, ubuntu-latest, windows-latest]
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Npm Install And Build
24 | uses: actions/setup-node@v2
25 | with:
26 | node-version: '14'
27 | - name: Build/release Electron app
28 | uses: samuelmeuli/action-electron-builder@v1.6.0
29 | with:
30 | build_script_name: build:prod
31 | # GitHub token, automatically provided to the action
32 | # (No need to define this secret in the repo settings)
33 | github_token: ${{ secrets.github_token }}
34 |
35 | # If the commit is tagged with a version (e.g. "v1.0.0"),
36 | # release the app after building
37 | release: ${{ startsWith(github.ref, 'refs/tags/v') }}
38 | - uses: actions/upload-artifact@v2
39 | with:
40 | name: win
41 | path: release/*.exe
42 | if-no-files-found: ignore
43 | - uses: actions/upload-artifact@v2
44 | with:
45 | name: mac
46 | path: release/*.dmg
47 | if-no-files-found: ignore
48 | - uses: actions/upload-artifact@v2
49 | with:
50 | name: linux
51 | path: release/*.AppImage
52 | if-no-files-found: ignore
53 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/app/service/electron.service.ts:
--------------------------------------------------------------------------------
1 | import {EventEmitter, Injectable} from '@angular/core';
2 |
3 | // If you import a module but never use any of the imported values other than as TypeScript types,
4 | // the resulting javascript file will look as if you never imported the module at all.
5 | import {ipcRenderer, remote, webFrame} from 'electron';
6 | import * as childProcess from 'child_process';
7 | import * as fs from 'fs';
8 |
9 | @Injectable({
10 | providedIn: 'root'
11 | })
12 | export class ElectronService {
13 | ipcRenderer: typeof ipcRenderer;
14 | webFrame: typeof webFrame;
15 | remote: typeof remote;
16 | childProcess: typeof childProcess;
17 | fs: typeof fs;
18 | dubboResponse: EventEmitter = new EventEmitter();
19 |
20 | get isElectron(): boolean {
21 | return !!(window && window.process && window.process.type);
22 | }
23 |
24 | constructor() {
25 | // Conditional imports
26 | if (this.isElectron) {
27 | this.ipcRenderer = window.require('electron').ipcRenderer;
28 | this.webFrame = window.require('electron').webFrame;
29 |
30 | // If you wan to use remote object, pleanse set enableRemoteModule to true in main.ts
31 | this.remote = window.require('electron').remote;
32 |
33 | this.childProcess = window.require('child_process');
34 | this.fs = window.require('fs');
35 |
36 | this.ipcRenderer.on('dubbo_response', (event, token, echo, response, err) => {
37 | if (response) {
38 | this.dubboResponse.emit({
39 | type: 1,
40 | message: JSON.stringify(response, null, 2),
41 | echo: echo
42 | })
43 | } else {
44 | this.dubboResponse.emit({
45 | type: 0,
46 | message: JSON.stringify(err),
47 | echo: echo
48 | })
49 | }
50 | });
51 | }
52 | }
53 |
54 | sendEvent(type: EventType, message: any): void {
55 | switch (type) {
56 | case EventType.REQUEST:
57 | this.ipcRenderer.send('dubbo_request', message);
58 | break;
59 | }
60 | }
61 |
62 | }
63 |
64 | export enum EventType {
65 | REQUEST
66 | }
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dubbo 泛化调用客户端-Electron
2 |
3 | [](https://github.com/itning/generic-service-client-electron/stargazers)
4 | [](https://github.com/itning/generic-service-client-electron/network/members)
5 | [](https://github.com/itning/generic-service-client-electron/watchers)
6 | [](https://github.com/itning?tab=followers)
7 |
8 | [](https://github.com/itning/generic-service-client-electron/actions/workflows/main.yml)
9 | [](https://github.com/itning/generic-service-client-electron/issues)
10 | [](https://github.com/itning/generic-service-client-electron/blob/master/LICENSE)
11 | [](https://github.com/itning/generic-service-client-electron/commits)
12 | [](https://github.com/itning/generic-service-client-electron/releases)
13 | [](https://github.com/itning/generic-service-client-electron)
14 | [](http://hits.dwyl.com/itning/generic-service-client-electron)
15 | [](https://github.com/itning/generic-service-client-electron)
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 {ChangeDetectorRef, 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 | private ref: ChangeDetectorRef) {
25 | }
26 |
27 | ngOnInit(): void {
28 | this.genericService.connectionResultWebSocketReply()
29 | .subscribe(model => {
30 | const tabInfo = this.tabs.find(it => it.id === model.echo);
31 | if (tabInfo) {
32 | this.renderResultView(model, tabInfo);
33 | } else {
34 | this.tabs.forEach(it => it.resultData.push(model.message));
35 | }
36 | this.ref.markForCheck();
37 | this.ref.detectChanges();
38 | setTimeout(() => this.util.scrollToEndSmooth(this.resultBoxElementRef.nativeElement as Element), 250);
39 | }, error => {
40 | this.notification.error('网络错误', `WebSocket连接出现错误!`);
41 | console.error(error);
42 | });
43 | }
44 |
45 | private renderResultView(resultModel: WebSocketResultModel, tab: TabInfo): void {
46 | switch (resultModel.type) {
47 | case WebSocketMessageType.PLAINTEXT:
48 | resultModel.message.split('\n').forEach(item => tab.resultData.push(item));
49 | break;
50 | case WebSocketMessageType.JSON:
51 | this.lastJsonInfo.emit(resultModel.message);
52 | tab.resultData.push(`${resultModel.message}`);
53 | break;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/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.ts';
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/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 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "generic-service-client-electron",
3 | "version": "1.1.1",
4 | "description": "Generic Service Client By Angular 11 with Electron (Typescript + SASS + Hot Reload)",
5 | "homepage": "https://github.com/itning/generic-service-client-electron",
6 | "author": {
7 | "name": "itning",
8 | "email": "itning@itning.top"
9 | },
10 | "keywords": [
11 | "angular",
12 | "angular 11",
13 | "electron",
14 | "nodejs",
15 | "typescript",
16 | "spectron",
17 | "sass",
18 | "windows",
19 | "mac",
20 | "linux"
21 | ],
22 | "main": "main.js",
23 | "private": true,
24 | "scripts": {
25 | "postinstall": "electron-builder install-app-deps",
26 | "ng": "ng",
27 | "start": "npm-run-all -p electron:serve ng:serve",
28 | "build": "npm run electron:serve-tsc && ng build --base-href ./",
29 | "build:dev": "npm run build -- -c dev",
30 | "build:prod": "npm run build -- -c production",
31 | "ng:serve": "ng serve -c web",
32 | "electron:serve-tsc": "tsc -p tsconfig.serve.json",
33 | "electron:serve": "wait-on tcp:4200 && npm run electron:serve-tsc && npx electron . --serve",
34 | "electron:local": "npm run build:prod && npx electron .",
35 | "electron:build": "npm run build:prod && electron-builder build --publish=never",
36 | "test": "ng test --watch=false",
37 | "test:watch": "ng test",
38 | "e2e": "npm run build:prod && cross-env TS_NODE_PROJECT='e2e/tsconfig.e2e.json' mocha --timeout 300000 --require ts-node/register e2e/**/*.e2e.ts",
39 | "version": "conventional-changelog -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md",
40 | "lint": "ng lint"
41 | },
42 | "dependencies": {
43 | "apache-dubbo-js": "3.0.0-rc6"
44 | },
45 | "devDependencies": {
46 | "@angular-builders/custom-webpack": "11.1.1",
47 | "@angular-devkit/build-angular": "0.1102.5",
48 | "@angular/animations": "11.2.6",
49 | "@angular/cli": "11.2.5",
50 | "@angular/common": "11.2.6",
51 | "@angular/compiler": "11.2.6",
52 | "@angular/compiler-cli": "11.2.6",
53 | "@angular/core": "11.2.6",
54 | "@angular/forms": "11.2.6",
55 | "@angular/platform-browser": "11.2.6",
56 | "@angular/platform-browser-dynamic": "11.2.6",
57 | "@angular/router": "11.2.6",
58 | "@types/jasmine": "3.6.7",
59 | "@types/jasminewd2": "2.0.8",
60 | "@types/js-to-java": "2.4.1",
61 | "@types/mocha": "8.2.1",
62 | "@types/node": "14.14.35",
63 | "chai": "4.2.0",
64 | "codelyzer": "6.0.1",
65 | "conventional-changelog-cli": "2.1.1",
66 | "core-js": "3.6.5",
67 | "cross-env": "7.0.3",
68 | "dayjs": "1.10.4",
69 | "electron": "12.0.1",
70 | "electron-builder": "22.10.5",
71 | "electron-reload": "1.5.0",
72 | "jasmine-core": "3.6.0",
73 | "jasmine-spec-reporter": "6.0.0",
74 | "js-base64": "3.6.0",
75 | "jsoneditor": "9.2.0",
76 | "karma": "6.1.1",
77 | "karma-coverage-istanbul-reporter": "3.0.3",
78 | "karma-electron": "6.3.3",
79 | "karma-jasmine": "4.0.1",
80 | "karma-jasmine-html-reporter": "1.5.4",
81 | "ng-zorro-antd": "11.3.0",
82 | "npm-run-all": "4.1.5",
83 | "rxjs": "6.6.6",
84 | "spectron": "13.0.0",
85 | "ts-node": "9.1.1",
86 | "tslib": "2.1.0",
87 | "typescript": "4.0.5",
88 | "uuid": "3.4.0",
89 | "wait-on": "5.0.1",
90 | "webdriver-manager": "12.1.8",
91 | "zone.js": "0.10.3"
92 | },
93 | "engines": {
94 | "node": ">=14.0.0"
95 | },
96 | "browserslist": [
97 | "chrome 89"
98 | ]
99 | }
100 |
--------------------------------------------------------------------------------
/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/attribute-item/attribute-item.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
42 |
43 |
44 |
45 |
46 |
47 |
52 |
53 |
57 |
58 |
59 |
65 |
66 |
--------------------------------------------------------------------------------
/src/app/service/utils.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Artifact, Type} from './generic.service';
3 | import {NzMessageService} from 'ng-zorro-antd/message';
4 | import * as dayjs from 'dayjs';
5 |
6 | @Injectable({
7 | providedIn: 'root'
8 | })
9 | export class UtilsService {
10 |
11 | static readonly DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
12 |
13 | static readonly DATE_8301_FORMAT = 'YYYY-MM-DDTHH:mm:ss';
14 |
15 | constructor(private message: NzMessageService) {
16 | }
17 |
18 | /**
19 | * 获取URL的查询参数值
20 | * @param url URL
21 | * @param key KEY
22 | * @private
23 | */
24 | getParamValue(url: string, key: string): string | null {
25 | const regex = new RegExp(key + '=([^&]*)', 'i');
26 | const matchResult = url.match(regex);
27 | if (!matchResult || matchResult.length < 1) {
28 | return null;
29 | }
30 | return url.match(regex)[1];
31 | }
32 |
33 |
34 | /**
35 | * 获取对象的类型
36 | * @param o 对象
37 | */
38 | getObjectType(o: any): Type {
39 | const type = Object.prototype.toString.call(o);
40 | switch (type) {
41 | case '[object Array]':
42 | return Type.ARRAY;
43 | case '[object Object]':
44 | return Type.OBJECT;
45 | case '[object Number]':
46 | return Type.NUMBER;
47 | case '[object Boolean]':
48 | return Type.BOOLEAN;
49 | case '[object String]':
50 | return Type.STRING;
51 | default:
52 | console.error(`Not Supported Type:${type}`);
53 | }
54 | }
55 |
56 | /**
57 | * 复制到粘贴板
58 | * @param info 信息
59 | */
60 | copyToClip(info: string): void {
61 | const aux = document.createElement('input');
62 | aux.setAttribute('value', info);
63 | document.body.appendChild(aux);
64 | aux.select();
65 | document.execCommand('copy');
66 | document.body.removeChild(aux);
67 | this.message.success('复制成功');
68 | }
69 |
70 | /**
71 | * 滚动
72 | * @param el 元素
73 | * @param top 上
74 | * @param left 左
75 | */
76 | scrollToWithSmooth(el: Element, top: number, left: number): void {
77 | el.scrollTo({top, left, behavior: 'smooth'});
78 | }
79 |
80 | /**
81 | * 滚动到末尾
82 | * @param el 元素
83 | */
84 | scrollToEndSmooth(el: Element): void {
85 | const scrollHeight = el.scrollHeight;
86 | this.scrollToWithSmooth(el, scrollHeight, 0);
87 | }
88 |
89 | isMatchDateString(value: string): boolean {
90 | return /([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8])))\s([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/.test(value);
91 | }
92 |
93 | isMatchDate_8301String(value: string): boolean {
94 | return /([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8])))[\s,T]([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])/.test(value);
95 | }
96 |
97 | getNowDate2String(): string {
98 | return dayjs().format(UtilsService.DATE_FORMAT);
99 | }
100 |
101 | getNowDate2_8301String(): string {
102 | return dayjs().format(UtilsService.DATE_8301_FORMAT);
103 | }
104 |
105 | formatDate2DateString(date: Date): string {
106 | return dayjs(date).format(UtilsService.DATE_FORMAT);
107 | }
108 |
109 | formatDate2Date_8301String(date: Date): string {
110 | return dayjs(date).format(UtilsService.DATE_8301_FORMAT);
111 | }
112 |
113 | formatDateString2Date(value: string): Date {
114 | return dayjs(value, UtilsService.DATE_FORMAT).toDate();
115 | }
116 |
117 | formatDate_8301String2Date(value: string): Date {
118 | return dayjs(value, UtilsService.DATE_8301_FORMAT).toDate();
119 | }
120 |
121 | genericMavenDependencyXml(artifact: Artifact): string {
122 | return `
123 | ${artifact.groupId}
124 | ${artifact.artifactId}
125 | ${artifact.version}
126 | `;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/main.ts:
--------------------------------------------------------------------------------
1 | import {app, BrowserWindow, dialog, ipcMain, Menu, screen, WebContents} from 'electron';
2 | import * as path from 'path';
3 | import * as url from 'url';
4 | import * as fs from 'fs';
5 | import * as os from 'os';
6 |
7 | const {DirectlyDubbo, java} = require('apache-dubbo-js');
8 |
9 | class Main {
10 | private win: BrowserWindow = null;
11 | private args = process.argv.slice(1);
12 | private isServe = this.args.some(val => val === '--serve');
13 | private openDev = this.args.some(val => val === '--openDev');
14 |
15 | private createWindow(): BrowserWindow {
16 | const size = screen.getPrimaryDisplay().workAreaSize;
17 |
18 | // Create the browser window.
19 | const win = this.win = new BrowserWindow({
20 | x: size.width / 6,
21 | y: size.height / 6,
22 | width: size.width / 4 * 3,
23 | height: size.height / 4 * 3,
24 | webPreferences: {
25 | nodeIntegration: true,
26 | allowRunningInsecureContent: this.isServe,
27 | contextIsolation: false, // false if you want to run 2e2 test with Spectron
28 | enableRemoteModule: true // true if you want to run 2e2 test with Spectron or use remote module in renderer context (ie. Angular)
29 | },
30 | });
31 |
32 | this.handleDubboRequest(win.webContents);
33 |
34 | if (this.isServe) {
35 | win.webContents.openDevTools();
36 | require('electron-reload')(__dirname, {
37 | electron: require(`${__dirname}/node_modules/electron`)
38 | });
39 | win.loadURL('http://localhost:4200').catch(err => {
40 | dialog.showErrorBox('加载失败!', err);
41 | return err;
42 | });
43 | } else {
44 | if (this.openDev) {
45 | win.webContents.openDevTools();
46 | }
47 | win.loadURL(url.format({
48 | pathname: path.join(__dirname, 'dist/index.html'),
49 | protocol: 'file:',
50 | slashes: true
51 | })).catch(err => {
52 | dialog.showErrorBox('加载失败!', err);
53 | return err;
54 | });
55 | }
56 |
57 | win.on('close', () => win.destroy());
58 |
59 | win.on('closed', () => this.win = null);
60 | return win;
61 | }
62 |
63 | private handleDubboRequest(webContents: WebContents) {
64 | ipcMain.on('dubbo_request', (ipc, requestInfo) => {
65 |
66 | const application = DirectlyDubbo.from({
67 | dubboAddress: requestInfo.url,
68 | dubboInvokeTimeout: requestInfo.timeout ? requestInfo.timeout : 5000,
69 | dubboVersion: '2.7.8',
70 | });
71 |
72 | const actuator = application.proxyService({
73 | dubboInterface: requestInfo.interfaceName,
74 | methods: {
75 | [requestInfo.method]: function (...ids) {
76 | return ids;
77 | }
78 | },
79 | version: requestInfo.version,
80 | timeout: requestInfo.timeout ? requestInfo.timeout : 5000,
81 | group: requestInfo.group
82 | });
83 |
84 | const allParam = [];
85 | if (requestInfo.params) {
86 | try {
87 | for (let item of requestInfo.params) {
88 | const keys = Object.keys(item);
89 | if (keys.length < 1) {
90 | continue;
91 | }
92 | const itemParamClassName = keys[0];
93 | allParam.push(java(itemParamClassName, item[itemParamClassName]))
94 | }
95 | } catch (e) {
96 | webContents.send('dubbo_response', null, e);
97 | return;
98 | }
99 | }
100 |
101 | let flag = false;
102 | const result: Promise = actuator[requestInfo.method](...allParam);
103 |
104 | const timeout = setTimeout(() => {
105 | webContents.send('dubbo_response', requestInfo.token, requestInfo.echo, null, 'timeout');
106 | }, requestInfo.timeout ? requestInfo.timeout : 5000);
107 |
108 | result.then(response => {
109 | flag = true;
110 | if (response.res) {
111 | webContents.send('dubbo_response', requestInfo.token, requestInfo.echo, response.res, null)
112 | } else {
113 | webContents.send('dubbo_response', requestInfo.token, requestInfo.echo, null, response.err)
114 | }
115 | })
116 | .catch(err => {
117 | flag = true;
118 | webContents.send('dubbo_response', requestInfo.token, requestInfo.echo, null, err);
119 | })
120 | .finally(() => {
121 | clearTimeout(timeout);
122 | });
123 | })
124 | }
125 |
126 | run(): void {
127 | try {
128 | // This method will be called when Electron has finished
129 | // initialization and is ready to create browser windows.
130 | // Some APIs can only be used after this event occurs.
131 | // Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947
132 | app.on('ready', () => {
133 | setTimeout(() => this.createWindow(), 400);
134 | Menu.setApplicationMenu(null);
135 | });
136 |
137 | // Quit when all windows are closed.
138 | app.on('window-all-closed', () => {
139 | // On OS X it is common for applications and their menu bar
140 | // to stay active until the user quits explicitly with Cmd + Q
141 | if (process.platform !== 'darwin') {
142 | app.quit();
143 | }
144 | });
145 |
146 | app.on('activate', () => {
147 | // On OS X it's common to re-create a window in the app when the
148 | // dock icon is clicked and there are no other windows open.
149 | if (this.win === null) {
150 | this.createWindow();
151 | }
152 | });
153 |
154 | } catch (e) {
155 | console.error(e);
156 | // https://segmentfault.com/a/1190000018878931
157 | fs.appendFileSync(path.join(process.cwd(), 'error.log'), e.toString() + e.stack + os.EOL);
158 | }
159 | }
160 | }
161 |
162 | new Main().run();
163 |
--------------------------------------------------------------------------------
/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-electron": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "architect": {
11 | "build": {
12 | "builder": "@angular-builders/custom-webpack:browser",
13 | "options": {
14 | "outputPath": "dist",
15 | "index": "src/index.html",
16 | "main": "src/main.ts",
17 | "tsConfig": "src/tsconfig.app.json",
18 | "polyfills": "src/polyfills.ts",
19 | "assets": [
20 | "src/assets"
21 | ],
22 | "styles": [
23 | "src/styles.scss",
24 | "node_modules/jsoneditor/dist/jsoneditor.min.css",
25 | {
26 | "input": "src/styles/default.less",
27 | "bundleName": "default",
28 | "inject": false
29 | },
30 | {
31 | "input": "src/styles/dark.less",
32 | "bundleName": "dark",
33 | "inject": false
34 | }
35 | ],
36 | "stylePreprocessorOptions": {
37 | "includePaths": [
38 | "src/styles/themes"
39 | ]
40 | },
41 | "scripts": [],
42 | "customWebpackConfig": {
43 | "path": "./angular.webpack.js"
44 | }
45 | },
46 | "configurations": {
47 | "dev": {
48 | "optimization": false,
49 | "outputHashing": "all",
50 | "sourceMap": true,
51 | "namedChunks": false,
52 | "aot": false,
53 | "extractLicenses": true,
54 | "vendorChunk": false,
55 | "buildOptimizer": false,
56 | "fileReplacements": [
57 | {
58 | "replace": "src/environments/environment.ts",
59 | "with": "src/environments/environment.dev.ts"
60 | }
61 | ]
62 | },
63 | "web": {
64 | "optimization": false,
65 | "outputHashing": "all",
66 | "sourceMap": true,
67 | "namedChunks": false,
68 | "aot": false,
69 | "extractLicenses": true,
70 | "vendorChunk": false,
71 | "buildOptimizer": false,
72 | "fileReplacements": [
73 | {
74 | "replace": "src/environments/environment.ts",
75 | "with": "src/environments/environment.web.ts"
76 | }
77 | ]
78 | },
79 | "production": {
80 | "optimization": true,
81 | "outputHashing": "all",
82 | "sourceMap": false,
83 | "namedChunks": false,
84 | "aot": true,
85 | "extractLicenses": true,
86 | "vendorChunk": false,
87 | "buildOptimizer": true,
88 | "fileReplacements": [
89 | {
90 | "replace": "src/environments/environment.ts",
91 | "with": "src/environments/environment.prod.ts"
92 | }
93 | ]
94 | }
95 | }
96 | },
97 | "serve": {
98 | "builder": "@angular-builders/custom-webpack:dev-server",
99 | "options": {
100 | "browserTarget": "generic-service-client-electron:build"
101 | },
102 | "configurations": {
103 | "dev": {
104 | "browserTarget": "generic-service-client-electron:build:dev"
105 | },
106 | "web": {
107 | "browserTarget": "generic-service-client-electron:build:web"
108 | },
109 | "production": {
110 | "browserTarget": "generic-service-client-electron:build:production"
111 | }
112 | }
113 | },
114 | "extract-i18n": {
115 | "builder": "@angular-devkit/build-angular:extract-i18n",
116 | "options": {
117 | "browserTarget": "generic-service-client-electron:build"
118 | }
119 | },
120 | "test": {
121 | "builder": "@angular-builders/custom-webpack:karma",
122 | "options": {
123 | "main": "src/test.ts",
124 | "polyfills": "src/polyfills-test.ts",
125 | "tsConfig": "src/tsconfig.spec.json",
126 | "karmaConfig": "src/karma.conf.js",
127 | "scripts": [],
128 | "styles": [
129 | "src/styles.scss"
130 | ],
131 | "assets": [
132 | "src/assets"
133 | ],
134 | "customWebpackConfig": {
135 | "path": "./angular.webpack.js"
136 | }
137 | }
138 | },
139 | "lint": {
140 | "builder": "@angular-eslint/builder:lint",
141 | "options": {
142 | "eslintConfig": ".eslintrc.json",
143 | "lintFilePatterns": [
144 | "src/**.ts",
145 | "main.ts"
146 | ]
147 | }
148 | }
149 | }
150 | },
151 | "generic-service-client-electron-e2e": {
152 | "root": "e2e",
153 | "projectType": "application",
154 | "architect": {
155 | "lint": {
156 | "builder": "@angular-eslint/builder:lint",
157 | "options": {
158 | "eslintConfig": ".eslintrc.json",
159 | "lintFilePatterns": [
160 | "e2e/**.ts"
161 | ]
162 | }
163 | }
164 | }
165 | }
166 | },
167 | "defaultProject": "generic-service-client-electron",
168 | "schematics": {
169 | "@schematics/angular:component": {
170 | "prefix": "app",
171 | "style": "scss"
172 | },
173 | "@schematics/angular:directive": {
174 | "prefix": "app"
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/attribute/attribute.component.html:
--------------------------------------------------------------------------------
1 |
2 |
5 | 1" [nzTitle]="tab.tabName"
6 | (nzClick)="handleTabClick(i)">
7 |
64 |
69 |
70 |
71 |
72 |
75 |
76 |
77 |
78 |
81 |
82 |
83 |
84 |
85 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/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/service/generic.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {RequestModel} from '../module/generic/component/attribute/attribute.component';
3 | import {HttpClient} 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 {AppConfig} 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 | import {ElectronService, EventType} from "./electron.service";
12 |
13 | @Injectable({
14 | providedIn: 'root'
15 | })
16 | export class GenericService {
17 |
18 | private token: string;
19 | private resultSubject: Subject;
20 | private textDecoder: TextDecoder;
21 |
22 | constructor(private http: HttpClient,
23 | private message: NzMessageService,
24 | private fb: FormBuilder,
25 | private electronService: ElectronService) {
26 | this.textDecoder = new TextDecoder();
27 | }
28 |
29 | sendGenericRequest(requestModel: RequestModel, echo: string): boolean {
30 | this.getWebSocketToken().subscribe(token => {
31 | // tslint:disable-next-line
32 | requestModel['token'] = token;
33 | // tslint:disable-next-line
34 | requestModel['echo'] = echo;
35 | this.electronService.sendEvent(EventType.REQUEST, requestModel);
36 | });
37 | return true;
38 | }
39 |
40 | sendMavenRequest(mavenRequest: MavenRequest): Observable> {
41 | return this.getWebSocketToken().pipe(mergeMap(token => {
42 | mavenRequest.token = token;
43 | return this.http.post>(`http://${AppConfig.baseUrl}/nexus/dependency/download`, mavenRequest);
44 | }));
45 | }
46 |
47 | getWebSocketToken(): Observable {
48 | if (!this.token) {
49 | return this.http.get(`http://${AppConfig.baseUrl}/socket_token`, {responseType: 'text'}).pipe(
50 | map(i => {
51 | if (this.token) {
52 | return this.token;
53 | }
54 | this.token = i;
55 | return i;
56 | })
57 | );
58 | } else {
59 | return new Observable(subscriber => {
60 | subscriber.next(this.token);
61 | subscriber.complete();
62 | });
63 | }
64 | }
65 |
66 | generateFormParams(url = '',
67 | interfaceName = '',
68 | method = '',
69 | version = '',
70 | group = '',
71 | path = ''): FormGroup {
72 | return this.fb.group({
73 | url: [url, [Validators.required]],
74 | interfaceName: [interfaceName, [Validators.required]],
75 | method: [method, [Validators.required]],
76 | version: [version, []],
77 | group: [group, []],
78 | path: [path]
79 | });
80 | }
81 |
82 | private getRealValue(item: Item): string | number | boolean | null {
83 | switch (item.type) {
84 | case Type.STRING:
85 | case Type.DATE:
86 | case Type.DATE_8601:
87 | return item.attributeValue as string;
88 | case Type.NUMBER:
89 | const value = Number(item.attributeValue);
90 | if (Number.isNaN(value)) {
91 | this.message.error(`${item.attributeValue}非数字!`);
92 | throw new Error(`${item.attributeValue}非数字!`);
93 | }
94 | return value;
95 | case Type.BOOLEAN:
96 | return item.attributeValue === 'true';
97 | default:
98 | console.error(`非法调用 Type:${item.type}`);
99 | return null;
100 | }
101 | }
102 |
103 | connectionResultWebSocketReply(): Subject {
104 | if (this.resultSubject) {
105 | return this.resultSubject;
106 | }
107 | this.resultSubject = new Subject();
108 | //this.getWebSocketToken().pipe(mergeMap(token => this.connectionResultWebSocket(token))).subscribe(this.resultSubject);
109 | this.electronService.dubboResponse.subscribe(this.resultSubject)
110 | return this.resultSubject;
111 | }
112 |
113 | /**
114 | * 转换请求
115 | * @param items 参数列表
116 | * @private
117 | */
118 | conversionRequest(items: Item[]): any {
119 | const r = [];
120 | items.forEach(item => {
121 | const re = {};
122 | if (item.type === Type.OBJECT) {
123 | re[item.attributeName] = this.conversionRequestForItem(item.attributeValue as Item[]);
124 | } else if (item.type === Type.ARRAY) {
125 | re[item.attributeName] = this.conversionRequestForItemArray(item.attributeValue as Item[]);
126 | } else {
127 | re[item.attributeName] = this.getRealValue(item);
128 | }
129 | r.push(re);
130 | });
131 | return r;
132 | }
133 |
134 | conversionRequestForItem(items: Item[]): any {
135 | const result = {};
136 | items.map(item => {
137 | if (item.type === Type.OBJECT) {
138 | result[item.attributeName] = this.conversionRequestForItem(item.attributeValue as Item[]);
139 | } else if (item.type === Type.ARRAY) {
140 | result[item.attributeName] = (item.attributeValue as Item[]).map(it => {
141 | if (it.type === Type.OBJECT) {
142 | return this.conversionRequestForItem(it.attributeValue as Item[]);
143 | } else if (it.type === Type.ARRAY) {
144 | return this.conversionRequestForItemArray(it.attributeValue as Item[]);
145 | } else {
146 | return this.getRealValue(it);
147 | }
148 | });
149 | } else {
150 | result[item.attributeName] = this.getRealValue(item);
151 | }
152 | });
153 | return result;
154 | }
155 |
156 | conversionRequestForItemArray(items: Item[]): any {
157 | return items.map(it => {
158 | if (it.type === Type.OBJECT) {
159 | return this.conversionRequestForItem(it.attributeValue as Item[]);
160 | } else if (it.type === Type.ARRAY) {
161 | return this.conversionRequestForItemArray(it.attributeValue as Item[]);
162 | } else {
163 | return this.getRealValue(it);
164 | }
165 | });
166 | }
167 | }
168 |
169 | class WebSocketResultWrap {
170 | localMessage: boolean;
171 | message: string;
172 | data: ArrayBuffer;
173 |
174 | static local(message: string): WebSocketResultWrap {
175 | const w = new WebSocketResultWrap();
176 | w.localMessage = true;
177 | w.message = message;
178 | return w;
179 | }
180 |
181 | static wrap(data: ArrayBuffer): WebSocketResultWrap {
182 | const w = new WebSocketResultWrap();
183 | w.localMessage = false;
184 | w.data = data;
185 | return w;
186 | }
187 | }
188 |
189 | export class Artifact {
190 | groupId: string;
191 | artifactId: string;
192 | version: string;
193 | }
194 |
195 | export class MavenRequest {
196 | token: string;
197 | echo: string;
198 | dependency: string;
199 | interfaceName: string;
200 | methodName: string;
201 | }
202 |
203 | export class MavenResponse {
204 | success: boolean;
205 | message: string;
206 | data: T;
207 | }
208 |
209 | export class WebSocketResultModel {
210 | type: WebSocketMessageType;
211 | message: string;
212 | echo: string;
213 |
214 | constructor(type: WebSocketMessageType, echo: string, message: string) {
215 | this.type = type;
216 | this.message = message;
217 | this.echo = echo;
218 | }
219 | }
220 |
221 | export enum WebSocketMessageType {
222 | PLAINTEXT,
223 | JSON
224 | }
225 |
226 | export type AttributeNameType = string | undefined;
227 | export type AttributeValueType =
228 | Item
229 | | number
230 | | string
231 | | boolean
232 | | number[]
233 | | string[]
234 | | Item[]
235 | | boolean[]
236 | | undefined;
237 |
238 | /**
239 | * 参数每一项
240 | */
241 | export class Item {
242 | id: string;
243 | attributeName: AttributeNameType;
244 | attributeValue: AttributeValueType;
245 | type: Type;
246 | placeholder: string;
247 | autoComplete: AutocompleteDataSource = [];
248 | show: boolean;
249 | use: boolean;
250 | attributeValueDate: Date;
251 |
252 | static generate(type: Type,
253 | attributeName: AttributeNameType,
254 | attributeValue: AttributeValueType,
255 | placeholder?: string,
256 | autoComplete?: AutocompleteDataSource,
257 | attributeValueDate?: Date): Item {
258 | const item = new Item();
259 | item.id = uuidv4();
260 | item.type = type;
261 | item.attributeName = attributeName;
262 | item.attributeValue = attributeValue;
263 | item.placeholder = placeholder;
264 | item.autoComplete = autoComplete;
265 | item.show = true;
266 | item.use = true;
267 | item.attributeValueDate = attributeValueDate;
268 | return item;
269 | }
270 |
271 | static generateObject(attributeName: AttributeNameType, attributeValue: AttributeValueType): Item {
272 | return Item.generate(Type.OBJECT, attributeName, attributeValue);
273 | }
274 |
275 | static generateArray(attributeName: AttributeNameType, attributeValue: AttributeValueType): Item {
276 | return Item.generate(Type.ARRAY, attributeName, attributeValue);
277 | }
278 |
279 | static generateString(attributeName: AttributeNameType,
280 | attributeValue: AttributeValueType,
281 | placeholder = '',
282 | autoComplete: AutocompleteDataSource = []): Item {
283 | return Item.generate(Type.STRING, attributeName, attributeValue, placeholder, autoComplete);
284 | }
285 |
286 | static generateNumber(attributeName: AttributeNameType,
287 | attributeValue: AttributeValueType,
288 | placeholder = '',
289 | autoComplete: AutocompleteDataSource = []): Item {
290 | return Item.generate(Type.NUMBER, attributeName, attributeValue, placeholder);
291 | }
292 |
293 | static generateBoolean(attributeName: AttributeNameType,
294 | attributeValue: AttributeValueType,
295 | placeholder = '',
296 | autoComplete: AutocompleteDataSource = []): Item {
297 | return Item.generate(Type.BOOLEAN, attributeName, attributeValue, placeholder);
298 | }
299 |
300 | static generateDate(attributeName: AttributeNameType,
301 | attributeValue: AttributeValueType,
302 | placeholder = '',
303 | autoComplete: AutocompleteDataSource = [],
304 | attributeValueDate: Date): Item {
305 | return Item.generate(Type.DATE, attributeName, attributeValue, placeholder, autoComplete, attributeValueDate);
306 | }
307 |
308 | static generateDATE_8601(attributeName: AttributeNameType,
309 | attributeValue: AttributeValueType,
310 | placeholder = '',
311 | autoComplete: AutocompleteDataSource = [],
312 | attributeValueDate: Date): Item {
313 | return Item.generate(Type.DATE_8601, attributeName, attributeValue, placeholder, autoComplete, attributeValueDate);
314 | }
315 | }
316 |
317 | export enum Type {
318 | STRING,
319 | NUMBER,
320 | BOOLEAN,
321 | ARRAY,
322 | OBJECT,
323 | DATE,
324 | DATE_8601
325 | }
326 |
327 | /**
328 | * TAB页信息
329 | */
330 | export class TabInfo {
331 | id: string;
332 | tabName: string;
333 | formParams: FormGroup;
334 | parameterValue: Item[] = [];
335 | resultData: string[] = [];
336 | selectEnv: EnvInfo;
337 | availableInterface: string[];
338 | availableMethod: string[];
339 | isRequestLoading: boolean;
340 |
341 | constructor(id: string, tabName: string, formParams: FormGroup, parameterValue: Item[], resultData: string[], selectEnv?: EnvInfo) {
342 | this.id = id;
343 | this.tabName = tabName;
344 | this.formParams = formParams;
345 | this.parameterValue = parameterValue;
346 | this.resultData = resultData;
347 | this.selectEnv = selectEnv;
348 | }
349 | }
350 |
351 | /**
352 | * 环境信息
353 | */
354 | export class EnvInfo {
355 | tag: string;
356 | env: string;
357 |
358 | constructor(info: string) {
359 | const splitIndex = info.indexOf('||');
360 | this.tag = info.substring(0, splitIndex);
361 | this.env = info.substring(splitIndex + 2);
362 | }
363 | }
364 |
365 | export class MethodInfo {
366 | signature: string;
367 | paramClassName: string[];
368 | property: { [key: string]: any }[];
369 | }
370 |
--------------------------------------------------------------------------------
/src/app/module/generic/component/attribute/attribute.component.ts:
--------------------------------------------------------------------------------
1 | import {ChangeDetectorRef, 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 | GenericService,
9 | Item,
10 | MethodInfo,
11 | TabInfo,
12 | Type,
13 | WebSocketMessageType
14 | } from '../../../../service/generic.service';
15 | import JSONEditor from 'jsoneditor';
16 | import {NzMessageService} from 'ng-zorro-antd/message';
17 | import {UtilsService} from '../../../../service/utils.service';
18 | import {AutocompleteDataSource} from 'ng-zorro-antd/auto-complete/autocomplete.component';
19 |
20 | @Component({
21 | selector: 'app-attribute',
22 | templateUrl: './attribute.component.html',
23 | styleUrls: ['./attribute.component.scss']
24 | })
25 | export class AttributeComponent implements OnInit {
26 |
27 | /**
28 | * 每个TAB页的实例
29 | */
30 | @Input()
31 | tabs: TabInfo[] = [];
32 |
33 | /**
34 | * 当前选择的TAB页
35 | */
36 | @Input()
37 | selectedIndex = 0;
38 |
39 | /**
40 | * 发送请求
41 | */
42 | @Output()
43 | request: EventEmitter = new EventEmitter();
44 |
45 | /**
46 | * TAB选择改变事件
47 | */
48 | @Output()
49 | tabSelectChange: EventEmitter = new EventEmitter();
50 |
51 | // noinspection JSUnusedGlobalSymbols
52 | private options = {
53 | mode: 'code',
54 | modes: ['code', 'form', 'text', 'tree', 'view', 'preview'], // allowed modes
55 | onError: (err) => {
56 | alert(err.toString());
57 | },
58 | onModeChange: (newMode, oldMode) => {
59 | console.log('Mode switched from', oldMode, 'to', newMode);
60 | }
61 | };
62 |
63 | /**
64 | * JSONEditor实例
65 | */
66 | editor: any;
67 |
68 | @ViewChild('jsonEditor') set content(content: ElementRef) {
69 | if (content && this.needEditParamArray) {
70 | const editor = new JSONEditor(content.nativeElement, this.options);
71 | const conversionRequest2 = this.genericService.conversionRequest(this.needEditParamArray);
72 | editor.set(conversionRequest2);
73 | this.editor = editor;
74 | }
75 | }
76 |
77 | /**
78 | * 需要修改的参数
79 | */
80 | needEditParamArray: Item[];
81 |
82 | /**
83 | * 模态框显示
84 | */
85 | modalShow = {
86 | editInterfaceParam: false,
87 | resolveUrl: false,
88 | resolveUrlSelect: false,
89 | methodOverloading: false,
90 | tabReName: false,
91 | maven: false,
92 | download: false,
93 | mavenVersion: false
94 | };
95 |
96 | /**
97 | * 自动完成
98 | */
99 | autocomplete = {
100 | interfaces: {
101 | availableFilter: []
102 | },
103 | methods: {
104 | availableFilter: []
105 | }
106 | };
107 |
108 | /**
109 | * 用户填的解析URL值
110 | */
111 | resolveURLValue: string;
112 |
113 | /**
114 | * 多个提供者信息
115 | */
116 | providerInfoArray: ProviderInfo[] = [];
117 |
118 | /**
119 | * 方法重载信息数组
120 | */
121 | methodInfoArray: MethodInfo[];
122 |
123 | /**
124 | * 选择的方法重载信息
125 | */
126 | resolveMethodInfo: MethodInfo;
127 |
128 | /**
129 | * 原来的TAB页信息,用于修改TAB名展示
130 | */
131 | oldTab: TabInfo;
132 |
133 | /**
134 | * TAB的新名称
135 | */
136 | tabNameForReName: string;
137 |
138 | /**
139 | * 下载进度信息
140 | */
141 | downloadProgress: number;
142 |
143 | /**
144 | * 取消下载时的TOKEN信息
145 | */
146 | cancelToken: string;
147 |
148 | /**
149 | * 可选的maven版本信息
150 | */
151 | mavenArtifacts: Artifact[];
152 |
153 | /**
154 | * 选择的maven版本信息
155 | */
156 | resolveMavenArtifact: Artifact;
157 |
158 | constructor(private modal: NzModalService,
159 | private persistenceService: PersistenceService,
160 | private genericService: GenericService,
161 | private message: NzMessageService,
162 | private util: UtilsService,
163 | private ref: ChangeDetectorRef) {
164 | }
165 |
166 | ngOnInit(): void {
167 | this.genericService.connectionResultWebSocketReply().subscribe(model => {
168 | const tabInfo = this.tabs.find(it => it.id === model.echo);
169 | if (tabInfo) {
170 | if (model.type === WebSocketMessageType.PLAINTEXT || model.type === WebSocketMessageType.JSON) {
171 | tabInfo.isRequestLoading = false;
172 | this.ref.markForCheck();
173 | this.ref.detectChanges();
174 | }
175 | }
176 | });
177 | }
178 |
179 | /**
180 | * 递归删除某一个参数
181 | * @param items 接口参数信息
182 | * @param id 要删除的参数ID
183 | * @private
184 | */
185 | private delArrayItemById(items: Item[], id: string): void {
186 |
187 | for (let i = 0; i < items.length; i++) {
188 | if (items[i].id === id) {
189 | items.splice(i, 1);
190 | return;
191 | }
192 | if (items[i].attributeValue instanceof Array) {
193 | this.delArrayItemById(items[i].attributeValue as Item[], id);
194 | }
195 | }
196 | }
197 |
198 | /**
199 | * 解析参数
200 | * @param methodInfo 方法信息
201 | * @private
202 | */
203 | private parsingParameters(methodInfo: MethodInfo): void {
204 | const result = [];
205 | for (let i = 0; i < methodInfo.paramClassName.length; i++) {
206 | const re = {};
207 | const prop = methodInfo.property[i];
208 | re[methodInfo.paramClassName[i]] = prop[Object.keys(prop)[0]];
209 | result.push(re);
210 | }
211 | this.tabs[this.selectedIndex].parameterValue = this.parseTheModifiedParameters(result, true);
212 | }
213 |
214 | /**
215 | * 关闭TAB页
216 | * @param index 索引
217 | */
218 | closeTab({index}: { index: number }): void {
219 | this.modal.confirm({
220 | nzTitle: '确定关闭吗?',
221 | nzOnOk: () => {
222 | this.tabs.splice(index, 1);
223 | this.persistenceService.saveGenericParamInfo(this.tabs);
224 | this.persistenceService.saveMetaInfo('nowSelectedTabIndex', this.selectedIndex);
225 | }
226 | });
227 | }
228 |
229 | /**
230 | * 新打开TAB页
231 | */
232 | newTab(): void {
233 | this.tabs.push(new TabInfo(
234 | uuidv4(), 'Unnamed Tab',
235 | this.genericService.generateFormParams(), [], []));
236 | this.selectedIndex = this.tabs.length;
237 | this.persistenceService.saveGenericParamInfo(this.tabs);
238 | }
239 |
240 | /**
241 | * 添加接口参数
242 | * @param parameterValue 当前页的接口参数信息
243 | */
244 | addInterfaceParam(parameterValue: Item[]): void {
245 | parameterValue.push(Item.generateString('', ''));
246 | }
247 |
248 | /**
249 | * 处理参数删除事件
250 | * @param id 要删除的参数ID
251 | * @param parameterValue 当前页的接口参数信息
252 | */
253 | handleDeleteEvent(id: string, parameterValue: Item[]): void {
254 | this.delArrayItemById(parameterValue, id);
255 | }
256 |
257 | /**
258 | * 处理新增参数事件
259 | * @param data 某一项参数信息
260 | */
261 | handleAddEvent(data: AttributeValueType): void {
262 | (data as Item[]).push(Item.generateString('', ''));
263 | }
264 |
265 | /**
266 | * 发送请求;持久化参数信息
267 | * @param tab 哪个TAB页调用的
268 | */
269 | sendRequest(tab: TabInfo): void {
270 | for (const i in tab.formParams.controls) {
271 | if (tab.formParams.controls.hasOwnProperty(i)) {
272 | tab.formParams.controls[i].markAsDirty();
273 | tab.formParams.controls[i].updateValueAndValidity();
274 | }
275 | }
276 | if (tab.formParams.valid) {
277 | this.request.emit(tab);
278 | }
279 | }
280 |
281 | /**
282 | * 处理TAB切换
283 | */
284 | handleTabChange(): void {
285 | const tabInfo = this.tabs[this.selectedIndex];
286 | this.autocomplete.methods.availableFilter = tabInfo.availableMethod;
287 | this.autocomplete.interfaces.availableFilter = tabInfo.availableInterface;
288 | this.tabSelectChange.emit(this.selectedIndex);
289 | }
290 |
291 | /**
292 | * 修改参数
293 | * @param parameterValue 参数
294 | */
295 | editInterfaceParam(parameterValue: Item[]): void {
296 | this.needEditParamArray = parameterValue;
297 | this.modalShow.editInterfaceParam = true;
298 | }
299 |
300 | /**
301 | * 修改参数确认
302 | */
303 | doEditInterfaceParam(): void {
304 | if (this.editor) {
305 | try {
306 | const editorItems = this.editor.get();
307 | if (!this.checkEditDataOrderly(editorItems)) {
308 | return;
309 | }
310 | this.tabs[this.selectedIndex].parameterValue = this.parseTheModifiedParameters(editorItems);
311 | } catch (e) {
312 | console.warn(e);
313 | this.message.error('解析失败!');
314 | }
315 | this.needEditParamArray = null;
316 | this.modalShow.editInterfaceParam = false;
317 | }
318 | }
319 |
320 | /**
321 | * 检查修改的数据是否正确
322 | * @param data 数据
323 | * @private
324 | */
325 | private checkEditDataOrderly(data: any): boolean {
326 | if (Type.ARRAY !== this.util.getObjectType(data)) {
327 | this.message.error('最外层必须是个数组!');
328 | return false;
329 | }
330 | for (const item of data) {
331 | if (Type.OBJECT !== this.util.getObjectType(item)) {
332 | this.message.error('最外层数组中每一项必须是对象!');
333 | return false;
334 | }
335 | if (Object.keys(item).length > 1) {
336 | this.message.error('最外层数组中每一项的对象只能有一个KEY!');
337 | return false;
338 | }
339 | }
340 | return true;
341 | }
342 |
343 | /**
344 | * 解析修改的参数
345 | * @param editorItems 修改后的参数
346 | * @param isUpload 上传调用
347 | * @private
348 | */
349 | private parseTheModifiedParameters(editorItems: any, isUpload = false): Item[] {
350 | const result: Item[] = [];
351 | for (const item of editorItems) {
352 | const key = Object.keys(item)[0];
353 | const value = item[key];
354 | result.push(this.generateItem(key, value, isUpload));
355 | }
356 | return result;
357 | }
358 |
359 | /**
360 | * 根据值类型生成Item
361 | * @param name Item名字
362 | * @param value Item值
363 | * @param needTransform 需要将文本转换到对应的类型
364 | * @private
365 | */
366 | private generateItem(name: string, value: any, needTransform = false): Item {
367 | let valueType = this.util.getObjectType(value);
368 | if (valueType === Type.ARRAY) {
369 | return Item.generateArray(name, this.generateItemForArray(value, needTransform));
370 | } else if (valueType === Type.OBJECT) {
371 | return Item.generateObject(name, this.generateItemForObject(value, needTransform));
372 | } else {
373 | const autoComplete: AutocompleteDataSource = [];
374 | let originalValue = '';
375 | if (needTransform) {
376 | originalValue = `参数类型:${value}`;
377 | if (value.startsWith('enum|')) {
378 | const start = value.indexOf('|');
379 | const temp = value.substring(start + 1);
380 | const classNameSplit = temp.indexOf('|');
381 | const className = temp.substring(0, classNameSplit);
382 | const json = temp.substring(classNameSplit + 1);
383 | const enums = JSON.parse(json);
384 | autoComplete.push(...enums);
385 | originalValue = `参数类型:${className}`;
386 | }
387 | switch (value) {
388 | case 'java.lang.Integer':
389 | case 'java.lang.Long':
390 | case 'java.lang.Short':
391 | case 'byte':
392 | case 'short':
393 | case 'int':
394 | case 'long' :
395 | valueType = Type.NUMBER;
396 | value = '';
397 | break;
398 | case 'java.lang.Double':
399 | case 'java.lang.Float':
400 | case 'float':
401 | case 'double':
402 | valueType = Type.NUMBER;
403 | value = '';
404 | break;
405 | case 'java.lang.Character':
406 | case 'java.lang.String':
407 | valueType = Type.STRING;
408 | value = '';
409 | break;
410 | case 'java.lang.Boolean':
411 | case 'boolean':
412 | valueType = Type.BOOLEAN;
413 | value = '';
414 | autoComplete.push('true', 'false');
415 | break;
416 | case 'java.util.Date':
417 | case 'java.sql.Date':
418 | case 'java.sql.Timestamp':
419 | case 'java.sql.Time':
420 | valueType = Type.DATE;
421 | value = this.util.getNowDate2String();
422 | originalValue = '日期格式:yyyy-MM-dd HH:mm:ss';
423 | break;
424 | case 'java.time.LocalDate':
425 | case 'java.time.LocalTime':
426 | case 'java.time.LocalDateTime':
427 | valueType = Type.DATE_8601;
428 | value = this.util.getNowDate2_8301String();
429 | originalValue = '日期格式:yyyy-MM-ddTHH:mm:ss';
430 | break;
431 | default:
432 | valueType = Type.STRING;
433 | value = '';
434 | }
435 | }
436 | switch (valueType) {
437 | case Type.NUMBER:
438 | return Item.generateNumber(name, value, originalValue, autoComplete);
439 | case Type.BOOLEAN:
440 | return Item.generateBoolean(name, value, originalValue, autoComplete);
441 | case Type.DATE:
442 | return Item.generateDate(name, value, originalValue, autoComplete, new Date());
443 | case Type.DATE_8601:
444 | return Item.generateDATE_8601(name, value, originalValue, autoComplete, new Date());
445 | default:
446 | if (this.util.isMatchDateString(value)) {
447 | const attributeValueDate = this.util.formatDateString2Date(value);
448 | return Item.generateDate(name, value, '日期格式:yyyy-MM-dd HH:mm:ss', autoComplete, attributeValueDate);
449 | } else if (this.util.isMatchDate_8301String(value)) {
450 | const attributeValueDate = this.util.formatDate_8301String2Date(value);
451 | return Item.generateDATE_8601(name, value, '日期格式:yyyy-MM-ddTHH:mm:ss', autoComplete, attributeValueDate);
452 | } else {
453 | return Item.generateString(name, value, originalValue, autoComplete);
454 | }
455 | }
456 | }
457 | }
458 |
459 | /**
460 | * 生成对象类型
461 | * @param items 每一项
462 | * @param needTransform 需要将文本转换到对应的类型
463 | */
464 | generateItemForObject(items: any, needTransform = false): Item[] {
465 | const result: Item[] = [];
466 | for (const key in items) {
467 | if (items.hasOwnProperty(key)) {
468 | result.push(this.generateItem(key, items[key], needTransform));
469 | }
470 | }
471 | return result;
472 | }
473 |
474 | /**
475 | * 生成数组类型
476 | * @param items 每一项
477 | * @param needTransform 需要将文本转换到对应的类型
478 | */
479 | generateItemForArray(items: any, needTransform = false): Item[] {
480 | const result: Item[] = [];
481 | for (const item of items) {
482 | result.push(this.generateItem('', item, needTransform));
483 | }
484 | return result;
485 | }
486 |
487 | /**
488 | * 清空
489 | */
490 | clearAll(): void {
491 | const tabInfo = this.tabs[this.selectedIndex];
492 | if (tabInfo) {
493 | tabInfo.parameterValue = [];
494 | tabInfo.formParams.reset({url: '', interfaceName: '', method: '', version: '', group: ''});
495 | }
496 | }
497 |
498 | /**
499 | * 解析URL
500 | */
501 | resolveURL(): void {
502 | if (!this.resolveURLValue) {
503 | this.message.warning('URL不能为空!');
504 | return;
505 | }
506 | if (!this.resolveURLValue.startsWith('dubbo://')) {
507 | this.message.warning('URL必须以dubbo://开头!');
508 | return;
509 | }
510 | const host = this.resolveURLValue.substring(8, this.resolveURLValue.indexOf('/', 8));
511 | const interfaceName = this.util.getParamValue(this.resolveURLValue, 'interface');
512 | const group = this.util.getParamValue(this.resolveURLValue, 'group');
513 | const version = this.util.getParamValue(this.resolveURLValue, 'version');
514 | const methods = this.util.getParamValue(this.resolveURLValue, 'methods');
515 | const path = this.util.getParamValue(this.resolveURLValue, 'path');
516 | const tabInfo = this.tabs[this.selectedIndex];
517 | tabInfo.formParams.setValue({url: host, interfaceName, group, version, method: '', path});
518 | this.autocomplete.methods.availableFilter = tabInfo.availableMethod = methods.split(',');
519 | this.message.success('解析完成!');
520 | this.modalShow.resolveUrl = false;
521 | }
522 |
523 | /**
524 | * 可用方法过滤
525 | * @param $event Event
526 | */
527 | availableMethodsAutoCompleteFilter($event: Event): void {
528 | $event.preventDefault();
529 | const value = ($event.target as HTMLInputElement).value;
530 | this.autocomplete.methods.availableFilter = this.tabs[this.selectedIndex].availableMethod.filter(item => item.indexOf(value) !== -1);
531 | }
532 |
533 |
534 | /**
535 | * 可用方法过滤
536 | * @param $event Event
537 | */
538 | availableInterFacesAutoCompleteFilter($event: Event): void {
539 | $event.preventDefault();
540 | const value = ($event.target as HTMLInputElement).value;
541 | this.autocomplete.interfaces.availableFilter =
542 | this.tabs[this.selectedIndex].availableInterface.filter(item => item.indexOf(value) !== -1);
543 | }
544 |
545 | /**
546 | * 解析选择的URL
547 | */
548 | resolveSelectURL(): void {
549 | this.resolveURL();
550 | this.modalShow.resolveUrlSelect = false;
551 | }
552 |
553 | /**
554 | * 解析选择的方法
555 | */
556 | resolveSelectMethod(): void {
557 | if (this.resolveMethodInfo) {
558 | this.parsingParameters(this.resolveMethodInfo);
559 | this.message.success('选择成功');
560 | }
561 | this.modalShow.methodOverloading = false;
562 | }
563 |
564 | /**
565 | * Tab单击事件
566 | */
567 | handleTabClick(index: number): void {
568 | if (index !== this.selectedIndex) {
569 | return;
570 | }
571 | this.oldTab = this.tabs[index];
572 | this.modalShow.tabReName = true;
573 | }
574 |
575 | /**
576 | * TAB重命名
577 | */
578 | doTabReName(): void {
579 | if (!this.tabNameForReName) {
580 | this.message.warning('输入不能为空!');
581 | return;
582 | }
583 | this.oldTab.tabName = this.tabNameForReName;
584 | this.tabNameForReName = '';
585 | this.persistenceService.saveGenericParamInfo(this.tabs);
586 | this.modalShow.tabReName = false;
587 | }
588 | }
589 |
590 | /**
591 | * 接口提供者信息
592 | */
593 | class ProviderInfo {
594 | url: string;
595 | info: string;
596 |
597 | constructor(url: string, info: string) {
598 | this.url = url;
599 | this.info = info;
600 | }
601 | }
602 |
603 | /**
604 | * 请求参数模型
605 | */
606 | export type RequestParamModel = { [name: string]: RequestParamModel | string | string[] }[];
607 |
608 | /**
609 | * 请求模型
610 | */
611 | export type RequestModel = FormParamsInfo & { params: RequestParamModel };
612 |
--------------------------------------------------------------------------------