feat: local mock service (#62)

This commit is contained in:
bqy_fe 2022-06-06 01:51:08 +08:00
parent a9b61b76c9
commit ef815767b3
28 changed files with 3046 additions and 2233 deletions

View File

@ -19,17 +19,21 @@
"test:workbench": "cd src/workbench/browser && yarn test", "test:workbench": "cd src/workbench/browser && yarn test",
"electron:serve": "wait-on tcp:4200 && npm run electron:dev", "electron:serve": "wait-on tcp:4200 && npm run electron:dev",
"electron:dev:static": "npm run electron:tsc && electron .", "electron:dev:static": "npm run electron:tsc && electron .",
"electron:dev": "npm run electron:tsc && electron . --development", "electron:dev": "npm run electron:tsc && npm run copyfile:out && electron . --development",
"build": "npm-run-all -s build:workbench electron:tsc && electron-builder build", "build": "npm-run-all -s build:workbench electron:tsc && electron-builder build",
"build:static": "npm run electron:tsc && electron-builder build", "build:static": "npm run electron:tsc && electron-builder build",
"release": "npm-run-all -s build:workbench electron:tsc && electron-builder --publish=always", "release": "npm-run-all -s build:workbench electron:tsc && electron-builder --publish=always",
"test": "npm-run-all --serial test:*", "test": "npm-run-all --serial test:*",
"electron:tsc": "tsc -p tsconfig.json" "electron:tsc": "tsc -p tsconfig.json",
"copyfile:out": "copyfiles -u 1 src/**/*.json src/app/common/images/** out"
}, },
"dependencies": { "dependencies": {
"@bqy/node-module-alias": "^1.0.1", "@bqy/node-module-alias": "^1.0.1",
"@electron/remote": "2.0.8", "@electron/remote": "2.0.8",
"@koa/cors": "3.3.0",
"@koa/router": "10.1.1",
"content-disposition": "^0.5.4", "content-disposition": "^0.5.4",
"copyfiles": "2.4.1",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"dexie": "3.2.2", "dexie": "3.2.2",
@ -38,13 +42,19 @@
"fix-path": "3.0.0", "fix-path": "3.0.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"koa": "2.13.4",
"koa-bodyparser": "4.3.0",
"mockjs": "1.1.0",
"npm": "6.14.17", "npm": "6.14.17",
"portfinder": "1.0.28",
"resolve": "^1.22.0", "resolve": "^1.22.0",
"rxjs": "7.5.5", "rxjs": "7.5.5",
"xml2js": "^0.4.23" "xml2js": "^0.4.23"
}, },
"devDependencies": { "devDependencies": {
"@types/cross-spawn": "6.0.2", "@types/cross-spawn": "6.0.2",
"@types/koa": "2.13.4",
"@types/koa__router": "8.0.11",
"@types/node": "17.0.32", "@types/node": "17.0.32",
"@typescript-eslint/eslint-plugin": "5.23.0", "@typescript-eslint/eslint-plugin": "5.23.0",
"@typescript-eslint/parser": "5.23.0", "@typescript-eslint/parser": "5.23.0",

View File

@ -12,13 +12,16 @@ import { deleteFile, readJson } from 'eo/shared/node/file';
import { STORAGE_TEMP as storageTemp } from 'eo/shared/common/constant'; import { STORAGE_TEMP as storageTemp } from 'eo/shared/common/constant';
import { UnitWorkerModule } from 'eo/workbench/node/unitWorker'; import { UnitWorkerModule } from 'eo/workbench/node/unitWorker';
import Configuration from 'eo/platform/node/configuration/lib'; import Configuration from 'eo/platform/node/configuration/lib';
import { ConfigurationInterface } from 'eo/platform/node/configuration'; import { ConfigurationInterface } from 'src/platform/node/configuration';
import { MockServer } from 'eo/platform/node/mock-server';
let win: BrowserWindow = null; let win: BrowserWindow = null;
export const subView = { export const subView = {
appView: null, appView: null,
mainView: null, mainView: null,
}; };
const eoUpdater = new EoUpdater(); const eoUpdater = new EoUpdater();
const mockServer = new MockServer();
const moduleManager: ModuleManagerInterface = ModuleManager(); const moduleManager: ModuleManagerInterface = ModuleManager();
const configuration: ConfigurationInterface = Configuration(); const configuration: ConfigurationInterface = Configuration();
// Remote // Remote
@ -85,9 +88,11 @@ try {
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
// Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947 // Added 400 ms to fix the black background issue while using transparent window. More detais at https://github.com/electron/electron/issues/15947
app.on('ready', () => { app.on('ready', async () => {
setTimeout(createWindow, 400); setTimeout(createWindow, 400);
eoUpdater.check(); eoUpdater.check();
// 启动mock服务
await mockServer.start();
}); });
//!TODO only api manage app need this //!TODO only api manage app need this
// setupUnit(subView.appView); // setupUnit(subView.appView);
@ -99,6 +104,7 @@ try {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
app.quit(); app.quit();
} }
mockServer.stop();
}); });
app.on('activate', () => { app.on('activate', () => {
@ -204,6 +210,20 @@ try {
returnValue = configuration.getModuleSettings(arg.data.moduleID); returnValue = configuration.getModuleSettings(arg.data.moduleID);
} else if (arg.action === 'getSidePosition') { } else if (arg.action === 'getSidePosition') {
returnValue = subView.appView?.sidePosition; returnValue = subView.appView?.sidePosition;
// 注册单个mock路由
} else if (arg.action === 'registerMockRoute') {
const { method, path, data } = arg.data;
returnValue = mockServer.registerRoute(method, path, data);
// 注销mock路由
} else if (arg.action === 'unRegisterMockRoute') {
const { method, path } = arg.data;
returnValue = mockServer.unRegisterRoute(method, path);
// 获取mock服务地址
} else if (arg.action === 'getMockUrl') {
returnValue = mockServer.getMockUrl();
// 重置并初始化mock路由
} else if (arg.action === 'resetAndInitRoutes') {
returnValue = mockServer.resetAndInitRoutes();
} else if (arg.action === 'hook') { } else if (arg.action === 'hook') {
returnValue = 'hook返回'; returnValue = 'hook返回';
} else { } else {

View File

@ -64,7 +64,6 @@ export interface Environment extends StorageModel {
parameters?: object; parameters?: object;
} }
/** /**
* *
*/ */
@ -391,6 +390,10 @@ export interface ApiData extends StorageModel {
* @type {JsonRootType|string} * @type {JsonRootType|string}
*/ */
responseBodyJsonType?: JsonRootType | string; responseBodyJsonType?: JsonRootType | string;
/**
* mock列表
*/
mockList?: ApiEditMock[];
} }
/** /**
@ -450,6 +453,18 @@ export interface ParamsEnum {
*/ */
description: string; description: string;
} }
export type ApiEditMock = {
/** mock名称 */
name: string;
/** mock地址 */
url: string;
/** mock返回值 */
response: string;
/** 是否系统默认mock */
isDefault?: boolean;
};
export interface BasiApiEditParams { export interface BasiApiEditParams {
/** /**
* *
@ -550,7 +565,10 @@ export interface StorageInterface {
apiDataBulkLoad: (uuids: Array<number | string>) => Observable<Array<object>>; apiDataBulkLoad: (uuids: Array<number | string>) => Observable<Array<object>>;
apiDataLoadAllByProjectID: (projectID: number | string) => Observable<Array<object>>; apiDataLoadAllByProjectID: (projectID: number | string) => Observable<Array<object>>;
apiDataLoadAllByGroupID: (groupID: number | string) => Observable<Array<object>>; apiDataLoadAllByGroupID: (groupID: number | string) => Observable<Array<object>>;
apiDataLoadAllByProjectIDAndGroupID: (projectID: number|string, groupID: number|string) => Observable<Array<object>>; apiDataLoadAllByProjectIDAndGroupID: (
projectID: number | string,
groupID: number | string
) => Observable<Array<object>>;
// Api Test History // Api Test History
apiTestHistoryCreate: (item: ApiTestHistory) => Observable<object>; apiTestHistoryCreate: (item: ApiTestHistory) => Observable<object>;
apiTestHistoryUpdate: (item: ApiTestHistory, uuid: number | string) => Observable<object>; apiTestHistoryUpdate: (item: ApiTestHistory, uuid: number | string) => Observable<object>;
@ -583,11 +601,11 @@ export enum StorageHandleStatus {
success = 'success', success = 'success',
empty = 'empty', empty = 'empty',
error = 'error', error = 'error',
invalid = 'invalid' invalid = 'invalid',
} }
export enum StorageProcessType { export enum StorageProcessType {
default = 'default', default = 'default',
remote = 'remote', remote = 'remote',
sync = 'sync' sync = 'sync',
} }

View File

@ -147,3 +147,22 @@ window.eo.getSettings = (settings) => {
window.eo.getModuleSettings = (moduleID) => { window.eo.getModuleSettings = (moduleID) => {
return ipcRenderer.sendSync('eo-sync', { action: 'getModuleSettings', data: { moduleID: moduleID } }); return ipcRenderer.sendSync('eo-sync', { action: 'getModuleSettings', data: { moduleID: moduleID } });
}; };
// 注册单个mock路由
window.eo.registerMockRoute = ({ method, path, data }) => {
return ipcRenderer.sendSync('eo-sync', { action: 'registerMockRoute', data: { method, path, data } });
};
// 注销mock路由
window.eo.unRegisterMockRoute = ({ method, path }) => {
return ipcRenderer.sendSync('eo-sync', { action: 'unRegisterMockRoute', data: { method, path } });
};
// 获取mock服务地址
window.eo.getMockUrl = () => {
return ipcRenderer.sendSync('eo-sync', { action: 'getMockUrl' });
};
// 重置并初始化路由
window.eo.resetAndInitRoutes = () => {
return ipcRenderer.sendSync('eo-sync', { action: 'resetAndInitRoutes' });
};

View File

@ -59,7 +59,7 @@ export class Configuration implements ConfigurationInterface {
data.nestedSettings ??= {}; data.nestedSettings ??= {};
data.settings[moduleID] = settings; data.settings[moduleID] = settings;
const propArr = moduleID.split('.'); const propArr = moduleID.split('.');
const target = propArr.slice(0, -1).reduce((p, k) => p[k], data.nestedSettings); const target = propArr.slice(0, -1).reduce((p, k) => p?.[k], data.nestedSettings);
target[propArr.at(-1)] = settings; target[propArr.at(-1)] = settings;
return this.saveConfig(data); return this.saveConfig(data);
} }

View File

@ -0,0 +1,53 @@
const Koa = require('koa');
const Router = require('koa-router');
const glob = require('glob');
const logger = require('koa-logger');
const { resolve } = require('path');
const fs = require('fs');
const app = new Koa();
const router = new Router({ prefix: '/api' });
const routerMap = {}; // 存放路由映射
app.use(logger());
// 注册路由
glob.sync(resolve('./api', '**/*.json')).forEach((item, i) => {
let apiJsonPath = item && item.split('/api')[1];
let apiPath = apiJsonPath.replace('.json', '');
router.get(apiPath, (ctx, next) => {
try {
let jsonStr = fs.readFileSync(item).toString();
ctx.body = {
data: JSON.parse(jsonStr),
state: 200,
type: 'success', // 自定义响应体
};
} catch (err) {
ctx.throw('服务器错误', 500);
}
});
// 记录路由
routerMap[apiJsonPath] = apiPath;
});
fs.writeFile('./routerMap.json', JSON.stringify(routerMap, null, 4), (err) => {
if (!err) {
console.log('路由地图生成成功!');
}
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(3040);
// const Koa = require('koa');
// const app = new Koa();
// app.use(async (ctx) => {
// ctx.body = 'hello koa2';
// });
// app.listen(3000);
// console.log('[demo] start-quick is starting at port 3000');

View File

@ -0,0 +1,146 @@
import Koa from 'koa';
import Router from '@koa/router';
import cors from '@koa/cors';
import bodyParser from 'koa-bodyparser';
import portfinder from 'portfinder';
import mockjs from 'mockjs';
export class MockServer {
private app: Koa;
private router: Router;
/** mock服务地址 */
private mockUrl = '';
constructor(prefix = '') {
this.app = new Koa();
this.router = new Router({ prefix });
// 使用ctx.body解析中间件
this.app.use(bodyParser());
// 加载路由中间件
this.app.use(this.router.routes()).use(this.router.allowedMethods());
// 允许跨域请求
this.app.use(cors());
this.initRoutes();
}
/**
* mock服务
* @param port mock服务端口号
*/
async start(port = 3040) {
portfinder.basePort = port;
// 使用 portfinder 做端口检测若发现端口被占用则端口自增1
const _port = await portfinder.getPortPromise();
return new Promise((resolve, reject) => {
this.app
.listen(_port, () => {
this.mockUrl = `http://localhost:${_port}`;
console.log(`mock服务已启动: ${this.mockUrl}`);
resolve(this.mockUrl);
})
.on('error', (error) => {
console.error('mock服务启动失败: ' + error);
reject(error);
});
});
}
/**
* mock服务
*/
stop() {
process.exit(1);
}
/**
* mock服务地址
* @returns mock服务地址
*/
getMockUrl() {
return this.mockUrl;
}
/**
* \
*/
resetRoutes() {
this.router.stack = [];
}
/**
* \
*/
resetAndInitRoutes() {
this.resetRoutes();
this.initRoutes();
}
/**
*
* @param method
* @param path
* @param data
*/
registerRoute(method: string, path: string, data = {}) {
const { pathname, search } = new URL(path, this.mockUrl);
// Object.fromEntries(searchParams.entries())
// console.log('registerRoute', method.toLocaleLowerCase(), pathname + search);
this.router[method.toLocaleLowerCase()](pathname + search, async (ctx, next) => {
try {
const mockData = typeof data === 'string' ? JSON.parse(data) : data;
ctx.body = mockjs.mock(mockData);
} catch (e) {
ctx.body = {
tips: '返回数据格式有误,请检查!',
errorMsg: e.message,
originData: data,
};
ctx.status = 500;
}
await next();
});
}
/**
*
* @param methods
* @param path
*/
unRegisterRoute(methods: string[], path: string) {
const _methods = methods.map((n) => n.toLocaleUpperCase());
// 将匹配到的路由注销掉
this.router.stack = this.router.stack.filter((item) => {
const isMatch = item.methods.some((n) => _methods.includes(n)) && item.path === path;
return !isMatch;
});
}
/**
*
*/
initRoutes() {
this.router.get('/', async (ctx, next) => {
const mockPeople = mockjs.mock({
'peoples|10': [
{
'id|+1': 1,
guid: '@guid',
name: '@cname',
age: '@integer(20, 50)',
birthday: '@date("MM-dd")',
address: '@county(true)',
email: '@email',
},
],
});
ctx.body = mockPeople;
await next();
});
this.router.get('/mock_stack', async (ctx, next) => {
ctx.body = this.router;
await next();
});
}
}

View File

@ -20,13 +20,11 @@
<div *ngIf="apiData.requestBody?.length"> <div *ngIf="apiData.requestBody?.length">
<div class="api_line"> <div class="api_line">
Body 请求参数<nz-tag class="ml10" nzColor="default">{{ CONST.BODY_TYPE[apiData.requestBodyType] }}</nz-tag> Body 请求参数<nz-tag class="ml10" nzColor="default">{{ CONST.BODY_TYPE[apiData.requestBodyType] }}</nz-tag>
<nz-tag *ngIf="apiData.requestBodyType==='json'" nzColor="default">最外层结构为:{{ CONST.JSON_ROOT_TYPE[apiData.requestBodyJsonType] }}</nz-tag> <nz-tag *ngIf="apiData.requestBodyType==='json'" nzColor="default">最外层结构为:{{
CONST.JSON_ROOT_TYPE[apiData.requestBodyJsonType] }}</nz-tag>
</div> </div>
<eo-api-detail-body <eo-api-detail-body [bodyType]="apiData.requestBodyType" [model]="apiData.requestBody"
[bodyType]="apiData.requestBodyType" [jsonRootType]="apiData.requestBodyJsonType"></eo-api-detail-body>
[model]="apiData.requestBody"
[jsonRootType]="apiData.requestBodyJsonType"
></eo-api-detail-body>
</div> </div>
<div *ngIf="apiData.responseHeaders && apiData.responseHeaders.length"> <div *ngIf="apiData.responseHeaders && apiData.responseHeaders.length">
<p class="api_line">返回头部</p> <p class="api_line">返回头部</p>
@ -34,10 +32,11 @@
</div> </div>
<div *ngIf="apiData.responseBody && apiData.responseBody.length"> <div *ngIf="apiData.responseBody && apiData.responseBody.length">
<p class="api_line">返回参数</p> <p class="api_line">返回参数</p>
<eo-api-detail-body <eo-api-detail-body [bodyType]="apiData.responseBodyType" [model]="apiData.responseBody"
[bodyType]="apiData.responseBodyType" [jsonRootType]="apiData.responseBodyJsonType"></eo-api-detail-body>
[model]="apiData.responseBody" </div>
[jsonRootType]="apiData.responseBodyJsonType" <div>
></eo-api-detail-body> <p class="api_line">MOCK </p>
<eo-api-detail-mock [model]="apiData.mockList"></eo-api-detail-mock>
</div> </div>
</section> </section>

View File

@ -14,6 +14,8 @@
margin-bottom: 10px; margin-bottom: 10px;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
border-left: 3px solid var(--GREEN_NORMAL);
text-indent: 5px;
} }
.t-1 { .t-1 {

View File

@ -9,38 +9,30 @@ import { SharedModule } from '../../../shared/shared.module';
import { NzButtonModule } from 'ng-zorro-antd/button'; import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzIconModule } from 'ng-zorro-antd/icon'; import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzTagModule } from 'ng-zorro-antd/tag'; import { NzTagModule } from 'ng-zorro-antd/tag';
import { NzModalModule } from 'ng-zorro-antd/modal';
import { NzFormModule } from 'ng-zorro-antd/form';
import { ApiDetailComponent } from './api-detail.component'; import { ApiDetailComponent } from './api-detail.component';
import { ApiDetailHeaderComponent } from './header/api-detail-header.component'; import { ApiDetailHeaderComponent } from './header/api-detail-header.component';
import { ApiDetailBodyComponent } from './body/api-detail-body.component'; import { ApiDetailBodyComponent } from './body/api-detail-body.component';
import { ApiDetailQueryComponent } from './query/api-detail-query.component'; import { ApiDetailQueryComponent } from './query/api-detail-query.component';
import { ApiDetailRestComponent } from './rest/api-detail-rest.component'; import { ApiDetailRestComponent } from './rest/api-detail-rest.component';
import { ApiDetailMockComponent } from './mock/api-detail-mock.component';
import { ApiDetailService } from './api-detail.service'; import { ApiDetailService } from './api-detail.service';
const NZ_COMPONETS = [ const NZ_COMPONETS = [NzButtonModule, NzIconModule, NzTagModule, NzModalModule, NzFormModule];
NzButtonModule,
NzIconModule,
NzTagModule
];
const COMPONENTS = [ const COMPONENTS = [
ApiDetailComponent, ApiDetailComponent,
ApiDetailHeaderComponent, ApiDetailHeaderComponent,
ApiDetailBodyComponent, ApiDetailBodyComponent,
ApiDetailQueryComponent, ApiDetailQueryComponent,
ApiDetailRestComponent ApiDetailRestComponent,
ApiDetailMockComponent,
]; ];
@NgModule({ @NgModule({
declarations: [...COMPONENTS], declarations: [...COMPONENTS],
imports: [ imports: [FormsModule, ReactiveFormsModule, Ng1Module, CommonModule, ...NZ_COMPONETS, EouiModule, SharedModule],
FormsModule, providers: [ApiDetailService],
ReactiveFormsModule,
Ng1Module,
CommonModule,
...NZ_COMPONETS,
EouiModule,
SharedModule,
],
providers:[ApiDetailService]
}) })
export class ApiDetailModule {} export class ApiDetailModule {}

View File

@ -0,0 +1,5 @@
<eo-table [(model)]="model" [columns]="mockListColumns" [dataModel]="{ name: '', value: '', description: '' }">
<ng-template cell="url" let-scope="scope" let-index="index">
<span>{{ scope.url }}</span>
</ng-template>
</eo-table>

View File

@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ModalService } from '../../../../shared/services/modal.service';
import { ApiDetailService } from '../api-detail.service';
import { ApiDetailHeaderComponent } from './api-detail-mock.component';
describe('ApiDetailHeaderComponent', () => {
let component: ApiDetailHeaderComponent;
let fixture: ComponentFixture<ApiDetailHeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [ApiDetailService, { provide: ModalService, useValue: {} }],
declarations: [ApiDetailHeaderComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ApiDetailHeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,27 @@
import { Component, Input, OnChanges } from '@angular/core';
import { ApiEditMock } from 'eo/platform/browser/IndexedDB';
import { ApiDetailService } from '../api-detail.service';
@Component({
selector: 'eo-api-detail-mock',
templateUrl: './api-detail-mock.component.html',
styleUrls: ['./api-detail-mock.component.scss'],
})
export class ApiDetailMockComponent implements OnChanges {
@Input() model: ApiEditMock[] = [];
listConf: object = {};
isVisible = false;
mockListColumns = [
{ title: '名称', key: 'name' },
{ title: 'URL', slot: 'url' },
];
constructor(private detailService: ApiDetailService) {}
ngOnChanges(changes) {
// if (changes.model&&!changes.model.previousValue&&changes.model.currentValue) {
// this.model.push(Object.assign({}, this.itemStructure));
// }
}
handleEditMockItem(index: number) {}
handleDeleteMockItem(index: number) {}
}

View File

@ -1,5 +1,5 @@
<eo-message></eo-message> <eo-message></eo-message>
<div class="!pt-0 p15"> <nz-collapse class="!pt-0 p15">
<div class="sticky top-0 z-10 pt-6 bg-white"> <div class="sticky top-0 z-10 pt-6 bg-white">
<button type="submit" nz-button nztype="primary" class="eo_theme_btn_success" (click)="saveApi()">保存</button> <button type="submit" nz-button nztype="primary" class="eo_theme_btn_success" (click)="saveApi()">保存</button>
<nz-divider></nz-divider> <nz-divider></nz-divider>
@ -122,4 +122,8 @@
</nz-tabset> </nz-tabset>
</nz-collapse-panel> </nz-collapse-panel>
</nz-collapse> </nz-collapse>
</div> <!-- mock -->
<nz-collapse class="eo_collapse mt40" [nzGhost]="true" *ngIf="electron.isElectron">
<p class="api_line">MOCK <a class="text-xs text-blue-500" (click)="openAddMockModal()">+添加</a></p>
<eo-api-edit-mock [model]="apiData.mockList"></eo-api-edit-mock>
</nz-collapse>

View File

@ -30,6 +30,10 @@ import {
getExpandGroupByKey, getExpandGroupByKey,
} from '../../../utils/tree/tree.utils'; } from '../../../utils/tree/tree.utils';
import { ApiParamsNumPipe } from '../../../shared/pipes/api-param-num.pipe'; import { ApiParamsNumPipe } from '../../../shared/pipes/api-param-num.pipe';
import { tree2obj } from '../../../utils/tree/tree.utils';
import { ApiEditMockComponent } from './mock/api-edit-mock.component';
import { ElectronService } from 'eo/workbench/browser/src/app/core/services/electron/electron.service';
@Component({ @Component({
selector: 'eo-api-edit-edit', selector: 'eo-api-edit-edit',
templateUrl: './api-edit.component.html', templateUrl: './api-edit.component.html',
@ -37,6 +41,7 @@ import { ApiParamsNumPipe } from '../../../shared/pipes/api-param-num.pipe';
}) })
export class ApiEditComponent implements OnInit, OnDestroy { export class ApiEditComponent implements OnInit, OnDestroy {
@ViewChild('apiGroup') apiGroup: NzTreeSelectComponent; @ViewChild('apiGroup') apiGroup: NzTreeSelectComponent;
@ViewChild(ApiEditMockComponent) apiEditMockComp: ApiEditMockComponent;
validateForm!: FormGroup; validateForm!: FormGroup;
apiData: ApiData; apiData: ApiData;
groups: any[]; groups: any[];
@ -53,7 +58,8 @@ export class ApiEditComponent implements OnInit, OnDestroy {
private message: EoMessageService, private message: EoMessageService,
private messageService: MessageService, private messageService: MessageService,
private apiTab: ApiTabService, private apiTab: ApiTabService,
private storage: StorageService private storage: StorageService,
public electron: ElectronService
) {} ) {}
getApiGroup() { getApiGroup() {
this.groups = []; this.groups = [];
@ -88,12 +94,26 @@ export class ApiEditComponent implements OnInit, OnDestroy {
getApi(id) { getApi(id) {
this.storage.run('apiDataLoad', [id], (result: StorageHandleResult) => { this.storage.run('apiDataLoad', [id], (result: StorageHandleResult) => {
if (result.status === StorageHandleStatus.success) { if (result.status === StorageHandleStatus.success) {
this.apiData = result.data;
// 如果没有mock则生成系统默认mock
if ((window.eo?.getMockUrl && !Array.isArray(this.apiData.mockList)) || this.apiData.mockList?.length === 0) {
const url = new URL(this.apiData.uri, window.eo.getMockUrl());
this.apiData.mockList = [
{
name: '系统默认期望',
url: url.toString(),
response: JSON.stringify(tree2obj([].concat(this.apiData.responseBody))),
isDefault: true,
},
];
}
['requestBody', 'responseBody'].forEach((tableName) => { ['requestBody', 'responseBody'].forEach((tableName) => {
if (['xml', 'json'].includes(result.data[`${tableName}Type`])) { if (['xml', 'json'].includes(result.data[`${tableName}Type`])) {
result.data[tableName] = treeToListHasLevel(result.data[tableName]); result.data[tableName] = treeToListHasLevel(result.data[tableName]);
} }
}); });
this.apiData = result.data;
this.changeGroupID$.next(this.apiData.groupID); this.changeGroupID$.next(this.apiData.groupID);
this.validateForm.patchValue(this.apiData); this.validateForm.patchValue(this.apiData);
} }
@ -112,8 +132,15 @@ export class ApiEditComponent implements OnInit, OnDestroy {
} }
const formData: any = Object.assign({}, this.apiData, this.validateForm.value); const formData: any = Object.assign({}, this.apiData, this.validateForm.value);
formData.groupID = Number(formData.groupID === '-1' ? '0' : formData.groupID); formData.groupID = Number(formData.groupID === '-1' ? '0' : formData.groupID);
['requestBody', 'queryParams', 'restParams', 'requestHeaders', 'responseHeaders', 'responseBody'].forEach( [
(tableName) => { 'requestBody',
'queryParams',
'restParams',
'requestHeaders',
'responseHeaders',
'responseBody',
'mockList',
].forEach((tableName) => {
if (typeof this.apiData[tableName] !== 'object') { if (typeof this.apiData[tableName] !== 'object') {
return; return;
} }
@ -123,8 +150,8 @@ export class ApiEditComponent implements OnInit, OnDestroy {
formData[tableName] = listToTreeHasLevel(formData[tableName]); formData[tableName] = listToTreeHasLevel(formData[tableName]);
} }
} }
} });
);
this.editApi(formData); this.editApi(formData);
} }
bindGetApiParamNum(params) { bindGetApiParamNum(params) {
@ -142,6 +169,13 @@ export class ApiEditComponent implements OnInit, OnDestroy {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
} }
/**
* mock弹窗
*/
openAddMockModal() {
this.apiEditMockComp.openAddModal();
}
private initApi(id) { private initApi(id) {
this.resetForm(); this.resetForm();
this.initBasicForm(); this.initBasicForm();
@ -149,7 +183,6 @@ export class ApiEditComponent implements OnInit, OnDestroy {
if (this.apiTab.currentTab && this.apiTab.tabCache[this.apiTab.tabID]) { if (this.apiTab.currentTab && this.apiTab.tabCache[this.apiTab.tabID]) {
let tabData = this.apiTab.tabCache[this.apiTab.tabID]; let tabData = this.apiTab.tabCache[this.apiTab.tabID];
this.apiData = tabData.apiData; this.apiData = tabData.apiData;
this.validateForm.patchValue(this.apiData);
return; return;
} }
if (!id) { if (!id) {

View File

@ -20,6 +20,7 @@ import { NzTabsModule } from 'ng-zorro-antd/tabs';
import { NzRadioModule } from 'ng-zorro-antd/radio'; import { NzRadioModule } from 'ng-zorro-antd/radio';
import { NzDividerModule } from 'ng-zorro-antd/divider'; import { NzDividerModule } from 'ng-zorro-antd/divider';
import { NzAffixModule } from 'ng-zorro-antd/affix'; import { NzAffixModule } from 'ng-zorro-antd/affix';
import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
import { ApiEditComponent } from './api-edit.component'; import { ApiEditComponent } from './api-edit.component';
import { ApiEditHeaderComponent } from './header/api-edit-header.component'; import { ApiEditHeaderComponent } from './header/api-edit-header.component';
@ -27,6 +28,7 @@ import { ApiEditBodyComponent } from './body/api-edit-body.component';
import { ApiEditQueryComponent } from './query/api-edit-query.component'; import { ApiEditQueryComponent } from './query/api-edit-query.component';
import { ApiEditRestComponent } from './rest/api-edit-rest.component'; import { ApiEditRestComponent } from './rest/api-edit-rest.component';
import { ApiParamsExtraSettingComponent } from './extra-setting/api-params-extra-setting.component'; import { ApiParamsExtraSettingComponent } from './extra-setting/api-params-extra-setting.component';
import { ApiEditMockComponent } from './mock/api-edit-mock.component';
import { ApiEditService } from './api-edit.service'; import { ApiEditService } from './api-edit.service';
@ -44,6 +46,7 @@ const NZ_COMPONETS = [
NzRadioModule, NzRadioModule,
NzDividerModule, NzDividerModule,
NzAffixModule, NzAffixModule,
NzPopconfirmModule,
]; ];
const COMPONENTS = [ const COMPONENTS = [
ApiEditComponent, ApiEditComponent,
@ -52,6 +55,7 @@ const COMPONENTS = [
ApiEditQueryComponent, ApiEditQueryComponent,
ApiEditRestComponent, ApiEditRestComponent,
ApiParamsExtraSettingComponent, ApiParamsExtraSettingComponent,
ApiEditMockComponent,
]; ];
@NgModule({ @NgModule({
declarations: [...COMPONENTS], declarations: [...COMPONENTS],
@ -65,6 +69,6 @@ const COMPONENTS = [
SharedModule, SharedModule,
ParamsImportModule, ParamsImportModule,
], ],
providers:[ApiEditService] providers: [ApiEditService],
}) })
export class ApiEditModule {} export class ApiEditModule {}

View File

@ -0,0 +1,38 @@
<eo-table [(model)]="mocklList" [columns]="mockListColumns" [dataModel]="{ name: '', value: '', description: '' }">
<ng-template cell="url" let-scope="scope" let-index="index">
<span>{{ scope.url }}</span>
</ng-template>
<ng-template cell="action" let-scope="scope" let-index="index">
<div class="flex justify-evenly">
<a nz-button nzType="link" *ngIf="scope.name || scope.url" (click)="handleEditMockItem(index)">编辑</a>
<a nz-button nzType="link" *ngIf="scope.name || scope.url" nz-popconfirm nzPopconfirmTitle="您确定要删除此mock吗?"
nzPopconfirmPlacement="topRight" (nzOnConfirm)="handleDeleteMockItem(index)" [disabled]="scope.isDefault">删除</a>
</div>
</ng-template>
</eo-table>
<nz-modal [(nzVisible)]="isVisible" nzWidth="70%" nzTitle="{{this.currentEditMockIndex === -1 ? '添加' : '编辑' }}mock">
<section *nzModalContent class="flex">
<div class="main-content">
<form nz-form nzLayout="vertical">
<nz-form-item>
<nz-form-label nzFor="currentEditMock.name">mock名称</nz-form-label>
<nz-form-control>
<input nz-input name="name" type="text" [(ngModel)]="currentEditMock.name" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzFor="currentEditMock.response">返回值</nz-form-label>
<nz-form-control>
<eo-editor [(code)]="currentEditMock.response" (codeChange)="rawDataChange()"
[eventList]="['type', 'format', 'copy', 'download', 'newTab', 'search', 'replace']"></eo-editor>
</nz-form-control>
</nz-form-item>
</form>
</div>
</section>
<div *nzModalFooter class="footer">
<button nz-button nzType="primary" (click)="handleSave()">保存</button>
<button nz-button nzType="default" (click)="handleCancel()">取消</button>
</div>
</nz-modal>

View File

@ -0,0 +1,27 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ModalService } from '../../../../shared/services/modal.service';
import { ApiDetailService } from '../api-detail.service';
import { ApiDetailHeaderComponent } from './api-detail-mock.component';
describe('ApiDetailHeaderComponent', () => {
let component: ApiDetailHeaderComponent;
let fixture: ComponentFixture<ApiDetailHeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [ApiDetailService, { provide: ModalService, useValue: {} }],
declarations: [ApiDetailHeaderComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ApiDetailHeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,84 @@
import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core';
import { ApiEditMock } from 'eo/platform/browser/IndexedDB';
import { Subject } from 'rxjs';
import { takeUntil, debounceTime } from 'rxjs/operators';
@Component({
selector: 'eo-api-edit-mock',
templateUrl: './api-edit-mock.component.html',
styleUrls: ['./api-edit-mock.component.scss'],
})
export class ApiEditMockComponent implements OnChanges {
@Input() model: ApiEditMock[] = [];
@Output() modelChange: EventEmitter<any> = new EventEmitter();
isVisible = false;
mockUrl = window.eo?.getMockUrl?.();
private $mocklList: ApiEditMock[] = [];
get mocklList() {
return this.$mocklList;
}
set mocklList(_) {
this.$mocklList = this.model.map((item) => {
const { pathname, search } = new URL(item.url, this.mockUrl);
item.url = `${new URL(pathname + search, this.mockUrl)}`;
return item;
});
}
mockListColumns = [
{ title: '名称', key: 'name' },
{ title: 'URL', slot: 'url' },
{ title: '', slot: 'action', width: '15%' },
];
/** 当前被编辑的mock */
currentEditMock: ApiEditMock;
/** 当前被编辑的mock索引 */
currentEditMockIndex = -1;
private destroy$: Subject<void> = new Subject<void>();
private rawChange$: Subject<string> = new Subject<string>();
constructor() {
this.rawChange$.pipe(debounceTime(700), takeUntil(this.destroy$)).subscribe(() => {});
this.mocklList = [...this.model];
}
ngOnChanges(changes: SimpleChanges): void {
const { model } = changes;
if (model.currentValue) {
this.mocklList = [...this.model];
}
}
rawDataChange() {
this.rawChange$.next(this.currentEditMock.response);
}
handleEditMockItem(index: number) {
this.currentEditMock = { ...this.model[index] };
this.currentEditMockIndex = index;
this.isVisible = true;
}
handleDeleteMockItem(index: number) {
this.model.splice(index, 1);
this.mocklList = [...this.model];
}
handleSave() {
this.isVisible = false;
this.currentEditMockIndex === -1
? this.model.push(this.currentEditMock)
: (this.model[this.currentEditMockIndex] = this.currentEditMock);
this.mocklList = [...this.model];
this.modelChange.emit([...this.model]);
}
handleCancel() {
this.isVisible = false;
}
openAddModal() {
this.currentEditMockIndex = -1;
this.isVisible = true;
this.currentEditMock = {
url: this.model.at(0).url,
name: '',
response: '',
};
}
}

View File

@ -13,6 +13,7 @@ import { NzTreeComponent } from 'ng-zorro-antd/tree';
import { ModalService } from '../../../../shared/services/modal.service'; import { ModalService } from '../../../../shared/services/modal.service';
import { StorageService } from '../../../../shared/services/storage'; import { StorageService } from '../../../../shared/services/storage';
import { ElectronService } from '../../../../core/services'; import { ElectronService } from '../../../../core/services';
import { tree2obj } from '../../../../utils/tree/tree.utils';
@Component({ @Component({
selector: 'eo-api-group-tree', selector: 'eo-api-group-tree',
templateUrl: './api-group-tree.component.html', templateUrl: './api-group-tree.component.html',
@ -114,11 +115,16 @@ export class ApiGroupTreeComponent implements OnInit, OnDestroy {
}); });
} }
getApis() { getApis() {
// 注册mock路由
const registerMockRoute = window.eo?.registerMockRoute;
// 重置并初始化路由
window.eo?.resetAndInitRoutes?.();
this.storage.run('apiDataLoadAllByProjectID', [this.projectID], (result: StorageHandleResult) => { this.storage.run('apiDataLoadAllByProjectID', [this.projectID], (result: StorageHandleResult) => {
const { success, empty } = StorageHandleStatus; const { success, empty } = StorageHandleStatus;
if ([success, empty].includes(result.status)) { if ([success, empty].includes(result.status)) {
let apiItems = {}; let apiItems = {};
result.data.forEach((item) => { result.data.forEach((item: ApiData) => {
delete item.updatedAt; delete item.updatedAt;
apiItems[item.uuid] = item; apiItems[item.uuid] = item;
this.treeItems.push({ this.treeItems.push({
@ -129,6 +135,21 @@ export class ApiGroupTreeComponent implements OnInit, OnDestroy {
method: item.method, method: item.method,
isLeaf: true, isLeaf: true,
}); });
if (this.electron.isElectron && registerMockRoute) {
if (Array.isArray(item.mockList) && item.mockList.length > 0) {
item.mockList.forEach((n) => {
registerMockRoute({ method: item.method, path: n.url, data: n.response });
});
} else {
registerMockRoute({
method: item.method,
path: item.uri,
data: tree2obj(item.responseBody as any[]),
});
}
// console.log('registerMockRoute', { method: item.method, path: item.uri, data: tree2obj(item.responseBody) });
}
}); });
this.apiDataItems = apiItems; this.apiDataItems = apiItems;
this.messageService.send({ type: 'loadApi', data: this.apiDataItems }); this.messageService.send({ type: 'loadApi', data: this.apiDataItems });
@ -136,6 +157,7 @@ export class ApiGroupTreeComponent implements OnInit, OnDestroy {
this.generateGroupTreeData(); this.generateGroupTreeData();
this.restoreExpandStatus(); this.restoreExpandStatus();
} }
console.log('result', result.data);
}); });
} }
restoreExpandStatus() { restoreExpandStatus() {

View File

@ -2,7 +2,7 @@
(nzSelectChange)="pickTab()" [nzTabBarExtraContent]="extraTemplate"> (nzSelectChange)="pickTab()" [nzTabBarExtraContent]="extraTemplate">
<nz-tab *ngFor="let tab of tabSerive.tabs; let i = index" nzClosable [nzTitle]="titleTemplate"> <nz-tab *ngFor="let tab of tabSerive.tabs; let i = index" nzClosable [nzTitle]="titleTemplate">
<ng-template #titleTemplate> <ng-template #titleTemplate>
<span class="mr5 method_text method_text_{{ tab.method }}" *ngIf="tab.method">{{ tab.method }}</span> <span class="mr5 method_text_{{ tab.method }}" *ngIf="tab.method">{{ tab.method }}</span>
<span class="text_omit tab_text"> {{ tab.title }}</span> <span class="text_omit tab_text"> {{ tab.title }}</span>
</ng-template> </ng-template>
</nz-tab> </nz-tab>

View File

@ -90,7 +90,7 @@ export class ApiTestComponent implements OnInit, OnDestroy {
*/ */
restoreHistory(item) { restoreHistory(item) {
let result = this.apiTest.getTestDataFromHistory(item); let result = this.apiTest.getTestDataFromHistory(item);
console.log('restoreHistory',result) console.log('restoreHistory', result);
//restore request //restore request
this.apiData = result.testData; this.apiData = result.testData;
this.changeUri(); this.changeUri();

View File

@ -152,7 +152,7 @@ export class SettingComponent implements OnInit {
* *
*/ */
private init() { private init() {
if (!window.eo && !window.eo.getFeature) return; if (!window.eo && !window.eo?.getFeature) return;
this.isVisible = true; this.isVisible = true;
this.settings = {}; this.settings = {};
this.nestedSettings = {}; this.nestedSettings = {};

View File

@ -1,4 +1,11 @@
import { GroupTreeItem } from '../../shared/models'; import { GroupTreeItem } from '../../shared/models';
export type TreeToObjOpts = {
key?: string;
valueKey?: string;
childKey?: string;
};
/** /**
* Convert old component listBlock array items has level without parent id to tree nodes * Convert old component listBlock array items has level without parent id to tree nodes
* @param list Array<GroupTreeItem> * @param list Array<GroupTreeItem>
@ -146,3 +153,22 @@ export const getExpandGroupByKey = (component, key) => {
} }
return expandKeys; return expandKeys;
}; };
/**
* key => value
*
* @param list
* @param opts
* @returns
*/
export const tree2obj = (list: any[], opts: TreeToObjOpts = {}, initObj = {}) => {
const { key = 'name', valueKey = 'description', childKey = 'children' } = opts;
return list.reduce((prev, curr) => {
prev[curr[key]] = curr[valueKey];
if (Array.isArray(curr[childKey]) && curr[childKey].length > 0) {
tree2obj(curr[childKey], opts, (prev[curr[key]] = {}));
}
return prev;
}, initObj);
};

View File

@ -1,12 +1,18 @@
{ {
"compilerOptions": { "compilerOptions": {
"allowJs": true,
"checkJs": false,
"sourceMap": true, "sourceMap": true,
"declaration": false, "declaration": false,
"moduleResolution": "node", "moduleResolution": "node",
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"skipLibCheck": true,
"module": "commonjs", "module": "commonjs",
"outDir": "./out", "outDir": "./out",
"rootDir": "./src",
"target": "es5", "target": "es5",
"types": [ "types": [
"node" "node"
@ -25,12 +31,15 @@
} }
}, },
"include": [ "include": [
"**/**.ts", "./src/**/**.ts",
"./src/**/**.js",
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"**/*.spec.ts", "**/*.spec.ts",
"**/browser/**/*.ts" "**/browser/**/*.ts",
"**/browser/**/*.js",
"out"
], ],
"angularCompilerOptions": { "angularCompilerOptions": {
"enableIvy": true "enableIvy": true

4520
yarn.lock

File diff suppressed because it is too large Load Diff